Synchronized锁的用法及其升级原理
Synchronized锁的用法及其升级原理
简介
Synchronized锁是用来解决多线程并发访问共享数据的安全性问题的,可以保证对共享数据访问的原子性和可见性。
原子性:Synchronized可以保证只有同时只有一个线程能够访问共享数据,也就是一个互斥锁,通过这种互斥机制,保证的操作的原子性。
可见性 :一个线程在获取到锁时,或强制将线程工作内存中的变量失效,从主内存中重新读取该变量。当线程释放锁时,会将工作内存中的变量刷新回主内存,然后删除工作内存中的变量。可以理解为,Synchronized锁会在操作前重新从主存中读取数据,保证数据是最新的,操作完成后立即刷新回主存中,保证其它线程读取到最新数据,这样就保证了操作的可见性。
锁的基本使用
Synchronized可以用于修饰实例方法、静态方法、代码块。
修饰实例方法
Synchronized修饰实例方法时,线程进入该方法前,需要获取到对应实例的锁。
示例:
给User类的实例方法getName加Synchronized锁:
public class User {private int age = 18;public synchronized String getName(String name) {System.out.println(name + " 已成功获取到锁");try {// 睡眠3秒,模拟业务处理Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(name + " 业务处理完成,释放锁");return name;}public int getAge() {return age;}
构造两个线程竞争同一个示例user1中的方法getName:
public class Main {public static void main(String[] args) {User user1 = new User();Thread thread1 = new Thread(() -> {user1.getName("thread1");});Thread thread2 = new Thread(() -> {user1.getName("thread2");});thread1.start();thread2.start();}
}
运行结果显示,只有先抢到锁的thread1释放锁后,thread2才能获得锁去访问getName方法:
thread1 已成功获取到锁
thread1 业务处理完成,释放锁
thread2 已成功获取到锁
thread2 业务处理完成,释放锁
如果thread2访问的是另一个实例的getName方法:
public class Main {public static void main(String[] args) {User user1 = new User();User user2 = new User();Thread thread1 = new Thread(() -> {user1.getName("thread1");});Thread thread2 = new Thread(() -> {user2.getName("thread2");});thread1.start();thread2.start();}
}
运行结果显示,thread2不会受到thread1的影响,thread2可以在thread1还未释放锁时就获取到锁,因为user1和user2属于两个实例,二者的实例锁相互独立:
thread1 已成功获取到锁
thread2 已成功获取到锁
thread2 业务处理完成,释放锁
thread1 业务处理完成,释放锁
修饰静态方法
Synchronized修饰静态方法时,线程进入该方法前,需要获取到对应类的锁
我们让getName方法变成静态方法:
public synchronized static String getName(String name) {System.out.println(name + " 已成功获取到锁");try {// 睡眠3秒,模拟业务处理Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(name + " 业务处理完成,释放锁");return name;}
thread1和thread2访问不同实例的getName方法:
public static void main(String[] args) {User user1 = new User();User user2 = new User();Thread thread1 = new Thread(() -> {user1.getName("thread1");});Thread thread2 = new Thread(() -> {user2.getName("thread2");});thread1.start();thread2.start();}
结果显示,只有先抢到锁的thread1释放锁后,thread2才能获得锁去访问getName方法,因为user1和user2属于同一个类,他们的对象锁是同一个:
thread1 已成功获取到锁
thread1 业务处理完成,释放锁
thread2 已成功获取到锁
thread2 业务处理完成,释放锁
此时对于User类中的实例方法getAge:
public class User {private int age = 18;public synchronized static String getName(String name) {System.out.println(name + " 已成功获取到锁");try {// 睡眠3秒,模拟业务处理Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(name + " 业务处理完成,释放锁");return name;}public int getAge(String name) {System.out.println(name + " 成功获取到age");return age;}
}
如果thread2访问任一实例的getAge方法:
public class Main {public static void main(String[] args) {User user1 = new User();User user2 = new User();Thread thread1 = new Thread(() -> {user1.getName("thread1");});Thread thread2 = new Thread(() -> {user2.getAge("thread2");});thread1.start();try {Thread.sleep(1000); // 确保thread1先获取锁} catch (InterruptedException e) {throw new RuntimeException(e);}thread2.start();}
}
结果显示,thread2访问实例方法getAge并不受thread1影响,因为访问getAge方法不需要获取锁:
thread1 已成功获取到锁
thread2 成功获取到age
thread1 业务处理完成,释放锁
可以推理出,不需要锁的方法,不会受锁的影响。
静态方法的对象锁和实例方法的实例锁也是互不影响的。
当然,如果方法getName和getAge都是静态方法,且都被Synchronized修饰,那么两个方法是同属于一个对象,则受制于同一个锁。
修饰代码块
当锁的是实例时,结果是和修饰实例方法是一样的,本质上是进入该代码块前,需要先获取到实例的锁:
public String getName(String name) {synchronized (this) {System.out.println(name + " 已成功获取到锁");try {// 睡眠3秒,模拟业务处理Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(name + " 业务处理完成,释放锁");}return name;}
public static void main(String[] args) {User user1 = new User();User user2 = new User();Thread thread1 = new Thread(() -> {user1.getName("thread1");});Thread thread2 = new Thread(() -> {user1.getName("thread2");});thread1.start();try {Thread.sleep(1000); // 确保thread1先获取锁} catch (InterruptedException e) {throw new RuntimeException(e);}thread2.start();}
thread1 已成功获取到锁
thread1 业务处理完成,释放锁
thread2 已成功获取到锁
thread2 业务处理完成,释放锁
当锁的是对象时,结果是和修饰静态方法是一样的,本质上是进入该代码块前,需要先获取到对象的锁:
public String getName(String name) {synchronized (User.class) {System.out.println(name + " 已成功获取到锁");try {// 睡眠3秒,模拟业务处理Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(name + " 业务处理完成,释放锁");}return name;}
public static void main(String[] args) {User user1 = new User();User user2 = new User();Thread thread1 = new Thread(() -> {user1.getName("thread1");});Thread thread2 = new Thread(() -> {user2.getName("thread2");});thread1.start();try {Thread.sleep(1000); // 确保thread1先获取锁} catch (InterruptedException e) {throw new RuntimeException(e);}thread2.start();}
thread1 已成功获取到锁
thread1 业务处理完成,释放锁
thread2 已成功获取到锁
thread2 业务处理完成,释放锁
锁的升级
由于Synchronized锁的获取和释放需要调用操作系统的方法将线程挂起和阻塞,就涉及到用户态和内核态的转换,会消耗大量的cpu资源,效率低下,被成为重量级锁,所以在JDK6,引入了锁升级机制来优化:偏向锁->轻量级锁->重量级锁。
- 偏向锁
JVM启动后,会启动偏向锁,此时有线程来访问共享资源时,就会进入偏向锁状态(在启动偏向锁之前会有一个延迟时间,在此时间内访问共享资源,会直接进入轻量级锁状态)。此时,会将共享资源对象的Markword的锁信息标志为偏向锁,偏向锁ID存储为当前线程的ID,当该线程再次访问该共享资源对象时,比对偏向锁ID和线程ID,如果一致,就可以直接访问。相当于在共享资源对象里记录一下线程ID,表示当前“偏向”与这个线程,当这个线程再来访问的话,就直接访问,相当于无并发竞争的单线程情况了。在竞争很弱的情况下,偏向锁效率较高。
值得注意的是,在 JDK15 中,偏向锁被默认关闭(仍然可以使用 -XX:+UseBiasedLocking 启用偏向锁),在 JDK18 中,偏向锁已经被彻底废弃。
因为偏向锁只在无并发竞争的情况下才高效,但是现代程序越来越多的有多线程并发情况,偏向锁的应用情况较少。但是JVM还需要维护偏向锁及其升级逻辑,带来的性能提升难以弥补其开销。
- 轻量级锁
当有第二个线程试图访问共享资源对象时,如果第一个线程已经释放了偏向锁,第二个线程就持有该偏向锁,此时锁继续是偏向锁。如果第一个线程没有释放偏向锁,也就是第二个线程竞争访问共享资源对象失败,此时,偏向锁升级为轻量级锁。此时,JVM会通过CAS(Compare And Swap)操作尝试:将共享资源对象的Markword拷贝到线程栈帧的LockRecord区域中,在Markword中存储指向LockRecord的指针,将LockRecord中的owner指针指向Markword。此时是通过CAS尝试获取锁,线程并没有被挂起,没有涉及内核状态的转变,所以相对“轻量”,在竞争不强的情况效率高。如果尝试CAS多次(自旋)失败,或者有太多线程同时尝试的话,锁就升级为重量级锁。
- 重量级锁
重量级锁是通过底层操作系统的服务来实现的,会为共享资源对象分配一个Monitor,在线程的LockRecord中存储一个指向Monitor的指针,代表持有该锁。操作系统会通过Monitor来维护线程与锁的状态(维护持有者线程,递归计数,等待队列等数据结构)。需要通过操作系统内核态的操作来控制锁,所以相对“重量”。