计算机能够同时完成多项任务,例如:让浏览器执行0.0001秒,让QQ执行0.0001秒,这就是多线程技术。
计算机中的CPU即使是单核也可以同时运行多个任务,因为操作系统执行多个任务时
就是让CPU对多个任务轮流交替执行。
在一个操作系统中,每个独立执行的程序都可称之为一个进程,也就是“正在运行的程序”。
表面上看是支持进程并发执行的,列如:可以一边听音乐一边聊天。但实际这些进程并不是同时运行的。
由于CPU运行速度很快,能在极短时间内在不同的进程之间进行不断转换,
所以给人同时执行多个程序的感觉。
每个运行的程序都是一个进程,在一个进程中还可以有多个执行单元同时运行,
这些执行单元可以看作程序执行程序的一条条线索,被称为线程。
操作系统中每一进程中都至少存在一个线程。
和多线程相比,多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多进程也有一些缺点:
java中提供两种多线程实现方式,一种是继承java.lang包下的Thread类,覆写Thread类的run()方法,在run()方法中实现运行在线程上的代码;另一种是实现Java.lang.Runnable接口,同样是在run()方法中实现运行在线程上的代码。
先看熟悉的单线程程序
public class Example{
public static void main(String[] args){
MyThread myThread=new MyThread();//创建MyThread实例对象
myThread.run(); //调用MyThread类的run()方法
while(true){
System.out.println("Main方法在运行");
}
}
}
class MyThread{
public void run(){
while(true){ //该循环是死循环
System.out.println("MyThread类的run()方法在运行");
}
}
程序一直打印”MyThread类的run()方法在运行"。这是因为该程序是一个单线程程序。
如果希望程序中的两个while循环中的打印语句能够并发执行,就需要实现多线程。为此java提供一个线程类Thread,通过继承Thread类,并重写Thread类中的run()方法。
在Thread类中,提供一个start()方法用于启动新线程。
修改如下
public class Example02{
public static void main(String[] args){
MyThread myThread=new MyThread(); //创建线程MyThread的线程对象
myThread.start();//开启线程
while(true){//通过死循环语句打印输出
System.out.println("main()方法在运行");
}
}
}
class MyThread extends Thread{
public void run(){
while(true){//通过死循环语句打印输出
System.out.println("MyThread类的run()方法在运行");
}
}
}
通过继承Thread类可以实现多线程,但是这种方式有一定的局限性。因为java只支持单继承,一个类一旦继承某个父类就无法再继承Thread类。
public class Example03{
public static void main(String[] args){
MyThread myThread=new Mythread();//创建MyThread的实例对象
Thread thread =new Thread(myThread);//创建线程对象
thread.start(); //开启线程,执行线程中的run()方法
while(true){
System.out.println("main()方法在运行");
}
}
}
class MyThread implements Runnable{
public void run(){ //线程的代码段,当调用start()方法时,线程从此开始执行
while(true){
System.out.println("MyThread类的run()方法在运行");
}
}
}
既然直接继承Thread类和实现Runnable接口都能实现多线程,那么这两种实现多线程的方式在实际应用中又有什么区别呢?接下来通过一种应用场景来分析。
假设售票厅有四个窗口可发售某日某次列车的100张车票,这时,100张车票可以看做共享资源,四个售票窗口需要创建四个线程。为了更直观显示窗口的售票情况,可以通过Thread的currentThread()方法得到当前的线程的实例对象,然后调用getName()方法可以获取到线程的名称。
public class Example04 {
public static void main(String[] args) {
new TicketWindow().start(); // 创建第一个线程对象TicketWindow并开启
new TicketWindow().start(); // 创建第二个线程对象TicketWindow并开启
new TicketWindow().start(); // 创建第三个线程对象TicketWindow并开启
new TicketWindow().start(); // 创建第四个线程对象TicketWindow并开启
}
}
class TicketWindow extends Thread {
private int tickets = 100;
public void run() {
while (true) { // 通过死循环语句打印语句
if (tickets > 0) {
Thread th = Thread.currentThread(); // 获取当前线程
String th_name = th.getName(); // 获取当前线程的名字
System.out.println(th_name + " 正在发售第 " + tickets-- + " 张票 ");
}
}
}
}
由于现实中铁路系统的票资源是共享的,因此上面的运行结果显然不合理。为了保证资源共享,在程序中只能创建一个售票对象,然后开启多个线程去运行同一个售票对象的售票方法。简单来说就是四个线程运行同一个售票程序,这时就需要用到多线程的第二种实现方式。
接下来,通过实现Runnable接口的方式来实现多线程的创建。修改上述程序,并使用构造方法Thread(Runnable target, String name)在创建线程对象时指定线程的名称。
public class Example05 {
public static void main(String[] args) {
TicketWindow tw = new TicketWindow(); // 创建TicketWindow实例对象tw
new Thread(tw, "窗口1").start(); // 创建线程对象并命名为窗口1,开启线程
new Thread(tw, "窗口2").start(); // 创建线程对象并命名为窗口2,开启线程
new Thread(tw, "窗口3").start(); // 创建线程对象并命名为窗口3,开启线程
new Thread(tw, "窗口4").start(); // 创建线程对象并命名为窗口4,开启线程
}
}
class TicketWindow implements Runnable {
private int tickets = 100;
public void run() {
while (true) {
if (tickets > 0) {
Thread th = Thread.currentThread(); // 获取当前线程
String th_name = th.getName(); // 获取当前线程的名字
System.out.println(th_name + " 正在发售第 " + tickets-- + " 张票 ");
}
}
}
}
通过继承Thread类可以实现多线程,通过实现Runnable接口也可以实现多线程,实现Runnable接口相对于继承Thread类来说,具有以下优势:
(1)适合多个相同程序代码的线程去处理同一个资源的情况,把线程同程序代码、数据有效的分离,很好的体现了面向对象的设计思想。
(2)可以避免由于Java的单继承带来的局限性。在开发中经常碰到这样一种情况,就是使用一个已经继承了某一个类的子类创建线程,由于一个类不能同时有两个父类,因此不能使用继承Thread类的方式,只能采用实现Runnable接口的方式。
在Java中,任何对象都有生命周期,线程也不例外,它也有自己的生命周期。当Thread对象创建完成时,线程的生命周期便开始了。当run()方法中代码正常执行完毕或者线程抛出一个未捕获的异常(Exception)或者错误(Error)时,线程的生命周期便会结束。线程整个生命周期可以分为五个阶段,分别是新建状态(New)、就绪状态(Runnable)、运行状态(Running)、阻塞状态(Blocked)和死亡状态(Terminated),线程的不同状态表明了线程当前正在进行的活动。
接下来针对线程生命周期中的五种状态分别进行详细讲解,具体如下:
下面就列举一下线程由运行状态转换成阻塞状态的原因,以及如何从阻塞状态转换成就绪状态。
当线程试图获取某个对象的同步锁时,如果该锁被其他线程所持有,则当前线程会进入阻塞状态,如果想从阻塞状态进入就绪状态必须得获取到其他线程所持有的锁。
当线程调用了一个阻塞式的IO方法时,该线程就会进入阻塞状态,如果想进入就绪状态就必须要等到这个阻塞的IO方法返回。
当线程调用了某个对象的wait()方法时,也会使线程进入阻塞状态,如果想进入就绪状态就需要使用notify()方法唤醒该线程。
5.死亡状态(Terminated)
当线程调用stop()方法或run()方法正常执行完毕后,或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他状态。
在计算机中,线程调度有两种模型,分别是分时调度模型和抢占式调度模型。所谓分时调度模型是指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU的时间片。抢占式调度模型是指让可运行池中优先级高的线程优先占用CPU,而对于优先级相同的线程,随机选择一个线程使其占用CPU,当它失去了CPU的使用权后,再随机选择其他线程获取CPU使用权。Java虚拟机默认采用抢占式调度模型,通常情况下程序员不需要去关心它,但在某些特定的需求下需要改变这种模式,由程序自己来控制CPU的调度。
在应用程序中,如果要对线程进行调度,最直接的方式就是设置线程的优先级。优先级越高的线程获得CPU执行的机会越大,而优先级越低的线程获得CPU执行的机会越小。线程的优先级用1~10之间的整数来表示,数字越大优先级越高。
程序在运行期间,处于就绪状态的每个线程都有自己的优先级,例如,main线程具有普通优先级。然而线程优先级不是固定不变的,可以通过Thread类的setPriority(int newPriority)方法进行设置,setPriority()方法中的参数newPriority接收的是1~10之间的整数或者Thread类的三个静态常量。
虽然Java中提供了10个线程优先级,但是这些优先级需要操作系统的支持,不同的操作系统对优先级的支持是不一样的,不会和Java中线程优先级一一对应,因此,在设计多线程应用程序时,其功能的实现一定不能依赖于线程的优先级,而只能把线程优先级作为一种提高程序效率的手段。
1 // 定义类MaxPriority实现Runnable接口
2 class MaxPriority implements Runnable {
3 public void run() {
4 for (int i = 0; i < 10; i++) {
5 System.out.println(Thread.currentThread().getName() + "正在输出:" + i);
6 }
7 }
8 }
9 // 定义类MinPriority实现Runnable接口
10 class MinPriority implements Runnable {
11 public void run() {
12 for (int i = 0; i < 10; i++) {
13 System.out.println(Thread.currentThread().getName() + "正在输出:" + i);
14 }
15 }
16 }
17 public class Example06 {
18 public static void main(String[] args) {
19 // 创建两个线程
20 Thread minPriority = new Thread(new MinPriority(), "优先级较低的线程");
21 Thread maxPriority = new Thread(new MaxPriority(), "优先级较高的线程");
22 minPriority.setPriority(Thread.MIN_PRIORITY); // 设置线程的优先级为1
23 maxPriority.setPriority(Thread.MAX_PRIORITY); // 设置线程的优先级为10
24 // 开启两个线程
25 maxPriority.start();
26 minPriority.start();
27 }
28 }
在前面已经讲过线程的优先级,优先级高的程序会先执行,而优先级低的程序会后执行。如果希望人为地控制线程,使正在执行的线程暂停,将CPU让给别的线程,这时可以使用静态方法sleep(long millis),该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。当前线程调用sleep(long millis)方法后,在指定时间(单位毫秒)内该线程是不会执行的,这样其他的线程就可以得到执行的机会了。
sleep(long millis)方法声明会抛出InterruptedException
异常,因此在调用该方法时应该捕获异常,或者声明抛出该异常。
1 public class Example07 {
2 public static void main(String[] args) throws Exception {
3 // 创建一个线程
4 new Thread(new SleepThread()).start();
5 for (int i = 1; i <= 10; i++) {
6 if (i == 5) {
7 Thread.sleep(2000); // 当前线程休眠2秒
8 }
9 System.out.println("主线程正在输出:" + i);
10 Thread.sleep(500); // 当前线程休眠500毫秒
11 }
12 }
13 }
14 // 定义SleepThread类实现Runnable接口
15 class SleepThread implements Runnable {
16 public void run() {
17 for (int i = 1; i <= 10; i++) {
18 if (i == 3) {
19 try {
20 Thread.sleep(2000); // 当前线程休眠2秒
21 } catch (InterruptedException e) {
22 e.printStackTrace();
23 }
24 }
25 System.out.println("SleepThread线程正在输出:" + i);
26 try {
27 Thread.sleep(500); // 当前线程休眠500毫秒
28 } catch (Exception e) {
29 e.printStackTrace();
30 }
31 }
32 }
33 }
在主线程与SleepThread类线程中分别调用了Thread的sleep(500)方法让其线程休眠,目的是让一个线程在打印一次后休眠500毫秒,从而使另一个线程获得执行的机会,这样就可以实现两个线程的交替执行。
从运行结果可以看出,主线程输出2后,SleepThread类线程没有交替输出3,而是主线程接着输出了3和4,这说明了当i等于3时,SleepThread类线程进入了休眠等待状态。对于主线程也一样,当i等于5时,主线程会休眠2000毫秒。
线程让步可以通过yiled()方法来实现,该方法和sleep()方法有点相似,都可以让当前正在运行的线程暂停,区别在于yield()方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。当某个线程调用yield()方法之后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会。
1 // 定义YieldThread类继承Thread类
2 class YieldThread extends Thread {
3 // 定义一个有参的构造方法
4 public YieldThread(String name) {
5 super(name); // 调用父类的构造方法
6 }
7 public void run() {
8 for (int i = 0; i < 6; i++) {
9 System.out.println(Thread.currentThread().getName() + "---" + i);
10 if (i == 3) {
11 System.out.print("线程让步:");
12 Thread.yield(); // 线程运行到此,作出让步
13 }
14 }
15 }
16 }
17 public class Example08 {
18 public static void main(String[] args) {
19 // 创建两个线程
20 Thread t1 = new YieldThread("线程A");
21 Thread t2 = new YieldThread("线程B");
22 // 开启两个线程
23 t1.start();
24 t2.start();
25 }
26 }
在上述代码中,第20-21行代码中创建了两个线程t1和t2,它们的优先级相同。在8~14行代码的for循环中线程在变量i等于3时,调用Thread的yield()方法,使当前线程暂停,这时另一个线程就会获得执行,从运行结果可以看出,当线程B输出3以后,会做出让步,线程A继续执行,同样,线程A输出3后,也会做出让步,线程B继续执行。
现实生活中经常能碰到“插队”的情况,同样,在Thread类中也提供了一个join()方法来实现这个“功能”。当在某个线程中调用其他线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完成后它才会继续运行。
1 public class Example09{
2 public static void main(String[] args) throws Exception {
3 // 创建线程
4 Thread t = new Thread(new EmergencyThread(),"线程一");
5 t.start(); // 开启线程
6 for (int i = 1; i < 6; i++) {
7 System.out.println(Thread.currentThread().getName()+"输入:"+i);
8 if (i == 2) {
9 t.join(); // 调用join()方法
10 }
11 Thread.sleep(500); // 线程休眠500毫秒
12 }
13 }
14 }
15 class EmergencyThread implements Runnable {
16 public void run() {
17 for (int i = 1; i < 6; i++) {
18 System.out.println(Thread.currentThread().getName()+"输入:"+i);
19 try {
20 Thread.sleep(500); // 线程休眠500毫秒
21 } catch (InterruptedException e) {
22 e.printStackTrace();
23 }
24 }
25 }
26 }
在上述代码中,在第4行代码中开启了一个线程t,两个线程的循环体中都调用了Thread的sleep(500)方法,以实现两个线程的交替执行。当main线程中的循环变量为2时,调用t线程的join()方法,这时,t线程就会“插队”优先执行。从运行结果可以看出,当main线程输出2以后,线程一就开始执行,直到线程一执行完毕,main线程才继续执行。
多线程的并发执行可以提高程序的效率,但是,当多个线程去访问同一个资源时,也会引发一些安全问题。例如,当统计一个班级的学生数目时,如果有同学进进出出,则很难统计正确。为了解决这样的问题,需要实现多线程的同步,即限制某个资源在同一时刻只能被一个线程访问。
1 public class Example10 {
2 public static void main(String[] args) {
3 SaleThread saleThread = new SaleThread(); // 创建SaleThread对象
4 // 创建并开启四个线程
5 new Thread(saleThread, "线程一").start();
6 new Thread(saleThread, "线程二").start();
7 new Thread(saleThread, "线程三").start();
8 new Thread(saleThread, "线程四").start();
9 }
10 }
11 // 定义SaleThread类实现Runnable接口
12 class SaleThread implements Runnable {
13 private int tickets = 10; // 10张票
14 public void run() {
15 while (tickets > 0) {
16 try {
17 Thread.sleep(10); // 经过此处的线程休眠10毫秒
18 } catch (InterruptedException e) {
19 e.printStackTrace();
20 }
21 System.out.println(Thread.currentThread().getName() + "---卖出的票"
22 + tickets--);
23 }
24 }
25 }
在上述代码中,第12-25行代码定义了一个SaleThread类并实现了Runnable接口,第13行代码定义了总票数为10,第14-24行代码重写了run()方法,在run()方法中使用while循环售票,并在第17行代码中添加了sleep()方法休眠线程10毫秒,用于模拟售票过程中线程的延迟,最后在第3~8行代码的中创建并开启四个线程。用于模拟四个售票窗口。
在运行结果中,最后打印售出的票出现了0和负数,这种现象是不应该出现的,因为售票程序中只有当票号大于0时才会进行售票。运行结果中之所以出现了负数的票号是因为多线程在售票时出现了安全问题。
出现这样的安全问题的原因是在售票程序的while循环中添加了sleep()方法,由于线程有延迟,当票号减为1时,假设线程1此时出售1号票,对票号进行判断后,进入while循环,在售票之前通过sleep()方法让线程休眠,这时线程二会进行售票,由于此时票号仍为1,因此线程二也会进入循环,同理,四个线程都会进入while循环,休眠结束后,四个线程都会进行售票,这样就相当于将票号减了四次,结果中出现了0、-1、-2这样的票号。
为了实现这种限制,Java中提供了同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放在一个使用synchronized关键字修饰的代码块中,这个代码块被称作同步代码块。使用synchronized关键字创建同步代码块的语法格式如下:
synchronized(lock){ 操作共享资源代码块 }
上面的格式中,lock是一个锁对象,它是同步代码块的关键。当某一个线程执行同步代码块时,其他线程将无法执行当前同步代码块,会发生阻塞,等当前线程执行完同步代码块后,所有的线程开始抢夺线程的执行权,抢到执行权的线程将进入同步代码块,执行其中的代码。循环往复,直到共享资源被处理完为止。这个过程就好比一个公用电话亭,只有前一个人打完电话出来后,后面的人才可以打。
1 //定义Ticket1类继承Runnable接口
2 class Ticket1 implements Runnable {
3 private int tickets = 10; // 定义变量tickets,并赋值10
4 Object lock = new Object(); // 定义任意一个对象,用作同步代码块的锁
5 public void run() {
6 while (true) {
7 synchronized (lock) { // 定义同步代码块
8 try {
9 Thread.sleep(10); // 经过的线程休眠10毫秒
10 } catch (InterruptedException e) {
11 e.printStackTrace();
12 }
13 if (tickets > 0) {
14 System.out.println(Thread.currentThread().getName()
15 + "---卖出的票" + tickets--);
16 } else { // 如果 tickets小于0,跳出循环
17 break;
18 }
19 }
20 }
21 }
22 }
23public class Example11 {
24 public static void main(String[] args) {
25 Ticket1 ticket = new Ticket1(); // 创建Ticket1对象
26 // 创建并开启四个线程
27 new Thread(ticket, "线程一").start();
28 new Thread(ticket, "线程二").start();
29 new Thread(ticket, "线程三").start();
30 new Thread(ticket, "线程四").start();
31 }
32 }
上述代码中,将有关tickets变量的操作全部都放到同步代码块中。为了保证线程的持续执行,将同步代码块放在死循环中,直到ticket<0时跳出循环。从运行结果可以看出,售出的票不再出现0和负数的情况,这是因为售票的代码实现了同步,之前出现的线程安全问题得以解决。运行结果中并没有出现线程二和线程三售票的语句,出现这样的现象是很正常的,因为线程在获得锁对象时有一定的随机性,在整个程序的运行期间,线程二和线程三始终未获得锁对象,所以未能显示它们的输出结果。
同步代码块可以有效解决线程的安全问题,当把共享资源的操作放在synchronized定义的区域内时,便为这些操作加了同步锁。在方法前面同样可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能,具体语法格式如下:
synchronized 返回值类型 方法名([参数1,…]){}
被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行该方法。
1 // 定义Ticket1类实现Runnable接口
2 class Ticket1 implements Runnable {
3 private int tickets = 10;
4 public void run() {
5 while (true) {
6 saleTicket(); // 调用售票方法
7 if (tickets <= 0) {
8 break;
9 }
10 }
11 }
12 // 定义一个同步方法saleTicket()
13 private synchronized void saleTicket() {
14 if (tickets > 0) {
15 try {
16 Thread.sleep(10); // 经过的线程休眠10毫秒
17 } catch (InterruptedException e) {
18 e.printStackTrace();
19 }
20 System.out.println(Thread.currentThread().getName() + "---卖出的票"
21 + tickets--);
22 }
23 }
24 }
25 public class Example12 {
26 public static void main(String[] args) {
27 Ticket1 ticket = new Ticket1(); // 创建Ticket1对象
28 // 创建并开启四个线程
29 new Thread(ticket,"线程一").start();
30 new Thread(ticket,"线程二").start();
31 new Thread(ticket,"线程三").start();
32 new Thread(ticket,"线程四").start();
33 }
34 }
上述代码中,第12~23行代码将售票代码抽取为售票方法saleTicket(),并用synchronized关键字把saleTicket()修饰为同步方法,然后在第6行代码中调用saleTicket()。从图8-16所示的运行结果可以看出,同样没有出现0号和负数号的票,说明同步方法实现了和同步代码块一样的效果。
读者可能会有这样的疑问:同步代码块的锁是自己定义的任意类型的对象,那么同步方法是否也存在锁?如果有,它的锁是什么呢?答案是肯定的,同步方法也有锁,它的锁就是当前调用该方法的对象,也就是this指向的对象。这样做的好处是,同步方法被所有线程所共享,方法所在的对象相对于所有线程来说是唯一的,从而保证了锁的唯一性。当一个线程执行该方法时,其他的线程就不能进入该方法中,直到这个线程执行完该方法为止。从而达到了线程同步的效果。
有这样一个场景:一个中国人和一个美国人在一起吃饭,美国人拿了中国人的筷子,中国人拿了美国人的刀叉,两个人开始争执不休:
中国人:“你先给我筷子,我再给你刀叉!”
美国人:“你先给我刀叉,我再给你筷子!”
结果可想而知,两个人都吃不到饭。这个例子中的中国人和美国人相当于不同的线程,筷子和刀叉就相当于锁。两个线程在运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象称为死锁。
1 class DeadLockThread implements Runnable {
2 static Object chopsticks = new Object(); // 定义Object类型的chopsticks锁对象
3 static Object knifeAndFork = new Object(); // 定义Object类型的knifeAndFork锁对象
4 private boolean flag; // 定义boolean类型的变量flag
5 DeadLockThread(boolean flag) { // 定义有参的构造方法
6 this.flag = flag;
7 }
8 public void run() {
9 if (flag) {
10 while (true) {
11 synchronized (chopsticks) { // chopsticks锁对象上的同步代码块
12 System.out.println(Thread.currentThread().getName()
13 + "---if---chopsticks");
14 synchronized (knifeAndFork) { // knifeAndFork锁对象上的同步代码块
15 System.out.println(Thread.currentThread().getName()
16 + "---if---knifeAndFork");
17 }
18 }
19 }
20 }
21 } else {
22 while (true) {
23 synchronized (knifeAndFork) { // knifeAndFork锁对象上的同步代码块
24 System.out.println(Thread.currentThread().getName()
25 + "---else---knifeAndFork");
26 synchronized (chopsticks) { // chopsticks锁对象上的同步代码块
27 System.out.println(Thread.currentThread().getName()
28 + "---else---chopsticks");
29 }
30 }
31 }
32 }
33 }
34 }
35 public class Example13 {
36 public static void main(String[] args) {
37 // 创建两个DeadLockThread对象
38 DeadLockThread d1 = new DeadLockThread(true);
39 DeadLockThread d2 = new DeadLockThread(false);
40 // 创建并开启两个线程
41 new Thread(d1, "Chinese").start(); // 创建开启线程Chinese
42 new Thread(d2, "American").start(); // 创建开启线程American
43 }
44 }
在上述代码中,第1-33行代码的DeadLockThread类中创建了Chinese和American两个线程,分别执行run()方法中if和else代码块中的同步代码块。第10~19行代码中设置Chinese线程中拥有chopsticks锁,只有获得knifeAndFork锁才能执行完毕;第21 ~30行代码中设置American线程拥有knifeAndFork锁,只有获得chopsticks锁才能执行完毕。两个线程都需要对方所占用的锁,但是都无法释放自己所拥有的锁,于是这两个线程都处于挂起状态,从而造成了死锁。