线程安全问题的原因和解决方案

365足球外围网站 2026-01-13 04:02:17 阅读: 9154

前言

如果某个代码,在单线程执行下没有问题,在多线程执行下执行也没有问题,则称“线程安全”,反之称“线程不安全”。

一、简述线程不安全案例代码语言:javascript复制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.

代码语言:javascript复制 synchronized public void a(){

// working

}当前对象是该线程。

2.

代码语言:javascript复制//方法内部

synchronized (this){

//working

}当前对象是this指的对象(静态方法内是类对象,实例方法内是线程对象)。

3.

代码语言:javascript复制//方法内部

synchronized (某个对象){

//working

}当前对象是括号内的对象。

4.

代码语言:javascript复制synchronized static public void a(){

//working

}当前对象是类对象。

这里的锁不是对整个代码块加锁,而是争对某个特定的对象加锁。如:

这里的synchronized代码块有两条执行语句,实际上这把锁只对 count++; 进行了加锁。

注意:

如果两个线程针对同一个对象加锁,就会出现锁竞争/锁冲突,一个加锁成功,一个阻塞等待。

如果两个线程针对不同对象加锁,就不会产生锁竞争等。

!!具体是针对哪一个对象加锁不重要,重要的是两个线程是不是针对同一个对象加锁!!!

(二)volatilevolatile关键字是修饰变量的(只能修饰实例变量、类变量),不能保证原子性。

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 的静态方法。结语这篇博客如果对你有帮助,给博主一个免费的点赞以示鼓励,欢迎各位🔎点赞👍评论收藏⭐,谢谢!!!