synchronized关键字用于保证多个线程之间访问资源的同步性,可以保证被它修饰的方法后者代码块在同一时间只能由一个线程访问。
使用
同步方法
使用synchronized修饰方法时,该方法被称为同步方法,同步方法会对整个方法进行加锁,确保同一时刻最多只有一个线程能够执行该方法,其他线程必须等待当前线程执行完毕后才能继续执行。
同步方法时的加锁对象是实例对象本身。同一个类中不同实例对象的同步方法之间并不会相互阻塞,因为它们使用的是不同的锁。
public synchronized void method(){
//method
}
同步静态方法
使用synchronized修饰方法时,该方法被称为同步静态方法。会给当前类加锁,会作用于类的所有对象实例,进入同步代码块前需要获取当前class的锁。
静态成员不是实例对象,是类成员,如果当一个类中存在多个加锁的静态方式时,此时会发生互斥线程,会去争抢锁。
public synchronized static void method2(){
//method
}
同步代码块
使用 synchronized 关键字加锁代码块时,可以精确地控制需要同步的代码范围。只有当指定对象(或类)的锁被获取时,才能执行同步代码块中的代码。
指定加锁对象,对给定对象/类加锁。
synchronized(this | Object) 表示进入同步代码块前需要获取指定的对象的锁。
synchronized(类.class) :表示进入同步代码块前要获取到当前class的锁
public void method(){
synchronized (this){
//method
}
}
原理
Synchronized的原理是基于对象监视器(monitor)实现的,每个JAVA对象都与一个监视器相关联,当一个线程进入同步代码块或方法时,它会尝试会去对象的监视器,如果监视器被其他线程持有,则当前线程将被阻塞,直到获取到监视器为止。
代码解释
先调用 javac 编译出class文件,然后再通过 javap -c -s -v -l xxx.class 查看相关字节码信息。
public static void main(String[] args) {
LockDemoApplication lockDemoApplication = new LockDemoApplication();
lockDemoApplication.method1();
lockDemoApplication.method3();
}
//修饰方法
public synchronized void method1(){
//method
System.out.println("java");
}
//修饰代码块
public void method3(){
synchronized (this){
//method
System.out.println("java");
}
}
}
同步代码块
synchronized 同步代码块使用的是 monitorenter 和 monitorexit 指令,monitorenter 指令指向同步代码块开始的位置,monitorexit 指向同步代码块结束的位置。
当执行 monitorenter 指令时,线程试图获取对象监视器 monitor 的持有权。
每个对象中都有一个 monitor 对象
在执行 monitorenter 指令时,会尝试去获取对象的锁,如果锁的计数器为0则表示锁可以被获取,获取后将锁计数器设为1也就是加1.
在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放,如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另一个线程释放为止。
第一个 monitorexit 指令时正常结束时执行的,第二个 monitorexit 指令是保证同步代码块抛出异常时能正确的释放锁而存在的。
同步方法
synchronized 修饰的方法并没有 monitorenter 和 monitorexit 指令,而是通过 ACC_SYNCHRONIZED 标识来指明该方法是一个同步方法,JVM通过该标识来辨别一个方法是否声明为同步方法
两者本质都是对对象监视器monitor的获取。
锁升级
在jdk1.6以前, synchronized 是属于重量级锁,效率低下。在jdk1.6及以后,对 synchronized 进行了优化,从而有了锁升级的概念。
锁的级别从低到高依次是:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁,这几个状态会随着竞争情况逐渐上升,但是不能降级。
无锁
没有对资源进行锁定(偏向锁标志位为“0”,锁标志位为“01”),JDK1.8的默认对象头是无锁的。
偏向锁
当一个线程访问同步代码块时,会尝试获取对象的偏向锁,如果对象没有被其他线程访问过,则当前线程会获得偏向锁,并且对象头的标记位会被设置为偏向锁;执行完同步代码块后,线程并不会主动去释放锁,当线程第二次执行同步代码块时,线程会判断此时持有锁的线程是否是自己,如果是则直接执行同步代码块(因为之前没有释放锁,所以现在无需加锁),假如自始至终使用锁的线程只有一个,偏向锁机会是没有额外的开销的。
偏向锁的目标是减少无竞争情况下的同步操作的开销
偏向锁的加锁
当一个线程访问同步块并成功获取锁时, 会在锁对象的对象头和栈帧中的锁记录里存储锁偏向的线程ID。以后该线程进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要检查一下对象头的 Mark Word 里是否存储着指向当前线程的偏向锁,如果是,表示线程已经获取到了锁,可以直接执行同步代码块,如果不是,则需要再去测试一下 Mark Word中偏向锁的标识是否设置成1,如果没有设置(表示当前对象没有被偏向锁定),则使用 CAS操作来获取对象的轻量级锁;如果设置了,则尝试通过 CAS操作来撤销偏向锁,将对象头的偏向锁重新指向当前线程。
偏向锁只需要在置换ThreadID的时候依赖一次 CAS
偏向锁的解锁
当前线程首先检查对象的偏向线程ID是否等于当前线程ID,如果不相等,表示对象的偏向锁已经被其他线程获取,当前线程无法解锁偏向锁,解锁操作结束。
如果偏向线程ID等于当前线程ID,则当前线程尝试使用CAS操作将对象的偏向锁标记位清零。
如果CAS操作成功,表示当前线程成功解锁了对象的偏向锁,此时,当前线程会更新对象的状态,将偏向线程ID清零,同时将偏向锁标记位清零,以表示对象不再处于偏向锁状态。
关闭偏向锁
偏向锁在JDK1.6和JDK1.7里是默认启用的,但是它在程序启动几秒后才激活。
如何关闭延迟?通过JVM参数
-XX:-BiasedLockingStartupDelay=0
如何关闭偏向锁?通过JVM参数,关闭后会默认进入轻量级锁状态
-XX:-UseBiasedLocking=false
轻量级锁(自旋锁)
如果多个线程同时尝试获取对象的锁,但是对象的偏向锁已经被占用,则锁会升级为轻量级锁。在轻量级锁状态下,锁会尝试使用 CAS 操作来比较并交换对象头中的锁信息。如果成功获取到锁,线程会顺利进入临界区执行同步操作;如果失败,则会升级为重量级锁。
轻量级锁是针对线程竞争不激烈的情况设计的,采用了乐观锁的策略。
轻量级锁加锁
当一个对象被多个线程竞争时,偏向锁就会失效,对象的锁状态会直接升级为轻量级锁。
- 如果对象当前没有被锁定,当前线程会尝试使用CAS操作将对象的Mark Word中的锁标记位设置为轻量级锁状态。
- 如果CAS操作成功,表示当前线程成功获取了轻量级锁,可以顺利进入临界区执行同步操作
- 如果CAS操作失败,说明对象已经被其他线程抢先获取了锁,此时会进行锁升级,具体是升级为轻量级锁或者重量级锁,取决于竞争的情况。
轻量级锁解锁
当前线程执行完同步代码块后,会尝试释放对象的锁。
- 当前线程会通过CAS操作尝试将对象的Mark Word中的锁标记位清零,以释放锁。
- 如果CAS操作成功,表示当前线程已经释放轻量级锁,整个解锁过程结束
- 如果CAS操作失败,说明有其他线程同时在尝试获取该对象的锁,可能需要进行重试或者升级为重量级锁。
重量级锁(互斥锁)
当锁升级到重量级锁时,表示有多个线程竞争同一个锁,这时采用的是传统的互斥量方式,即通过操作系统提供的互斥量来实现锁。重量级锁的效率相对较低,因为它涉及到内核态和用户态的切换,而且会引起线程阻塞和唤醒,所以尽量避免使用重量级锁。
当多个线程竞争同步资源时,如果无法通过偏向锁和轻量级锁解决竞争,synchronized就会升级为重量级锁。
重量级锁的加锁和解锁过程是由操作系统提供的原语来完成的,通常包括了线程的阻塞和唤醒等操作,涉及到用户态和内核态之间的切换,因此开销相对较高。
偏向锁/轻量级锁/重量级锁的区别
- 偏向锁是为了优化无竞争情况下的同步操作
- 轻量级锁是为了在少量线程竞争同步资源时提供更低的开销和更高的性能。
- 重量级锁将除了拥有锁以外的其他竞争线程都阻塞。
本文暂时没有评论,来添加一个吧(●'◡'●)