文档章节

剖析同步器

LCZ777
 LCZ777
发布于 2014/05/26 20:21
字数 2260
阅读 7
收藏 0

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

注:本文的内容是哥本哈根信息技术大学一个由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 }

本文转载自:

LCZ777
粉丝 53
博文 248
码字总数 65899
作品 0
杭州
程序员
私信 提问
ConcurrencyMode.Multiple 模式下的WCF服务就一定是并发执行的吗:探讨同步上下文对并发的影响[下篇]

在《上篇》中,我通过一个具体的实例演示了WCF服务宿主的同步上下文对并发的影响,并简单地介绍了同步上下文是什么东东,以及同步上下文在多线程中的应用。那么,同步上下文在WCF并发体系的内...

长平狐
2012/09/04
454
0
Java系列文章(全)

JVM JVM系列:类装载器的体系结构 JVM系列:Class文件检验器 JVM系列:安全管理器 JVM系列:策略文件 Java垃圾回收机制 深入剖析Classloader(一)--类的主动使用与被动使用 深入剖析Classloader(二...

www19
2017/07/04
0
0
Linux的相关资源帖

http://www.kerneltravel.net/?page_id=8 2.6内核模块编程实例指导 内核模块编程之入门(一)-话说模块 内核模块编程之入门(二)—必备知识 内核模块编程之入门(三)-模块实用程序简介 内核...

AlphaJay
2010/05/21
221
0
剖析 | 高可扩展性、高性能、生产级的 SOFARPC 框架

小蚂蚁说: SOFA(Scalable Open Financial Architecture)是蚂蚁金服自主研发的金融级分布式中间件,包含了构建金融级云原生架构所需的各个组件,是在金融场景里锤炼出来的最佳实践。 本篇文...

平生栗子
2018/10/26
0
0
WCF服务端运行时架构体系详解[下篇]

作为WCF中一个核心概念,终结点在不同的语境中实际上指代不同的对象。站在服务描述的角度,我们所说的终结点实际上是指ServiceEndpoint对象。如果站在WCF服务端运行时框架来说,终结点实际上...

长平狐
2012/09/04
608
0

没有更多内容

加载失败,请刷新页面

加载更多

64.监控平台介绍 安装zabbix 忘记admin密码

19.1 Linux监控平台介绍 19.2 zabbix监控介绍 19.3/19.4/19.6 安装zabbix 19.5 忘记Admin密码如何做 19.1 Linux监控平台介绍: 常见开源监控软件 ~1.cacti、nagios、zabbix、smokeping、ope...

oschina130111
今天
10
0
当餐饮遇上大数据,嗯真香!

之前去开了一场会,主题是「餐饮领袖新零售峰会」。认真听完了餐饮前辈和新秀们的分享,觉得获益匪浅,把脑子里的核心纪要整理了一下,今天和大家做一个简单的分享,欢迎感兴趣的小伙伴一起交...

数澜科技
今天
7
0
DNS-over-HTTPS 的下一代是 DNS ON BLOCKCHAIN

本文作者:PETER LAI ,是 Diode 的区块链工程师。在进入软件开发领域之前,他主要是在做工商管理相关工作。Peter Lai 也是一位活跃的开源贡献者。目前,他正在与 Diode 团队一起开发基于区块...

红薯
今天
6
0
CC攻击带来的危害我们该如何防御?

随着网络的发展带给我们很多的便利,但是同时也带给我们一些网站安全问题,网络攻击就是常见的网站安全问题。其中作为站长最常见的就是CC攻击,CC攻击是网络攻击方式的一种,是一种比较常见的...

云漫网络Ruan
今天
11
0
实验分析性专业硕士提纲撰写要点

为什么您需要研究论文的提纲? 首先当您进行研究时,您需要聚集许多信息和想法,研究论文提纲可以较好地组织你的想法, 了解您研究资料的流畅度和程度。确保你写作时不会错过任何重要资料以此...

论文辅导员
今天
8
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部