剖析同步器

原文链接 作者:Jakob Jenkov 译者:丁一

虽然许多同步器(如锁,信号量,阻塞队列等)功能上各不相同,但它们的内部设计上却差别不大。换句话说,它们内部的的基础部分是相同(或相似)的。了解这些基础部件能在设计同步器的时候给我们大大的帮助。这就是本文要细说的内容。

注:本文的内容是哥本哈根信息技术大学一个由Jakob Jenkov,Toke Johansen和Lars Bjørn参与的M.Sc.学生项目的部分成果。在此项目期间我们咨询Doug Lea是否知道类似的研究。有趣的是在开发Java 5并发工具包期间他已经提出了类似的结论。Doug Lea的研究,我相信,在《Java Concurrency in Practice》一书中有描述。这本书有一章“剖析同步器”就类似于本文,但不尽相同。

大部分同步器都是用来保护某个区域(临界区)的代码,这些代码可能会被多线程并发访问。要实现这个目标,同步器一般要支持下列功能:

  1. 状态
  2. 访问条件
  3. 状态变化
  4. 通知策略
  5. Test-and-Set方法
  6. Set方法

并不是所有同步器都包含上述部分,也有些并不完全遵照上面的内容。但通常你能从中发现这些部分的一或多个。

状态

同步器中的状态是用来确定某个线程是否有访问权限。在Lock中,状态是boolean类型的,表示当前Lock对象是否处于锁定状态。在BoundedSemaphore中,内部状态包含一个计数器(int类型)和一个上限(int类型),分别表示当前已经获取的许可数和最大可获取的许可数。BlockingQueue的状态是该队列中元素列表以及队列的最大容量。

下面是Lock和BoundedSemaphore中的两个代码片段。

01 public class Lock{
02   //state is kept here
03   private boolean isLocked = false;
04   public synchronized void lock()
05   throws InterruptedException{
06     while(isLocked){
07       wait();
08     }
09     isLocked = true;
10   }
11   ...
12 }
01 public class BoundedSemaphore {
02   //state is kept here
03   private int signals = 0;
04   private int bound   = 0;
05        
06   public BoundedSemaphore(int upperBound){
07     this.bound = upperBound;
08   }
09   public synchronized void take() throws InterruptedException{
10     while(this.signals == bound) wait();
11     this.signal++;
12     this.notify();
13   }
14   ...
15 }

访问条件

访问条件决定调用test-and-set-state方法的线程是否可以对状态进行设置。访问条件一般是基于同步器状态的。通常是放在一个while循环里,以避免虚假唤醒问题。访问条件的计算结果要么是true要么是false。

Lock中的访问条件只是简单地检查isLocked的值。根据执行的动作是“获取”还是“释放”,BoundedSemaphore中实际上有两个访问条件。如果某个线程想“获取”许可,将检查signals变量是否达到上限;如果某个线程想“释放”许可,将检查signals变量是否为0。

这里有两个来自Lock和BoundedSemaphore的代码片段,它们都有访问条件。注意观察条件是怎样在while循环中检查的。

01 public class Lock{
02   private boolean isLocked = false;
03   public synchronized void lock()
04   throws InterruptedException{
05     //access condition
06     while(isLocked){
07       wait();
08     }
09     isLocked = true;
10   }
11   ...
12 }
01 public class BoundedSemaphore {
02   private int signals = 0;
03   private int bound = 0;
04    
05   public BoundedSemaphore(int upperBound){
06     this.bound = upperBound;
07   }
08   public synchronized void take() throws InterruptedException{
09     //access condition
10     while(this.signals == bound) wait();
11     this.signals++;
12     this.notify();
13   }
14   public synchronized void release() throws InterruptedException{
15     //access condition
16     while(this.signals == 0) wait();
17     this.signals--;
18     this.notify();
19   }
20 }

状态变化

一旦一个线程获得了临界区的访问权限,它得改变同步器的状态,让其它线程阻塞,防止它们进入临界区。换而言之,这个状态表示正有一个线程在执行临界区的代码。其它线程想要访问临界区的时候,该状态应该影响到访问条件的结果。

Lock中,通过代码设置isLocked = true来改变状态,在信号量中,改变状态的是signals–或signals++;

这里有两个状态变化的代码片段:

01 public class Lock{
02  
03   private boolean isLocked = false;
04  
05   public synchronized void lock()
06   throws InterruptedException{
07     while(isLocked){
08       wait();
09     }
10     //state change
11     isLocked = true;
12   }
13  
14   public synchronized void unlock(){
15     //state change
16     isLocked = false;
17     notify();
18   }
19 }
01 public class BoundedSemaphore {
02   private int signals = 0;
03   private int bound   = 0;
04  
05   public BoundedSemaphore(int upperBound){
06     this.bound = upperBound;
07   }
08  
09   public synchronized void take() throws InterruptedException{
10     while(this.signals == bound) wait();
11     //state change
12     this.signals++;
13     this.notify();
14   }
15  
16   public synchronized void release() throws InterruptedException{
17     while(this.signals == 0) wait();
18     //state change
19     this.signals--;
20     this.notify();
21   }
22 }

通知策略

一旦某个线程改变了同步器的状态,可能需要通知其它等待的线程状态已经变了。因为也许这个状态的变化会让其它线程的访问条件变为true。

通知策略通常分为三种:

  1. 通知所有等待的线程
  2. 通知N个等待线程中的任意一个
  3. 通知N个等待线程中的某个指定的线程

通知所有等待的线程非常简单。所有等待的线程都调用的同一个对象上的wait()方法,某个线程想要通知它们只需在这个对象上调用notifyAll()方法。

通知等待线程中的任意一个也很简单,只需将notifyAll()调用换成notify()即可。调用notify方法没办法确定唤醒的是哪一个线程,也就是“等待线程中的任意一个”。

有时候可能需要通知指定的线程而非任意一个等待的线程。例如,如果你想保证线程被通知的顺序与它们进入同步块的顺序一致,或按某种优先级的顺序来通知。想要实现这种需求,每个等待的线程必须在其自有的对象上调用wait()。当通知线程想要通知某个特定的等待线程时,调用该线程自有对象的notify()方法即可。饥饿和公平中有这样的例子。

下面是通知策略的一个例子(通知任意一个等待线程):

01 public class Lock{
02  
03   private boolean isLocked = false;
04  
05   public synchronized void lock()
06   throws InterruptedException{
07     while(isLocked){
08       //wait strategy - related to notification strategy
09       wait();
10     }
11     isLocked = true;
12   }
13  
14   public synchronized void unlock(){
15     isLocked = false;
16     notify(); //notification strategy
17   }
18 }

Test-and-Set方法

同步器中最常见的有两种类型的方法,test-and-set是第一种(set是另一种)。Test-and-set的意思是,调用这个方法的线程检查访问条件,如若满足,该线程设置同步器的内部状态来表示它已经获得了访问权限。

状态的改变通常使其它试图获取访问权限的线程计算条件状态时得到false的结果,但并不一定总是如此。例如,在读写锁中,获取读锁的线程会更新读写锁的状态来表示它获取到了读锁,但是,只要没有线程请求写锁,其它请求读锁的线程也能成功。

test-and-set很有必要是原子的,也就是说在某个线程检查和设置状态期间,不允许有其它线程在test-and-set方法中执行。

test-and-set方法的程序流通常遵照下面的顺序:

  1. 如有必要,在检查前先设置状态
  2. 检查访问条件
  3. 如果访问条件不满足,则等待
  4. 如果访问条件满足,设置状态,如有必要还要通知等待线程

下面的ReadWriteLock类的lockWrite()方法展示了test-and-set方法。调用lockWrite()的线程在检查之前先设置状态(writeRequests++)。然后检查canGrantWriteAccess()中的访问条件,如果检查通过,在退出方法之前再次设置内部状态。这个方法中没有去通知等待线程。

01 public class ReadWriteLock{
02     private Map<Thread, Integer> readingThreads =
03         new HashMap<Thread, Integer>();
04  
05     private int writeAccesses    = 0;
06     private int writeRequests    = 0;
07     private Thread writingThread = null;
08  
09     ...
10      
11     public synchronized void lockWrite() throws InterruptedException{
12       writeRequests++;
13       Thread callingThread = Thread.currentThread();
14       while(! canGrantWriteAccess(callingThread)){
15         wait();
16       }
17       writeRequests--;
18       writeAccesses++;
19       writingThread = callingThread;
20     }
21      
22     ...
23 }

下面的BoundedSemaphore类有两个test-and-set方法:take()和release()。两个方法都有检查和设置内部状态。

01 public class BoundedSemaphore {
02   private int signals = 0;
03   private int bound   = 0;
04  
05   public BoundedSemaphore(int upperBound){
06     this.bound = upperBound;
07   }
08  
09   public synchronized void take() throws InterruptedException{
10     while(this.signals == bound) wait();
11     this.signals++;
12     this.notify();
13   }
14  
15   public synchronized void release() throws InterruptedException{
16     while(this.signals == 0) wait();
17     this.signals--;
18     this.notify();
19   }
20 }

set方法

set方法是同步器中常见的第二种方法。set方法仅是设置同步器的内部状态,而不先做检查。set方法的一个典型例子是Lock类中的unlock()方法。持有锁的某个线程总是能够成功解锁,而不需要检查该锁是否处于解锁状态。

set方法的程序流通常如下:

  1. 设置内部状态
  2. 通知等待的线程

这里是unlock()方法的一个例子:

1 public class Lock{
2   private boolean isLocked = false;
3    
4   public synchronized void unlock(){
5     isLocked = false;
6     notify();
7   }
8 }
时间: 2024-08-31 09:21:53

剖析同步器的相关文章

Java并发性和多线程介绍目录

Java并发性和多线程介绍 多线程的优点 多线程的代价 并发编程模型 如何创建并运行java线程 竞态条件与临界区 线程安全与共享资源 线程安全及不可变性 Java内存模型 JAVA同步块 线程通信 Java ThreadLocal Thread Signaling (未翻译) 死锁 避免死锁 饥饿和公平 嵌套管程锁死 Slipped Conditions Java中的锁 Java中的读/写锁 重入锁死 信号量 阻塞队列 线程池 CAS 剖析同步器 无阻塞算法 阿姆达尔定律 文章转自 并发编程网

Java并发框架——AQS之如何使用AQS构建同步器

AQS的设计思想是通过继承的方式提供一个模板让大家可以很容易根据不同场景实现一个富有个性化的同步器.同步器的核心是要管理一个共享状态,通过对状态的控制即可以实现不同的锁机制.AQS的设计必须考虑把复杂重复且容易出错的队列管理工作统一抽象出来管理,并且要统一控制好流程,而暴露给子类调用的方法主要就是操作共享状态的方法,以此提供对状态的原子性操作.一般子类的同步器中使用AQS提供的getState.setState.compareAndSetState三个方法,前两个为普通的get和set方法,要使

WCF技术剖析之十一:异步操作在WCF中的应用(下篇)

说完了客户端的异步服务调用(参阅WCF技术剖析之十一:异步操作在WCF中的应用(上篇)),我们在来谈谈服务端如何通过异步的方式为服务提供实现.在定义服务契约的时候,相信大家已经注意到了OperationContractAttribute特性具有一个bool类型的AsynPattern.该属性可以将一个服务操作定义成异步实现模式,接下来的内容主要是着眼于介绍异步操作的定义和实现原理. 一.异步操作的定义和实现原理 实现WCF异步服务操作模式在编程上具有一些限制:异步服务操作是通过两个配对的方法实现

Mongoose源码剖析:外篇之web服务器

引言 在深入Mongoose源码剖析之前,我们应该清楚web服务器是什么?它提供什么服务?怎样提供服务?使用什么协议?客户端如何唯一标识web服务器的资源?下面我们抛开Mongoose,来介绍一个web服务的这些通性. web服务器:通常是指一个计算机程序(web服务器是什么?),在World Wide Web上提供诸如web页面的服务(提供什么服务?),使用HyperText Transfer Protocol(HTTP)(使用什么协议?).当然web服务器也可以指运行这个程序的计算机或虚拟机

剖析在WAS 6.1.0.19上碰到/snoop时执行不完整而出现ClassFormatError

剖析在WAS 6.1.0.19上碰到/snoop时执行不完整而出现ClassFormatError 关键字: WAS Linux 6.1.0.19 snoop ClassFormatError 环境RedHat Linux + WAS 6.1.0.19 部署默认的应用程序 DefaultApplication.ear,访问 /snoop时,出现页面显示不完整,只显示 Servlet Name: 并到 Request Information: 这两项的内容, 然后在页面的源码最下面有一行,虽然源码

通过剖析仿阿宝色图层来练习调色

下面的图是真正的阿宝色效果图,非常养眼的.前期的化妆拍摄占据着重要的地位,而真正的后期秘笈和秘技是不太可能公开的. 一段时间来总看到一些自称为新手的同学问,说怎么样速成PS?这倒是个难题,PS是个实践性很强的学习,只在岸边刻苦学习理论知识,从不下水或者很少下水实践,那么永远不能进步.也就是说学习PS是需要大量的时间来动手操作的,看帖十次不如手动一次.然而真要动起手来,如何下手?怎么下手才能进步更快?可能网络上有很多的捷径和方法.而我个人认为有一个小方法可以更直观地更有效地学习和进步,那就是通过分

TaintDroid深入剖析之启动篇

TaintDroid深入剖析之启动篇 简行.走位@阿里聚安全 1 背景知识 1.1   Android平台软件动态分析现状 众所周知,在计算机领域中所有的软件分析方法都可以归为静态分析和动态分析两大类,在Android平台也不例外.而随着软件加固.混淆技术的不断改进,静态分析越来越难以满足安全人员的分析要求,因此天生对软件加固.混淆免疫的动态分析技术应运而生.虽然动态分析技术本身有很多局限性,诸如:代码覆盖率低,执行效率低下等等,但是瑕不掩瑜,个人认为熟悉各种动态分析技术的核心原理也应当是安全从

HDFS-Architecture剖析

1.概述 从HDFS的应用层面来看,我们可以非常容易的使用其API来操作HDFS,实现目录的创建.删除,文件的上传下载.删除.追加 (Hadoop2.x版本以后开始支持)等功能.然而仅仅局限与代码层面是不够的,了解其实现的具体细节和过程是很有必要的,本文笔者给大家从以下几个方 面进行剖析: Create Delete Read Write Heartbeat 下面开始今天的内容分享. 2.Create 在HDFS上实现文件的创建,该过程并不复杂,Client到NameNode的相关操作,如:修改

BAT解密:互联网技术发展之路(8)- 用户层技术剖析

互联网业务用户层技术主要包括:用户管理.消息推送.存储云.图片云. 用户管理 互联网业务的一个典型特征就是通过互联网将众多分散的用户连接起来,因此用户管理是互联网业务必不可少的一部分. 稍微大一点的互联网业务,肯定会涉及到多个子系统,这些子系统不可能每个都自己来管理这么庞大的用户,由此引申出用户管理的第一个目标:SSO,单点登录,又叫统一登录.单点登录的技术实现手段较多,例如cookie.token等,最有名的开源方案当属CAS. 除此之外,当业务做大成为了平台后,开放成为了促进业务进一步发展的