• 多线程---线程安全问题及解决


    一个线程不安全的案例

    题目:有较短时间让变量count从0加到10_0000

    解决方案:我们创建两个线程分别让count加5_0000次

    结果:count < 10_0000

    
    class Count{
        public int count = 0;
        
        public void increase(){
           count++;
        }
    
    }
    public class Demo {
        //验证线程不安全问题
        public static void main(String[] args) {
            Count count1 = new Count();
    
    
            // 操作同一个变量
            Thread thread1 = new Thread(() -> {
                for (int i = 0; i < 50000; i++){
                    count1.increase();
                }
            });
            Thread thread2 = new Thread(() -> {
                for (int i = 0; i < 50000; i++){
                    count1.increase();
                }
            });
    
            thread1.start();
            thread2.start();
    
            try {
                thread1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                thread2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println(count1.count);   //  <100000
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    造成线程不安全的原因

    抢占式执行

    操作系统调度线程的时候,是一个“随机”的过程,当两个线程都参与调度的时候,谁先谁后不确定。

    多个线程修改同一个变量

    线程之间是“并发执行的”,当多个线程修改同一个变量时,多个线程同时获取到了变量值。某一个线程修改了变量,修改的结果不能被其他线程知道,其他线程还会修改原先获取到的值,导致结果错误。

    如果修改的是不同的变量,线程之间独立执行,不会出现问题。

    修改操作不是原子的

    count++操作底层是三条指令在CPU上完成的:

    load:把内存中的值读到CPU寄存器中;
    add:count+1;
    save:把寄存器的值写回内存

    由于这三条指令不是原子的,两个线程在执行时就会有不同的执行顺序:
    在这里插入图片描述
    在这些执行顺序下,都会使count没有正确的++,使最终结果出错。

    内存可见性问题

    JVM优化引入的BUG。例如,两个线程在操作同一个变量,一个线程读并且比较,一个线程修改。假设读操作非常频繁的情况下,比较操作也会非常的频繁。但是读是从内存中读,比较是在CPU里比较。比较的速度远远大于读的速度。而且每次读到的值还一样,这时编译器就会大胆优化:只读取一次,后面就不从内存中读了。每次比较都和前面读取到的值比较,不和内存中的值比较。这时另一个线程把内存中的值修改了但是这个线程比较的还时原来的值,就会有问题。

    指令重排序问题

    JVM优化引入的BUG。由我们自己写的代码在大多数情况下的执行流程中,指令的执行顺序往往都不是最优选择,即没有使运行速度达到最快。因此,JVM在编译时,就会在逻辑等价的前提下,对我们的指令进行重新排序使代码的运行速度变快。

    这样的优化在单线程时,是没有问题的。但是在多线程的情况下,线程之间是抢占式执行的,哪条指令先执行哪条指令后执行不确定,就可能有问题。

    如何让线程变得安全?

    “抢占式执行”是线程调度的基本方式,我们无法干预。

    “多个线程修改同一个变量”:我们在特定场景下就是得修改同一个变量,也无法改变。

    “操作不是原子的”:我们保证线程安全的主要方式,通过synchronized加锁。

    “内存可见性”“指令重排序”:JVM优化的问题。使用volatile解决

    加锁

    锁,具有独占的特性。如果当前锁还没有被加上,加锁操作就能成功;如果锁已经被人加上了,加锁操作就不能成功,会进入阻塞等待的状态。即:加锁操作会让”并发执行“变成”串行执行“

    在这里插入图片描述

    注:

    1. 处于同一个规则:所有线程都会加锁的情况下,只有获取到锁才能执行相关操作。
    2. 一个线程加锁,另一个线程不加锁。这样不能保证线程安全。 因为它俩没有采用同一个规则办事儿,不会产生锁竞争。

    synchronized

    加锁操作是通过synchronized关键字来实现的,只要对同一个对象加锁,就会产生“锁竞争”。而synchronized有三种用法:

    • synchronized修饰代码块

    synchronized修饰代码块时,必须指定锁对象。如果锁对象是this,即对当前对象加锁。
    像下面的代码,count调用了increase就是对count加锁,对同一个对象加锁会出现“锁竞争”

    	public void increase(){
            synchronized (this){
                count++;
            }
        }
    
    		Count count = new Count();
    		Thread thread1 = new Thread(() -> {
                for (int i = 0; i < 50000; i++){
                    count.increase();
                }
            });
            Thread thread2 = new Thread(() -> {
                for (int i = 0; i < 50000; i++){
                    count.increase();
                }
            });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    当两个线程针对两个对象加锁时,不会产生“锁竞争”,不会阻塞等待。

    	public void increase(){
            synchronized (this){
                count++;
            }
        }
    
    		Count count1 = new Count();
    		Count count2 = new Count();
    		Thread thread1 = new Thread(() -> {
                for (int i = 0; i < 50000; i++){
                    //对count1加锁
                    count1.increase();
                }
            });
            Thread thread2 = new Thread(() -> {
                for (int i = 0; i < 50000; i++){
                    //对count2加锁
                    count2.increase();
                }
            });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • synchronized修饰普通方法

    synchronized修饰普通方法时,相当于是锁this对象,只要是同一个对象调用到这个方法都会产生“锁竞争”。

    	public synchronized void increase(){
                count++;
        }
    
    		Count count = new Count();
    		Thread thread1 = new Thread(() -> {
                for (int i = 0; i < 50000; i++){ 
                    count.increase();
                }
            });
            Thread thread2 = new Thread(() -> {
                for (int i = 0; i < 50000; i++){
                    count.increase();
                }
            });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • synchronized修饰静态方法

    静态方法是随着类加载而加载的而且只加载一次,所以静态方法是和类绑定在一起的只有一份。

    	public static Object locker = new Object();
    	public void increase(){
    	     synchronized(locker){
                count++;
             }
        }
    
    		Count count = new Count();
    		Count count1 = new Count();
    		//count和count1都是Count类的对象  里面的静态方法是同一个。
    		//所以它俩虽然是不同的对象  但是也会产生“锁竞争”
    		Thread thread1 = new Thread(() -> {
                for (int i = 0; i < 50000; i++){ 
                    count.increase();
                }
            });
            Thread thread2 = new Thread(() -> {
                for (int i = 0; i < 50000; i++){
                    count1.increase();
                }
            });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    Count.class是对类对象加锁,而类也只有一份儿。静态方法也是只有一份儿,所以它们俩也会产生“锁竞争”

    	public static Object locker = new Object();
    	public void increase(){
    	     synchronized(locker){
                count++;
             }
        }
        public void increase1(){
             synchronized(Count.class){
                  count++
             }
        }
    
    		Count count = new Count();
    		Count count1 = new Count();
    		//count和count1都是Count类的对象 类只有一个,静态方法也是只有一个。它们俩绑定在一起
    		//所以它俩虽然是不同的对象  但是也会产生“锁竞争”
    		Thread thread1 = new Thread(() -> {
                for (int i = 0; i < 50000; i++){ 
                    count.increase();
                }
            });
            Thread thread2 = new Thread(() -> {
                for (int i = 0; i < 50000; i++){
                    count1.increase();
                }
            });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    volatile

        public volatile int count = 0;
    
    • 1

    volatile只有一个用法就是修饰变量,表示该变量的值必须从内存中读取,不能从缓存中读取。
    即:volatile禁止了编译器优化,避免了直接读取CPU寄存器中缓存的数据,而是每次都读取内存。

    但是volatile并不能保证是操作是原子性的,因此,它只适合用于一个线程读,一个线程修改的场景。不适合用于两个线程都修改的场景。

    谈到volatile就会联想到JMM(Java Memory Model):Java内存模型

    在JMM中,引入了新的术语:
    工作内存(work memory):即CPU寄存器(缓存)
    主内存(main memory):真正读取的内存

    站在JMM的角度看待volatile:
    正常程序的执行过程中,先会把主内存的数据加载到工作内存中,再进行计算处理。编译器优化可能会导致不是每次都会真正的读取主内存,而是直接读取工作内存中的缓存数据,就可能导致内存可见性问题。volatile起到的效果就是保证每次读取数据都是真的从主内存中重新读取。

  • 相关阅读:
    [NISACTF 2022]join-us - 报错注入&无列名注入
    英语学习(娱乐篇)
    【力扣】136. 只出现一次的数字
    蓝牙技术|新能源汽车正快速发展,蓝牙无钥匙进入助力智能化新能源汽车
    流程控制结构及equal和随机函数
    [Java中将数据转JSON]
    【centos7安装ElasticSearch】
    JAVA:实现Matrix Graphs矩阵图算法(附完整源码)
    前端程序员需要了解的MySQL
    简单试验:用Excel进行爬虫
  • 原文地址:https://blog.csdn.net/weixin_62976968/article/details/134083561