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

线程安全问题

  • 案例说明
  • 产生线程不安全的原因
  • 线程不安全的解决方法
    • 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张图片

产生线程不安全的原因

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

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

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

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

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

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

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

线程不安全的解决方法

1.Synchronized关键字

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

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

synchronized 主要有三个特性

1. 互斥

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

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

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

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

多线程(四) 线程不安全问题的原因及解决方法_第4张图片

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

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

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

3. 可重入

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

因为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 内存模型
多线程(四) 线程不安全问题的原因及解决方法_第7张图片

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

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

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

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

HashTable (同理不建议使用)

ConcurrentHashMap

StringBuffer(核心方法都带有synchronized)

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

你可能感兴趣的:(JavaEE,开发语言,后端,java-ee)