线程间同步的机制

来源:互联网 发布:淘宝心等级查询 编辑:程序博客网 时间:2024/05/17 14:16

一家之言,仅供擦考。

希望能在J.U.C系列的文章,尽最大的可能将Java的多线程同步的知识用较为轻松的语言描述清晰。


在正式开始J.U.C之前,我想先把一些理论知识描述清楚,毕竟不管是哪门高级语言,理论基础都是不能避开的.


同步和异步

我们在很多场合都会听到这个词,目前java语言的发展,大量的第三库Framework采用异步任务的方式来架构自己的模型。

这个概念在多线程编程中出现的非常频繁,下面来说说我对这两个概念的理解:

1.从OS的发展来看,最早期的单任务OS(硬件环境:单CPU单计算单元),不存在多线程,假设N个Task需要完成,那么OS需要串行化执行这N个任务,这里个N个Task利用CPU一个一个的顺序执行(这里我们可以用同步或者异步去描述这N个任务么?可以说成是这N个任务同步的执行吗?)。

2.随着OS的发展,出现了多任务(多道处理)操作系统,这个技术彻底改变了计算机的运行方式,运行的OS之上的Task不在是 task1  -  > task2 -  >..... -  >taskn串行计算模式,而是利用OS对CPU的调度算法(最常见的CPU时间分片)来并行执行这N个Task的执行(即使依旧是单CPU单计算单元,在人类的感知体系下好像是并行一样)。OS提出了了一个重要的概念就是线程。

什么是线程(Thread)?

a.OS调度CPU的最小单位;

b.OS不能独立为其分配RAM,必须依赖其所属的Process(进程);

c.Task运行的容器。

自从有了线程的概念之后,随之而来的就是异步的概念:多个线程各自独立的运行各自的Task,互相不收影响。所以这个N个任务的执行就被说成了异步执行(隐藏条件:这N个Task各自占用一个线程)。再然后就成了这N个线程异步的执行这N个Task。后面再演变就有了Task1是异步执行的这样的说法。

3.随着运行在OS上的应用发展,这N个Task之间产生了一些微妙的变化,Task1假设是启动音频程序,Task2是某个音乐App。那么就存在Task1必须首先执行完毕,然后Task2才能运行。Task1和Task2分别运行在自己的容器中(Thread A和Thread B),这时有趣的事情发生了,OS在调度CPU的时候要保证Thread A必须完全执行完毕才执行Thread B。然后Thread A和Thread B产生的这种关系被称为同步关系。

在2和3中,我们看到同步和异步说的是线程间的一种关系,而产生线程这种关系的原因是由于Task之间的关系来决定的:Task之间没有关系,则运行Task的线程是异步关系;Task之间存在先后顺序关系,则运行Task的线程是同步关系。现在我们回头来看1中的问题,Task之间不存在关系,它们则应该是异步关系,但是,他们运行的环境可以看成是一个单线程环境,他们的执行有明显的顺序关系,他们则是同步关系。有点晕了,那到底1中的Task之间是什么关系,其实很简单,他们确实没有关系,让Task之间产生关系的实际上是那时计算机的运行模式,所以不用去管怎么称呼它,而且Task之间的关系严格上来说,不应该用同步和异步来描述。


是时候进行一波总结了:

在严谨的知识体系下,我们是在描述下面的概念:

线程间同步:描述OS在调度Thread在CPU的使用权的问题上存在顺序关系。(多线程环境下,线程间的等待唤醒机制)

线程间异步:描述OS在调度Thread在CPU的使用权的问题上没有顺序关系。

在描述业务的时候,我们经常指下面的概念(比如HTTP请求响应式的业务网络模型,Http Server先读后写,存在同步关系):

任务间同步:描述两个Task存在某种顺序关系。

任务间异步:描述两个Task没有顺序关系。


线程间同步和临界资源的互斥访问

在线程间同步的问题上,有一类是多个线程共同操作同一块内存,导致数据不一致.在OS的书中,将这种共享资源统称为临界资源.对于临界资源的访问,需要互斥的进行. 
互斥区域:保证只能有切只有一个线程访问当前的区域。

同步的实现方式分为两种:软件实现和硬件实现(中断屏蔽或者CPU指令).
1965年荷兰的计算科学家Dijkstra提出了线程间的同步的解决方案:信号量

信号量的思路:
让多个线程的使用相同的信号量进行协调,从而达到线程间的同步。

信号量的概念:
用一个整形的数字表示信号量,除了初始化操作外,有且只有两个同步原语对信号量进行写操作,这两个原语分别wait和signal,这两个操作也被称为PV操作原语
P(wait)导致信号量-1;V(signal)导致信号量+1.
这里有个需要注意地方,P和V操作本身各自都是不可分割的原子,但是P和V操作在不同线程进行时,需要保证互斥的进行。

信号量的类型:
二元信号量:信号量的值仅仅由0和1表示,主要用于线程间互斥.
一般信号量:信号量的值由整数构成,主要用户线程间同步.
a.当信号量>0时:  表示临界资源的可用数量;(此时线程可以继续运行)
b.当信号量<=0时:该值的绝对值表示等待该临界资源的线程数量.(此时线程必须等待信号量>0才能运行)
线程的等待一般可分为两类:(设信号量的值为S)
waitsignalblocking等待S = S -1;
if(S<0){
  将当前线程置入
   S的等待队列中,
   当前线程阻塞
}else{
继续运行
}S = S+1;
if(s <=0 ){
将S的等待队列中的线程
取出来一个,并将其置为 就绪状态
}else{
继续运行
}busy等待flag : if(s > 0){
s = s -1;
}else{
loop(s > 0){
  goto flag:
}
}s =  s + 1;

从上面等待模型上可以看出:
阻塞等待的CPU效率应该更高,忙等待有个Loop循环,需要不断的检查信号量,造成CPU时间浪费。

下面来看看信号量的数据结构:
blocking等待:
strcut{
int S;                              //信号量的值
queue waitThreadQueue;//等待信号量的blocking线程队列
}

busy等待:
int S;                                       //只需要一个信号量的值

我们上面反复用到一个概念---原语.
其物理意义是CPU执行的最小单元集.(有可能是一个CPU指令,也有可能是OS包装的多个CPU指令的集合),这个操作不可分割,不能中断.

信号量原语的实现方式:
1.CPU指令集提供实现方式采用busy等待方式;
2.OS实现:blocking等待和busy等待都可以.


OS提供的线程同步方案

 一.Mutex互斥量

理论基础:对二元信号量S的P和V操作在同一个线程上执行,模型P->X->V,保证在X区域成为互斥区域,在X区域进行临界资源的操作,从而保证数据安全.
Mutex互斥量的语义:保证在在同一时间点上只能有一个线程可以访问互斥代码区,在互斥区域对临界资源进行读写操作就都是原子的,从而保证了数据一致性。
Mutex理解:其他可以理解为一把锁,某一时刻线程需要对临界资源进行操作,那么该线程对二元信号量的P操作可以看成是对其加锁,然后进入互斥区域,然后对临界资源做操作,完成后,该线程对该二元信号量进行V操作可以看成是解锁。
Mutex伪代码展示对various这个临界资源的互斥操作:
    share int various;
    share boolean mutex;     //mutex标识(本质上是二元信号量)
    function add(count){
<span style="white-space:pre"></span>P(mutex);         //lock
<span style="white-space:pre"></span>various = various + <span style="font-family: Arial, Helvetica, sans-serif;">count</span>;
<span style="white-space:pre"></span><span style="font-size:14px;">V</span>(mutex);<span style="white-space:pre"></span>  //unLock
    }

递归Mutex和非递归Mutex(可重入锁[Reentrant]和非可重入锁)

递归Mutex可以多次对同一个mutex进行加锁操作,非递归的多次操作加锁会导致死锁.

二.Condition条件变量

理论基础:对二元信号量S的PV操作在不同的线程上执行,这时为了保证P和V操作必须互斥的进行,所以增加一个非递归Mutex,在进行P和V操作的时候,首先需要获得Mutex的锁定。
Condition条件变量的语义:线程在进入Mutex的互斥代码区,等待一个断言(Bool表达式)的值为真才能继续执行,否则需要等待(同时释放Mutex,导致其他资源可以进入互斥区)
关于Condition的使用,一般编程模型如下描述:
a.该bool表达式的读写操作必须配合mutex使用(保证修改该bool表达式的操作线程安全);
b.在进入mutex保护的互斥区内访问临界资源,  对临界资源做出断言,断言成立则继续,否则需要等待(wait方法会将mutex退出,从而可使其他线程进入该mutex保护的其他互斥区).

Condition伪代码展示一个生产消费模型:
share Array queue;  //临界资源
share bool isFullCondition = flase; //二元信号量
share bool isEmptyCondition = true; //二元信号量
share int  size = 0; 
share object mutex; //mutex(非递归)
function put(Object obj){
<span style="white-space:pre"></span>P(mutex);
<span style="white-space:pre"></span>while(isFullCondition){
<span style="white-space:pre"></span>P(<span style="font-family: Arial, Helvetica, sans-serif;">isFullCondition</span><span style="font-family: Arial, Helvetica, sans-serif;">);</span><span style="white-space:pre"></span>}
<span style="white-space:pre"></span>queue.put(obj);
<span style="white-space:pre"></span>size = size + 1;
<span style="white-space:pre"></span>if(size == 10){
<span style="white-space:pre"></span>isFullCondition = true;
<span style="white-space:pre"></span>V(<span style="font-family: Arial, Helvetica, sans-serif;">isEmptyCondition</span><span style="font-family: Arial, Helvetica, sans-serif;">);</span>
<span style="white-space:pre"></span>}
<span style="white-space:pre"></span>V(mutex);
<span style="font-family: Arial, Helvetica, sans-serif;"><span style="white-space:pre"></span>}</span>
function get(){
P(mutex);
while(isEmptyCondition){
P(<span style="font-family: Arial, Helvetica, sans-serif;">isEmptyCondition</span><span style="font-family: Arial, Helvetica, sans-serif;">);</span>
}
queue.get();
<span style="white-space:pre"></span>size = size - 1;
<span style="white-space:pre"></span>if(size == 0){
<span style="white-space:pre"></span>isEmptyCondition = true;
<span style="white-space:pre"></span><span style="font-family: Arial, Helvetica, sans-serif;">V(</span><span style="font-family: Arial, Helvetica, sans-serif;">isFullCondition</span><span style="font-family: Arial, Helvetica, sans-serif;">);</span>
<span style="white-space:pre"></span>}
<span style="white-space:pre"></span>V(mutex);
}

三.Semaphore信号量

这个就是上面理论的实现。不多啰嗦了。





下一章我们看看管程monitor。














0 0