java并发编程——同步

在上一篇 java并发编程——内存模型中我们提到:并发编程中,我们需要处理两个关键问题:线程之间如何通信线程之间如何同步。线程之间如何通信已经在上篇文章中讲述,本文主要来阐述线程之间如何同步。

1. 同步概念

同步是指程序用于控制不同线程之间操作发生相对顺序的机制。

  • 在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。
  • 在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。

Java的并发采用的是共享内存模型,需要程序员显示的指定某个方法或某段代码需要线程之间互斥执行。
同步的目的:在多线程编程里面,一些敏感共享资源不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。

2. java中同步方法

Java提供了很多同步操作,比如synchronized关键字、wait/notifyAll、ReentrantLock、Condition、一些并发包下的工具类、Semaphore,ThreadLocal、AbstractQueuedSynchronizer等,下面就其中常见操作进行详细说明

2.1 synchronized关键字

在java中,synchronized关键字主要有以下三个作用:

  • 确保线程互斥的访问同步代码
  • 保证共享变量的修改能够及时可见
  • 有效解决重排序问题

后两个作用,内存模型文章中提到过,这里主要讲述synchronized如何确保线程同步互斥。

2.1.1 synchronized使用

synchronized提供了两种方式对线程进行同步,分别是同步代码块和同步方法。如下图给出了具体的示例:

synchronized使用

使用synchronized效果几点说明(原因见下一节原理说明):

  1. 当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
  2. 当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
  3. 当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。

同步代码块和同步方法的选择原则,能缩小代码段的范围就尽量缩小,能在代码段上加同步就不要再整个方法上加同步。这叫减小锁的粒度,使代码更大程度的并发。

2.1.2 synchronized原理

首先我们反编译synchronized使用中给出的示例的代码如下图所示,其中标注出了monitorenter和monitorexit两个字节码指令,这两个指令都需要一个reference类型的参数来指明要锁定和解锁的对象

反编译synchronized代码块

java程序中如果synchronized明确指定了对象参数,那么就是这个对象的reference;如果没有明确指出,则根据synchronized修饰的实例方法还是类方法来确定,如果是类方法则取Class对象作为锁对象。总结下来,锁对象分为如下两类:

  1. synchronized(this)以及非static的synchronized方法,则锁定调用对象本身。
  2. static修饰的静态方法以及synchronized(xxx.class),则锁定类的Class对象,因为一个类的Class对象只有一个,所以该类的所有相关对象都共享一把锁。

根据jvm规范,在执行monitorenter指令,线程首先要尝试获取reference对应的对象锁。

  • 如果该对象锁没有被锁定占有,或者改线程之前已经拥有了该对象锁,则把锁的计数器加1。
  • 相应地,在执行monitorexit指令时会将该对象锁计数器减1,当计数器为0时,锁就被释放了。其他被这个对象锁阻塞的线程可以尝试去获取这个对象锁的所有权。
2.1.3 对象锁和类锁概念

每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁。java内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,同时是一个可重入锁,锁对象有计数器记录相应线程进入次数只有清空为0才表示该线程不再持有该锁。
java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,只是类锁是用于类的静态方法或者一个类的class对象上的而对象锁是用于对象实例方法。

类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁,所有对象共享一个类锁。

2.2 Lock接口

javaSE5之后,并法包新增了Lock接口用来实现锁功能,它提供了与synchronized类似的同步功能,只是使用的时候需要显示的获取和释放锁,同时这些接口也提供了synchronized不具备的特性如下表所示:

特性 描述
尝试非阻塞获取锁 当前线程尝试获取锁,如果这一刻锁没有被其他线程获取到,则成功获取持有锁 ,不会阻塞等待锁释放
被中断的获取锁 与synchronized不同,获取到锁的线程能够响应中断,当获取到的锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
超时获取锁 接口在指定的截止时间之前获取锁,如果截止时间到了依旧无法获取锁,则返回

下面就其中一些常用Lock接口实现类进行讲述。

2.2.1 ReentrantLock重入锁

ReentrantLock是Lock接口一种常见的实现,它是支持重进入的锁即表示在调用lock()方法时,已经获取锁的线程能够再次调用lock()方法而不被阻塞。同时,该锁还支持获取锁时的公平与非公平的选择。 最后,ReentrantLock是排他锁,该锁在同一时刻只允许一个线程来访问。
关于公平与非公平几点说明:

  • 如果在绝对时间上,先对于锁进行获取的请求一定先被满足,那么这个锁就是公平的,反之就是非公平的。
  • 公平的获取锁也就是等待时间最久的线程优先获取到锁。ReentrantLock的构造函数来控制是否为公平锁。
  • 通常情况下,公平锁保证了获取锁按照FIFO原则,而代价就是大量的线程切换,导致性能下降。而非公平有可能导致部分线程饥饿,但是保证了更大的吞吐量。
2.2.2 读写锁

前面提到的ReentrantLock是排他锁,该锁在同一时刻只允许一个线程来访问,而读写锁在同一时刻允许可以有多个线程来访问,但在写线程访问时,所有的读线程和其他写线程被阻塞。
读写锁维护了一对锁,一个读锁和一个写锁,其中读锁是一个共享锁可以被多个线程同时获取,而写锁是一个支持冲进入的排它锁。读写锁实例ReentrantReadWriteLock有以下特性:

  • 公平性选择:和ReentrantLock类似
  • 重进入:可重入锁,特别注意写线程在获取写锁之后能够再次获取写锁,同时也可以获取读锁。
  • 锁降级:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级为读锁。

下面给出一个读写锁使用示例:

读写锁示例

该示例中使用非线程安全的HashMap作为缓存实现,通过使用读写锁来保证线程的安全。分析代码,在读取操时需要获取读锁为共享锁支持多线程同时访问不被阻塞。在写操作时,首先获取写锁,当获取写锁后,其他线程对于读锁和写锁的获取均被阻塞,只有写锁被释放后,其他操作才可以继续,这样也保证了所有读操作都是最新数据。

2.3 闭锁

闭锁是一种同步工具类,可以延迟线程的进度直到其达到终止状态。闭锁的作用相当于一扇门:在闭锁达到技术状态之前,这扇门一直是关闭的,没有任何线程能通过,当达到结束状态时,这扇门会打开并允许所有线程通过。

闭锁常见使用场景:

  • 确保某个计算在其需要的所有资源都被初始化之后才继续执行。
  • 确保某个服务在其依赖所有服务都已经启动之后才启动。

CountDownLatch作为闭锁的实现类,拥有一个计数器初始化为一个正数,表示需要等待的事件数量。countDown方法递减计数器,表示有一个事件已经发生,而await会一直阻塞直到计数器为0,所有等待事件已经发生。下面给出一个使用示例:

CountDownLatch示例.png

分析示例代码,startGate作为启动门,由主线程控制所有的线程都准备就绪后打开启动门。而endGate作为结束门,每一个线程在执行结束后countDown减1,最后endGate.await()等待所有线程执行结束。

2.4 信号量

计数信号量(Counting Semaphore)用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。Semaphore常见使用情景:

  • 用于资源池,控制资源池大小
  • 将任何一种容器变成有界阻塞容器

Semaphore管理着一组虚拟的许可(permit),许可的初始数量通过构造函数来指定,在执行操作时可以首先获取许可(有剩余许可),并在执行结束以后释放许可。如果没有许可剩余,则acquire将阻塞(或者中断或者超时)。下面给出一个有界阻塞容器的示例:

Semaphore示例.png

2.5 栅栏

栅栏(Barrier)类似于闭锁,它能阻塞一组线程直到某个事件发生。栅栏和闭锁的关键区别,所有线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。
CyclicBarrier作为栅栏实现类,可以使一定数量的参与方反复在栅栏位置汇集。当线程到达栅栏位置时会调用await方法,该方法会阻塞调用线程直到所有线程到达栅栏位置。当所有线程到达指定位置,那么栅栏会被打开,所有线程被释放,而栅栏将重置等待下次执行。下面给出一个使用示例:

CyclicBarrier示例.png

2.6 ThreadLocal

对于多线程资源共享的问题,前面叙述的同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式
ThreadLocal实现原理,查看Thread类源码可以看到其中有一个hash结构的ThreadLocal内部类ThreadLocalMap对象,该对象实例threadLocals即是用来保存具体值的对象,定义如下图所示

ThreadLocalMap实例.png

ThreadLocal类负责管理threadLocals中保存的变量,具体set代码见下图:

ThreadLocal Set方法

在set方法中以当前线程的实例为参数调用getMap获取当前线程的ThreadLocalMap对象,如果线程的threadLocals不为null,就将value保存到threadLocals中,反之,先创建一个ThreadLocalMap实例。
可以看出,隔离的value并不是保存到ThreadLocal中,而是在每个线程对象的内部来保存。因为是每个线程自己来保存value,所以做到了线程间相互隔离。
接下来看下ThreadLocalMap类作为ThreadLocal内部类的定义如下图所示:

ThreadLocalMap定义.png

其中,Entry继承与WeakReference,ThreadLocalMap中的键ThreadLocal对象通过软引用来保存,值则保存到一个Object的实例value中。因为key是一个软引用,所以每次gc之后就可能有key为null。
最后,一个需要注意的坑:
通过上述分析,可以看出ThreadLocal其实是与线程绑定的一个变量,这样出现一个问题:如果没有将ThreadLocal内的变量删除或替换,他的生命周期将会与线程共存。
只有线程真正注销的时候,ThreadLocal才会被回收,但是类似线程池管理对线程都是采用复用的过程,那么生命周期都是不可预测的。假设Threadlocal中存放了一个HashMap里面有很大的对象,这样对象就会在内存中一直无法释放。
通常使用ThreadLocal的set和remove有始有终,外部调用代码使用finally来remove数据,如果是第三方包引起的可以通过BTrace工具定位

参考文章

《java并发编程实战》
http://langgufu.iteye.com/blog/2152608
http://www.cnblogs.com/paddix/p/5367116.html
https://www.ibm.com/developerworks/cn/java/j-threads/
http://fangjian0423.github.io/2016/04/18/java-synchronize-way/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容

  • 一.线程安全性 线程安全是建立在对于对象状态访问操作进行管理,特别是对共享的与可变的状态的访问 解释下上面的话: ...
    黄大大吃不胖阅读 828评论 0 3
  • layout: posttitle: 《Java并发编程的艺术》笔记categories: Javaexcerpt...
    xiaogmail阅读 5,802评论 1 19
  • 前几天超市买水果,将捡好的水果拿给水果阿姨打秤,她碰到我的手说:“你的手怎么这么冰啊。”我笑说我是冰雕人。小时候,...
    米粒2020阅读 169评论 0 0
  • 在使用Twisted.web模块搭建web服务时,发现要定义isLeaf变量。 很奇怪。这个变量出现的莫名其妙,所...
    ikaroskun阅读 512评论 0 0