前言
如果某个代码,在单线程执行下没有问题,在多线程执行下执行也没有问题,则称“线程安全”,反之称“线程不安全”。
目录
前言
一、简述线程不安全案例
二、线程安全问题的原因
(一)(根本问题)线程调度是随机的
(二)代码的结构问题
(三)代码执行不是原子的
(四)内存可见性问题
(五)指令重排序
三、解决线程安全问题
(一)synchronized
(二)volatile
(三)wait-notify
(四)wait 和 sleep 的区别
结语
一、简述线程不安全案例
public class Main {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
count++;
}
}
});
t.start();
for (int i = 0; i < 10000; i++) {
count++;
}
t.join();
System.out.println(count);
}
}
代码中有两个线程,线程t和线程main都对count进行自增操作,理想结果下,输出结果是 20000,但是运行截图如下:
首先对于一个简单的自增操作,可以分为如下三步:
- 读取内存数据,加载到CPU寄存器中;
- 把寄存器数据进行+1操作;
- 把寄存器数据写回到内存中。
那么在该代码实现过程中就可能会出现如下步骤:
当两个线程都对count进行+1操作后,count应该是在原有的值上面+2,但是因为线程问题,使count只进行了 +1 操作。这种问题,我们称之为线程不安全问题。
二、线程安全问题的原因
(一)(根本问题)线程调度是随机的
多个线程之间的调度是随机的,操作系统使用“抢占式”执行的策略来调度线程。
如上述代码运行count++操作,多条指令的调度顺序是不确定的,如还有如下几种指令调度顺序的可能:
(二)代码的结构问题
多个线程同时修改同一个变量,容易产生线程安全问题。
上述案例是修改同一个变量,如果是修改不同变量,那么多个线程之间的寄存器数据修改对内存中的数据修改影响不大。
如:
(三)代码执行不是原子的
在Java中,我们称原子为最小单位,就像0无法再次拆分一样。
上述案例中关键执行语句就是 count++; 但是这条语句可以再次细分为三条语句,这就说明该语句不是原子的,便也是导致线程不安全问题的关键。
(四)内存可见性问题
内存可见性问题有三个原因:编译器优化、内存模型、多线程。
1)编译器优化:我们的代码在编译运行时,编译器会给我们进行优化操作,而其中,读取内存操作有可能被优化成读取寄存器(能节约大量的时间)。
2)内存模型:Java虚拟机内存模型导致读取内存读取操作特别复杂,消耗大量的资源。
3)多线程问题:上述案例中,内存和寄存器互相不可见问题。
(五)指令重排序
比如:
三、解决线程安全问题
对于引起线程安全问题的原因1是由JVM底层决定的,是无法改变的。synchronized可以解决问题原因2和3,volatile解决4和5。
(一)synchronized
解决线程安全问题,最主要的切入手段是:加锁。
synchronized搭配代码块进行加锁解锁操作:
- 进了代码块就加锁;
- 出了代码块就解锁。
有如下几种形式:
1.
synchronized public void a(){
// working
}
当前对象是该线程。
2.
//方法内部
synchronized (this){
//working
}
当前对象是this指的对象(静态方法内是类对象,实例方法内是线程对象)。
3.
//方法内部
synchronized (某个对象){
//working
}
当前对象是括号内的对象。
4.
synchronized static public void a(){
//working
}
当前对象是类对象。
这里的锁不是对整个代码块加锁,而是争对某个特定的对象加锁。如:
这里的synchronized代码块有两条执行语句,实际上这把锁只对 count++; 进行了加锁。
注意:
如果两个线程针对同一个对象加锁,就会出现锁竞争/锁冲突,一个加锁成功,一个阻塞等待。
如果两个线程针对不同对象加锁,就不会产生锁竞争等。
!!具体是针对哪一个对象加锁不重要,重要的是两个线程是不是针对同一个对象加锁!!!
(二)volatile
volatile关键字是修饰变量的(只能修饰实例变量、类变量),不能保证原子性。
1)当volatile解决内存可见性问题时,主要是解决编译器优化导致的问题。
禁止编译器进行读取内存操作被优化成读取寄存器。
加上volatile强制读取内存,虽然速度变慢了,但是数据更精确了。
2)保证有序性。
禁止指令重排序。编译时JVM编译器遵循内存屏障的约束,运行时靠屏障指令组织指令顺序。
(三)wait-notify
为了线程能按照规定的顺序执行,使用wait-notify。这两个都是Object提供的方法。
wait在执行时:
- 解锁;
- 阻塞等待;
- 当被其他线程唤醒之后,尝试重新加锁,加锁成功,wait执行完毕,继续往下执行其他逻辑。
故我们的 wait 方法和 notify 方法都要在 synchronized 内部使用,并且和synchronized的对象一致,如:
如果 wait 没有搭配synchronized 使用,会直接抛出异常。
有如下代码:
该输出结果,是因为其执行语句顺序,如图:
notifyAll则可以唤醒所有处于wait中的线程。
注意事项:
- 要想让 notify 能顺利唤醒 wait ,需要确保 wait 和 notify 都是使用同一个对象调用的;
- wait 和 notify 都需要在 synchronized 内部执行,notify 在 synchronized 内部执行是 Java强制要求的;
- 如果进行 notify 时,另一个线程没有处于 wait 状态不会有任何影响。
当 wait 引起线程阻塞时,可以使用 interrupt 方法打断当前线程的阻塞状态。
(四)wait 和 sleep 的区别
- wait 需要搭配synchronized 使用,sleep 不需要;
- wait 是 Object 的方法,sleep 是Thread 的静态方法。
结语
这篇博客如果对你有帮助,给博主一个免费的点赞以示鼓励,欢迎各位🔎点赞👍评论收藏⭐,谢谢!!!