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

前言

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

目录

前言

一、简述线程不安全案例

二、线程安全问题的原因

(一)(根本问题)线程调度是随机的

(二)代码的结构问题

(三)代码执行不是原子的

(四)内存可见性问题

(五)指令重排序

三、解决线程安全问题

(一)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,但是运行截图如下:

 首先对于一个简单的自增操作,可以分为如下三步:

  1. 读取内存数据,加载到CPU寄存器中;
  2. 把寄存器数据进行+1操作;
  3. 把寄存器数据写回到内存中。

 那么在该代码实现过程中就可能会出现如下步骤:

 当两个线程都对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. 进了代码块就加锁;
  2. 出了代码块就解锁

有如下几种形式:

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在执行时:

  1. 解锁;
  2. 阻塞等待;
  3. 当被其他线程唤醒之后,尝试重新加锁,加锁成功,wait执行完毕,继续往下执行其他逻辑。

故我们的 wait 方法和 notify 方法都要在 synchronized 内部使用,并且和synchronized的对象一致,如:

 如果 wait 没有搭配synchronized 使用,会直接抛出异常。

有如下代码:

 该输出结果,是因为其执行语句顺序,如图:

 notifyAll则可以唤醒所有处于wait中的线程。

注意事项

  1.  要想让 notify 能顺利唤醒 wait ,需要确保 wait 和 notify 都是使用同一个对象调用的;
  2.  wait 和 notify 都需要在 synchronized 内部执行,notify 在 synchronized 内部执行是   Java强制要求的;
  3.  如果进行 notify 时,另一个线程没有处于 wait 状态不会有任何影响。

当 wait 引起线程阻塞时,可以使用 interrupt 方法打断当前线程的阻塞状态

(四)wait 和 sleep 的区别

  1. wait 需要搭配synchronized 使用,sleep 不需要;
  2. wait 是 Object 的方法,sleep 是Thread 的静态方法。

结语

这篇博客如果对你有帮助,给博主一个免费的点赞以示鼓励,欢迎各位🔎点赞👍评论收藏⭐,谢谢!!!

  • 21
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 25
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 25
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Y君的进化史

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值