多线程(四) 线程不安全问题的原因及解决方法

2年前 (2022) 程序员胖胖胖虎阿
305 0 0

线程安全问题

  • 案例说明
  • 产生线程不安全的原因
  • 线程不安全的解决方法
    • 1.Synchronized关键字
      • synchronized 主要有三个特性
        • 1. 互斥
        • 2. 刷新内存,保持内存可见性
        • 3. 可重入
    • 2.volatile 关键字
    • 标准库(集合类)的线程安全的类

案例说明

class Counter {
    public int count = 0;

    public void increase() {
        count++;
    }
}
public class Java3_9_4 {
    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();
                }
            }
        };

        Thread t2 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();
                }
            }
        };
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter.count);
    }
}

这段代码理论上能自增10w次,但是最终结果确实不确定
由于多线程并发执行,导致了代码中出现了BUG,这种情况称为"线程不安全"

执行的过程 :
多线程(四) 线程不安全问题的原因及解决方法
多线程(四) 线程不安全问题的原因及解决方法

产生线程不安全的原因

  1. 线程之间是抢占式执行的(根本原因,线程不安全的万恶之源)

    抢占式执行,导致两个线程里面的操作先后顺序无法确定 这种随机性是导致线程不安全的根本原因 (无力改变,操作系统的内核实现)

  2. 多个线程修改同一个变量

  3. 原子性
    像 ++ 这样的操作,本质上是三个步骤(LOAD,ADD,SAVE),是一个"非原子" 的操作,像 = 操作,本质上就是一个步骤,认为是一个"原子" 操作 (可通过加锁方式解决,变成原子的)

  4. 内存可见性(与编译器优化有关)
    一个内存修改,一个内存读取
    由于编译器的优化,可能把中间环节的 SAVE 和 LOAD 操作去掉了
    此时读取的线程可能是未修改的结果
    多线程(四) 线程不安全问题的原因及解决方法
    (可以用volatile 解决)

  5. 指令重排序(也与编译器优化有关)

    编译器会自动调整执行指令的顺序,以达到提高执行效率的效果,前提是需要保证最终效果不变,但是在多线程下,会影响结果

线程不安全的解决方法

1.Synchronized关键字

最朴实的方法,从原子性入手 加锁 !!!
synchronized 关键字 一定要会拼,会写

synchronized public void increase() {
    count++;
}

synchronized 主要有三个特性

1. 互斥

英文原意为 同步 存在歧义 理解成互斥更合适 如果两个线程同时并发的尝试调用这个synchronized 修饰方法 此时一个线程会先执行这个方法,另一个线程会等待,等到第一个线程执行完之后,第二个线程才会继续执行.

这就相当于 "加锁" 和 "解锁" 
进入 synchronized 修饰的方法,就相当于加锁 
处理 synchronized 修饰的方法,就相当于解锁
如果当前是已经加锁了的状态,其他线程就无法执行这里的逻辑,就只能阻塞等待

synchronized 还可以修饰代码块
修饰代码块的时候, ( ) 中要你指定一个加锁的对象,如果修饰的是非静态方法,相当于加锁的对象是this

public void increase() {
     synchronized (this) {
         count++;
     }
}

多线程(四) 线程不安全问题的原因及解决方法

2. 刷新内存,保持内存可见性

synchronized 不光能起互斥的效果,还能够刷新内存 (解决内存可见性问题)
多线程(四) 线程不安全问题的原因及解决方法

会让程序跑的慢,但是算的准,用了之后可能就与"高性能" 无关了

3. 可重入

同一个线程连续针对同一个同一个锁进行加锁,不会死锁
synchronized 允许可重入
synchronized 允许一个线程针对一把锁,连续锁两次
多线程(四) 线程不安全问题的原因及解决方法

因为synchronized 内部记录了当前这个锁是哪个线程持有的

synchronized 修饰普通方法的话,相当是针对 this 进行加锁
如果两个线程并发的调用了这个方法,此时是否会触发锁竞争,就看实际的锁对象是否是同一个 synchronized

修饰的是静态方法的话,相当于针对 类对象 进行加锁
由于类对象是单例,两个线程并发调用该方法,一定会触发锁竞争

2.volatile 关键字

(可变的,容易改变的)
功能是保证内存可见性,但是不能保证原子性

volatile 的用法比较单一,只能修饰一个具体属性 ,此时代码中针对这个属性的读写操作就一定是内存操作了

public class java3_9_5 {
    // 一旦给这个 flag 加上 volatile 后,此时后序针对 flag 的读写操作,都能保证一定是内存操作了
    public static volatile int flag = 0;
    public static void main(String[] args) {

        Thread t1 = new Thread() {
            @Override
            public void run() {
                while (flag == 0) {

                }
                System.out.println("线程结束了");
            }
        };

        Thread t2 = new Thread() {
            @Override
            public void run() {
                Scanner scanner = new Scanner(System.in);
                System.out.println("请输入");
                flag = scanner.nextInt();
            }
        };
        t1.start();
        t2.start();
    }
}

但是 volatile 不能保证原子性
volatile 是和优化密切相关的 东西

一般来说一个某个变量,在一个线程中读,一个线程中写,此时大概率要用 volatile

volatile 涉及重要知识点 JMM 内存模型
多线程(四) 线程不安全问题的原因及解决方法

标准库(集合类)的线程安全的类

集合,大部分是线程不安全的,ArrayList,LinkedList,…都是线程不安全的

线程安全的 :
Vector (不建议使用)也是一个顺序表,能自动扩容什么的,使用了很多 synchronized 来保证线程安全,但是给了很多方法都加上了 synchronized 修饰,在大多数情况下并不需要在多线程下使用 Vector,而我们加太多的 synchronized就会对单线程环境下的操作效率造成负面影响

Stack 继承自 Vector ,所以 Stack 是线程安全的

HashTable (同理不建议使用)

ConcurrentHashMap

StringBuffer(核心方法都带有synchronized)

有的虽然没有加锁,但是不涉及"修改",仍然是线程安全的 : String
String 是不可变对象,不可能存在两个线程并发的修改同一个 String

版权声明:程序员胖胖胖虎阿 发表于 2022年10月20日 下午12:00。
转载请注明:多线程(四) 线程不安全问题的原因及解决方法 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...