synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。
注意:synchronized修饰方法时必须是显式调用,如果没有显式调用,例如子类重写该方法时没有显式加上synchronized,则不会有加锁效果。
类锁和对象锁不是同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的线程是映射到操作系统的原生线程上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要用户态和内核态切换,大量的状态转换需要耗费很多处理器的时间。
在JDK1.6中对锁的实现引入了大量的优化:
在JDK1.6之后,synchronized不再是重量级锁,锁的状态变成以下四种状态:
无锁->偏向锁->轻量级锁->重量级锁
大部分时候,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。这项技术就是所谓的自旋锁。
自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有获取到锁,则该线程应该被挂起。在JDK1.6中引入了自适应的自旋锁,自适应意味着自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
所谓自旋,不是获取不到就阻塞,而是在原地等待一会儿,再次尝试(当然次数或者时长有限),他是以牺牲CPU为代价来换取内核状态切换带来的开销。借助于适应性自旋,可以在CPU时间片的损耗和内核状态的切换开销之间相对的找到一个平衡,进而能够提高性能
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的MarkWord里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下MarkWord中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程,如果失败则进行轻量锁的升级。
如果说偏向锁是只允许一个线程获得锁,那么轻量级锁就是允许多个线程获得锁,但是只允许他们顺序拿锁,不允许出现竞争,也就是拿锁失败的情况,轻量级锁的步骤如下:
自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于轻量级锁的状态,如果自旋失败则进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己,需要从用户态切换到内核态实现。(当竞争竞争激烈时,线程直接进入阻塞状态。不过在高版本的JVM中不会立刻进入阻塞状态而是会自旋一小会儿看是否能获取锁如果不能则进入阻塞状态。)
可以简单总结是如下场景:
最后,限于笔者经验水平有限,欢迎读者就文中的观点提出宝贵的建议和意见。如果想获得更多的学习资源或者想和更多的技术爱好者一起交流,可以关注我的公众号『全菜工程师小辉』后台回复关键词领取学习资料、进入后端技术交流群和程序员副业群。同时也可以加入程序员副业群Q群:735764906 一起交流。