多线程
进程:进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程
什么是多线程呢?即就是一个程序中有多个线程在同时执行。
通过下图来区别单线程程序与多线程程序的不同:
l 单线程程序:即,若有多个任务只能依次执行。当上一个任务执行结束后,下一个任务开始执行。如,去网吧上网,网吧只能让一个人上网,当这个人下机后,下一个人才能上网。
l 多线程程序:即,若有多个任务可以同时执行。如,去网吧上网,网吧能够让多个人同时上网
创建多线程方式:
1.Thread
public class ThreadTest01 extends Thread{
private String url;
private String name;
public ThreadTest01(String url,String name){
this.url=url;
this.name=name;
}
//多线程执行体
@Override
public void run() {
WebDownLoader webDownLoader = new WebDownLoader();
webDownLoader.downLoad(url,name);
System.out.println("下载好了文件名为"+name);
}
public static void main(String[] args) {
ThreadTest01 t1 = new ThreadTest01("https://dss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=2252107727,1607907313&fm=11&gp=0.jpg", "1.jpg");
ThreadTest01 t2 = new ThreadTest01("https://dss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1978332745,2310111244&fm=11&gp=0.jpg", "2.jpg");
ThreadTest01 t3 = new ThreadTest01("https://dss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=668812725,634795817&fm=11&gp=0.jpg", "3.jpg");
t1.start();
t2.start();
t3.start();
}
}
class WebDownLoader{
//下载方法
public void downLoad(String url,String name){
try {
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("io异常");
}
}
}
2.Runnable
//创建线程方法2:实现Runnable接口,重写run方法,把Runnable接口实现类丢入执行线程,调用start方法
public class ThreadTest02 implements Runnable {
@Override
public void run() {
for (int i = 0; i <200 ; i++) {
System.out.println("我在看代码"+i);
}
}
public static void main(String[] args) {
//创建Runnable接口实现类
ThreadTest02 threadTest02 = new ThreadTest02();
//创建线程对象,通过线程对象来开启我们的线程,代理
Thread thread=new Thread(threadTest02);
thread.start();
for (int i = 0; i <1000 ; i++) {
System.out.println("现在学习多线程"+i);
}
}
}
3.实现callable接口
FutureTask介绍
Callable需要使用FutureTask类帮助执行,FutureTask类结构如下:
Future接口:
判断任务是否完成:isDone()
能够中断任务:cancel()
能够获取任务执行结果:get()
public class MyCallable implements Callable {
@Override
public Object call() throws Exception {
for (int i = 0; i <10 ; i++) {
System.out.println(Thread.currentThread().getName()+"执行时间是:"+new Date()+"循环次数是"+i);
}
return "MyCallable执行完成";
}
public static void main(String[] args) {
//创建FutureTask实例,创建Callable实例
FutureTask<String> task = new FutureTask<String>(new MyCallable());
//创建Thread实例,执行FutureTask
new Thread(task).start();
//获取MyCallable执行结果
try {
String result = task.get();
} catch (Exception e) {
e.printStackTrace();
System.out.println("执行结果出错");
}
}
}
4.使用线程池Executor
线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
我们详细的解释一下为什么要使用线程池?
在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。
线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。
常见的池化技术:Tomcat 线程池、数据库连接池、HTTP 连接池等。
Executor接口:
声明了execute(Runnable runnable)方法,执行任务代码
ExecutorService接口:
继承Executor接口,声明方法:submit、invokeAll、invokeAny以及shutDown等
AbstractExecutorService抽象类:
实现ExecutorService接口,基本实现ExecutorService中声明的所有方法
ScheduledExecutorService接口:
继承ExecutorService接口,声明定时执行任务方法
ThreadPoolExecutor类:
继承类AbstractExecutorService,实现execute、submit、shutdown、shutdownNow方法
ScheduledThreadPoolExecutor类:
继承ThreadPoolExecutor类,实现ScheduledExecutorService接口并实现其中的方法
Executors类:
提供快速创建线程池的方法
如何创建线程池
Java 中创建线程池有以下两种方式:
- 通过 ThreadPoolExecutor 类创建(推荐)
- 通过 Executors 类创建
其实这两种方式在本质上是一种方式,都是通过 ThreadPoolExecutor 类的方式创建,因为 Exexutors 类调用了 ThreadPoolExecutor 类的方法。
1 ThreadPoolExecutor 方式
查看 JDK1.8 的源码,ThreadPoolExecutor 类源码有四个构造函数:
ThreadPoolExecutor 类在 java.util.concurrent 包下,部分源码:
package java.util.concurrent;
public class ThreadPoolExecutor extends AbstractExecutorService {
//七个参数的构造函数
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize keepAliveTime 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
//...其他
}
1. 参数 corePoolSize
- 表示线程池的常驻核心线程数。如果设置为 0,则表示在没有任何任务时,销毁线程池;如果大于 0,即使没有任务时也会保证线程池的线程数量等于此值;
- 应该结合实际业务设置此值的大小。若 corePoolSize 的值较小,则会出现频繁创建和销毁线程情况;若值较大,则会浪费系统资源。
2. 参数 maximumPoolSize
- 表示线程池最大可以创建的线程数。官方规定此值必须大于 0,也必须大于等于 corePoolSize 的值;
- 此值只有在任务比较多,且不能存放在任务队列时,才会用到。
3. 参数 keepAliveTime
- 表示线程的存活时间。当线程池空闲时并且超过了此时间,多余的线程就会销毁,直到线程池中的线程数等于 corePoolSize 的值为止;
- 若 maximumPoolSize 的值 等于 corePoolSize 的值,则线程池在空闲的时候不会销毁任何线程。
4. 参数 unit
- 表示存活时间的单位,配合 keepAliveTime 参数共同使用。
5. 参数 workQueue
- 表示线程池执行的任务队列;
- 当线程池的所有线程都在处理任务时,若来了新任务则会缓存到此任务队列中,然后等待执行。
6. 参数 threadFactory
- 表示线程的创建工厂,一般使用默认的线程创建工厂的方法 Executors.defaultThreadFactory()来创建线程。
7. 参数 RejectedExecutionHandler
- 表示指定线程池的拒绝策略,属于一种限流保护的机制;
- 当线程池的任务已经在缓存队列 workQueue 中存满了之后,并且不能创建新的线程来执行此任务时,就会用到此拒绝策略
- 四种拒绝策略
(1) AbortPolicy: 丢弃任务并抛出异常。
(2) DiscardPolicy:丢弃任务但不抛出异常。
(3) DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务
(4) CallerRunsPolicy:由调用线程处理该任务
2 Executors 方式
查看 JDK1.8 的源码,Executors 类源码有 12 个创建线程的静态方法:
Executors 类在 java.util.concurrent 包下,部分源码:E
package java.util.concurrent;
public class Executors {
//参数一般使用默认的,对编程不可见
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}//...其他
}
从源码看来,Exexutors 类本质上调用了 ThreadPoolExecutor 类的构造方法,实现线程的创建。
public class MyRunnale implements Runnable {
@Override
public void run() {
for (int i = 0; i <10 ; i++) {
System.out.println(Thread.currentThread().getName()+"当前执行时间"+System.currentTimeMillis());
}
}
public static void main(String[] args) {
//使用线程池创建线程
//使用Executors获取线程池对象
ExecutorService executorService = Executors.newFixedThreadPool(10);
//使用线程池对象获取线程并执行MyRunnable实例
executorService.execute(new MyRunnale());
for (int i = 0; i <10 ; i++) {
System.out.println("主线程"+"执行次数"+i);
}
}
}
执行流程
线程池开始工作,通过执行方法 execute()开始,此方法源码:
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); //Proceed in 3 steps: int c = ctl.get(); //Step1 当前工作的线程数小于核心线程数corePoolSize值 if (workerCountOf(c) // 则创建新的线程执行此任务 if (addWorker(command, true)) return; c = ctl.get(); } //Step2 检查线程池是否处于运行状态,如果是则把任务添加到队列 if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); // 如果是非运行状态,则将刚加入队列的任务移除 if (! isRunning(recheck) && remove(command)) reject(command); // 如果线程池的线程数为 0 时(corePoolSize 为 0 ) else if (workerCountOf(recheck) == 0) // 新建线程执行任务 addWorker(null, false); } //Step3 核心线程都在忙且队列都已爆满,尝试新启动一个线程执行失败 else if (!addWorker(command, false)) // 执行拒绝策略 reject(command); }
线程池任务执行的主要工作流程:
- 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
- 当执行 execute() 方法添加一个任务时,线程池会判断:(a) 若正在运行的线程数量小于 corePoolSize 值,则立刻创建线程运行此任务;(b) 若正在运行的线程数量大于或等于 corePoolSize 值,则将此任务放入队列;(c) 若此时队列满了,而且正在运行的线程数量小于 maximumPoolSize 值,则创建非核心线程立刻运行这个任务;(d) 若队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize 值,那么线程池会执行设置的拒绝策略。
- 当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 当一个线程空闲时,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize 值,那么这个线程会被销毁。
线程池任务执行的主要流程图如下
-
实现接口和继承Thread类比较
- 接口更适合多个相同的程序代码的线程去共享同一个资源。
- 接口可以避免java中的单继承的局限性。
- 接口代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runable或Callable接口的线程,不能直接放入继承Thread的类。
扩充:
在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。
-
Runnable和Callable接口比较
相同点:
- 两者都是接口;
- 两者都可用来编写多线程程序;
- 两者都需要调用Thread.start()启动线程;
不同点:
- 实现Callable接口的线程能返回执行结果;而实现Runnable接口的线程不能返回结果;
- Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的不允许抛异常;
- 实现Callable接口的线程可以调用Future.cancel取消执行 ,而实现Runnable接口的线程不能
注意点:
Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!
线程生命周期
线程的生命周期会经历以下几个状态:
新建:new创建线程对象时
就绪:调用 start()方法时
运行:调用 run()方法时
阻塞: 多种原因可导致阻塞
死亡:多种原因
1. 新建
new关键字创建了一个线程之后,该线程就处于新建状态
JVM为线程分配内存,初始化成员变量值
2. 就绪
当线程对象调用了start()方法之后,该线程处于就绪状态
JVM为线程创建方法栈和程序计数器,等待线程调度器调度
3. 运行
就绪状态的线程获得CPU资源,开始运行run()方法,该线程进入运行状态
4. 阻塞
当发生如下情况时,线程将会进入阻塞状态
线程调用sleep()方法主动放弃所占用的处理器资源
线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
线程试图获得一个同步锁(同步监视器),但该同步锁正被其他线程所持有。
线程在等待某个通知(notify)
程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法
5. 死亡
线程会以如下3种方式结束,结束后就处于死亡状态:
run()或call()方法执行完成,线程正常结束。
线程抛出一个未捕获的Exception或Error。
调用该线程stop()方法来结束该线程,该方法容易导致死锁,不推荐使用。
线程安全问题
并发问题
public class ThreadTest03 implements Runnable {
private static int ticketNum=10;
@Override
public void run() {
while (true){
if(ticketNum<=0){
break;
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"--->拿到第"+ticketNum-- +"---票");
}
}
public static void main(String[] args) {
ThreadTest03 threadTest03=new ThreadTest03();
new Thread(threadTest03,"小明").start();
new Thread(threadTest03,"小华").start();
new Thread(threadTest03,"小花").start();
}
}
问题分析
线程安全问题都是由全局变量及静态变量引起的。
若每个线程对全局变量、静态变量只读,不写,一般来说,这个变量是线程安全的;
若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
综上所述,线程安全问题根本原因:
多个线程在操作共享的数据;
操作共享数据的线程代码有多条;
多个线程对共享数据有写操作;
问题解决-线程同步
要解决以上线程问题,只要在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
为了保证每个线程都能正常执行共享资源操作,Java引入了7种线程同步机制。
1) 同步代码块(synchronized)
2) 同步方法(synchronized)
3) 同步锁(ReenreantLock)
4) 特殊域变量(volatile)
5) 局部变量(ThreadLocal)
6) 阻塞队列(LinkedBlockingQueue)
7) 原子变量(Atomic*)
同步代码块(synchronized)
同步代码块 :
synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
语法:
synchronized(同步锁){
需要同步操作的代码
}
同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
- 锁对象可以是任意类型。
- 多个线程要使用同一把锁。
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
public class ThreadTest03 implements Runnable {
private static int ticketNum=10;
/**
* 一个锁对象
*/
private Object obj=new Object();
@Override
public void run() {
while (true){
synchronized (obj){
if(ticketNum<=0){
break;
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"--->拿到第"+ticketNum-- +"---票");
}
}
}
public static void main(String[] args) {
ThreadTest03 threadTest03=new ThreadTest03();
new Thread(threadTest03,"小明").start();
new Thread(threadTest03,"小华").start();
new Thread(threadTest03,"小花").start();
}
}
同步方法:
使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
格式:
public synchronized void method(){
可能会产生线程安全问题的代码
}
同步锁是谁?
- 对于非static方法,同步锁就是this。
- 对于static方法,同步锁是当前方法所在类的字节码对象(类名.class)。
public class ThreadTest04 implements Runnable {
private static int ticketNum=10;
@Override
public void run() {
while (true){
saleTickets();
}
}
public static void main(String[] args) {
ThreadTest03 threadTest03=new ThreadTest03();
new Thread(threadTest03,"小明").start();
new Thread(threadTest03,"小华").start();
new Thread(threadTest03,"小花").start();
}
private synchronized void saleTickets(){
if(ticketNum>0){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"--->拿到第"+ticketNum-- +"---票");
}
}
}
同步锁:
java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
同步锁方法:
public void lock() :加同步锁。
public void unlock() :释放同步锁。
public class ThreadTest05 implements Runnable {
private static int ticketNum=10;
//定义锁对象:构造函数参数为线程是否公平获取锁true-公平;false-不公平,即由某个线程独占,默认是false
Lock lock = new ReentrantLock(true);
@Override
public void run() {
while(true){
//加锁
lock.lock();
try {
if(ticketNum>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"--->拿到第"+ticketNum-- +"---票");
}
}finally {
//释放锁
lock.unlock();
}
}
}
public static void main(String[] args) {
ThreadTest05 threadTest05 = new ThreadTest05();
new Thread(threadTest05,"小红").start();
new Thread(threadTest05,"小明").start();
new Thread(threadTest05,"小白").start();
}
}
小结:
Synchronized和Lock区别
- synchronized是java内置关键字,在jvm层面,Lock是个java类;
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
- synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
- 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
- synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题
线程死锁
1.什么是死锁
多线程以及多进程改善了系统资源的利用率并提高了系统的处理能力。然而,并发执行也带来了新的问题--死锁。
所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
死锁产生的必要条件
以下这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
1. 互斥条件
进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
2. 不可剥夺条件
进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
3. 请求与保持条件
进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
.4. 循环等待条件
存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有,如图所示。
死锁代码示例:
public class DeadLockDemo implements Runnable{
//决定线程走向的标记
private int flag;
//static静态变量是所有实例所共享的
private static Object obj1=new Object();
private static Object obj2=new Object();
public DeadLockDemo() {
}
public DeadLockDemo(int flag) {
this.flag = flag;
}
@Override
public void run() {
if(flag==1){
synchronized (obj1){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2){
System.out.println("flag:"+flag);
}
}
}else {
System.out.println("flag:"+flag);
synchronized (obj2){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj1){
System.out.println("flag:"+flag);
}
}
}
}
public static void main(String[] args) {
DeadLockDemo deadLock1 = new DeadLockDemo(1);
DeadLockDemo deadLock2 = new DeadLockDemo(2);
Thread thread1 = new Thread(deadLock1);
Thread thread2 = new Thread(deadLock2);
thread1.start();
thread2.start();
}
}
死锁处理
预防死锁:通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来防止死锁的发生。
避免死锁:在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免死锁的发生。
检测死锁:允许系统在运行过程中发生死锁,但可设置检测机构及时检测死锁的发生,并采取适当措施加以清除。
解除死锁:当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来。
1.死锁预防
预防死锁是设法至少破坏产生死锁的四个必要条件之一,严格的防止死锁的出现。
1.1. 破坏“互斥”条件
“互斥”条件是无法破坏的。因此,在死锁预防里主要是破坏其他几个必要条件,而不去涉及破坏“互斥”条件。
1.2. 破坏“占有并等待”条件
破坏“占有并等待”条件,就是在系统中不允许进程在已获得某种资源的情况下,申请其他资源。即要想出一个办法,阻止进程在持有资源的同时申请其他资源。
方法一:一次性分配资源,即创建进程时,要求它申请所需的全部资源,系统或满足其所有要求,或什么也不给它。
方法二:要求每个进程提出新的资源申请前,释放它所占有的资源。这样,一个进程在需要资源S时,须先把它先前占有的资源R释放掉,然后才能提出对S的申请,即使它可能很快又要用到资源R。
1.3. 破坏“不可抢占”条件
破坏“不可抢占”条件就是允许对资源实行抢夺。
方法一:如果占有某些资源的一个进程进行进一步资源请求被拒绝,则该进程必须释放它最初占有的资源,如果有必要,可再次请求这些资源和另外的资源。
方法二:如果一个进程请求当前被另一个进程占有的一个资源,则操作系统可以抢占另一个进程,要求它释放资源。只有在任意两个进程的优先级都不相同的条件下,方法二才能预防死锁。
1.4. 破坏“循环等待”条件
破坏“循环等待”条件的一种方法,是将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。这样做就能保证系统不出现死锁。
2. 死锁避免
避免死锁不严格限制产生死锁的必要条件的存在,因为即使死锁的必要条件存在,也不一定发生死锁。
2.1. 有序资源分配法
该算法实现步骤如下:
必须为所有资源统一编号,例如打印机为1、传真机为2、磁盘为3等
同类资源必须一次申请完,例如打印机和传真机一般为同一个机器,必须同时申请
不同类资源必须按顺序申请
例如:有两个进程P1和P2,有两个资源R1和R2
P1请求资源:R1、R2
P2请求资源:R1、R2
这样就破坏了环路条件,避免了死锁的发生。
2.2. 银行家算法
银行家算法(Banker's Algorithm)是一个避免死锁(Deadlock)的著名算法,是由艾兹格•迪杰斯特拉在1965年为T.H.E系统设计的一种避免死锁产生的算法。它以银行借贷系统的分配策略为基础,判断并保证系统的安全运行。流程图如下:
银行家算法的基本思想是分配资源之前,判断系统是否是安全的;若是,才分配。它是最具有代表性的避免死锁的算法。
设进程i提出请求REQUEST [i],则银行家算法按如下规则进行判断。
- 如果REQUEST [i]<= NEED[i,j],则转(2);否则,出错。
- 如果REQUEST [i]<= AVAILABLE[i],则转(3);否则,等待。
- 系统试探分配资源,修改相关数据:
AVAILABLE[i]-=REQUEST[i];//可用资源数-请求资源数
ALLOCATION[i]+=REQUEST[i];//已分配资源数+请求资源数
NEED[i]-=REQUEST[i];//需要资源数-请求资源数
4.系统执行安全性检查,如安全,则分配成立;否则试探险性分配作废,系统恢复原状,进程等待。
2.3. 顺序加锁
当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。
例如以下两个线程就会死锁:
Thread 1:
lock A (when C locked)
lock B (when C locked)
wait for C
Thread 2:
wait for A
wait for B
lock C (when A locked)
如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。 例如以下两个线程就不会死锁
Thread 1:
lock A
lock B
lock C
Thread 2:
wait for A
wait for B
wait for C
按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要事先知道所有可能会用到的锁,但总有些时候是无法预知的,所以该种方式只适合特定场景。
2.4. 限时加锁
限时加锁是线程在尝试获取锁的时候加一个超时时间,若超过这个时间则放弃对该锁请求,并回退并释放所有已经获得的锁,然后等待一段随机的时间再重试
以下是一个例子,展示了两个线程以不同的顺序尝试获取相同的两个锁,在发生超时后回退并重试的场景:
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’s lock attempt on B times out
Thread 1 backs up and releases A as well
Thread 1 waits randomly (e.g. 257 millis) before retrying.
Thread 2’s lock attempt on A times 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也可以顺利的获得这两个锁。
这种方式有两个缺点:
1) 当线程数量少时,该种方式可避免死锁,但当线程数量过多,这些线程的加锁时限相同的概率就高很多,可能会导致超时后重试的死循环。
2) Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用Java5中java.util.concurrent包下的工具。
3.3. 死锁检测
预防和避免死锁系统开销大且不能充分利用资源,更好的方法是不采取任何限制性措施,而是提供检测和解脱死锁的手段,这就是死锁检测和恢复。
死锁检测数据结构:
E是现有资源向量(existing resource vector),代码每种已存在资源的总数
A是可用资源向量(available resource vector),那么Ai表示当前可供使用的资源数(即没有被分配的资源)
C是当前分配矩阵(current allocation matrix),C的第i行代表Pi当前所持有的每一种类型资源的资源数
R是请求矩阵(request matrix),R的每一行代表P所需要的资源的数量
死锁检测步骤:
1) 寻找一个没有结束标记的进程Pi,对于它而言R矩阵的第i行向量小于或等于A。
2) 如果找到了这样一个进程,执行该进程,然后将C矩阵的第i行向量加到A中,标记该进程,并转到第1步
3) 如果没有这样的进程,那么算法终止
4) 算法结束时,所有没有标记过的进程都是死锁进程。
3.4. 死锁恢复
利用抢占恢复。
临时将某个资源从它的当前所属进程转移到另一个进程。
这种做法很可能需要人工干预,主要做法是否可行需取决于资源本身的特性。
利用回滚恢复
周期性的将进程的状态进行备份,当发现进程死锁后,根据备份将该进程复位到一个更早的,还没有取得所需的资源的状态,接着就把这些资源分配给其他死锁进程。
通过杀死进程恢复
最直接简单的方式就是杀死一个或若干个进程。
尽可能保证杀死的进程可以从头再来而不带来副作用。