前言
Hello,大家好!我是Leo!
你是否曾经遇到过这样的情况:在多线程环境下,你的代码运行得像个疯子一样,输出的结果让你感到困惑不解?如果是这样,那么恭喜你,你已经体验到了数据竞争和线程不安全的痛苦。但是别担心,Java的Synchronized关键字可以帮助你摆脱这些问题,就像一位超级英雄一样,保护你的代码免受恶意线程的攻击。在本文中,我们将带你进入Synchronized关键字的世界,让你感受到线程同步的魔力!好了废话不多说,咱们开始吧!
synchronized关键字
synchronized关键字的使用
- synchronized可以用来修饰方法或者代码块;可以把任意一个非NULL的对象当成是一把锁。它属于悲观锁,也属于可重入锁。
- 一把锁只能同时被一个线程获取,没有获取到锁的线程只能等待。
- 每个实例都对应的有自己的一把锁 ,不同实例之间互不影响。除了:锁对象是 .class以及synchronized修饰的静态方法的时候,所有对象公用同一把锁.
- synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁。
synchronized的对象锁和类锁
- 修饰方法
- 普通方法(非静态方法):锁定的是方法的调用对象
- 静态方法:锁定的是类
- 修饰代码块:锁定的是传参的对象 注意: 判断Synchronized到底锁的资源是什么?如果锁的是相同资源,就是同步的;否则就是不同步的。
对象锁
指的是方法锁(默认锁对象为this<当前实例对象>)和同步代码块锁(自己指定锁对象)
代码块
手动指定锁定对象,this或者自定义的锁
this:
java
public class SynchronizedTest implements Runnable{
@Override
public void run() {
// 同步代码块形式------锁为this,两个线程使用的锁是一样的,线程1必须要等到线程0释放了该锁后,才能执行
synchronized (this) {
System.out.println("当前线程: " + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 运行结束");
}
}
public static void main(String[] args) {
SynchronizedTest synchronizedTest = new SynchronizedTest();
Thread thread1 = new Thread(synchronizedTest);
Thread thread2 = new Thread(synchronizedTest);
thread1.start();
thread2.start();
}
}
运行结果:
java
当前线程: Thread-0
Thread-0 运行结束
当前线程: Thread-1
Thread-1 运行结束
自定义的锁(可以是Object,也可以是任意指定对象)
java
public class SynchronizedTest implements Runnable{
// 创建两把锁
Object obj1 = new Object();
Object obj2 = new Object();
@Override
public void run() {
// 当前代码块使用的是第一把锁,当他释放后,后面的代码块由于使用的是第二把锁,因此马上执行
synchronized (obj1) {
System.out.println("obj1锁线程: " + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("obj1锁线程" + Thread.currentThread().getName() + " 运行结束");
}
synchronized (obj2) {
System.out.println("obj2锁线程: " + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("obj2锁线程" + Thread.currentThread().getName() + " 运行结束");
}
}
public static void main(String[] args) {
SynchronizedTest synchronizedTest = new SynchronizedTest();
Thread thread1 = new Thread(synchronizedTest);
Thread thread2 = new Thread(synchronizedTest);
thread1.start();
thread2.start();
}
运行结果:
java
obj1锁线程: Thread-0
obj1锁线程Thread-0 运行结束
obj1锁线程: Thread-1
obj2锁线程: Thread-0
obj1锁线程Thread-1 运行结束
obj2锁线程Thread-0 运行结束
obj2锁线程: Thread-1
obj2锁线程Thread-1 运行结束
普通方法
synchronized修饰普通方法,锁对象默认为this。
java
public class SynchronizedTest implements Runnable{
@Override
public void run() {
a();
}
public synchronized void a(){
System.out.println("当前线程: " + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程" + Thread.currentThread().getName() + " 运行结束");
}
public static void main(String[] args) {
SynchronizedTest synchronizedTest = new SynchronizedTest();
Thread thread1 = new Thread(synchronizedTest);
Thread thread2 = new Thread(synchronizedTest);
thread1.start();
thread2.start();
}
}
运行结果:
java
当前线程: Thread-0
当前线程Thread-0 运行结束
当前线程: Thread-1
当前线程Thread-1 运行结束
类锁
类锁指的事 synchronized 修饰静态的方法或指定锁对象为Class对象或者 synchronized 修饰静态方法。
Synchronized修饰静态方法
java
public class SynchronizedTest implements Runnable{
@Override
public void run() {
a();
}
public static synchronized void a(){
// synchronized用在静态方法上,默认的锁就是当前所在的Class类,所以无论是哪个线程访问它,只需要一把锁。
System.out.println("当前线程: " + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程" + Thread.currentThread().getName() + " 运行结束");
}
public static void main(String[] args) {
SynchronizedTest synchronizedTest1 = new SynchronizedTest();
SynchronizedTest synchronizedTest2 = new SynchronizedTest();
Thread thread1 = new Thread(synchronizedTest1);
Thread thread2 = new Thread(synchronizedTest2);
thread1.start();
thread2.start();
}
}
运行结果:
java
当前线程: Thread-0
当前线程Thread-0 运行结束
当前线程: Thread-1
当前线程Thread-1 运行结束
synchronized指定锁对象为Class对象
java
public class SynchronizedTest implements Runnable{
@Override
public void run() {
synchronized (SynchronizedTest.class){
// 所有线程需要同一把锁
System.out.println("当前线程: " + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程" + Thread.currentThread().getName() + " 运行结束");
}
}
public static void main(String[] args) {
SynchronizedTest synchronizedTest1 = new SynchronizedTest();
SynchronizedTest synchronizedTest2 = new SynchronizedTest();
Thread thread1 = new Thread(synchronizedTest1);
Thread thread2 = new Thread(synchronizedTest2);
thread1.start();
thread2.start();
}
}
运行结果:
java
当前线程: Thread-0
当前线程Thread-0 运行结束
当前线程: Thread-1
当前线程Thread-1 运行结束
Synchronized原理
加锁和释放锁的原理
深入JVM查看字节码,看Synchonized 在 JVM 里的实现原理。
首先创建一个Synchronized同步代码块,如下:
java
public class SynchronizedTest implements Runnable{
@Override
public void run() {
synchronized (SynchronizedTest.class){
System.out.println("当前线程: " + Thread.currentThread().getName());
System.out.println("当前线程" + Thread.currentThread().getName() + " 运行结束");
}
}
}
通过javac命令进行编译生成 .Class 文件:
sh
javac SynchronizedTest.java
通过 javap 命令反编译生成 .Class 文件的信息:
sh
javap -verbose SynchronizedTest.class
生成的文件信息如下:
monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处和异常处,JVM 要保证每个 monitorenter 必须有对应的monitorexit 与之配对。monitorenter和monitorexit指令,会让对象在执行,使其锁计数器加1或者减1。任何对象在同一时间只能一个monitor相关联;当且一个 monitor 被持有后,它将处于锁定状态。线程执行到 monitorenter 指令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。一个对象在尝试获得与这个对象相关联的Monitor锁的所有权的时候,monitorenter指令会发生如下3中情况之一:
- monitor计数器为0,意味着目前还没有被获得,那这个线程就会立刻获得然后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待。
- 如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加。
- 这把锁已经被别的线程获取了,等待锁释放著作权归
monitorexit指令:释放对于monitor的所有权,释放过程很简单,就是将monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁。
由上图看出来:任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态为阻塞会进入同步队列中,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。
可重入锁原理
什么是可重入锁?
可重入锁,也叫做递归锁,是指在一个线程中可以多次获取同一把锁,比如:一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法(即可重入),而无需重新获得锁。
请看如下举例:
java
public class SynchronizedTest {
public static void main(String[] args) {
SynchronizedTest synchronizedTest = new SynchronizedTest();
synchronizedTest.method1();
}
private synchronized void method1() {
System.out.println("当前执行 method1方法...");
method2();
}
private synchronized void method2() {
System.out.println("当前执行 method2方法...");
method3();
}
private synchronized void method3() {
System.out.println("当前执行 method3方法...");
}
}
结合Synchronized中的加锁和释放锁原理:
- 首先执行monitorenter获取锁
- 当前monitor计数器=0,可获取锁
- 执行method1()方法,monitor计数器+1 <当前计数:1> (获取到锁)
- 执行method2()方法,monitor计数器+1 <当前计数:2>
- 执行method3()方法,monitor计数器+1 <当前计数:3>
- 执行monitorexit命令
- method3()方法执行完,monitor计数器-1 <当前计数:2>
- method2()方法执行完,monitor计数器-1 <当前计数:1>
- method2()方法执行完,monitor计数器-1 <当前计数:0> (释放了锁)
- monitor计数器=0,锁被释放了
Synchronized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会 +1,释放锁后就会将计数器 -1。
Synchronized锁存储
Java对象头
synchronized 用的锁是存在 Java 对象头里的,在JVM中, 对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。
- 对象头:Mark Word 里默认存储对象的 HashCode、分代年龄和锁标记位。32位 JVM 的 Mark Word 的默认存储结构;
- 实例数据:存放类的属性数据信息,包括父类的属性信息;
- 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;
Synchronized锁升级
聊到锁升级,接下来咱们就重点聊聊对象头中的Mark Word。 Mark Word:指的是【运行时数据】主要来代表Java对象的县城说状态以及GC标志位。线程锁状态分别为:无锁、偏向锁、轻量级锁、重量级锁。
在JDK1.6之前,内置锁都是重量级锁,所以效率比较低下,具体表现在以下几个方面:
- 竞争激烈的性能问题:在高并发环境下,Synchronized锁的性能会受到严重影响,应为多个线程会抢占同一把锁,导致大量线程进入阻塞状态,从而降低执行效率。
- 重量级锁的问题:在1.6之前版本中,Synchronized是一种重量级锁,这就意味着每次获取锁和释放锁都需要进行额外的操作,这些操作会增加系统的延迟和负担。
- 不可中断性:在1.6之前版本中,Synchronized锁是不可以中断的,如果一个线程获取了锁并进入了阻塞阶段,其他的线程只能等待当前线程释放锁,无法中断他的执行。
因此,在Java 1.6及之前的版本中,Synchronized锁在高并发环境下的性能表现不佳,容易出现死锁、饥饿等问题。为了解决这些问题,Java引入了更轻量级的锁实现,如基于CAS算法的乐观锁和基于自旋的偏向锁、轻量级锁等。
在JDK1.6之后 为了提高Synchronized的效率,引入了偏向锁和轻量级锁。 随着锁竞争逐渐激烈,其状态会按照无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁这个方向逐渐升级,并且升级是不可逆的,只可以进行锁升级,不可以进行锁降级。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
了解完Synchronized在1.6之前和之后锁的区别,接下来我们一块来看看Mark Word的四种不同的锁状态,探究其内部结构。(64位)
- 无锁 无锁就是不使用 synchronized 关键字,无锁的锁标志位为 01。
- 偏向锁
什么是偏向锁? 如果一个线程获得了锁,那么在未释放锁之前,该线程下一次获取锁时可以直接获得锁,而不需要重新竞争。原因是线程不会主动释放锁对象头中存储了锁偏向的线程ID,当尝试获取锁的线程ID与锁对象的对象头Mark Word中存储的偏向线程ID一致时,就认为该线程已持有锁,直接进入同步代码块。这种优化技术可以减少锁竞争带来的性能损失,从而提高程序的并发性能。
看到这里,你会发现无锁、偏向锁的 lock 标志位是一样的,即都是 01,这是因为无锁、偏向锁是靠字段 biased_lock 来区分的,0 代表没有使用偏向锁,1 代表启用了偏向锁。
- 轻量级锁
当有第二个线程参与竞争,就会立即膨胀为轻量级锁。企图抢占的线程一开始会使用自旋的方式尝试获取锁。JDK 1.7 之前是普通自旋,会设定一个最大的自旋次数,默认是 10 次,超过这个阈值就停止自旋。JDK 1.7 之后,引入了适应性自旋。简单来说就是:这次自旋获取到锁了,自旋的次数就会增加;这次自旋没拿到锁,自旋的次数就会减少。
- 重量级锁
上面提到,试图抢占的线程自旋达到阈值,就会停止自旋,那么此时锁就会膨胀成重量级锁。当其膨胀成重量级锁后,其他竞争的线程进来就不会自旋了,而是直接阻塞等待,并且 Mark Word 中的内容会变成一个监视器(monitor)对象,用来统一管理排队的线程。
小结一下
synchronized在JDK1.6之前,完全就是获取不到锁,立即挂起当前线程,所以synchronized性能比较差;
synchronized就在JDK1.6做了领升级的优化
-
无锁 : 当前对象没有作为锁存在
-
偏向债 : 如果当前锁资源,只有一个线理在频繁的获取和释放,那么这个线理过来,只需要判断,当前指向的线程是否是当前线程。
- 如果是,直接拿着锁资液走。
- 如果当前线程不是,基于CAS的方式,尝试将偏向锁指向当前线程。如果获取不到,触发锁升级,升级为轻量级锁。 (偏向锁状态出现了锁竞争的情况)。
-
轻量级锁: 会采用自旋锁的方式去频繁的以CAS的形式获取锁资源(采用的是自适应自旋锁)
- 如果成功获取到,拿着锁资源走。
- 如果自旋了一定次数,没拿到锁资源,锁升级。
-
重量级锁: 就是最传统的synchronized方式,拿不到锁资源,就挂起当前线程。
参考文章:
《Java并发编程艺术》《Java并发编程艺术实战》