规律总结
- 并发。首先应该意识到只有在多线程(并发)的情况下才会有锁的竞争、获取的问题,单线程根本是不需要考虑锁的。
- 同步。一个非同步的操作,在任何情况下都是可以被无阻塞执行的,无论对象或者是否处于锁状态。例如:对象A被锁住,但是这时对象A仍然可以被调用进行任何非同步操作。
- synchronized。synchronized修饰的代码块必须执行完(退出)才会释放锁。 synchronized修饰的代码块肯定要获得类锁或者对象锁。代码块执行完才会释放它持有的锁,比如:synchronized修饰方法method()时,直到method执行完退出(return)才会释放锁,return之后是肯定会释放锁的。(例外:wait()方法会提前释放锁,notify()方法不会提前释放锁)
- 类锁。每一个类只有一个锁,同步静态方法会持有类锁。但是static对象、final对象是获取对象锁,不是类锁。public static synchronized void method() 这种同步、静态方法在运行的时候会直接占有类锁。并发状态下,此时任何想要获取类锁的操作都会失败。
- 对象锁。每一个对象都有一把锁。这里的可以是类的实例,也可以是静态对象、final对象。应该注意的是:当用synchronized 修饰一个类的静态对象的时候,获取的是该静态对象锁,而不是类锁。
类锁
获取类锁的两种操作
- 同步静态方法。就是上面说的public static synchronized void method()。同步静态方法中synchronized(this)也会获取类锁,因为这里的this就是class对象。
- 直接用synchronized修饰类名。例如synchronized (LockClass.class) 代码示例:
public class LockClass{
public static synchronized void method() {
}
public void testMethod() {
synchronized (LockClass.class) {
}
}
}
关于类锁的说明
- 当类锁被占用的时候,其他线程中上述两种获取的类锁的操作都会被阻塞(blocked)。
- 因为类锁会锁住当前类的所有同步静态方法,应该减少类锁的使用,减小synchronized修饰的代码块的范围,代码块越小,释放锁的速度越快。缩小锁范围的示例:
public class LockClass {
static Object object = new Object();
public static synchronized void Bad() {
}
public static void Better() {
synchronized (object) {
}
}
}
- 在Android开发中,由于类锁范围很大,比对象锁更容易造成ANR,所以应该考虑上面减小锁范围的方式。
对象锁
获取对象锁
- public synchronized void method()会获取调用者的对象锁。
synchronized(object)会获取object对象的对象锁。 - 同步非静态方法中的synchronized(this)会获取调用者的对象锁。
关于对象锁的说明
- 注意不同实例对象的锁互不影响。当对象A调用同步非静态方法method时,此时对象A的锁是被获取的,对象A不能进行其他的同步操作,但是此时对象B也是可以调用方法method。
- static对象,final对象,也是有对象锁的,并不会获取类锁,对类锁没有影响。
一个非同步的操作,在任何情况下都是可以被无阻塞执行的,无论对象是否处于锁状态。 - 应该考虑用synchronized(this)的方式减小锁的代码块的范围。
synchronized 的几种类型锁
对象头中的Mark World
- 每个对象都存在一个对象头, 对象头中存在一个Mark World数据段.
- Mark World 数据段包含:
- 对象的hash码
- 分代年龄信息(永久代、老年代、新生代)
- 偏向锁标记位 用于标记当前是否是偏向锁状态. 1 表示偏向锁, 0 表示不是偏向锁
- 锁状态 标记当前对象适用的锁类型(偏向、轻量级锁、重量级锁)
- Thread ID: 使用偏向锁的线程ID
- epoch 偏向时间戳
- 要注意: 对象头中的锁相关的标志位的修改都是通过CAS进行原子操作修改的.
- 锁一旦膨胀升级,就不能再降级. 重量级锁是无法重新降级到偏向锁的.
偏向锁
- 当某个线程要锁住某个对象时,先将偏向锁的 Thread ID修改为自己线程的ID,执行同步代码块时,就将偏向锁标志位通过CAS修改位1, 表示当前有线程在使用该对象同步执行代码.
- 当本线程再次尝试使用对象进行锁同步操作时, 首先检查对象头中的Thread Id, 和本线程Id相同,直接获取锁执行同步操作, 这样锁同步的性能损耗就很小.
- 当其他线程使用对象进行锁同步时,首先检查Thread ID,发现和本线程Id不同, 然后检查偏向锁状态,如果发现偏向锁为0, 那么直接竞争锁. 如果发现是偏向锁,那么尝试CAS修改 Thread ID, 修改当前对象头指向的线程.
- 偏向锁的特性是: 只有当有其他线程来竞争锁的时候,才会释放锁(修改Thread ID). 如果没有竞争,那么一直是Thread ID指向的线程持有锁.
轻量级锁
- 当一个线程在使用对象进行锁同步时,其他线程通过 自旋 和 CAS 尝试竞争锁. 这种情况下其他线程没有被挂起, 可能进行了短暂的自旋和CAS操作就获得了锁.
- 性能损耗也较小. 执行速度快. 响应时间也较短.
- 缺点是如果一直无法通过CAS获取到锁, 自旋时会消耗CPU.
重量级锁
- 当一个线程在使用对象进行锁同步操作是, 其他线程尝试竞争锁时,就会直接进入挂起阻塞状态.等待其他线程释放锁之后,才会进行锁竞争.
- 适合同步的代码块执行耗时较长的操作, 比如IO操作.
- 缺点:线程会被阻塞, 响应时间慢,相对耗时较长