– 阅读《并发编程的艺术》所写
并发的挑战
上下文切换带来的时间消耗
策略:
- 使用无锁并发编程。如数据分段,不同线程处理不同分段
- CAS算法。
- 避免创建不必要的线程。如降低JBOSS的maxThreads
- 使用协程。
死锁
- 避免一个线程获取多个锁
- 尝试使用定时锁
并发机制的底层实现
volatile
volatile是轻量级的synchronized,用于保证共享变量的可见性(即一个线程修改变量时,另一个能够读取到修改后的值)。
底层原则:
- 使用Lock前缀指令来引起处理器缓存回写到内存中。
- 一个处理器的缓存回写到内存中会导致其它处理器的缓存无效。
synchronized
Java中每一个对象都可以作为锁。
- 对于普通同步方法,锁时当前实例对象。
- 对于静态同步方法,锁时当前类的Class对象。
- 对于同步方法块,锁时Synchronized里面配置的对象。
JDK1.6为了减少获取和释放锁的性能消耗
,引入了偏向锁
和轻量级锁
,
所以一共有4种锁状态,由低到高:
- 无锁状态
- 偏向锁状态
- 偏向锁原理:研究发现大多数情况下只有同一线程多次获取锁的情况,为了降低获取锁的代价引入了偏向锁。其原理是,当一个线程访问同步代码块并获取锁时,会在Java对象头和栈帧的锁记录里面存放
当前锁偏向的线程ID
,下次再次访问时,不要进行CAS操作,只需要简单测试对象头是否存储了指向该线程的偏向锁即可。如果成功,表示已经获取了锁。如果失败,看偏向锁的标志位
是否为1(表示当前是偏向锁),如果没有则用CAS竞争锁,如果设置了则尝试使用CAS将对象头的偏向锁指向当前线程。 - 偏向锁的撤销:由于偏向锁不适用于多个线程对锁的竞争,故当其他线程尝试竞争偏向锁时,持有偏向锁的线程会释放锁。
- 关闭偏向锁:JDK1.6以上默认开启偏向锁,如果认为锁通常处于竞争状态可以关闭偏向锁。
- 个人理解:偏向锁用于只有一个线程访问同步代码块的场景。
- 偏向锁原理:研究发现大多数情况下只有同一线程多次获取锁的情况,为了降低获取锁的代价引入了偏向锁。其原理是,当一个线程访问同步代码块并获取锁时,会在Java对象头和栈帧的锁记录里面存放
- 轻量级锁状态。
- 加锁过程:在线程执行同步代码块之前,JVM会先在当前线程的栈帧中创建存储锁记录的空间,然后复制对象头(竞争的锁对象)的Mark Word到其中。然后尝试用CAS将对象头的Mark Word
替换为指向锁记录的指针
,成功则获取锁,失败则表示其它线程在竞争,尝试自旋
获取锁 - 解锁过程:使用CAS将原理复制的记录替换回Mark Word,如果成功表示没有竞争,失败则表示存在竞争,锁会膨胀为重量级锁。
- 个人理解:线程在竞争锁时,先copy一份对象头的记录到栈空间作为备份,然后用CAS取修改对象头记录,使其指向自己,之所以该过程为轻量级锁,原因在于参与竞争的线程不会直接阻塞,而是可以使用自旋来尝试获取锁(缺点是会消耗CPU资源)。适用于同步代码块执行速度很快的场景。
- 加锁过程:在线程执行同步代码块之前,JVM会先在当前线程的栈帧中创建存储锁记录的空间,然后复制对象头(竞争的锁对象)的Mark Word到其中。然后尝试用CAS将对象头的Mark Word
- 重量级锁
- 适用于同步块执行时间较长的场景。
锁可以升级,不能降级,防止无用的自旋(如线程阻塞)
CAS
比较并交换,CAS操作需要两个值,一个旧值和一个新值,在操作期间先比较旧值有没有发生变化,如果没有发生变化才换成新值。
原子操作
从jdk1.5开始,并发包提供了原子操作类:AtomicBoolean、AtomicInteger、AtomicLong。
CAS实现原子操作面临的三大问题:
ABA问题:A变B再变A,CAS认为A没有发生变化,实际上却变化了。解决办法是在变量更新时添加版本号:如JDK1.5添加的AtomicStampedReference类(
reference代表引用
)的compareAndSet方法在检查值引用的同时会检查当前标志
是否等于预期标志1
2
3
4
5
6public boolean compareAndSet{
V exceptedReference, //预期引用
V newReference, //更新后的引用
int exceptedStamp, //预期标志
int newStamp //更新后的标准
}CAS自旋带来的CPU执行消耗。
只能保证一个共享变量的原子操作。解决办法是多个变量合并为一个或者放在一起,如JDK1.5提供的AtomicReference类保证引用对象之间的原子性,即把多个变量放在一个对象中进行CAS操作。