赞
踩
线程安全 : 在多线程各种随机的调度顺序下,代码都没有bug,都能符合 预期的方式执行
什么是bug : 不符合需求就算是bug
举一个实例来验证线程安全
class Counter{ public int count = 0; public void increase(){ count++; } } public class demo14 { public static Counter counter = new Counter(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(()->{ for (int i = 0; i < 5000; i++) { counter.increase(); } }); Thread t2 = new Thread(()->{ for (int i = 0; i < 5000; i++) { counter.increase(); } }); t1.start(); t2.start(); //阻塞main,先执行 t1 t2线程,等他们执行完了,再执行main t1.join(); t2.join(); System.out.println("counter="+ counter.count); } }
预期的是,有两个线程各累加5000次,静态的count应该会变成10000,可是结果却不到10000次,并且count还是随机的
在进行count++的时候,底层会在CPU上执行3条指令(不是原子性)
把内存的数据读取到CPU寄存器上 load
把CPU的寄存器上的值+1 add
把寄存器中的值,写到内存中 save
串行: 机器执行完一条指命后,才取出下一条指令来执行的一种工作方式。
极端的两种情况:
如果两个线程之间的调度全是串行执行,结果就是10000
如果两个线程全是其他的情况,没有一次串行执行,结果就是5000
所以最终情况就是5000 - 10000
所以以上的随机值就是一种线程不安全
- 多线程之间抢占式执行(多线程不安全的根本原因)
任何一种调度都是有可能 的
多个线程修改同一个变量
执行修改的操作不是原子的(上述的count++就涉及到了3个CPU指令LOAD ADD SAVE)
内存可见性
指令重排序 (4 5 两点主要是JVM优化代码的时候出现的bug)
…(具体还得看代码实现)
要想解决线程安全问题,最常见的方法就是将多个操作通过特殊手段变成一个原子操作
在上面的例子中,可以在count++ 之前进行加锁,在count++之后进行解锁,在加锁与解锁之间进行修改count,此时别的进程修改不了count,别的线程出于阻塞状态(BLOCKED状态)
在java中,进行加锁,要使用synchronized 关键字
加上锁之后就使别的线程变成了阻塞状态,由"并发"变成了 串行,运行效率确实降低,但是保证了多线程的安全
一定要知道: 加上锁不一定就能保证线程安全,正确的加锁是通过加锁,让并发修改同一个变量–>串行修改同一个变量
要是只给一个线程加锁,另一个不加锁,其实是没用的,只给一个线程加锁不会涉及到"锁竞争",也就不会有阻塞等待,也就不会并发执行–>串行执行(追根究底就是还是会抢占式执行)
关键字synchronized 不仅能修饰方法,还能修饰代码块
synchronized 后面括号填的是锁对象, 也就是针对该对象进行加锁,谁要是调用increase方法,谁就是this–锁对象
锁对象不止可以是this,还可以是任何的对象
上面的synchronized锁方法其实默认的锁对象就是this
写多线程代码的时候,最关心的是,两个线程锁的是否是同一个对象,只要锁同一个对象就会存在锁竞争
注意: 此处打印的是counter的count,所以要想达成10000,就要在counter上形成锁竞争,counter与counter2 里面的counter是不一样的
class Counter{ public int count = 0; public void increase(){ synchronized (this){ count++; } } } public class demo14 { public static Counter counter = new Counter(); public static Counter counter2 = new Counter();//再次设立一个新的对象 public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(()->{ for (int i = 0; i < 5000; i++) { counter.increase(); } }); Thread t2 = new Thread(()->{ for (int i = 0; i < 5000; i++) { counter2.increase(); } }); t1.start(); t2.start(); //阻塞main,先执行 t1 t2线程,等他们执行完了,再执行main t1.join(); t2.join(); System.out.println("counter="+ counter.count); } }
其实这种写法与之前的synchronized后直接加this,counter调用的写法在线程安全角度是一样的
注意: 此处打印的是counter的count,所以要想达成10000,就要在counter上形成锁竞争,counter与counter2 里面的counter是不一样的
总结就是一句话: 写多线程代码的时候,最关心的是,两个线程锁的是否是同一个对象,只要锁同一个对象就会存在锁竞争
对于一个线程,连续加锁两次,就会形成死锁
第二次加锁会阻塞等待,直到第一把锁解开,才能加第二把锁
第一把锁要想解开,要求第二把锁加锁成功
所以就这样子相互纠缠住了,就形成了死锁
但是,死锁问题有时候是很难避免的,要是加锁函数1嵌套函数2 ,函数2嵌套函数3,函数3嵌套加锁函数4,这样子就会很难发现死锁问题
要是不会产生死锁的话,这样的所就叫做"可重入锁"
synchronized就是可重入的
可重入锁的底层实现是很简单的
只要让锁记录好时哪个线程持有的这把锁
加锁: t 线程尝试对this来加锁,锁就会记录是 t 线程持有了它
第二次锁就会发现,还是t线程,此时就会直接通过,不会再次加锁
解锁: 在锁里增加一个计数器,每次加锁就++,每次解锁就–,如果计数器为0,此时才真加锁,当计数器为0,此时才真解锁
总结:
可重入锁的实现要点:
- 让锁里持有线程对象,记录哪个线程加了锁
- 维护一个计数器,用来衡量什么时候真加锁,什么时候真解锁,什么时候直接通过
就算加锁代码出现异常,也还是会解锁,还是不会死锁—不得不说synchronized关键字是一个十分优秀的设计
所以上面的代码是不会引起死锁的
复习一下final :
final修饰一个变量: 禁止修改
final 修饰类: 禁止进程
final 修饰方法: 禁止重写
线程不安全的其中一个原因就是内存可见性
要是程序需要频繁地读取数据,比较数据速度远快于LOAD的速度,此时编译器就会开始优化, 要是频繁地执行LOAD 并且 LOAD的结果还是一样的,编译器就会只执行一次LOAD,之后就不会重新读取内存了
import java.util.Scanner; public class Demo16 { public static class Counter{ public int count = 0; } public static void main(String[] args) { Counter counter = new Counter(); Thread t1 = new Thread(()->{ while (counter.count == 0){ //具体操作 } System.out.println("t1进程运行结束"); }); t1.start(); Thread t2 = new Thread(()->{ System.out.println("请输入一个整数:"); Scanner scanner = new Scanner(System.in); counter.count = scanner.nextInt();//修改count }); t2.start(); } }
运行以上的代码就会发现,while循环会一直执行,永远不会停止循环
原因: 内存中的count已经修改成了输入的1 ,但是刚才的修改并不会影响t1 的读内存操作,因为t1 的读内存已经被编译器优化成了不再循环读内存,只是读一次就好了,t1 还以为count还是0
也就是说, t2 把内存改了,但是t1没有没看见,这就是内存可见性问题
内存可见性是编译器优化惹的祸, 编译器在单线程情况下,对于代码的优化,逻辑是不会变的,但是编译器在多线程的情况下,很有可能会发生误判
要想解决内存可见性问题,就不要让编译器进行优化,由我们自己进行操作,此时就 要使用关键字volatile[ˈvɒlətaɪl]
使用volatile"可变的"来修饰一个变量,这样子编译器就不会进行优化,也就是说,每次编译器都会去读取变量的值,
import java.util.Scanner; public class Demo16 { public static class Counter{ volatile public int count = 0;//加上volatile } public static void main(String[] args) { Counter counter = new Counter(); Thread t1 = new Thread(()->{ while (counter.count == 0){ } System.out.println("t1进程运行结束"); }); t1.start(); Thread t2 = new Thread(()->{ System.out.println("请输入一个整数:"); Scanner scanner = new Scanner(System.in); counter.count = scanner.nextInt(); }); t2.start(); } }
此时编译器每次都会去读取内存中的数据
volatile能解决内存可见性问题,也就是一个线程读,一个线程修改的情况,它并不能保证原子性的问题
要是两个线程修改同一个变量还得是synchronized来保证原子性
public class Demo17 { static class Counter{ volatile int count = 0;//只能解决内存可见性 public void increase(){ count ++; } } public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Thread t1 = new Thread(()->{ for (int i = 0; i < 5000; i++) { counter.increase(); } }); Thread t2 = new Thread(()->{ for(int i = 0; i<5000; i++){ counter.increase(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("count = "+ counter.count); } } //两个线程修改同一个变量,还是要 synchronized来保证原子性
要是谈到volatile就一定要知道JMM(Java Memory Model) Java内存模型
- 通俗地讲:
volatile禁止了编译器优化,避免了直接读取从CPU寄存器缓存的数据,而是每次都会重新读内存
Java语言为了更加通用,尽可能 避免硬件的差异,就起了一些术语
CPU寄存器–>工作内存(work memory) 内存—> 主内存(main m emory )
- 站在JMM角度看volatile:
正常程序执行的过程中,会把主内存的数据先加载到工作内存中,再进行计算处理,要是编译器进行优化,就不会每次都去主内存中读取,而是直接去工作内存读取, 这样就会导致内存可见性问题
volatile 起到的效果就是 ,保证每次读取内存都是真的才能够主内存中读取
注意: 工作内存不是真的内存,它只是CPU的寄存器,这是术语而已
多线程中总是会出现抢占式执行,可以使用wait 和 notify 来控制线程的执行顺序\
线程1调用了wait,线程1 就会阻塞,直到别的线程调用notify之后,线程1 才会继续执行
package Threading;
public class Demo18 {
public static void main(String[] args) {
Object object = new Object();
System.out.println("wait之前");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("wait之后");
}
}
这样写的结果,会报错
不合法的锁状态异常
wait内部会进行一下三个操作:
- 释放当前的锁
- 进行等待
- 当有别的线程调用 notify 时,就会被唤醒,然后重新获取锁
所以要想要释放当前的锁的前提就是要先加上锁
package Threading; public class Demo18 { public static void main(String[] args) { Object object = new Object(); synchronized(object){ //先加上锁,wait之后object就会解锁,其他的线程会获取到锁 System.out.println("wait之前"); try { object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("wait之后"); } } }
要想使用wait 和 notify 就一定要先加上锁
举一个例子来理解wait与notify执行的具体顺序
package Threading; public class Demo19 { public static void main(String[] args) { Object object = new Object();//创建 一个对象 Thread t1 = new Thread(()->{ while (true) { synchronized (object) { System.out.println("wait之前"); try { object.wait();//前面加上锁 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("wait之后"); } } }); t1.start(); Thread t2 = new Thread(()->{ while (true){ synchronized (object){ System.out.println("notify之前"); object.notify();//前面已经加上锁 System.out.println("notify之后"); } try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t2.start(); } }
就像是上面写的,t1先调用了wait, t2后调用notify, 此时notify就会唤醒wait
但是,要是t2先执行了 notify , t1 后执行wait , 或者干脆就不调用wait , 其实也没有什么事,只是不符合上面的规定罢了,什么都不会发生
notifyAll : 唤醒所有被wait的线程,
wait notify是 用来控制多线程直接的执行先后顺序的
- wait 和 notify 都要先进行上锁(synchronized)
- 必须是同一个对象调用wait 和 notify
- 锁对象也要和 调用wait / notify 的对象一致
- 就算没有wait , 直接notify 也是没有副作用的
首先要知道,wait 和 sleep 都是 让线程进入阻塞等待的状态
两个方法所属类不一样, sleep是thread 类的方法, wait是Object类的方法
sleep是 通过时间来控制何时唤醒线程, wait 是 其他的线程通过notify 唤醒线程的(但是wait还有一个重载版本 , 参数可以传入时间, 表示等待的最大时间)
有无释放锁: 在调用wait之前, 必须要保证已经请求到锁, 调用之后会释放掉已经获得的锁,唤醒之会重新请求锁, sleep就不涉及到锁
有三个线程,线程名称分别为:a,b,c。
每个线程打印自己的名称。
需要让他们同时启动,并按 c,b,a的顺序打印
public static void main(String[] args) { Thread tc = new Thread(()->{ System.out.println("c"); }); Thread tb = new Thread(()->{ try { tc.join();//等待tc线程结束 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("b"); }); Thread ta = new Thread(()->{ try { tb.join();//等待tb线程结束 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("a"); }); ta.start(); tb.start(); tc.start(); }
总结来说,就是线程c先开始, b等c结束再开始, a等b结束再开始
进阶版
有三个线程,分别只能打印A,B和C
要求按顺序打印ABC,打印10次
输出示例:
ABC
ABC
ABC
ABC
ABC
ABC
ABC
ABC
ABC
ABC
public static boolean isThreadA = true; public static boolean isThreadB = false; public static boolean isThreadC = false; public static void main(String[] args) { final Test test = new Test();//其实创建一个Object对象也是一样的 Thread t1 = new Thread(()->{ for (int i = 0; i < 10; i++) { synchronized (test){ while (!isThreadA){ try { test.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.print("A"); isThreadA = false; isThreadB = true;//交给线程2 isThreadC = false; test.notifyAll();//唤醒 } //以上的代码必须要synchronized里面,保证原子性 } }); Thread t2 = new Thread(()->{ for (int i = 0; i < 10; i++) { synchronized (test){ while (!isThreadB){ try { test.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.print("B"); isThreadA = false; isThreadB = false; isThreadC = true; test.notifyAll(); } } }); Thread t3 = new Thread(()->{ for (int i = 0; i < 10; i++) { synchronized (test){ while (!isThreadC){ try { test.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.print("C"); isThreadA = true; isThreadB = false; isThreadC = false; test.notifyAll(); System.out.println(); } } }); t1.start(); t2.start(); t3.start(); }
这道题就不好向上面一样使用join了,使用的是标识位 + 加锁 + wait notify 控制顺序
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。