synchronized 关键字相信每一位 Java 工程师都不会陌生。而 ReentrantLock 作为大神 Doug Lea 编写的 concurrent 包中的重要一员,也在众多项目中发挥重要功能。

synchronized

synchronized 可以用来修饰以下 3 个层面:

  1. 修饰实例方法;
  2. 修饰静态类方法;
  3. 修饰代码块。

synchronized 修饰实例方法


这种情况下的锁对象是当前实例对象,因此只有同一个实例对象调用此方法才会产生互斥效果,不同实例对象之间不会有互斥效果。比如如下代码:

上述代码,在不同的线程中调用的是不同对象的 printLog 方法,因此彼此之间不会有排斥。运行效果如下:

可以看出,两个线程是交互执行的。

如果将代码进行如下修改,两个线程调用同一个对象的 printLog 方法:


则执行效果如下:

可以看出:只有某一个线程中的代码执行完之后,才会调用另一个线程中的代码。也就是说此时两个线程间是互斥的。

修饰静态类方法

如果 synchronized 修饰的是静态方法,则锁对象是当前类的 Class 对象。因此即使在不同线程中调用不同实例对象,也会有互斥效果。

执行后的打印效果如下:


可以看出,两个线程还是依次执行的。

synchronized 修饰代码块

除了直接修饰方法之外,synchronized 还可以作用于代码块,如下代码所示:

synchronized 作用于代码块时,锁对象就是跟在后面括号中的对象。上图中可以看出任何 Object 对象都可以当作锁对象。
运行结果如下:

实现细节

synchronized 既可以作用于方法,也可以作用于某一代码块。但在实现上是有区别的。 比如如下代码,使用 synchronized 作用于代码块:

public class TestSynchronized6 {private int number;public void test1() {int i = 0;synchronized (this) {number = i + 1;}}}

使用 javap 查看上述 test1 方法的字节码,可以看出,编译而成的字节码中会包含 monitorenter 和 monitorexit 这两个字节码指令。如下所示:

上面字节码中有 1 个 monitorenter 和 2 个 monitorexit。这是因为虚拟机需要保证当异常发生时也能释放锁。因此 2 个 monitorexit 一个是代码正常执行结束后释放锁,一个是在代码执行异常时释放锁。

再看下 synchronized 修饰方法有哪些区别:

使用 javap 查看上述 test2 方法的字节码:

从图中可以看出,被 synchronized 修饰的方法在被编译为字节码后,在方法的 flags 属性中会被标记为 ACC_SYNCHRONIZED 标志。当虚拟机访问一个被标记为 ACC_SYNCHRONIZED 的方法时,会自动在方法的开始和结束(或异常)位置添加 monitorenter 和 monitorexit 指令。

关于 monitorenter 和 monitorexit,可以理解为一把具体的锁。在这个锁中保存着两个比较重要的属性:计数器和指针。

  • 计数器代表当前线程一共访问了几次这把锁;
  • 指针指向持有这把锁的线程。

用一张图表示如下:

锁计数器默认为0,当执行monitorenter指令时,如锁计数器值为0 说明这把锁并没有被其它线程持有。那么这个线程会将计数器加1,并将锁中的指针指向自己。当执行monitorexit指令时,会将计数器减1。

ReentrantLock

ReentrantLock 基本使用

ReentrantLock 的使用同 synchronized 有点不同,它的加锁和解锁操作都需要手动完成,如下所示:

图中 lock() 和 unlock() 分别是加锁和解锁操作。运行效果如下:

可以看出,使用 ReentrantLock 也能实现同 synchronized 相同的效果。

你可能已经注意到,在上面 ReentrantLock 的使用中,我将 unlock 操作放在 finally 代码块中。这是因为 ReentrantLock 与 synchronized 不同,当异常发生时 synchronized 会自动释放锁,但是 ReentrantLock 并不会自动释放锁。因此好的方式是将 unlock 操作放在 finally 代码块中,保证任何时候锁都能够被正常释放掉。

公平锁实现

ReentrantLock 有一个带参数的构造器,如下:

默认情况下,synchronized 和 ReentrantLock 都是非公平锁。但是 ReentrantLock 可以通过传入 true 来创建一个公平锁。所谓公平锁就是通过同步队列来实现多个线程按照申请锁的顺序获取锁。

公平锁使用实例如下:

import java.util.concurrent.locks.ReentrantLock;public class TestReentrantLock2 implements Runnable {private int sharedNumber = 0;//创建公平锁ReentrantLock lock = new ReentrantLock(true);@Overridepublic void run() {while (sharedNumber < 20) {lock.lock();try {sharedNumber++;PrintlnUtils.println(Thread.currentThread().getName()+" 获得锁 ,sharedNumber is "+sharedNumber);}finally {lock.unlock();}}}public static void main(String[] args) {TestReentrantLock2 testReentrantLock2 = new TestReentrantLock2();Thread thread1 = new Thread(testReentrantLock2);Thread thread2 = new Thread(testReentrantLock2);Thread thread3 = new Thread(testReentrantLock2);thread1.start();thread2.start();thread3.start();}
}

运行效果如下:

可以看出,创建的 3 个线程依次按照顺序去修改 sharedNumber 的值。

网上对公平锁有一段例子很经典:假设有一口水井,有管理员看守,管理员有一把锁,只有拿到锁的人才能够打水,打完水要把锁还给管理员。每个过来打水的人都要得到管理员的允许并拿到锁之后才能去打水,如果前面有人正在打水,那么这个想要打水的人就必须排队。管理员会查看下一个要去打水的人是不是队伍里排最前面的人,如果是的话,才会给你锁让你去打水;如果你不是排第一的人,就必须去队尾排队,这就是公平锁。

读写锁(ReentrantReadWriteLock)

在常见的开发中,我们经常会定义一个线程间共享的用作缓存的数据结构,比如一个较大的 Map。缓存中保存了全部的城市 Id 和城市 name 对应关系。这个大 Map 绝大部分时间提供读服务(根据城市 Id 查询城市名称等)。而写操作占有的时间很少,通常是在服务启动时初始化,然后可以每隔一定时间再刷新缓存的数据。但是写操作开始到结束之间,不能再有其他读操作进来,并且写操作完成之后的更新数据需要对后续的读操作可见。

在没有读写锁支持的时候,如果想要完成上述工作就需要使用 Java 的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行通知之后,所有等待的读操作才能继续执行。这样做的目的是使读操作能读取到正确的数据,不会出现脏读。

但是如果使用 concurrent 包中的读写锁(ReentrantReadWriteLock)实现上述功能,就只需要在读操作时获取读锁,写操作时获取写锁即可。当写锁被获取到时,后续的读写锁都会被阻塞,写锁释放之后,所有操作继续执行,这种编程方式相对于使用等待通知机制的实现方式而言,变得简单明了。

接下来,我们看下读写锁(ReentrantReadWriteLock)如何使用:

  1. 创建读写锁对象:
public class TestReentrantReadWriteLock {public static void main(String[] args) {//1. 创建读写锁对象:ReadWriteLock rwLock = new ReentrantReadWriteLock();//2. 通过读写锁对象分别获取读锁(ReadLock)和写锁(WriteLock):ReentrantReadWriteLock.ReadLock readLock = (ReentrantReadWriteLock.ReadLock) rwLock.readLock();ReentrantReadWriteLock.WriteLock writeLock = (ReentrantReadWriteLock.WriteLock) rwLock.writeLock();//3. 使用读锁(ReadLock)同步缓存的读操作,使用写锁(WriteLock)同步缓存的写操作://读操作readLock.lock();try {//从缓存中读取数据} finally {}//写操作writeLock.lock();try {//向缓存中写数据} finally {writeLock.unlock();}}
}

具体实现,参考如下代码片段:

public class TestReentrantReadWriteLock2 {private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);private static String number = "0";public static void main(String[] args) {Thread thread1 = new Thread(new Reader(), "读线程 1 ");Thread thread2 = new Thread(new Reader(), "读线程 2 ");Thread thread3 = new Thread(new Writer(), "写线程");thread1.start();thread2.start();thread3.start();}static class Reader implements Runnable {@Overridepublic void run() {for (int i = 0; i <= 10; i++) {lock.readLock().lock();PrintlnUtils.println(Thread.currentThread().getName() + " ---> Number is " + number);lock.readLock().unlock();}}}static class Writer implements Runnable {@Overridepublic void run() {for (int i = 1; i <= 7; i += 2) {try {lock.writeLock().lock();PrintlnUtils.println(Thread.currentThread().getName() + " ---> 正在写入 " + i);number = number.concat("" + i);} finally {lock.writeLock().unlock();}}}}
}

解释说明:

图中的 number 是线程中共享的数据,用来模拟缓存数据;

图中①处,分别创建 2 个 Reader 线程并从缓存中读取数据,和 1 个 Writer 将数据写入缓存中;

图中②处,使用读锁(ReadLock)将读取数据的操作加锁;

图中③处,使用写锁(WriteLock)将写入数据到缓存中的操作加锁。

上述代码执行效果如下:

仔细查看日志,可以看出当写入操作在执行时,读取数据的操作会被阻塞。当写入操作执行成功后,读取数据的操作继续执行,并且读取的数据也是最新写入后的实时数据。

总结

主要学习了 Java 中两个实现同步的方式 synchronized 和 ReentrantLock。其中 synchronized 使用更简单,加锁和释放锁都是由虚拟机自动完成,而 ReentrantLock 需要开发者手动去完成。但是很显然 ReentrantLock 的使用场景更多,公平锁还有读写锁都可以在复杂场景中发挥重要作用。

示例代码代码在 LearnJava工程里的src目录下的deep8jvmdvm包里

源码

链接地址:https://gitee.com/benloogchang/LearnJava.git

第08讲:既生 Synchronized,何生 ReentrantLock相关推荐

  1. 既生 synchronized 何生 JUC 的 显式 locks ?

    新事物的出现要不是替代老事物,要么就是对老事物的补充 JUC 的 locks 就是对 synchronized 的补充 一.synchronized 有不足 从开始竞争锁 到拿到锁这段时间,调用线程一 ...

  2. 原创 | 既生synchronized,何生volatile?!

    △Hollis, 一个对Coding有着独特追求的人△ 这是Hollis的第 225篇原创分享 作者 l Hollis 来源 l Hollis(ID:hollischuang) 在我的博客和公众号中, ...

  3. 既生synchronized,何生volatile

    在作者博客和公众号(Hollis)中,发表过很多篇关于并发编程的文章,之前的文章中我们介绍过了两个在Java并发编程中比较重要的两个关键字:synchronized和volatile 我们简单回顾一下 ...

  4. 既生synchronized,何生volatile (synchronized与volatile的区别)

    既生synchronized,何生volatile (synchronized与volatile的区别) 我们知道,synchronized和volatile两个关键字是Java并发编程中经常用到的两 ...

  5. 《数学分析新讲》_张筑生,12.5节:隐函数定理(2)

    本文继承<数学分析新讲>_张筑生,12.5节,隐函数定理(1). 设函数$F(x,y)$在包含$(x_0,y_0)$的一个开集$\Omega$上连续可微,且满足条件 \begin{equa ...

  6. 深入浅出 Babel 下篇:既生 Plugin 何生 Macros

    接着上篇文章: <深入浅出 Babel 上篇:架构和原理 + 实战 ????> 欢迎转载,让更多人看到我的文章,转载请注明出处 这篇文章干货不少于上篇文章,这篇我们深入讨论一下宏这个玩意  ...

  7. 由爱而生,由生而爱,生生不息

    由爱而生,由生而爱,生生不息 - 由爱而生,由生而爱,生生不息 我不敢相信有上帝,可我相信冥冥之中有安排,不然每个人的面孔和秉性都赋有特色,声音和智慧都表现的那样鲜颜.仿佛奇迹就隐藏在每个人身上,谁与 ...

  8. 小学生计算机舞蹈,最近“泼水成画”很火?舞蹈生VS体育生,看到计算机:你是来添乱的?...

    最近泼水拍照非常的流行,不知道大家在私底下有没有关注过这个视频,而且在这个视频中,这些花放在水里确实也特别的好看,接下来就一起来看一下,不同的学生拍出来的泼水照片都是什么样的. 首先大家看到的就是舞蹈 ...

  9. 既生瑜何生亮 access_token VS refresh_token

    中国有句老话, 既生瑜何生亮, 既然有我周瑜在世, 为什么老天还要一个诸葛亮啊? 同样的, 众所周知, 在 OAuth 2.0 授权协议中, 也有两个令牌 token , 分别是 access_tok ...

  10. excel如何找到高频词_拟录取后:应届生和往届生档案哪里找;重灾院校区;高频词背诵表...

    今日消息1.应届生和往届生档案哪里找?2.重灾院校区3.考研云督学班高频词背诵表汇总1.应届生和往届生档案哪里找? 往年这个时候论文答辩.复试已经结束,已经进入毕业季!现在你们毕业答辩结束了吗?你们都 ...

最新文章

  1. php打印mysql sql_php的打印sql语句的方法
  2. 中科院自动化所与华为联合提出!视觉目标检测大模型GAIA
  3. python3 enumerate()函数笔记
  4. linux源文件安装,Linux下的源文件安装
  5. linux lilo命令,lilo命令_Linux lilo 命令用法详解:安装核心载入开机管理程序
  6. 工作室多拨宽带如何优化?
  7. 要怎样申请抖音蓝V认证?详谈蓝V认证的步骤
  8. 华硕天选2和华硕天选3哪个好 华硕天选2和华硕天选3区别
  9. 什么是策划?策划的真正含义是什么?
  10. TP TN FP FN
  11. 剑指Offer对答如流系列 - 构建乘积数组
  12. python 推导式
  13. 软体艺术系列--抽象工厂 (原文最终修订于2006年10月18日 凌晨04:25:06)
  14. 计算机可以辅助英语写作吗,计算机辅助下的英语写作教学_问答库
  15. 《持续集成实践指南》第2章 持续集成环境搭建Jenkins+Gitlab+Gerrit
  16. 【Spring5】004-IOC容器+基于注解的方式实现Bean管理
  17. Django实现短信注册功能
  18. JavaScript高级第2天:定义函数的三种方式、函数的原型链结构、完整原型链、作用域以及作用域链、函数的四种调用模式、闭包、计数器、斐波那契数列优化、三种继承方式
  19. 传苹果向三星购买5G基带芯片遭拒;ofo否认破产;阿里大股东宣布清算,抛售全部股份|雷锋早报...
  20. php封装协议查看zip,支持的协议和封装协议

热门文章

  1. Linux read的用法
  2. IOS-UIWebView字体控制
  3. 贝壳找房面试之c++基础问答
  4. 《数据结构与算法》(三)- 如何估算时间复杂度
  5. 金蝶k3显示加层服务器失败,金蝶k3提示:连接中间加密服务失败,请确认中间层加密服务已启动...
  6. 任务一:基于控制台的购书系统 java实验报告
  7. 威纶触摸屏中如何组态设置多国语言进行切换?
  8. 七周成为数据分析师01-数据分析思维
  9. 数字图像处理----第七章
  10. java计时器_Java 计时器