概念
是一种JVM的隐式锁,因为加锁和解锁过程都是JVM搞定的,不需要我们手动加锁解锁。
历史
以前的时候,jdk 小于 1.6时,synchronized的效率很低。当小于1.6 的时候,锁依赖于Java里的对象,每一个对象又会有一个Monitor管程,Monitor则依赖于Mutex互斥量,这个互斥量由操作系统来维护。JVM是运行在用户态的,如果要操作这个互斥量,就需要切换到内核态,该切换过程是一个重型操作,效率非常低。
由于synchronize效率太低了,doug li 发明了AQS,然后发明了ReentrantLock,效率比sychronized高。采用了CAP来做乐观锁,CAS又可称之为轻量级锁,线程并未阻塞,而是通过自旋来不断尝试修改值。
JDK>=1.6以后,对sychronized进行优化,会主键升级锁,从偏向锁 -> 轻量级锁 -> 重量级锁。
CAS是如何保证原子性的?
通过底层汇编命令 lock
和 cmpxchgq
来先加锁,然后再去做比较和交换。默认加的是缓存行锁,但是如果数据过大,需要放到几行缓存里,那么就会加总线锁。
因此,虽然CAS在代码层面看起来没有锁,但是在底层硬件级别还是有锁存在的。
CAS的ABA问题如何解决?
使用版本号。
锁升级过程
在1.6版本以后,对synchronized进行了优化,添加了不同级别的锁的状态和锁的升级。
一般流程是:无状态 -> 偏向锁 -> 轻量级锁 -> 重量级锁 ,
注意,如果没有开启偏向锁,直接加锁,那么会从无状态 -> 轻量级锁,跳过了偏向锁。
为什么需要锁升级?
因为有时候虽然我们加了synchronized锁,希望来防止线程安全问题的出现,但是大部分时间下,可能被加锁的代码块只有一个线程在运行。
如果一上来就加一个重量级锁,那么对资源的损耗是相当大的。
偏向锁
偏向锁是比轻量级锁还要轻量的锁,它仅仅在线程对象头中存储当前上锁的线程的ID,每次运行时仅仅比较当前线程的ID和添加到对象头中的ID是否一致,一致则可以继续运行。
通过偏向锁就可以改善在只有一个线程运行时的效率。
轻量级锁
当有少量线程进入时,都想去加锁时,使用CAS + 自旋来升级到轻量级锁。
重量级锁
当加锁的线程非常多时,那就有非常多的自旋,此时轻量级锁效率降低,就采用重量级锁。
sychronized的三种用法
- 加在静态方法上:
```java
public class Clazz1 {
public static sychronized void method1() {
} }...
此时,锁加在了类上,即加在了Clazz1.class上
2. 加在普通方法上:
```java
public class Clazz2 {
public sychronized void method2() {
...
}
}
此时加在了本对象,即this上。
- 同步代码块:
此时加在了object对象上。sychronized(object) { xxx }
锁的状态存在哪?
在加锁时,是对某个对象加锁,对象在堆上。堆上的对象有个对象头,其中就包含了锁的状态。
注意:当锁标志位为01时,有两种情况,要么是无锁,要么是偏向锁,此时我们要通过倒数第三位(倒数第二列)为0或者1来判断是否是偏向锁。
偏向锁分析
我们通过jol
依赖来打印出来对象的头部,来判断加锁状态,例如:
第一行的4个字节 + 第二行的4个字节表示MarkWord,刚好64位。注意要反过来和64位虚拟机的图做对比,例如打印出来的第一个8位00000001
实际上对应的是表中的后8位。
第三行的4个字节表示元数据指针;
接下来的字节表示实例数据。
最后的4个字节是对齐填充,因为JVM要求对象必须能被8个字节整除,这是为了保证最优的寻址方式。
我们拿下面的代码一步步分析:
public class TestLockUpgrade {
public static void main(String[] args) throws InterruptedException {
User userTemp = new User();
System.out.println("无状态(001):" + ClassLayout.parseInstance(userTemp).toPrintable());
// JVM默认需要4秒才会开启偏向锁,所以这里先睡5秒
Thread.sleep(5000);
User user = new User();
System.out.println("启用偏向锁(101):" + ClassLayout.parseInstance(user).toPrintable());
for (int i = 0; i < 2; i++) {
synchronized (user) {
System.out.println("偏向锁(101)(带线程id):" + ClassLayout.parseInstance(user).toPrintable());
}
System.out.println("偏向锁释放(101)(带线程id): " + ClassLayout.parseInstance(user).toPrintable());
}
}
}
class User {
private int id;
private String name;
}
第一份打印:
可以看到,第一个8位为00000001
, 最后三位为001
,对应到表中为:
说明此时为无锁状态.
第二份打印:
过了5秒打印了user对象(因为JVM默认4秒以后才启用偏向锁),注意启用偏向锁并不代表已经加了偏向锁,但是如果想要加偏向锁,需要先启用偏向锁。
第三份打印:
给user对象加了偏向锁,此时要保存线程的ID到Mark Word里面去,所以前面54位发生了变化。
第四份打印:
按照正常理解,运行完了同步代码块里的代码,应该释放锁,但是事实上,并不会释放偏向锁,因为偏向锁假定只有一个线程在运行,为了保证下一次该线程再次执行的效率,并不释放偏向锁。这样子当该线程下一次过来的时候,只需要比对偏向锁中的ID和该线程ID,如果一致那么直接执行。
轻量级锁分析
当有第二个线程来竞争锁的时候,会立刻从偏向锁升级到轻量级锁:
我们添加以下代码来竞争锁。
new Thread(() -> {
synchronized (user) {
System.out.println("轻量级锁(00)" + ClassLayout.parseInstance(user).toPrintable());
try {
System.out.println("睡眠3秒钟==============");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
可以看到第一个8bit的后两位为00,表示轻量级锁。
重量级锁分析
而如果我们再开启一个线程来竞争锁,因为在轻量级锁分析的过程中,让某个线程抱着锁睡了3秒钟,那么这个新的线程就竞争不到锁,就会将轻量级锁升级成重量级锁。
可以看到,第一个8bit的后两位变成了10,即重量级锁。