共享模型之管程
1.共享带来的问题
1.临界区
- 一个程序运行多个线程是没有什么问题,问题就是在于多个线程同时读写同一个资源的时候会发生指令交错,就会出现问题
- 一段代码内如果存在对共享资源的多线程读写操作,这段代码就称之为临界区
2.竞态条件
- 多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
2.synchronized 解决线程共享问题
1.应用之互斥
- 为了避免临界区发生的竞态条件发生,有多种方法可以避免
- 阻塞式的解决方案:synchronized、lock
- 非阻塞式的解决方案:原子变量
- 通过synchronized可以避免竞态条件的发生,俗称【对象锁】,采用的互斥方式就是在同一时刻至多只有一个线程能够持有【对象锁】,其他线程想要获取【对象锁】就会进入等待队列。这样就能保证拥有锁的线程可以安全执行临界区的代码,不用担心线程上下文切换。
- 需要注意到的是,即使获取到【对象锁】的线程时间片用完,但是临界区的代码还没完全执行完,仍然是不会释放锁的,这个时候其他线程即使获取到时间片也还是会被堵塞在门外。
2.变量的线程安全分析
1.成员变量和静态变量是否线程安全
- 如果它们没有被共享,那么是线程安全
- 如果它们被共享了,那么分两种情况来判断是否线程安全:
- 如果只有读操作,那么不存在线程安全问题
- 如果有读写操作,那么这段代码是临界区,需要考虑线程安全问题
2.局部变量是否线程安全
- 局部变量是线程安全的
- 但局部变量引用的对象未必是线程安全的:
- 如果该对象没有逃离方法的作用范围,那么是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全问题
3.常见的线程安全类
- String
- Integer
- Vecotr
- HashTable
- Random
- StringBuffer
- java.util.concurrent包下的类
2.Monitor概念
1.java对象头
2.Monitor原理
Monitor被翻译成监视器或管程 每个java对象可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被指向Monitor对象的指针 Monitor结构如下
- 刚开始的时候Owner为null,当Thread-2执行synchorized的时候就会把Monitor的Owner为Thread-2,Monitor中只能有一个Owner
- 在Thread-2上锁的过程中,如果Thread-3、Thread-4、Thread-5也来执行synchorized的话,就会进入EntryList Blocked
- Thread-2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争的时候是非公平的
- 图中WaitSet中的Thread-0、Thread-1是之间获得过锁,但条件不满足进入WAITNG状态的线程
3.synchroinzed原理
1.轻量级锁
- 轻量级锁的使用场景:如果一个对象虽然有很多线程要加锁,但是加锁的时间是错开的(也就是没有竞争),那么就可以使用轻量级锁进行优化。轻量级锁对使用者来说是透明的,仍然使用synchronized
- 创建锁记录(Lock Record)对象,每个线程的栈帧都包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
- 让锁记录中Object reference指向锁对象,并尝试用cas替换Object的Mark Word,将Mark Word的值存入锁记录
- 如果cas替换成功,对象头中存储了
锁记录地址和状态 00,表示由该线程给对象加锁 - 如果cas失败,有两种情况
- 如果是其他线程已经有了Object的轻量级锁,进入锁膨胀过程
- 如果是自己执行了锁重入,那么再添加一条Lock Record作为重入的计数
- 当退出synchronized代码块(解锁时)如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
- 当退出synchronized代码块(解锁时)所记录不为null,这时使用cas将mark word的值恢复给头像头
- 成功:解锁成功
- 失败:说明轻量级锁进行了锁膨胀或已经升级为了重量级锁,进入了重量级锁解锁流程
2.锁膨胀
- 如果在尝试加轻量级锁的过程中,CAS操作无法成功,有一种情况就是其他线程已经为此对象加上了轻量级锁(有竞争),这时需要膨胀,将轻量级锁变为重量级锁
static Object obj = new Object();
public static void main(String[] args){
synchronized(obj){
// 执行方法
}
}
- 当Thread-1进行轻量级锁加锁时,发现已经有线程对该对象加了轻量级锁,就会进入锁膨胀
- 这时Thread-1加轻量级锁失败,进入锁膨胀过程:
- 即为Object对象申请Monitor锁,让Object指向重量级锁地址
- 然后自己进入EntryList BLOCKED中
- 当Thread-0退出同步块解锁时,使用cas将mark word的值恢复给对象头,失败。这时会进入重量级锁解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList BLOCKED线程
3.自旋优化
- 重量级锁竞争的时候,还可以使用自旋来优化,如果当前线程自旋成功(即这时候持锁的线程退出了同步块,释放了锁),这时当前线程就可以避免堵塞
- 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU才能发挥自旋的优势
- 在java6中之后自旋锁是自适应的,比如对象刚刚的一次自旋锁成功了,那么认为它接下来也会成功,就会多自旋几次,反之,就少自旋甚至不自旋,比较智能
- java7之后不能控制是否开启自旋功能
4.偏向锁
- 轻量级锁在没有竞争的时候(就自己这个线程),每次重入仍然需要CAS操作
- java6中引入了偏向锁来进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,就不再在需要重新cas。以后只要不发生竞争,这个对象就归线程所有。
- 一个对象创建时,如果开启了偏向锁(默认是开启的),那么对象创建后,mark word值为0×05,即最后三位为101,这时它的thread、epoch、age都为0
- 偏向锁默认是延迟的,不会在程序运行后立即生效,如果想避免延迟可以添加JVM参数
- XX:BiasedLockingStartupDelay=0来禁止延迟 - 如果没有开启偏向锁,那么对象创建后,mark word最后三位为0×01即组后三位为001,这时它的hashcode和age都为0,第一次用到hashcode才会赋值
- 撤销-调用对象hashcode
- 调用对象的hashcode,因为偏向锁Mark word中存储的Thread的id,如果调用hashcode,会导致偏向锁被撤销
- 轻量级锁会在锁记录中记录hashcode
- 重量级锁会在monitor记录hashcode
- 撤销-其他线程使用对象
- 撤销-调用wait-notify
- 批量-重偏移
- 如果一个对象被多个线程访问,但是没有出现竞争,这时偏向线程T1的对象仍然有机会偏向T2,重偏移会重置对象的Thread-ID
- 当撤销片偏向锁阈值超过了20次之后,jvm就会觉得,是不是偏向错了,就会重新偏向至加锁的线程
- 批量-撤销
- 当撤销偏向阈值超过了40次后,jvm会觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都变为不可偏向的,新建的对象也是不可偏向的
- 锁消除
- 对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化的方式来优
- wait、notify原理
- Owner线程发现自己条件不满足,调用wait方法,即可进入WaitSet,变为WAITING状态
- BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
- BLOCKED线程会在Owner释放锁时被唤醒
- WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不以为着立刻获取到锁,仍需进入到EntrtyList重新竞争