线程安全问题

文章探讨了多线程环境下代码出现线程不安全的原因,包括抢占式执行、非原子操作、内存可见性和指令重排序等问题。为确保线程安全,介绍了synchronized关键字的使用,包括修饰方法、代码块和静态方法,以及可重入锁的概念。同时,提到了Java中线程安全和非线程安全的类,并指出volatile关键字用于保证内存可见性但不保证原子性。最后,讨论了wait和notify在控制线程执行顺序中的作用。
摘要由CSDN通过智能技术生成

有些代码在多线程环境下执行会出现 bug .这样的问题就称为 “线程不安全”

线程不安全的原因

1.抢占式执行

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

3.修改操作不是原子的

   例如:count++   =>    1.读取 2.加加 3.写入 

4.内存可见性问题

   * 编译器可能会把代码转为另外一种执行逻辑,等价转换后逻辑不变,但是效率变高了

5.指令重排序

例如:

class Counter {
    public int count = 0;
    public void increase() {
        count++;
    }
}
public class test6 {

    public static Counter counter = new Counter();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 50000;i++){
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 50000;i++){
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

预期输出 100000,很明显我们的多线程出现了问题

保障线程安全

锁(synchronized)

1.修饰方法(锁对象相当于 this)

对代码进行加锁后结果显示正确

2.修饰代码块

括号内需填写加锁的对象,如果填写 “this”,就是对当前对象加锁 (谁调用的 increase 方法谁就是 this)

任意对象都可以在 synchronized 里面作为锁对象,我们往往更关注两个线程是否锁同一个对象,是否会发生锁竞争

3. 修饰静态方法 (锁对象相当于类对象(不是锁整个类))

无论锁对象是什么形态,什么类型,核心原则都是两个线程争一个锁对象就会有竞争,不同锁对象就没有竞争

可重入锁

 如同上述代码,一个线程针对一把锁连续加锁两次:

第一次加锁,可以加锁成功

第二次加锁,就会加锁失败(锁已经被占用)

然而若是想让第一把锁解开,就必须加上第二把锁,因此锁死。

那这时候就会有个问题,既然构成了 “死锁” 为什么 IDEA 不给我们一点提示呢?

这时候就要引入一种概念 —— 可重入锁

我们可以简单认为:

针对上述情况,不会产生死锁的锁,就可以称为 “可重入锁”

针对上述情况,会产生死锁的锁,称为 “不可重入锁”

 很明显我们并不能在这个位置进行解锁,那我们的编译器又是怎么知道什么时候该干什么的呢?

这时候我们就要谈到 可重入锁的实现要点:

1. 让锁离持有线程对象,记录是谁加了锁

2. 维护一个计数器,用来平衡什么时候是真加锁,什么时候是真解锁,什么时候是直接放行

Java 标准库中的线程安全类

java 标准库中很多都是线程不安全的,这些类可能会涉及到多线程修改共享数据,又没有任何加锁的措施

   *  ArrayList

   *  LinkedList

   *  HashMap

   *  TreeMap

   *  HashSet

   *  TreeSet

   *  StringBulider

但是还是一些是线程安全,使用了一些锁机制来控制

   *  Vector(不推荐使用)

   *  HashTable(不推荐使用)

   *  ConcurrentHashMap

   *  StringBuffer

还有的虽然没有加锁,但是不涉及 “修改”,仍然是线程安全的

    *  String

Volatile

 

 观察上述代码和运行结果可以发现,当我们对 counter.count 进行修改之后 t1 线程的循环并没有结束,这就是内存可见性引来的线程安全问题

由于比较这个操作需要在内存中读取变量的值来进行比较,但是这个操作的开销又很大,并且 while 循环执行速度很快,因此编译器发现在 while 循环中没有人改 count 的内存(也会有编译器不优化的情况,在这里不进行过多分析),就读代码进行了 “优化” ,不再每次循环都读取内存,而是只读取一次,后续的读内存的操作则简化成直接读寄存器,所以没有发现在另一个线程中已经对 count 的内存进行了修改

那么优化方案便很明显了,就是不让编译器对我们的代码进行优化

做法非常简单,就是在我们的变量前面加上 volatile 关键字

 

 可以看见代码正确了!!!

注意:volatile 起到的效果是 “保证内存可见性”,但并不保证原子性,也就是说:针对一个线程读,一个线程改,使用 volatile 这个是合适的;而针对两个线程修改,volatile 是无能为力的

站在 JMM (Java 内存模型) 的角度来看待 volatile:

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

wait 和 notify

public class test9 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();

        // 第一个线程,进行 wait 操作
        Thread t1 = new Thread(() -> {
            while (true) {
                synchronized (object) {
                    System.out.println("wait 之前");
                    try {
                        object.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("wait 之后");
                }
            }
        });
        t1.start();

        // 第二个线程,进行 notify
        Thread t2 = new Thread(() -> {
            while (true) {
                synchronized (object) {
                    System.out.println("notify 之前");
                    object.notify();
                    System.out.println("notify 之后");
                }
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t2.start();


    }
}

  

wait / notify 控制多线程之间的执行先后顺序

1. 都要搭配 synchronized 来进行使用

2. wait 和 notify 得使用同一个对象才有效

3. 用来加锁的对象和 wait / notify 对象也得一致

4. 即使当前没有线程在 wait,直接 notify 也不会有副作用

wait 和 sleep 的对比

都是让线程进入阻塞等待的状态

sleep 是通过时间来控制何时唤醒的

wait 则是由其他线程通过 notify 来唤醒的

wait 其实还有一个重载版本,参数可以传时间,表示等待的最大时间

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值