Java并发编程(六)- 线程安全问题

目录

线程安全

可见性问题

Happens-Before规则

final修饰符

Synchronized

有序性问题

原子性问题

保护多个资源

细粒度锁的死锁问题

活跃性问题

常见线程安全问题

      数据竞争

      竞态条件


线程安全

        线程安全是指:运行结果是确定的。

       导致不确定的主要源头是:可见性问题,有序性问题,原子性问题。

可见性问题

        可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。

        可见性问题由CPU缓存导致;

        CPU缓存模型:

        解决方案:根据程序逻辑,按需禁用缓存。

        具体方法:

        volatile、synchronized 和 final 三个关键字,以及六项 Happens-Before 规则。

        volatile:告诉编译器,对这个变量的读写,不能使用CPU缓存,必须从内存中读取或者写入。

      Happens-Before规则

        Happens-Before:前面一个操作的结果对后续操作是可见的。

        Happens-Before约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定

遵守 Happens-Before 规则。

        1. 程序的顺序性规则

        按照程序顺序,前面的操作 Happens-Before 于后续的任意操作。

        2. volatile 变量规则

        这条规则是指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的

读操作。

        3. 传递性

        A Happens-Before B,且B Happens-Before C,那么A Happens-Before C。

        2和3可以组合使用;

        4. 管程中锁的规则

        指对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。

        synchronized 是 Java 里对管程的实现。

        管程中的锁在 Java 里是隐式实现的,在进入同步块之前,会自动加锁,而在代码块执行完会

自动释放锁,加锁以及释放锁都是编译器帮我们实现的。

        5. 线程 start() 规则

        主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。

        6. 线程 join() 规则

        主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B

完成后(主线程 A 中 join() 方法返回),主线程能够看到子线程的操作。当然所谓的“看到”,指的

是对共享变量的操作。

      final修饰符

        final修饰变量,表示变量不会发生变化,编译器可以充分优化;

        不发生变化的变量,不存在写操作,所以不存在不可见性;

     Synchronized

        对公用资源加锁,加上Happens-Before约束也可以解决可见性问题;

有序性问题

        有序性问题由编译优化导致;

        解决方案:根据程序逻辑,按需禁用编译优化。

        主要靠Happens-Before约束完成。

原子性问题

        原子性:一个或者多个操作在 CPU 执行的过程中不被中断;

        原子性问题由线程切换导致;

        解决方案:解决原子性问题,是要保证中间状态对外不可见。通过互斥锁,让一块代码区域

只有一个线程能够执行。

        具体方法:synchronized,并发包中的其他锁;

保护多个资源

        如果资源之间没有关系,每个资源一把锁就可以了。

        如果资源之间有关联关系,就要选择一个粒度更大的锁,这个锁应该能够覆盖所有相关的资源。

        还要梳理出有哪些访问路径,所有的访问路径都要设置合适的锁。

细粒度锁的死锁问题

        使用细粒度锁可以提高并行度,是性能优化的一个重要手段,但是会导致死锁。

        死锁:多个线程因竞争资源互相等待,导致永久阻塞的现象。

        

        下列四个条件都发生时才会出现死锁:

        互斥,共享资源 X 和 Y 只能被一个线程占用;

        占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;

        不可抢占,其他线程不能强行抢占线程 T1 占有的资源;

        循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。

        破坏其中一个,就可以成功避免死锁的发生。

        解决方案

        互斥这个条件我们没有办法破坏,因为用锁为的就是互斥。

        1,占用且等待:一次性申请所有的资源,就不存在相互等待了;需要设置一个管理类来统一

获,取或者释放资源。

        管理类统一获取多个资源时,在获取部分资源后,需要等待剩余资源,在等待的过程中:可

以通过循环来尝试获取剩余资源;

        简单场景下,使用循环等待尝试获取资源即可;

        复杂场景下,或者并发冲突量大的时候,循环等待这种方案就不适用了,可能要循环上万次

才能获取到锁,太消耗 CPU 了。

        优化方案:如果线程要求的条件不满足,则线程阻塞自己,进入等待状态;当线程要求的条

件满足后,通知等待的线程重新执行。

        

        2,不可抢占:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它

占有的资源,这样不可抢占这个条件就破坏掉了。synchronized不支持,并发包中的Lock支持上述

方案。

        3,循环等待:按序申请资源来预防;

        按序申请,资源在全局是有线性顺序的,申请的时候所有线程都按统一的顺序申请,这样线

性化后就不存在循环了。

        

        评估操作成本,从中选择合适的方案。

活跃性问题

        影响线程运行的除了死锁,还有活锁,饥饿。

        死锁:一直等待下去;解决方案:打破死锁的条件。

        活锁:一直没完没了地“谦让”下去;解决方案:随机等待。

        饥饿:在CPU繁忙的情况下,优先级低的线程得到执行的机会很小,就可能发生线程“饥饿”;

持有锁的线程,如果执行的时间过长,也可能导致“饥饿”问题;

        解决方案:

        1,保证资源充足;

        2,公平地分配资源,使用公平锁(主要方案);

        3,是避免持有锁的线程长时间执行;

        公平锁:一种先来后到的方案,线程的等待是有顺序的,排在等待队列前面的线程会优先获

得资源。

常见线程安全问题

      数据竞争

        当多个线程同时访问同一数据,并且至少有一个线程会写这个数据的时候,如果我们不采取

防护措施,那么就会导致并发 Bug。 

        

      竞态条件

        对共享变量的单一读写操作,是线程安全的,但是组合操作,会出现结果不确定的问题;

        例如:

        变量的set()方法和get()方法,都是线程安全的;

        但是set(get()+1)组合操作,不是线程安全的,一个线程执行set(get()+1)时,另一线程可能在

执行set方法,导致这个get方法获取的值不确定;

        解决方法:get和set方法用同一把锁。

        

        数据安全问题的解决方案

        资源分配(互斥)篇章中提到的方法:

                1,管程中的互斥锁;

                2,无锁方案(不变模式,写时复制模式,线程本地变量模式,CAS和原子类);

        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值