线程安全问题
- 根本原因
- 代码结构
- 原子性
- 解决方案:synchronized
- 内存可见性问题
- 解决方案 volatile
- 指令重排序问题
- wait和notify
- 判定一个代码是否线程安全,一定要==具体问题具体分析==!!!
根本原因
根本原因:多线程抢占式执行,随机调度。
代码结构
代码结构:多个线程同时修改同一个变量。
原子性
原子:不可拆分的基本单位
如果修改操作是非原子性的,出现线程安全问题的概率就比较高。
举个例子:
public class Thread_demo {
static class Counter{
int count=0;
public void increase(){
count++;
}
}
public static void main(String[] args) throws InterruptedException {
final Counter counter=new Counter();
Thread t1=new Thread(()->{
//count自增1000次
for (int i = 0; i <1000 ; i++) {
counter.increase();
}
});
Thread t2=new Thread(()->{
//count自增1000次
for (int i = 0; i <1000 ; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
运行结果:
"C:\Program Files\Java\jdk1.8.0_192\bin\java.exe" "-javaagent:D:\Program Files\IDEA\IntelliJ IDEA Community Edition 2021.3.2\lib\idea_rt.jar=56803:D:\Program Files\IDEA\IntelliJ IDEA Community Edition 2021.3.2\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_192\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\rt.jar;D:\Java\java\untitled\out\production\untitled" Thread_demo
1673
Process finished with exit code 0
需求是两个线程各自自增1000次,预期结果是输出2000,
而实际运行结果是1673,与预期不符。这是典型的线程安全问题。
下面针对 count++ 操作进行分析
++操作本质上分为三步:
- 先把内存中的值,读取到CPU的寄存器中。(load)
- 把CPU寄存器的数值进行+1运算。(add)
- 将得到的结果写回到内存中。(save)
这三个操作就是CPU上执行的三个指令
如果是两个线程并发的执行count++,此时就相当于两组 load add save进行执行,由于线程的抢占式执行,导致当前执行到任意一个指令时侯,线程都可能被调度走,cpu让别的线程执行,从而导致结果有差异。
解决方案:synchronized
通过使用 synchronized 关键字来对线程进行加锁操作。
如果两个线程同时尝试对同一个对象加锁,此时一个能获取锁成功,另一个只能阻塞等待(BLOCKED),一直阻塞到刚才的线程释放锁(解锁),当前线程才能加锁成功!!
1.修饰方法
- 修饰普通方法,相当于针对this加锁
Thread t1=new Thread(()->{
//count自增1000次
for (int i = 0; i <1000 ; i++) {
counter.increase();
}
});
Thread t2=new Thread(()->{
//count自增1000次
for (int i = 0; i <1000 ; i++) {
counter.increase();
}
});
加上关键字后的运行结果:
"C:\Program Files\Java\jdk1.8.0_192\bin\java.exe" "-javaagent:D:\Program Files\IDEA\IntelliJ IDEA Community Edition 2021.3.2\lib\idea_rt.jar=55120:D:\Program Files\IDEA\IntelliJ IDEA Community Edition 2021.3.2\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_192\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\rt.jar;D:\Java\java\untitled\out\production\untitled" Thread_demo
2000
Process finished with exit code 0
t1执行increase操作时,就针对counter这个对象加上锁了
t2执行increase操作时,也尝试对counter加锁,但是由于counter已经被t1占用了,这里的加锁操作就会阻塞
2.修饰静态代码块
修饰静态方法,和修饰一般方法,同理
3. 修饰代码块
注意:
修饰普通方法时,锁对象是this
修饰静态代码块时,锁对象是类对象
修饰代码块时,显示/手动指定锁对象
3.可重入
synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题
synchronized public void increase(){
synchronized (this){
count++;
}
}
内存可见性问题
一个线程针对一个变量进行读取操作,同时另一个线程针对这个变量进行修改,此时读到的值,不一定是修改之后的值,这个读线程没有感知到变量的变化,归根结底是编译器/jvm在多线程环境下优化时产生了误判。
举个例子:
public class Thread_demo2 {
static class MyCounter{
int flag=0;
}
public static void main(String[] args) throws InterruptedException {
MyCounter myCounter=new MyCounter();
Thread t1= new Thread(()->{
while (myCounter.flag==0){
//
}
System.out.println("t1循环结束");
});
Thread t2=new Thread(()->{
Scanner scanner=new Scanner(System.in);
System.out.println("请输入一个整数");
myCounter.flag=scanner.nextInt();
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
运行结果
"C:\Program Files\Java\jdk1.8.0_192\bin\java.exe" "-javaagent:D:\Program Files\IDEA\IntelliJ IDEA Community Edition 2021.3.2\lib\idea_rt.jar=59250:D:\Program Files\IDEA\IntelliJ IDEA Community Edition 2021.3.2\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_192\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\rt.jar;D:\Java\java\untitled\out\production\untitled" Thread_demo2
请输入一个整数
1
预期结果是 t2 把 flag 改成非0的值之后,t1 随之结束循环,但实际运行结果与预期不符。
下面针对 **while (myCounter.flag==0)**操作进行分析,大概分为两步操作:
1.load,把内存中flag的值,读取到寄存器里
2.cmp,把寄存器的值,和0进行比较,根据比较结果,决定下一步往哪执行
上述循环执行速度极快,在线程 t2 真正修改 flag 值之前,load得到的结果都相同,又加上 load 操作相比 cmp 操作执行速度慢很多,JVM就判定flag值不会被修改,从而不在真正重复load操作。
解决方案 volatile
用关键字volatile修饰变量:
当一个变量被声明为 “volatile” 时,编译器会确保对该变量的读取和写入操作按照严格的顺序进行,以避免出现内存可见性问题。
如图:
加上关键字后的运行结果:
"C:\Program Files\Java\jdk1.8.0_192\bin\java.exe" "-javaagent:D:\Program Files\IDEA\IntelliJ IDEA Community Edition 2021.3.2\lib\idea_rt.jar=54891:D:\Program Files\IDEA\IntelliJ IDEA Community Edition 2021.3.2\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_192\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\rt.jar;D:\Java\java\untitled\out\production\untitled" Thread_demo2
请输入一个整数
1
t1循环结束
Process finished with exit code 0
指令重排序问题
本质上是编译器优化出现bug.
在执行程序时,编译器器可能会对指令的执行顺序进行重新排列,以提高指令级并行性和系统性能。
由于重排序的存在,程序的执行结果可能与源代码的预期结果不一致。
wait和notify
wait和notify作用:针对多个线程执行顺序进行控制
wait会让调用线程进行阻塞
通过其他线程的notify进行通知
wait操作
- 先释放锁(wait操作需要搭配synchronized来使用)
- 进行阻塞等待(WAITING状态)
- 收到通知之后,重新尝试获取锁,并且在获取锁后,继续往下执行
notify操作:
- 和wait配对
- wait的锁对象和notify的锁对象要一致,否则notify不会有任何效果
举个例子:
public class Thread1 {
public static void main(String[] args) throws InterruptedException {
Object object=new Object();
Thread t1=new Thread(()->{
//这个线程负责进行等待
System.out.println("t1:wait之前");
try {
synchronized (object){
object.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2:wait之后");
});
Thread t2=new Thread(()->{
System.out.println("t2:notify之前");
synchronized (object){
//notify务必要获取到锁,才能进行通知
object.notify();
}
System.out.println("t2:notify之后");
});
t1.start();
Thread.sleep(500);
t2.start();
}
}
运行结果:
"C:\Program Files\Java\jdk1.8.0_192\bin\java.exe" "-javaagent:D:\Program Files\IDEA\IntelliJ IDEA Community Edition 2021.3.2\lib\idea_rt.jar=55923:D:\Program Files\IDEA\IntelliJ IDEA Community Edition 2021.3.2\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_192\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_192\jre\lib\rt.jar;D:\Java\java\untitled\out\production\untitled" Thread1
t1:wait之前
t2:notify之前
t2:notify之后
t2:wait之后
Process finished with exit code 0
wait的锁对象和notify的锁对象要一致,
如下图:
notify 方法用于唤醒正在等待同一个对象锁的一个线程。
如果有多个线程在等待,那么只有其中一个线程会被唤醒,具体唤醒哪个线程是不确定的。而 notifyAll 方法则会唤醒所有等待的线程。