详解Java多线程锁之synchronized

synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。

synchronized的四种使用方式

修饰代码块:被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用于调用对象

修饰方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用于调用对象

注意:synchronized修饰方法时必须是显式调用,如果没有显式调用,例如子类重写该方法时没有显式加上synchronized,则不会有加锁效果。

修饰静态方法:其作用的范围是整个静态方法,作用于所有对象

修饰类:其作用的范围是synchronized后面括号括起来的部分(例如:test.class),作用于所有对象

对象锁和类锁是否会互相影响么?

对象锁:Java的所有对象都含有1个互斥锁,这个锁由JVM自动获取和释放。线程进入synchronized方法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁,那么当前线程会等待;synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的1个好处,方法抛异常的时候,锁仍然可以由JVM来自动释放。

类锁:对象锁是用来控制实例方法之间的同步,类锁是用来控制静态方法(或静态变量互斥体)之间的同步。其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。java类可能会有很多个对象,但是只有1个Class对象,也就是说类的不同实例之间共享该类的Class对象。Class对象其实也仅仅是1个java对象,只不过有点特殊而已。由于每个java对象都有1个互斥锁,而类的静态方法是需要Class对象。所以所谓的类锁,不过是Class对象的锁而已。

类锁和对象锁不是同1个东西,一个是类的Class对象的锁,一个是类的实例的锁。也就是说:1个线程访问静态synchronized的时候,允许另一个线程访问对象的实例synchronized方法。反过来也是成立的,因为他们需要的锁是不同的。

对应的实验代码如下:

@Slf4j public class SynchronizedExample { // 修饰一个代码块 public void test1(int j) { synchronized (this) { for (int i = 0; i < 10; i++) { log.info("test1 {} - {}", j, i); } } } // 修饰一个方法 public synchronized void test2(int j) { for (int i = 0; i < 10; i++) { log.info("test2 {} - {}", j, i); } } // 修饰一个类 public static void test3(int j) { synchronized (SynchronizedExample.class) { for (int i = 0; i < 10; i++) { log.info("test3 {} - {}", j, i); } } } // 修饰一个静态方法 public static synchronized void test4(int j) { for (int i = 0; i < 10; i++) { log.info("test4 {} - {}", j, i); } } public static void main(String[] args) { SynchronizedExample example1 = new SynchronizedExample(); SynchronizedExample example2 = new SynchronizedExample(); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> { example1.test2(1); }); executorService.execute(() -> { example2.test2(2); }); } }

在JDK1.6之前,synchronized一直被称呼为重量级锁(重量级锁就是采用互斥量来控制对资源的访问)。通过反编译成字节码指令可以看到,synchronized会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit指令时会将锁计算器减1,当计数器为0时,锁就被释放,然后notify通知所有等待的线程。
Java的线程是映射到操作系统的原生线程上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要用户态和内核态切换,大量的状态转换需要耗费很多处理器的时间。

synchronized的优化

在JDK1.6中对锁的实现引入了大量的优化:

锁粗化(Lock Coarsening):将多个连续的锁扩展成一个范围更大的锁,用以减少频繁互斥同步导致的性能损耗。

锁消除(Lock Elimination):JVM即时编译器在运行时,通过逃逸分析,如果判断一段代码中,堆上的所有数据不会逃逸出去从来被其他线程访问到,就可以去除这个锁。

偏向锁(Biased Locking):目的是消除数据无竞争情况下的同步原语。使用CAS记录获取它的线程。下一次同一个线程进入则偏向该线程,无需任何同步操作。

适应性自旋(Adaptive Spinning):为了避免线程频繁挂起、恢复的状态切换消耗。线程会进入自旋状态。JDK1.6引入了自适应自旋。自旋时间根据之前锁自旋时间和线程状态,动态变化,可以能减少自旋的时间。

轻量级锁(Lightweight Locking):在没有多线程竞争的情况下避免重量级互斥锁,只需要依靠一条CAS原子指令就可以完成锁的获取及释放。

在JDK1.6之后,synchronized不再是重量级锁,锁的状态变成以下四种状态:
无锁->偏向锁->轻量级锁->重量级锁

锁的状态 自适应自旋锁

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zzyysj.html