并发编程(二)
临界区·
一个代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
竞态条件
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
Synchronized对象锁
语法
Synchronized(对象) {
临界区
}
锁在对象方法上锁的是this对象
锁在static方法上锁的是class对象
变量的线程安全分析
成员变量与静态变量是否线程安全?
- 如果它们没有被共享,则安全
- 如果它们被共享了,根据他们的状态能否改变,分为两种情况
○1如果只有读操作,安全
○2如果有读写操作,则这段代码是临界区,要考虑线程安全
局部变量是否线程安全?
- 局部变量是线程安全的
- 局部变量引用的对象未必
○1如果该对象没有逃离方法的作用访问,则安全
○2如果对象逃离方法的作用访问,需要考虑线程安全
常见线程安全类
String、Integer、StringBuffer、Random、Vector、Hashtable、 java.util.concurrent 包下的类
Moniter
1.当线程运行临界区中加锁的代码时,锁的对象会先与Moniter关联
2.刚开始Moniter中Owner为null
3.当Thread-2执行synchronized(obj)就会将Moniter的所有者Owner设为Thread-2.Moniter中只能有一个Owner
4.在Thread-2上锁过程中,如果Thread-3、Thread-4、Thread-5也来执行synchronized(obj),就会进入entrylist BLOCKED
5.Thread执行完同步代码块的内容,唤醒EntryList中等待的线程来竞争锁,竞争非公平
轻量级锁
如果一个对象虽然有多线程访问,但多线程访问时间是错开的,那么可以用轻量级锁来优化
Synchronized优先进行轻量级锁加锁,加锁失败则会用重量级锁
创建锁记录对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的MarkWord
让锁记录中Object referernce指向对象,并尝试替换Object的MarkWord,将MarkWord的值储存入锁记录
如果替换成功,对象头中存储了锁记录地址和状态,表示由该线程给对象加锁
如果失败,有两种情况
- 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
- 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
当退出synchronized 代码块(解锁时)锁记录的值不为null,这时使用 cas 将 Mark Word 的值恢复给对象头
成功,则解锁成功
失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
锁膨胀
如果在尝试加轻量级锁过程中,操作无法成功,这时一种情况是其它线程为此对象加上了轻量级锁,这时要进行锁膨胀,将轻量级锁变为重量级锁
加锁失败,进入锁膨胀流程
为Object对象申请Moniter锁,让Object指向重量级锁,未能获得锁的线程进入Moniter的EntryList BLOCKED
自旋优化
重量级锁竞争失败,先进行自旋优化而不是直接进入BLOCKED,阻塞要进行上下文切换,耗费性能,自旋能减少上下文切换
偏向优化
轻量级锁在没有竞争时,每次重入仍要进行CAS操作
偏向锁第一次使用CAS时就将线程ID设置到对象头的MaekWord中,之后发现这个线程ID是自己的,就不会重新CAS,以后只要不发生竞争,这个对象就归该线程所有
对象头中偏向锁后三位为101
程序运行时偏向锁默认开启
调用hasjcode()会禁用掉偏向锁,因为hashcode会占31位,对象头中会替换掉偏向状态
批量重偏向
当将thread1的偏向锁重偏向为thread2次数到达阈值后(默认20),会进行批量重偏向
批量撤销
当撤销偏向锁阈值超过40次后,jvm会觉得偏向错了,不该偏向,于是整个类的所有对象都变为不可偏向,新建的对象也不可偏向
锁消除
//控制是否加锁消除优化
-XX:-EliminateLocks
wait/notify 原理
- owner线程发现条件不满足,调用wait方法,即可进入waitset变为waiting状态
- BLOCKED和WATING的线程都会处于阻塞状态,不占用CPU时间片
- BLOCKED线程会在owner线程释放锁时被唤醒
- WATING线程在owner线程调用notify或notifyall时唤醒,但唤醒后并不立即获得锁,仍需进入EntryList重新竞争
wait会使线程进入waitset中等待
必须获得了锁才能进入所属的waitset
waitset中可以有多个线程等待)
sleep(long n)与wait(long n)区别
- Wait是对象方法,sleep是线程方法
- Wait需要和synchronized配合,sleep不需要和synchronized配合
- Wait时释放对象锁,sleep不会释放对象锁
park/unpark
unpark既可以在park之前调用,也可以在park之后调用,都可以恢复线程运行
park/unpark与wait/notify区别
- Wait,notify/notifyall必须配合object moniter一起使用,而park,unpark不用
- Park\unpark 是以线程为单位来阻塞和唤醒线程,而notify只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,没有那么精确
- Park/unpark可以先unpark,而wair/notify不能先notify
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁
- 线程进入 _cond 条件变量阻塞
- 设置 _counter = 0
- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
- 唤醒 _cond 条件变量中的 Thread_0
- Thread_0 恢复运行
- 设置 _counter 为 0
- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
- 设置 _counter 为 0
死锁
一个线程需要同时获取多把锁,这时就容易发生死锁
t1 线程获得A对象锁,接下来想获取B对象的锁
t2 线程获得B对象锁,接下来想获取A对象的锁
活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束
可增加随机睡眠时间,避免活锁的产生
饥饿
一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束
ReentrantLock
相对于synchronized它具备以下特点
- 可中断
- 可以设置超时时间
- 可以设置为公平锁,默认为不公平(公平锁,按进入阻塞队列的顺序,先进入,先获得锁)
- 支持多个条件变量
基本语法
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
可打断
竞争未能获得到锁,进入EntryList阻塞状态可以用interrupt打断,打断后会抛出异常
lock.lockInterruptibly(); //设置可打断
锁超时
lock.trylock //返回值为boolean类型
例:lock.trylock(1,Timeunit.SECONDS)
条件变量
synchronized中也有条件变量,就是waitset,当条件不满足时进入waitset
ReentrantLock支持多个条件变量(Condition),好比synchronized只有一件休息室,而ReentrantLock支持多间休息室,唤醒也是按休息室来唤醒
用法