并发编程的基本知识

1、概述

所谓并发编程是指在一台处理器上“同时”处理多个任务。并发是在同一实体上的多个事件,多个事件在同一时间间隔发生。java

1.并发和多线程联系

并发与多线程之间的关系就是目的与手段之间的关系。并发的反面是串行。串行比如多个车辆行驶在一股车道上,它们只能“鱼贯而行”。而并发比如多个车辆行驶在多股车道上,它们能够“并驾齐驱”。并发的极致就是并行。多线程就是将本来多是串行的计算“改成”并发(并行)的一种手段、途径或者模型。所以,有时咱们也称多线程编程为并发编程。多线程编程每每是其余并发编程模型的基础,因此多线程编程的重要性不言而喻。多线程能够实现任务并发执行,也能够实现任务串行有序执行。多线程仅是实现并发编程的一种有效手段,固然,目的与手段之间经常是一对多的关系,即实现并发的手段还有别的方式,诸如协程等。算法

2.并发与并行区别

并发和并行是十分容易混淆的概念。并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。实际上,若是系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能经过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出如今拥有多个CPU的系统中。编程

并发:与单位时间有关,在单位时间内能够处理问题的能力。安全

并行:同一时刻,能够同时处理事情的能力。bash

举个例子,假设不考虑超线程技术,一个4核cpu在任何一个时刻处理的是4个线程,并行数为4,而因为时间片轮起色制,它在1秒内能够支持处理100个线程,它在1秒内的并发数为100。数据结构

2、目的&优势

1.充分利用多核CPU的计算能力

并发编程的目标是充分的利用处理器的每个核,以达到最高的处理性能。该形式能够将多核CPU的计算能力发挥到极致,性能获得提高。多线程

2.方便进行业务拆分,提高应用性能

面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分。并发

3、问题&缺点

多线程技术有这么多的好处,难道就没有一点缺点么,就在任何场景下就必定适用么?很显然不是。dom

1.线程安全

多线程改善了系统资源的利用率并提升了系统的处理能力。然而,并发执行也带来了新的问题——死锁。所谓死锁是指多个线程因竞争资源而形成的一种僵局(互相等待),若无外力做用,这些进程都将没法向前推动。在下面会单独说明死锁的问题以及如何避免。ide

2.频繁的上下文切换

时间片是CPU分配给各个线程的时间,由于时间很是短,因此CPU不断经过切换线程,让咱们以为多个线程是同时执行的,时间片通常是几十毫秒。而每次切换时,须要保存当前的状态起来,以便可以进行恢复先前状态,而这个切换时很是损耗性能,过于频繁反而没法发挥出多线程编程的优点。一般减小上下文切换能够采用无锁并发编程,CAS算法,使用最少的线程和使用协程。

无锁并发编程:能够参照concurrentHashMap锁分段的思想,不一样的线程处理不一样段的数据,这样在多线程竞争的条件下,能够减小上下文切换的时间。

CAS算法:利用Atomic下使用CAS算法来更新数据,使用了乐观锁,能够有效的减小一部分没必要要的锁竞争带来的上下文切换。

使用最少线程:避免建立不须要的线程,好比任务不多,可是建立了不少的线程,这样会形成大量的线程都处于等待状态。

协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

因为上下文切换也是个相对比较耗时的操做,因此在"java并发编程的艺术"一书中有过一个实验,并发累加未必会比串行累加速度要快。 可使用Lmbench3测量上下文切换的时长 vmstat测量上下文切换次数。

4、死锁

1.定义

死锁是指多个线程因竞争资源而形成的一种僵局(互相等待),若无外力做用,这些线程都将没法向前推动。

下面咱们经过一些实例来讲明死锁现象。

先看生活中的一个实例,2我的一块儿吃饭可是只有一双筷子,2人轮流吃(同时拥有2只筷子才能吃)。某一个时候,一个拿了左筷子,一人拿了右筷子,2我的都同时占用一个资源,等待另外一个资源,这个时候甲在等待乙吃完并释放它占有的筷子,同理,乙也在等待甲吃完并释放它占有的筷子,这样就陷入了一个死循环,谁也没法继续吃饭。。。 在计算机系统中也存在相似的状况。例如,某计算机系统中只有一台打印机和一台输入设备,进程P1正占用输入设备,同时又提出使用打印机的请求,但此时打印机正被进程P2所占用,而P2在未释放打印机以前,又提出请求使用正被P1占用着的输入设备。这样两个进程相互无休止地等待下去,均没法继续执行,此时两个进程陷入死锁状态。

2.产生的缘由

多个线程同时被阻塞,它们中的一个或者所有都在等待某个资源被释放,而该资源又被其余线程锁定,从而致使每个线程都得等其它线程释放其锁定的资源,形成了全部线程都没法正常结束。死锁产生的四个必要条件:

(1)互斥使用,即当资源被一个线程占用时,别的线程不能使。

(2)不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。

(3)请求和保持,即当资源请求者在请求其余的资源的同时保持对原有资源的占有。

(4)循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就造成了一个等待环路。

当上述四个条件都成立的时候,便造成死锁。固然,死锁的状况下若是打破上述任何一个条件,即可让死锁消失。

下面是产生死锁的一个例子

public class DeadLock implements Runnable {  
    public int flag = 1;  
    //静态对象是类的全部对象共享的  
    private static Object o1 = new Object(), o2 = new Object();  
    @Override  
    public void run() {  
        System.out.println("flag=" + flag);  
        if (flag == 1) {  
            synchronized (o1) {  
                try {  
                    Thread.sleep(500);  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
                synchronized (o2) {  
                    System.out.println("1");  
                }  
            }  
        }  
        if (flag == 0) {  
            synchronized (o2) {  
                try {  
                    Thread.sleep(500);  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
                synchronized (o1) {  
                    System.out.println("0");  
                }  
            }  
        }  
    }  
  
    public static void main(String[] args) {  
          
        DeadLock td1 = new DeadLock();  
        DeadLock td2 = new DeadLock();  
        td1.flag = 1;  
        td2.flag = 0;  
        //td1,td2都处于可执行状态,但JVM线程调度先执行哪一个线程是不肯定的。  
        //td2的run()可能在td1的run()以前运行  
        new Thread(td1).start();  
        new Thread(td2).start();  
  
    }  
}  
复制代码

当DeadLock类的对象flag==1时(td1)线程启动,先锁定o1,睡眠500毫秒;

而td1在睡眠的时候,另外一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒;

td1睡眠结束后须要锁定o2才能继续执行,而此时o2已被td2锁定;

td2睡眠结束后须要锁定o1才能继续执行,而此时o1已被td1锁定;

td一、td2相互等待,都须要获得对方锁定的资源才能继续执行,从而死锁。

3.如何避免死锁

在有些状况下死锁是能够避免的。三种用于避免死锁的技术:

(1)加锁顺序(线程按照必定的顺序加锁)

(2)加锁时限(线程尝试获取锁的时候加上必定的时限,超过期限则放弃对该锁的请求,并释放本身占有的锁)

(3)死锁检测

加锁顺序

当多个线程须要相同的一些锁,可是按照不一样的顺序加锁,死锁就很容易发生。若是能确保全部的线程都是按照相同的顺序得到锁,那么死锁就不会发生。看下面这个例子:

Thread 1:
  lock A 
  lock B

Thread 2:
   wait for A
   lock C (when A locked)

Thread 3:
   wait for A
   wait for B
   wait for C
复制代码

若是一个线程(好比线程3)须要一些锁,那么它必须按照肯定的顺序获取锁。它只有得到了从顺序上排在前面的锁以后,才能获取后面的锁。

例如,线程2和线程3只有在获取了锁A以后才能尝试获取锁C。由于线程1已经拥有了锁A,因此线程2和3须要一直等到锁A被释放。在它们尝试对B或C加锁以前,必须成功地对A加了锁。

按照顺序加锁是一种有效的死锁预防机制。可是,这种方式须要事先知道全部可能会用到的锁并对这些锁作适当的排序,但总有些时候是没法预知的。

加锁时限

另一个能够避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程当中若超过了该时限,则该线程放弃对该锁请求。即线程A没有在给定的时限内成功得到全部须要的锁,则会进行回退并释放全部已经得到的锁,而后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取线程A以前持有的锁,而且让该应用在没有得到锁的时候能够继续运行,不至于卡死(加锁超时后能够先继续运行干点其它事情,再回头来重复以前加锁的逻辑)。

如下例子,展现了两个线程以不一样的顺序尝试获取相同的两个锁,在发生超时后回退并重试的场景:

Thread 1 locks A
Thread 2 locks B
Thread 1 attempts to lock B but is blocked
Thread 2 attempts to lock A but is blocked

Thread 1 lock attempt on B time out
Thread 1 backs up and releases A as well
Thread 1 waits randomly (e.g. 257 millis) before retrying.

Thread 2 lock attempt on A time out
Thread 2 backs up and releases B as well
Thread 2 waits randomly (e.g. 43 millis) before retrying.
复制代码

在上面的例子中,线程2比线程1早200毫秒进行重试加锁,所以它能够先成功地获取到两个锁。这时,线程1尝试获取锁A而且处于等待状态。当线程2结束时,线程1也能够顺利的得到这两个锁(除非线程2或者其它线程在线程1成功得到两个锁以前又得到其中的一些锁)。

须要注意的是,因为存在锁的超时,因此咱们不能认为这种场景就必定是出现了死锁。也多是由于得到了锁的线程须要很长的时间去完成它的任务,致使其它线程超时。

此外,若是有很是多的线程同一时间去竞争同一批资源,就算有超时和回退机制,仍是可能会致使这些线程重复地尝试但却始终得不到锁。若是只有两个线程,而且重试的超时时间设定为0到500毫秒之间,这种现象可能不会发生,可是若是是10个或20个线程状况就不一样了。由于这些线程等待相等的重试时间的几率就高的多(或者很是接近以致于会出现问题)。

死锁检测

死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁 & 锁超时也不可行的场景。

每当一个线程得到了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此以外,每当有线程请求锁,也须要记录在这个数据结构中。

当一个线程请求锁失败时,这个线程能够遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,可是锁7这个时候被线程B持有,这时线程A就能够检查一下线程B是否已经请求了线程A当前所持有的锁。若是线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)。

固然,死锁通常要比两个线程互相持有对方的锁这种状况要复杂的多。线程A等待线程B,线程B等待线程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它须要递进地检测全部被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,而后又找到了线程D,发现线程D请求的锁被线程A本身持有着。这是它就知道发生了死锁。

下面是一幅关于四个线程(A、B、C、D)之间锁占有和请求的关系图。像这样的数据结构就能够被用来检测死锁。

那么当检测出死锁时,这些线程该作些什么呢?

一个可行的作法是释放全部锁&回退,而且等待一段随机的时间后重试。这个和简单的加锁超时相似,不同的是只有死锁已经发生了才回退,而不会是由于加锁的请求超时了。虽然有回退和等待,可是若是有大量的线程竞争同一批锁,它们仍是会重复地死锁(缘由同超时相似,不能从根本上减轻竞争)。

一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁同样继续保持着它们须要的锁。若是赋予这些线程的优先级是固定不变的,同一批线程老是会拥有更高的优先级。为避免这个问题,能够在死锁发生的时候设置随机的优先级。

相关文章
相关标签/搜索