前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >线程安全问题的原因和解决方案

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

作者头像
用户10788736
发布2023-10-16 08:39:36
1340
发布2023-10-16 08:39:36
举报
文章被收录于专栏:CSDN搬移文章CSDN搬移文章

前言

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

一、简述线程不安全案例

代码语言: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,但是运行截图如下:

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

  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.

代码语言: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++; 进行了加锁。

注意:

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

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

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

(二)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 的静态方法。

结语

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

本文参与?腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-08-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客?前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、简述线程不安全案例
  • 二、线程安全问题的原因
    • (一)(根本问题)线程调度是随机的
      • (二)代码的结构问题
        • (三)代码执行不是原子的
          • (四)内存可见性问题
            • (五)指令重排序
            • 三、解决线程安全问题
              • (一)synchronized
                • (二)volatile
                  • (三)wait-notify
                    • (四)wait 和 sleep 的区别
                    • 结语
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
                    http://www.vxiaotou.com