8-线程安全问题

目录

1.概念

1.1.单线程

1.2.多线程

2.导致线程不安全的5个因素

①抢占式执行(首要原因)

②多个线程同时修改了同一个变量

③非原子性操作

④内存可见性

⑤指令重排序


  • 线程优点:加速程序性能。
  • 线程缺点:存在安全问题。

1.概念

线程不安全指的是程序在多线程的执行结果不符合预期。 例如:

1.1.单线程

public class ThreadDemo17 {
    static class Counter{
        //变量
        private int number = 0;

        //循环次数
        private int MAX_COUNT = 0;
        public Counter(int MAX_COUNT) {
            this.MAX_COUNT = MAX_COUNT;
        }

        //++方法
        public void incr() {
            for (int i = 0; i < MAX_COUNT; i++) {
                number++;
            }
        }

        //--方法
        public void decr() {
            for (int i = 0; i < MAX_COUNT; i++) {
                number--;
            }
        }

        public int getNumber() {
            return number;
        }
    }

    public static void main(String[] args) {
        Counter counter = new Counter(100000);
        //++操作
        counter.incr();

        //--操作
        counter.decr();

        //打印结果
        System.out.println("最终结果:" + counter.getNumber());
    }
}

1.2.多线程

public class ThreadDemo17 {
    static class Counter{
        //变量
        private int number = 0;

        //循环次数
        private int MAX_COUNT = 0;
        public Counter(int MAX_COUNT) {
            this.MAX_COUNT = MAX_COUNT;
        }

        //++方法
        public void incr() {
            for (int i = 0; i < MAX_COUNT; i++) {
                number++;
            }
        }

        //--方法
        public void decr() {
            for (int i = 0; i < MAX_COUNT; i++) {
                number--;
            }
        }

        public int getNumber() {
            return number;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter(100000);

        Thread t1 = new Thread(() -> {
            //++操作
            counter.incr();
        });

        Thread t2 = new Thread(() -> {
            //--操作
            counter.decr();
        });

        //启动多线程进行执行
        t1.start();
        t2.start();

        //等待两个线程执行完
        t1.join();
        t2.join();

        //打印结果
        System.out.println("最终结果:" + counter.getNumber());
    }
}

每次执行结果都不同。这就是线程不安全。

2.导致线程不安全的5个因素

①抢占式执行(首要原因)

由于CPU资源较少,而线程较多,狼多肉少,发生争抢混乱。

若将上面代码改为串行执行:线程1执行完之后,线程2再执行(相当于单线程效率),就不会争抢。

public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter(100000);

        Thread t1 = new Thread(() -> {
            //++操作
            counter.incr();
        });
        t1.start();
        t1.join();

        Thread t2 = new Thread(() -> {
            //--操作
            counter.decr();
        });
        t2.start();
        t2.join();

        System.out.println("最终结果:" + counter.getNumber());
    }

②多个线程同时修改了同一个变量

改动代码,让不同线程各自修改各自的变量,就ok了。

public class ThreadDemo17 {
    static class Counter{
        //变量
        private int number = 0;

        //循环次数
        private int MAX_COUNT = 0;
        public Counter(int MAX_COUNT) {
            this.MAX_COUNT = MAX_COUNT;
        }

        //++方法
        public int incr() {
            int temp = 0;
            for (int i = 0; i < MAX_COUNT; i++) {
                temp++;
            }
            return temp;
        }

        //--方法
        public int decr() {
            int temp = 0;
            for (int i = 0; i < MAX_COUNT; i++) {
                temp--;
            }
            return temp;
        }

        public int getNumber() {
            return number;
        }
    }

    static int num1 = 0;
    static int num2 = 0;

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter(100000);

        Thread t1 = new Thread(() -> {
            //++操作
            num1 = counter.incr();
        });

        Thread t2 = new Thread(() -> {
            //--操作
            num2 = counter.decr();
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("最终结果:" + (num1 + num2));
    }
}

③非原子性操作

什么是原子性? ——将一组操作封装成一个执行单元,要一次性执行完,中间不能停顿。

一条 java 语句不一定是原子的,也不一定只是一条指令。

⽐如刚才的 n++,其实是由三步操作组成的:

  1. 从内存把数据读到 CPU
  2. 进⾏数据更新
  3. 把数据写回到 CPU

不保证原子性会给多线程带来什么问题?

如果⼀个线程正在对⼀个变量操作,中途其他线程插⼊进来了,如果这个操作被打断了,结果就可能是错误的。

这点也和线程的抢占式调度密切相关。如果线程不是 "抢占" 的,就算没有原⼦性,也问题不⼤。

例如:

把⼀段代码想象成⼀个房间,每个线程就是要进⼊这个房间的⼈。如果没有任何机制保证,A进⼊房间后还没出来,B 也进⼊房间,打断 A 在房间⾥的隐私。这就是不具备原⼦性的。

如何解决?

给房间加⼀把锁,A 进去就把⻔锁上,其他⼈就进不来了。这样就保证了这段代码的原⼦性了。

有时也把这个现象叫做同步互斥,表示操作是互相排斥的。

改为串行执行即可。

④内存可见性

可见性指, ⼀个线程对共享变量值的修改,能够及时地被其他线程看到。

Java 内存模型 (JMM-JavaMemoryModel):Java虚拟机规范中定义了Java内存模型。

目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到⼀致的并发效果。

  • 线程之间的共享变量存在于"主内存" (Main Memory).
  • 每⼀个线程都有⾃⼰的 "⼯作内存" (Working Memory) .
  • 当线程要读取⼀个共享变量的时候, 会先把变量从主内存拷⻉到⼯作内存, 再从⼯作内存读取数据.
  • 当线程要修改⼀个共享变量的时候, 也会先修改⼯作内存中的副本, 再同步回主内存.

由于每个线程有自己的工作内存, 这些工作内存中的内容相当于同⼀个共享变量的 "副本". 此时修改线程1 的⼯作内存中的值, 线程2 的⼯作内存不⼀定会及时变化. 而JMM 带来的问题是会导致线程非安全问题的发生。

import java.time.LocalDateTime;

/**
 * 内存可见性问题
 */
public class ThreadDemo18 {
    //全局变量(类级别)
    private static boolean flag = true;

    public static void main(String[] args) {
        //创建子线程1
        Thread t1 = new Thread(() -> {
            System.out.println("线程 1:开始执行!" + LocalDateTime.now());
           while(flag) {
           }
            System.out.println("线程 1:结束执行!" + LocalDateTime.now());
        });
        t1.start();

        //创建子线程2
        Thread t2 = new Thread(() -> {
            //休眠1s
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程 2:修改 flag = false!" + LocalDateTime.now());
            flag = false;
        });
        t2.start();
    }
}

线程2早已把全局变量修改为另一个值,而线程1一直在执行,它并没有感知到全局变量flag的变化,这就是内存可见性问题。

⑤指令重排序

有很多种:

  1. 编译器指令重排序
  2. 运行期指令重排序

编译器优化是一件非常复杂的事情,其本质是调整代码的执⾏顺序,保证原有逻辑不变的情况下,提高程序执行效率。这在单线程下没问题,但在多线程并发情况下容易出现混乱,从而造成线程安全问题。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值