剖析同步器

2024-06-04 06:18
文章标签 剖析 同步器

本文主要是介绍剖析同步器,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

原文链接 作者: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的结果,但并不一定总是如此。例如,在Java中的读/写锁中,获取读锁的线程会更新读写锁的状态来表示它获取到了读锁,但是,只要没有线程请求写锁,其它请求读锁的线程也能成功。

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

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

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

下面的Java中的读/写锁类的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 }

(全文完)


这篇关于剖析同步器的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1029292

相关文章

Node.js 中 http 模块的深度剖析与实战应用小结

《Node.js中http模块的深度剖析与实战应用小结》本文详细介绍了Node.js中的http模块,从创建HTTP服务器、处理请求与响应,到获取请求参数,每个环节都通过代码示例进行解析,旨在帮... 目录Node.js 中 http 模块的深度剖析与实战应用一、引言二、创建 HTTP 服务器:基石搭建(一

深度剖析AI情感陪伴类产品及典型应用 Character.ai

前段时间AI圈内C.AI的受够风波可谓是让大家都丈二摸不着头脑,连C.AI这种行业top应用都要找谋生方法了!投资人摸不着头脑,用户们更摸不着头脑。在这之前断断续续玩了一下这款产品,这次也是乘着这个风波,除了了解一下为什么这么厉害的创始人 Noam Shazeer 也要另寻他路,以及产品本身的发展阶段和情况! 什么是Character.ai? Character.ai官网:https://

最新版 | 深入剖析SpringBoot3源码——分析自动装配原理(面试常考)

文章目录 一、自动配置概念二、半自动配置(误~🙏🙏)三、源码分析1、验证DispatcherServlet的自动配置2、源码分析入口@SpringBootApplication3、@SpringBootConfiguration的@Configuration4、@EnableAutoConfiguration的@AutoConfigurationPackage和@Import5、Auto

C语言深度剖析--不定期更新的第四弹

哈哈哈哈哈哈,今天一天两更! void关键字 void关键字不能用来定义变量,原因是void本身就被编译器解释为空类型,编译器强制地不允许定义变量 定义变量的本质是:开辟空间 而void 作为空类型,理论上不应该开辟空间(针对编译器而言),即使开辟了空间,也只是作为一个占位符看待(针对Linux来说) 所以,既然无法开辟空间,也无法作为正常变量使用,既然无法使用,干脆编译器不让它编译变

Java CAS 原理剖析

在Java并发中,我们最初接触的应该就是synchronized关键字了,但是synchronized属于重量级锁,很多时候会引起性能问题,volatile也是个不错的选择,但是volatile不能保证原子性,只能在某些场合下使用。   像synchronized这种独占锁属于悲观锁,它是在假设一定会发生冲突的,那么加锁恰好有用,除此之外,还有乐观锁,乐观锁的含义就是假设没有发生冲突,那么我正

STL源码剖析之【二分查找】

ForwardIter lower_bound(ForwardIter first, ForwardIter last,const _Tp& val)算法返回一个非递减序列[first, last)中的第一个大于等于值val的位置。      ForwardIter upper_bound(ForwardIter first, ForwardIter last, const _Tp& val

深入剖析 Redis 基础及其在 Java 应用中的实战演练

引言 在现代分布式系统和高并发应用中,缓存系统是不可或缺的一环,而 Redis 作为一种高性能的内存数据存储以其丰富的数据结构和快速的读写性能,成为了众多开发者的首选。本篇博客将详细介绍 Redis 的基础知识,并通过 Java 代码演示其在实际项目中的应用。 目录 什么是 Redis?Redis 数据结构详解Redis 与其他 NoSQL 数据库的对比在 Java 中使用 Redis 使用

我店平台商业模式深度剖析

“我店”平台采用了融合线上与线下资源,并以绿色积分为激励机制的创新商业模式。此模式旨在打造一个集购物、消费及积分兑换为一体的综合性服务平台。编辑v:qawsed2466。以下是对其商业模式的深入剖析: 平台定位与背景 “我店”由上海我店科技网络有限公司创立于2021年8月,作为一个本地生活服务平台,它致力于响应国家的环保政策,并运用绿色积分来促进经济活动,帮助实体店铺吸引客流。面对实体商业

剖析Cookie的工作原理及其安全风险

Cookie的工作原理主要涉及到HTTP协议中的状态管理。HTTP协议本身是无状态的,这意味着每次请求都是独立的,服务器不会保留之前的请求信息。为了在无状态的HTTP协议上实现有状态的会话,引入了Cookie机制。 1. Cookie定义 Cookie,也称为HTTP cookie、web cookie、互联网cookie或浏览器cookie,是一种用于在用户浏览网站时识别用户并为其准备

Yarn 源码 | 分布式资源调度引擎 Yarn 内核源码剖析

曾有人调侃:HBase 没有资源什么事情也做不了,Spark 占用了资源却没有事情可做?   那 YARN了解一下? 01 YARN! 伴随着Hadoop生态的发展,不断涌现了多种多样的技术组件 Hive、HBase、Spark……它们在丰富了大数据生态体系的同时,也引发了新的问题思考。   熟悉大数据底层平台的朋友,应该都了解这些为大数据场景设计的技术组件不仅个个都是消耗资源的大户,而且它们本