• 【多线程】线程安全(重点)


    1. 观察线程不安全

    1.1 示例1

    package Test;
    
    //观察线程不安全
    public class Test333 {
        public static int count = 0;
        public static void main(String[] args) throws InterruptedException {
            //线程t1对count加10000
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    add();
                }
            });
            //线程t2对count加10000
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    add();
                }
            });
            //启动两个线程
            t1.start();
            t2.start();
            Thread.sleep(1000);
            //输出应该为20000
            System.out.println(count);
        }
        public static void add(){
            count++;
        }
    }
    
    
    • 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

    运行两次结果:
    1
    2

    根据运行结果可以看出,每次运行结果并不相同,但是并没有一个是正确答案,这种情况便是线程不安全。多线程环境下运行代码结果和单线程环境下结果不相同,没有达到我们预期的结果,那么这个线程就是“非线程安全”。

    1.2 示例2

    package Test;
    
    import java.util.Scanner;
    
    public class Counter {
        public int flag = 0;
    
        public static void main(String[] args) {
            Counter counter = new Counter();
            Thread t1 = new Thread(() -> {
                while (counter.flag == 0) {
                // do nothing
                }
                System.out.println("循环结束!");
            });
            Thread t2 = new Thread(() -> {
                Scanner scanner = new Scanner(System.in);
                System.out.println("输入一个整数:");
                counter.flag = scanner.nextInt();
            });
            t1.start();
            t2.start();
        }
    }
    
    
    • 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

    运行结果:
    2

    并不会结束,还在运行。
    这种多线程运行结果,和我们预想的结果也不同,那么这也是非线程安全

    2. 线程不安全的原因

    2.1 修改共享数据

    通过上面的两次例子,我们变可以看出,他们都有一个共同的地方,就是两个线程共享数据。
    那么,当一个线程修改数据途中,另一个线程启动也会修改这个数据,这样就会造成结果与预想不符,造成非线程安全。

    2.2 原子性

    线程原子性指一个操作是不可在分的,不可中断的,在整个操作执行完毕前,不会有其他线程对它干扰。如果一个操作是原子性的,那么就不会发生造成竞态条件出现。
    相当于一个没锁的读书亭,一个人进行读书,因为读书亭没锁其他人可以随意进,从而会干扰到第一个人。而原子性就相当于给读书亭加上锁,第一个人进去时锁上门,那么就不会被打扰。
    而1.1中例子,执行一个count++语句时,它并不是原子性的,分为三步:1. 读取变量count的值到CPU的寄存器中
    2. 进行值加1
    3. 最后将新值写回count中
    N++
    当t1线程读取到数据count = 0,进行++运算中,新的值还没写回count中,t2线程也运行,最后写回count,count = 1,而不是2,造成非线程安全。

    2.3 可见性

    线程可见性指当一个线程修改共享资源时,其他线程能够及时知道最新值,从而不会发生线程安全事故。
    示例2就是因此出现bug。

    2.4 顺序性

    线程顺序性就是代码重排序指代码重排序是指编译器、处理器为了提高程序性能而对程序中的指令进行重新排序的过程。在单线程环境下,重排序不会影响程序最终的执行结果,因为编译器和处理器必须保证单线程程序的语义正确。但是,在多线程环境下,重排序会对程序的并发执行产生影响,如果不加以控制,可能会导致程序出现错误。

    3. synchronized同步方法

    如何解决示例中的问题,是线程达到我们预期的结果,使线程安全,那么synchronized这个关键字便可以起到重要作用。

    3.1 synchronized特性

    3.1.1 互斥

    synchronized具有互斥效果,当一个线程执行synchronized修饰对象时,其他线程在执行这个对象,便会堵塞,只有第一个线程执行结束,其他线程才可以执行这个对象。

    • 进入synchronized修饰的代码块,相当于加锁
    • 出相当于解锁
    3.1.2 刷新内存

    synchronized的工作过程:

    1. 获得互斥锁
    2. 从主内存拷贝变量的最新副本到工作的内存
    3. 执行代码
    4. 将更改后的共享变量的值刷新到主内存
    5. 释放互斥锁
    3.1.3 可重入

    synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。

    // 第一次加锁, 加锁成功
    lock();
    // 第二次加锁, 锁已经被占用, 阻塞等待.
    lock();
    
    • 1
    • 2
    • 3
    • 4

    3.2 synchronized使用

    3.2.1 直接修饰普通方法
    //锁的 SynchronizedDemo 对象
    public class SynchronizedDemo {
      public synchronized void methond() {
     }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    3.2.2 修饰静态方法
    //锁的 SynchronizedDemo 类的对象
    public class SynchronizedDemo {
      public synchronized static void method() {
     }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    3.2.3 修饰代码块
    //明确指定锁哪个对象
    public class SynchronizedDemo {
      public void method() {
        synchronized (this) {
         
       }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    3.3 使示例1安全

    示例1只需对add()方法加锁:

        public synchronized static void add(){
            count++;
        }
    
    • 1
    • 2
    • 3

    那么,再次运行:
    安全1

    4. volatile关键字

    4.1 概念

    volatile只可以修饰变量,而被修饰的变量将具有可见性。 加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了。
    但是volatile不保证原子性,而synchronized也可以保证可见性。

    4.2 使示例2安全

    只需加上volatile:

        public volatile int flag = 0;
    
    • 1

    那么,安全:
    安全2

    5. synchronized与volatile比较

    1. volatile是线程同步的轻量级实现,性能较好,但只可以修饰变量,而synchronized还可以修饰方法,代码块。开发中后者应用交多。
    2. 多线程访问volatile不会堵塞,synchronized会发生堵塞。
    3. volatile只可以保证可见性,不能保证原子性,synchronized都可。
  • 相关阅读:
    VoLTE题库(含解析)-中高级必看
    jaeger-ui项目win系统安装依赖报错问题
    使用van-dialog二次封装微信小程序模态框
    线性回归法学习笔记
    Vue中的mixin(混入)
    redis相关知识点
    05 程序流程控制
    SQL ZOO —— 7 JOIN Quiz
    Java 基础面试300题 (261-290)
    【全网最细致】SpringBoot整合Spring Security + JWT实现用户认证
  • 原文地址:https://blog.csdn.net/weixin_73392477/article/details/132526037