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

案例说明

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

w_xhjk

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值