基础概念
进程
进程是操作系统结构的基础;是一次程序的执行;是一个程序及其数据在处理机上顺序执行时所发生的活动。操作系统中,几乎所有运行中的任务对应一条进程(Process)。一个程序进入内存运行,即变成一个进程。进程是处于运行过程中的程序,并且具有一定独立功能。描述进程的有一句话非常经典的话——进程是系统进行资源分配和调度的一个独立单位。
进程是系统中独立存在的实体,拥有自己独立的资源,拥有自己私有的地址空间。进程的实质,就是程序在多道程序系统中的一次执行过程,它是动态产生,动态消亡的,具有自己的生命周期和各种不同的状态。进程具有并发性,它可以同其他进程一起并发执行,按各自独立的、不可预知的速度向前推进。
进程由程序、数据和进程控制块三部分组成。
并发与并行
并发性(concurrency)和并行性(parallel)是不同的。并行指的是同一时刻,多个指令在多台处理器上同时运行。并发指的是同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,看起来就好像多个指令同时执行一样。
线程
线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
线程是程序中一个单一的顺序控制流程。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
在Java Web中要注意,线程是JVM级别的,在不停止的情况下,跟JVM共同消亡,就是说如果一个Web服务启动了多个Web应用,某个Web应用启动了某个线程,如果关闭这个Web应用,线程并不会关闭,因为JVM还在运行,所以别忘了设置Web应用关闭时停止线程。
线程的生命周期及五种基本状态
Java线程具有五种基本状态
新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread()。
就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行。
运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中。
阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1.等待阻塞 – 运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
2.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3.其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
创建多线程的方式
继承Thread类,重写该类的run()方法
package com.demo.test;
public class MyThread extends Thread {
private String name;
public MyThread(String name){
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(name + "运行 :" + i);
}
}
}
package com.demo.test;
public class ThreadTest {
public static void main(String[] args) {
Thread myThread1 = new MyThread("A"); // 创建一个新的线程 myThread1 此线程进入新建状态
Thread myThread2 = new MyThread("B"); // 创建一个新的线程 myThread2 此线程进入新建状态
myThread1.start(); // 调用start()方法使得线程进入就绪状态
myThread2.start(); // 调用start()方法使得线程进入就绪状态
}
}
运行结果:
A运行 :0
B运行 :0
B运行 :1
B运行 :2
B运行 :3
B运行 :4
B运行 :5
B运行 :6
B运行 :7
B运行 :8
A运行 :1
A运行 :2
A运行 :3
B运行 :9
A运行 :4
A运行 :5
A运行 :6
A运行 :7
A运行 :8
A运行 :9
如上所示,继承Thread类,通过重写run()方法定义了一个新的线程类MyThread,其中run()方法的方法体代表了线程需要完成的任务,称之为线程执行体。当创建此线程类对象时一个新的线程得以创建,并进入到线程新建状态。
通过调用线程对象引用的start()方法,使得该线程进入到就绪状态,此时此线程并不一定会马上得以执行,这取决于CPU调度时机。
实现java.lang.Runnable接口
具体做法:实现Runnable接口,并重写该接口的run()方法,该run()方法同样是线程执行体,创建Runnable实现类的实例,并以此实例作为Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象。
package com.demo.test;
public class MyRunnable implements Runnable{
private String name;
public MyRunnable(String name){
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(name + "运行 :" + i);
}
}
}
package com.demo.test;
public class ThreadTest {
public static void main(String[] args) {
Runnable myRunnable = new MyRunnable("A"); // 创建一个Runnable实现类的对象
Thread thread1 = new Thread(myRunnable); // 将myRunnable作为Thread target创建新的线程
Runnable myRunnable1 = new MyRunnable("B");
Thread thread2 = new Thread(myRunnable1);
thread1.start(); // 调用start()方法使得线程进入就绪状态
thread2.start();
}
}
运行结果:
A运行 :0
B运行 :0
B运行 :1
B运行 :2
A运行 :1
A运行 :2
B运行 :3
A运行 :3
A运行 :4
B运行 :4
A运行 :5
A运行 :6
A运行 :7
B运行 :5
A运行 :8
A运行 :9
B运行 :6
B运行 :7
B运行 :8
B运行 :9
使用Callable和Future接口创建线程。
具体是创建Callable接口的实现类,并实现call()方法。并使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。
package pers.hwh.thread;
import java.util.concurrent.Callable;
public class MyCallable implements Callable {
//与run()方法不同的是,call()方法具有返回值
@Override
public Object call() throws Exception {
System.out.println("子线程正在计算:");
Thread.sleep(3000);
int sum=0;
for (int i = 0; i < 100; i++) {
sum+=i;
}
return sum;
}
}
package pers.hwh.thread;
import java.util.concurrent.*;
public class CallableTest {
public static void main(String[] args) {
Callable<Integer> myCallable = new MyCallable();
FutureTask<Integer> ft = new FutureTask<Integer>(myCallable);
Thread th = new Thread(ft);
th.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程正在执行");
try {
int sum = ft.get();
System.out.println("运行结果:sum="+sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}//获取运行结果
System.out.println("所有任务执行完毕!");
}
}
子线程在进行计算
主线程在执行任务
task运行结果,sum = 4950
所有任务执行完毕
首先,我们发现,在实现Callable接口中,此时不再是run()方法了,而是call()方法,此call()方法作为线程执行体,同时还具有返回值!在创建新的线程时,是通过FutureTask来包装MyCallable对象,同时作为了Thread对象的target。那么看下FutureTask类的定义:
public class FutureTask<V> implements RunnableFuture<V>
FutureTask类实现了RunnableFuture接口,我们看一下RunnableFuture接口的实现:
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
于是,我们发现FutureTask类实际上是同时实现了Runnable和Future接口,由此才使得其具有Future和Runnable双重特性。通过Runnable特性,可以作为Thread对象的target,而Future特性,使得其可以取得新创建线程中的call()方法的返回值。
执行下此程序,我们发现sum = 4950永远都是最后输出的。那么为什么sum =4950会永远最后输出呢?原因在于通过ft.get()方法获取子线程call()方法的返回值时,当子线程此方法还未执行完毕,ft.get()方法会一直阻塞,直到call()方法执行完毕才能取到返回值。
上述主要讲解了三种常见的线程创建方式,对于线程的启动而言,都是调用线程对象的start()方法,需要特别注意的是:不能对同一线程对象两次调用start()方法。
Runnable 与 Callable的区别
(1)Runnable是自从java1.1就有了,而Callable是1.5之后才加上去的。
(2)Callable规定的方法是call(),Runnable规定的方法是run()。
(3)
Callable的任务执行后可返回值,而Runnable的任务是不能返回值(是void),这是核心区别。
(4)
call方法可以抛出异常,run方法不可以。
(5)运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
(6)加入线程池运行,Runnable使用ExecutorService的execute方法,Callable使用submit方法。
注意点:
Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取到结果;当不调用此方法时,主线程不会阻塞!
线程调度
线程加入——join()
join —— 让一个线程等待另一个线程完成才继续执行。如A线程执行体中调用B线程的join()方法,则A线程被阻塞,直到B线程执行完为止,A才能得以继续执行。join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个线程运行结束,当前线程再由阻塞转为就绪状态。join是Thread类的一个方法,启动线程后直接调用,join() 的作用:让“主线程”等待“子线程”结束之后才能继续运行。
为什么要用join() 方法?
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。
不加join的情况:
package com.demo.test;
public class Thread1 extends Thread{
private String name;
public Thread1(String name) {
super(name);
this.name=name;
}
public void run() {
System.out.println(Thread.currentThread().getName() + " 线程运行开始!");
for (int i = 0; i < 5; i++) {
System.out.println("子线程"+name + "运行 : " + i);
try {
sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " 线程运行结束!");
}
}
package com.demo.test;
public class Main {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"主线程运行开始!");
Thread1 mTh1=new Thread1("A");
Thread1 mTh2=new Thread1("B");
mTh1.start();
mTh2.start();
System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");
}
}
运行结果:
main主线程运行开始!
main主线程运行结束!
A 线程运行开始!
B 线程运行开始!
子线程B运行 : 0
子线程A运行 : 0
子线程A运行 : 1
子线程B运行 : 1
子线程B运行 : 2
子线程A运行 : 2
子线程A运行 : 3
子线程A运行 : 4
子线程B运行 : 3
子线程B运行 : 4
B 线程运行结束!
A 线程运行结束!
如果不加join()方法,又因为主线程开始后才调用的子线程的start()方法,所以,主线程先执行完毕后,子线程交替执行。
加join 的情况:
package com.demo.test;
public class Main {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"主线程运行开始!");
Thread1 mTh1=new Thread1("A");
Thread1 mTh2=new Thread1("B");
mTh1.start();
mTh2.start();
try {
mTh1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
mTh2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");
}
}
main主线程运行开始!
A 线程运行开始!
B 线程运行开始!
子线程A运行 : 0
子线程B运行 : 0
子线程A运行 : 1
子线程B运行 : 1
子线程A运行 : 2
子线程B运行 : 2
子线程A运行 : 3
子线程B运行 : 3
子线程A运行 : 4
子线程B运行 : 4
A 线程运行结束!
B 线程运行结束!
main主线程运行结束!
加join()方法后,主线程会等待子程序交替执行完毕后再结束。
线程睡眠——sleep()
sleep()是Thread类的静态方法。该方法声明抛出了InterrupedException异常。所以使用时,要么捕捉,要么声明抛出。有两种重载方式:
static void sleep(long millis); //让当前正在执行的线程暂停millis毫秒,并进入阻塞状态,该方法受到系统计时器和线程调度器的精度和准度的影响。
static void sleep(long millis , int nanos); //让当前正在执行的线程暂停millis毫秒加nanos微秒,并进入阻塞状态,该方法受到系统计时器和线程调度器的
精度和准度的影响。
sleep() 的作用是让当前线程休眠,即当前线程会从“运行状态”进入到“休眠(阻塞)状态”。sleep()会指定休眠时间,线程休眠的时间会大于/等于该休眠时间;在线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”,从而等待cpu的调度执行。常用来暂停程序的运行。同时注意,
sleep()方法不会释放锁。
线程让步——yield()
yield()是Thread类的静态方法。它能让当前线程暂停,但不会阻塞该线程,而是由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行。因此,**使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。**但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权,**也有可能是当前线程又进入到“运行状态”继续运行!**值得注意的是,
yield()方法不会释放锁。
package com.demo.test;
public class ThreadYield extends Thread{
public ThreadYield(String name) {
super(name);
}
@Override
public void run() {
for (int i = 1; i <= 50; i++) {
System.out.println("" + this.getName() + "-----" + i);
// 当i为30时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)
if (i ==30) {
this.yield();
}
}
}
}
package com.demo.test;
public class Main {
public static void main(String[] args) {
ThreadYield yt1 = new ThreadYield("A");
ThreadYield yt2 = new ThreadYield("B");
yt1.start();
yt2.start();
}
}
第一种情况:A线程当执行到30时会将CPU时间让掉,这时B线程抢到CPU时间并执行。
第二种情况:A线程当执行到30时会将CPU时间让掉,这时A线程抢到CPU时间并执行。
第三种情况:B线程当执行到30时会将CPU时间让掉,这时A线程抢到CPU时间并执行。
第四种情况:B线程当执行到30时会将CPU时间让掉,这时B线程抢到CPU时间并执行。
也就是说:谁抢到就是谁的。
线程的状态
线程的唤醒interrupt()
import java.util.Date;
public class TestInterrupted implements Runnable {
public void run(){
System.out.println("开始运行run方法,时间:"+(new Date()));
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println("线程已被唤醒!");
}
System.out.println("结束运行run方法,时间:"+(new Date()));
}
}
public class TestMainInterrupted {
public static void main(String[] args) {
TestInterrupted t = new TestInterrupted();
Thread th = new Thread(t);
th.start();
try {
th.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
th.interrupt();
}
}
运行结果:
开始运行run方法,时间:Sat Dec 14 19:18:29 CST 2019
线程已被唤醒!
结束运行run方法,时间:Sat Dec 14 19:18:31 CST 2019
线程的唤醒是指线程从休眠的阻塞状态转变为运行状态,可以通过Thread类的interrupt()方法实现。
该线程th启动后即进入休眠,原本需要休眠5000ms,但是主线程启动后,两秒就将其唤醒。并且,将休眠的饿线程唤醒后会抛出异常,即输出catch语句块的内容。
判断线程是否被中断
1.isInterrupted()是一个实例方法,用于判断当前线程是否已经被中断,若是,则返回true,如果不是返回false。
线程的中断状态不会受该方法的影响。
2.interrupted()是一个静态的方法,用于判断执行中的线程是否已经被中断,若是,返回true,否则返回false,
调用此方法后,线程的中断状态将被消除,即处于中断状态的线程将变为非中断状态。
线程的同步
线程安全问题
其实是指多线程环境下对共享资源的访问可能会引起此共享资源的不一致性。因此,为避免线程安全问题,应该避免多线程环境下对此共享资源的并发访问。
java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。
同步代码块
同步代码块是指用synchronized /ˈsɪŋkrənaɪz/关键字修饰的代码块。obj表示的是任意对象,也可以是假设的。在java中,任意一个对象都有一个同步锁,以下为需要注意的内容:
-
对象O的同步锁,在任意一个时刻最多只能够被一个线程拥有。 -
如果对象O的同步锁被线程T所拥有,那么当其他线程来访问O时,这些线程将被放到对象O的锁池中,并将它们转换为同步阻塞状态。 -
拥有对象O的锁的线程执行完毕后,会自动释放O的锁。若在执行中,线程发生异常退出,也将自动释放O的锁。 -
如果线程T在执行同步代码块的时候,调用了O的wait()方法,则线程T同样会释放O的锁,线程T也将会转变成等待阻塞的状态。 -
如果线程T在执行同步代码块的时候,调用了Thread类的sleep()方法时,线程T将会放弃运行权,即放弃CPU。与此同时,线程T不会放弃O的锁。 -
如果线程T释放了对象O的锁,并放弃运行权,则CPU将会随机分配给对象O锁池中的线程,该线程也将会拥有对象O的锁。
关于synchronized/ˈsɪŋkrənaɪz/关键字的说明:
① 原理
在java中,
每一个对象有且仅有一个同步锁
。这也意味着,同步锁是依赖于对象而存在。当前线程调用某对象的synchronized/ˈsɪŋkrənaɪz/方法时,就获取了该对象的同步锁。例如,synchronized(obj),当前线程就获取了“obj这个对象”的同步锁。
==不同线程对同步锁的访问是互斥的。也就是说,某时间点,对象的同步锁只能被一个线程获取到!==通过同步锁,我们就能在多线程中,实现对“对象/方法”的互斥访问。 例如,现在有个线程A和线程B,它们都会访问“对象obj的同步锁”。假设,在某一时刻,线程A获取到“obj的同步锁”并在执行一些操作;而此时,线程B也企图获取“obj的同步锁” —— 线程B会获取失败,它必须等待,直到线程A释放了“该对象的同步锁”之后线程B才能获取到“obj的同步锁”从而才可以运行。
② 基本规则
第一条:当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的该“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
第二条:当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程仍然可以访问“该对象”的非同步代码块。
第三条:当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的其他的“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
③ 实例锁和全局锁
实例锁 – 锁在某一个实例对象上。如果该类是单例,那么该锁也具有全局锁的概念。实例锁对应的就是synchronized/ˈsɪŋkrənaɪz/关键字。
全局锁 – 该锁针对的是类,无论实例多少个对象,那么线程都共享该锁。全局锁对应的就是static synchronized(或者是锁在该类的class或者classloader对象上)。
就是说,一个非静态方法上的synchronized关键字,代表该方法依赖其所属对象。一个静态方法上synchronized关键字,代表该方法依赖这个类本身。
同步方法
对共享资源进行访问的方法定义中加上synchronized关键字修饰,使得此方法称为同步方法。可以简单理解成对此方法进行了加锁,其锁对象为当前方法所在的对象自身。多线程环境下,当执行此方法时,首先都要获得此同步锁(且同时最多只有一个线程能够获得),只有当线程执行完此同步方法后,才会释放锁对象,其他的线程才有可能获取此同步锁,以此类推…。
public synchronized void run() {
// ....有时间再补充具体例子
}
同步代码块
解决线程安全问题其实只需限制对共享资源访问的不确定性即可。使用同步方法时,使得整个方法体都成为了同步执行状态,会使得可能出现同步范围过大的情况,于是,针对需要同步的代码可以直接另一种同步方式——同步代码块来解决。
synchronized (obj) {
//...有时间再补充具体例子
}
同步锁
同步锁出现在JDK1.5开始时就出现的,之前的版本是不能使用的。可以通过java.util.conncurrent.looks.ReentrantLock类的对象调用Lock()方法来实现锁操作,结束时再用unlock()方法释放同步锁。
线程通信 wait()/notify()/notifyAll()
wait():导致当前线程等待并使其进入到等待阻塞状态。直到其他线程调用该同步锁对象的notify()或notifyAll()方法来唤醒此线程。
public final void wait() throws InterruptedException
public final void wait(long timeout) throws InterruptedException
//指定等待时长,毫秒为单位
public final void wait(long timeout,int nanos) throws InterruptedException
//指定等待时长,毫秒+纳秒,timeout毫秒为单位,nanos纳秒为单位。时间精度更高
notify():唤醒在此同步锁对象上等待的单个线程,如果有多个线程都在此同步锁对象上等待,则会任意选择其中某个线程进行唤醒操作,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。
public final void notify()
notifyAll():唤醒在此同步锁对象上等待的所有线程,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。
public final void notify()
在调用同一个对象的wait()方法和notify()方法的语句必须放在同步代码块中,并且同步代码块使用该对象的同步锁,否则在运行时则会抛出IllegalMonitorStateException异常
死锁
所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
也可以说是
指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
下面我们通过一些实例来说明死锁现象。
先看生活中的一个实例,两个人面对面过独木桥,甲和乙都已经在桥上走了一段距离,即占用了桥的资源,甲如果想通过独木桥的话,乙必须退出桥面让出桥的资源,让甲通过,但是乙不服,为什么让我先退出去,我还想先过去呢,于是就僵持不下,导致谁也过不了桥,这就是死锁。
死锁产生的原因
1、系统资源的竞争
通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞争才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。
2、进程推进顺序非法
进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都会因为所需资源被占用而阻塞。
Java中死锁最简单的情况是,一个线程T1持有锁L1并且申请获得锁L2,而另一个线程T2持有锁L2并且申请获得锁L1,因为默认的锁申请操作都是阻塞的,所以线程T1和T2永远被阻塞了。导致了死锁。这是最容易理解也是最简单的死锁的形式。但是实际环境中的死锁往往比这个复杂的多。可能会有多个线程形成了一个死锁的环路,比如:线程T1持有锁L1并且申请获得锁L2,而线程T2持有锁L2并且申请获得锁L3,而线程T3持有锁L3并且申请获得锁L1,这样导致了一个锁依赖的环路:T1依赖T2的锁L2,T2依赖T3的锁L3,而T3依赖T1的锁L1。从而导致了死锁。
从上面两个例子中,我们可以得出结论,
产生死锁可能性的最根本原因是:线程在获得一个锁L1的情况下再去申请另外一个锁L2,也就是锁L1想要包含了锁L2,也就是说在获得了锁L1,并且没有释放锁L1的情况下,又去申请获得锁L2,这个是产生死锁的最根本原因
。另一个原因是默认的锁申请操作是阻塞的。
死锁产生的必要条件
产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。
(1)互斥条件:一个资源每次只能被一个进程使用。独木桥每次只能通过一个人。
(2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。乙不退出桥面,甲也不退出桥面。
(3)不剥夺条件: 进程已获得的资源,在未使用完之前,不能强行剥夺。甲不能强制乙退出桥面,乙也不能强制甲退出桥面。
(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。如果乙不退出桥面,甲不能通过,甲不退出桥面,乙不能通过。