syn-ed
是一个可重入、不公平的重量级锁;synchronized使用对象锁保证了临界区代码的原子性,无论使用synchorized锁的是代码块还是方法,其本质都是锁住一个对象。
- 同步代码块,锁住的是括号里的对象
- 同步方法
- 普通方法,锁住的是当前实例对象,即this
- 静态方法,锁住的是当前类对象,即class对象
// 同步代码块
synchorized(锁对象) {
}
// 普通方法
class Test{
public synchorized test() {}
}
// 等价于
class Test{
public void test() {
synchronized(this) {}
}
}
// 静态方法
class Test{
public synchronized static void test() {}
}
// 等价于
class Test{
public void test() {
synchronized(Test.class) {}
}
}
变量线程安全性
- 成员变量和静态变量
- 如果没有被共享,那么一定是线程安全的
- 如果被共享:
- 只有读操作,一定是线程安全的
- 有读写操作,存在并发问题
- 局部变量
- 局部遍历是线程安全的
- 但局部变量引用的对象则不一定
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,存在并发问题
常见的线程安全类
- String
- Integer等包装类
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的类
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的,多个线程调用它们同一个实例的某个方法时,是线程安全的。
锁原理
Monitor
监视器或管程,每一个java对象都可以关联一个Monitor对象,使用synchronized给一个对象加锁(重量级锁),该对象的对象中的Mark Word就被设置指向一个Monitor对象的指针,这其实也就是重量级锁的加锁过程。
Mark Word
java对象的对象头由Mark Word、类型指针、数组长度(如果该对象是一个数组)组成。Mark Word的长度由32bit/64bit。Mark Word里默认存储对象的HashCode、分代年龄和锁标记位
32位的Mark Word:
64位的Mark Word:
Monitor的工作流程
一个对象对应一个Monitor对象。
- 开始时Monitor中Owner为null
- 当 Thread-2 执行 synchronized(obj) 就会将 Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner,obj对象的Mark Word指向Monitor,把对象原有的Mark Word存入线程栈中的锁记录
- 在 Thread-2 上锁的过程,Thread-3、Thread-4、Thread-5 也执行 synchronized(obj),就会进入 EntryList BLOCKED(双向链表)
- Thread-2 执行完同步代码块的内容,根据 obj 对象头中 Monitor 地址寻找,设置 Owner 为空,把线程栈的锁记录中的对象头的值设置回 MarkWord
- 唤醒 EntryList 中等待的线程来竞争锁,竞争是非公平的,如果这时有新的线程想要获取锁,可能直接就抢占到了,阻塞队列的线程就会继续阻塞
- WaitSet 中的 Thread-0,是以前获得过锁,但条件不满足进入 WAITING 状态的线程(wait-notify 机制)
锁升级
随着竞争的增加,只能锁升级,不能降级
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
偏向锁
在大多数情况下,锁总是由一个线程多次获得,让线程获得锁的代价更低而引入了偏向锁。就和名字一样,是偏向的:
- 当线程第一次获得锁对象时,其进入偏向状态,该对象的后三位是101,同时使用CAS操作将线程ID记录到Mark Word。如果CAS操作成功,这个线程以后进入这个锁相关的同步块,查看这个线程ID是自己的就表示没有竞争,就不需要再进行任何同步操作。
- 当另一个线程也尝试获取这个锁对象时,也会使用CAS进行替换,此时一定失败,偏向状态就会结束,撤销偏向后恢复到未锁定或轻量级锁状态。
在java中是默认开启偏向锁的,也就是说在一个对象创建的时候,其Mark Word的后三位是101,其余值均为0;当调用这个对象的hashCode时,就再也无法进入偏向状态了,即后三位是001。这是因为Mark Word会被hashCode占用。
偏向锁的撤销:
- 第一点就是我们前边提过的,调用该对象的hashcode方法。
- 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
- 调用 wait/notify,需要申请 Monitor,进入 WaitSet
轻量级锁
一个对象有多个线程要加锁,但加锁的时间是错开的(没有竞争),轻量级锁的使用对我们程序员来说,也是使用syn-ed来完成,只是底层的实现对我们透明的。
加锁过程:
当某一个线程对一个对象进行加锁时,会在栈帧中创建一个锁记录(Lock Record),并使用==CAS将对象的Mark Word中的信息保存到该锁记录中,而Mark Word中就记录该锁记录的地址,以及锁标志位(00)。==这样就完成了加锁。但是当CAS失败的时候,此时会有两种情况导致失败:
- 它线程已经持有了该Object的轻量级锁,我又想去获取,这时表明有竞争,进入锁膨胀过程。在膨胀之前还有一个自旋的过程。
- 线程自己执行synchronized锁重入,栈帧中还会存在一条Lock Record作为重入的计数,但是每次重入都会有CAS,所以才引入了偏向锁进行优化。
解锁过程(当退出synchronized代码块):
- 如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减 1
- 如果锁记录的值不为null,这时使用CAS将 Mark Word的值恢复给对象头
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。
重量级锁
在尝试加轻量级锁的过程中,CAS操作无法成功,可能是其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
- 当线程1使用CAS对一个对象obj进行加锁时,发现线程1已经持有该轻量锁,此时就会失败并导致锁膨胀
- 然后就会为obj对象申请Monitor锁,通过obj对象头获取到持锁线程,将Monitor的 Owner置为线程0,将obj的对象头指向重量级锁地址,然后自己进入Monitor的EntryList而BLOCKED。
- 当线程0释放锁时,使用CAS将Mark Word的值恢复给对象头失败,这时进入重量级解锁流程,即按照 Monitor地址找到Monitor对象,设置Owner为 null,唤醒EntryList中BLOCKED线程