【并发编程技术】「技术辩证分析」在并发编程模式下进行线程安全以及活跃性问题简析
什么是线程安全?
线程安全,有两个重要的特征说明:“共享”和“可变”。
共享是指可以被多个线程同时访问;
可变是指变量的值在生命周期内是可以变化的;
如何实现线程安全
一个对象是否需要线程安全的,取决于它是否被多个线程访问;
如何保证一个对象的线程安全,则需要采用同步机制来协同对对象可变状态的访问;
给线程安全下一个明确的定义:当多个线程访问这个对象或者资源时,如果这个对象或资源始终都能表现出数据的一致性的状态,那么就称这个对象或者资源是线程安全的;
数据资源的有无状态化
无状态的对象一定是线程安全的。
有状态的对象,多线程环境下,多个线程共享资源,且进行的不是原子性操作,这个时候就要考虑线程的安全控制问题
比如:count++,其实是不具备原子性的,因为这个步骤实际会被拆分为三个步骤,即 读取、修改和写入,而这三个步骤有可能在某个时刻因 CPU 时间片的切换问题,而只执行其中一两个步骤,这就不具备原子性。
原子化能力支持
在 Java 中,为了解决这个问题,java.util.concurrent.atomic 包提供了很多的类,来保证数据操作的原子性,比如我们之前的程序可以修改为
基本数据类型 AtomicInteger
数组类型 AtomicIntegerArray
内部的原理是采用了 CAS 机制
那么什么是 CAS 机制?
CAS 有人翻译为 Compare And Set 或 Compare And Swap 都是正确的。
多线程并发执行的状态下,锁的状态改变,基本都是使用 CAS 原理,它有一个比较别扭的叫法“CPU 硬件同步原语”,算法是基于 CPU 硬件的,原子性操作,不会被其他线程打断。
CAS 的算法,比较当前值和期望的值是否相等,如果相等,则将当前值赋予一个新值。
再比如修改一个 Boolean 的类型的变量的值,我们也可以采用
同步锁机制支持
只要程序中存在“先判断,再更新”,那么就要保证这两个操作在一个原子操作里面,才能保证线程安全。
Java 锁机制的一些特点
监视锁、互斥锁、可重入锁都是在这个锁的特点。
监视锁:java 的每一个对象都可以用来做监视锁,也就是为什么我们的 wait、notify 方法定义在 Object 类的原因。
互斥锁:表示最多只有一个线程可以持有这把锁。
可重入锁:是指当线程 A 请求一个由线程 B 持有的锁时,线程 B 会进入阻塞状态;而如果线程 A 如果再访问另一段代码,而这个代码的锁是已经被线程 A 持有的,这个时候请求是可以成功的,这就叫可重入。
Java 锁机制的简单原理
JVM 为每个锁设置两个属性,获取计数值和所有者线程,当计数值为 0 时,这个锁就被认为是没有被任何线程持有,当线程请求一个未被持有的锁时,JVM 将记录锁的持有者,并且计数值+1。
如果同一个线程再次获取这个锁,则计数值将递增,而当线程退出同步代码块时,计数器会相应递减,当计数值为 0,这个锁将被释放。
活跃性问题
承接上面解决安全性的问题分析,锁机制会存在活跃性问题,比如:死锁,饥饿,活锁,这些都是属于活跃性问题。
死锁
多个线程,各自占对方的资源,都不愿意释放,从而造成死锁,A 线程需要等待的锁被 B 线程占用,而 B 线程需要的等待的锁被 A 线程占用,所以相互都不释放,于是就陷入了死锁。
饥饿
多个线程访问同一个同步资源,有些线程总是没有机会得到互斥锁,这种就叫做饥饿。
出现饥饿的三种情况
高优先级的线程吞噬了低优先级的线程的 CPU 时间片
理论上来说,线程优先级高的线程会比线程优先级低的线程获得更多的执行机会,但是 java 的线程优先级不是绝对出现这样的效果。
一般而言:优先级高的出现频率会比优先级低的高很多
不同的操作系统对线程的优先级支持是不同的,规定是在 1-10 之间,java 通过 3 个常量来屏蔽这种操作系统的底层差异化。
线程被永久阻塞在等待进入同步代码块的状态
等待的线程永远不被唤醒
建议大家采用公平锁来代替 synchronized 这种互斥锁
活锁
两个人在走廊上碰见,大家都互相很有礼貌,互相礼让,A 从左到右,B 也从从左转向右,发现又挡住了地方,继续转换方向,但又碰到了,反反复复,一直没有机会运行下去。
版权声明: 本文为 InfoQ 作者【浩宇天尚】的原创文章。
原文链接:【http://xie.infoq.cn/article/67dd0a76da10146baa25c3a02】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论