进程和线程的关系就是:一个进程能够包含一个或多个线程,但至少会有一个线程。java
操做系统调度的最小任务单位其实不是进程,而是线程。经常使用的Windows、Linux等操做系统都采用抢占式多任务,如何调度线程彻底由操做系统决定,程序本身不能决定何时执行,以及执行多长时间。数据库
Java语言内置了多线程支持:一个Java程序其实是一个JVM进程,JVM进程用一个主线程来执行main()
方法,在main()
方法内部,咱们又能够启动多个线程。此外,JVM还有负责垃圾回收的其余工做线程等。编程
所以,对于大多数Java程序来讲,咱们说多任务,其实是说如何使用多线程实现多任务。安全
和单线程相比,多线程编程的特色在于:多线程常常须要读写共享数据,而且须要同步。例如,播放电影时,就必须由一个线程播放视频,另外一个线程播放音频,两个线程须要协调运行,不然画面和声音就不一样步。所以,多线程编程的复杂度高,调试更困难。网络
Java多线程编程的特色又在于:多线程
咱们但愿新线程能执行指定的代码,有如下几种方法:架构
Thread
派生一个自定义类,而后覆写run()
方法Thread
实例时,传入一个Runnable
实例直接调用run()
方法,至关于调用了一个普通的Java方法,当前线程并无任何改变,也不会启动新线程。并发
必须调用Thread
实例的start()
方法才能启动新线程,若是咱们查看Thread
类的源代码,会看到start()
方法内部调用了一个private native void start0()
方法,native
修饰符表示这个方法是由JVM虚拟机内部的C代码实现的,不是由Java代码实现的。性能
能够对线程设定优先级,设定优先级的方法是:网站
Thread.setPriority(int n) // 1~10, 默认值5
优先级高的线程被操做系统调度的优先级较高,操做系统对高优先级线程可能调度更频繁,但咱们决不能经过设置优先级来确保高优先级的线程必定会先执行。
Java用Thread
对象表示一个线程,经过调用start()
启动一个新线程;
一个线程对象只能调用一次start()
方法;
线程的执行代码写在run()
方法中;
线程调度由操做系统决定,程序自己没法决定调度顺序;
Thread.sleep()
能够把当前线程暂停一段时间。
在Java程序中,一个线程对象只能调用一次start()
方法启动新线程,并在新线程中执行run()
方法。一旦run()
方法执行完毕,线程就结束了。所以,Java线程的状态有如下几种:
run()
方法的Java代码;sleep()
方法正在计时等待;run()
方法执行完毕。用一个状态转移图表示以下:
┌─────────────┐ │ New │ └─────────────┘ │ ▼ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌─────────────┐ ┌─────────────┐ ││ Runnable │ │ Blocked ││ └─────────────┘ └─────────────┘ │┌─────────────┐ ┌─────────────┐│ │ Waiting │ │Timed Waiting│ │└─────────────┘ └─────────────┘│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ▼ ┌─────────────┐ │ Terminated │ └─────────────┘
当线程启动后,它能够在Runnable
、Blocked
、Waiting
和Timed Waiting
这几个状态之间切换,直到最后变成Terminated
状态,线程终止。
线程终止的缘由有:
run()
方法执行到return
语句返回;run()
方法由于未捕获的异常致使线程终止;Thread
实例调用stop()
方法强制终止(强烈不推荐使用)。一个线程还能够等待另外一个线程直到其运行结束。例如,main
线程在启动t
线程后,能够经过t.join()
等待t
线程结束后再继续运行
public class Main { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { System.out.println("hello"); }); System.out.println("start"); t.start(); t.join(); System.out.println("end"); } }
当main
线程对线程对象t
调用join()
方法时,主线程将等待变量t
表示的线程运行结束,即join
就是指等待该线程结束,而后才继续往下执行自身线程。因此,上述代码打印顺序能够确定是main
线程先打印start
,t
线程再打印hello
,main
线程最后再打印end
。
若是t
线程已经结束,对实例t
调用join()
会马上返回。此外,join(long)
的重载方法也能够指定一个等待时间,超过等待时间后就再也不继续等待。
Java线程对象Thread
的状态包括:New
、Runnable
、Blocked
、Waiting
、Timed Waiting
和Terminated
;
经过对另外一个线程对象调用join()
方法能够等待其执行结束;
能够指定等待时间,超过等待时间线程仍然没有结束就再也不等待;
对已经运行结束的线程调用join()
方法会马上返回。
若是线程须要执行一个长时间任务,就可能须要能中断线程。中断线程就是其余线程给该线程发一个信号,该线程收到信号后结束执行run()
方法,使得自身线程能马上结束运行。
咱们举个栗子:假设从网络下载一个100M的文件,若是网速很慢,用户等得不耐烦,就可能在下载过程当中点“取消”,这时,程序就须要中断下载线程的执行。
中断一个线程很是简单,只须要在其余线程中对目标线程调用interrupt()
方法,目标线程须要反复检测自身状态是不是interrupted状态,若是是,就马上结束运行。
@Slf4j public class Main { public static void main(String[] args) throws InterruptedException { Thread t = new MyThread(); t.start(); Thread.sleep(10); // 暂停1毫秒 t.interrupt(); // 中断t线程 t.join(); // 等待t线程结束 log.info("end"); } } @Slf4j class MyThread extends Thread { public void run() { int n = 0; while (!isInterrupted()) { n++; log.info(n + " hello!"); } } }
仔细看上述代码,main
线程经过调用t.interrupt()
方法中断t
线程,可是要注意,interrupt()
方法仅仅向t
线程发出了“中断请求”,至于t
线程是否能马上响应,要看具体代码。而t
线程的while
循环会检测isInterrupted()
,因此上述代码能正确响应interrupt()
请求,使得自身马上结束运行run()
方法。
若是线程处于等待状态,例如,t.join()
会让main
线程进入等待状态,此时,若是对main
线程调用interrupt()
,join()
方法会马上抛出InterruptedException
,所以,目标线程只要捕获到join()
方法抛出的InterruptedException
,就说明有其余线程对其调用了interrupt()
方法,一般状况下该线程应该马上结束运行。
咱们来看下面的示例代码:
@Slf4j public class Main { public static void main(String[] args) throws InterruptedException { Thread t = new MyThread(); t.start(); Thread.sleep(1000); t.interrupt(); // 中断t线程 t.join(); // 等待t线程结束 log.info("end"); } } @Slf4j class MyThread extends Thread { public void run() { Thread hello = new HelloThread(); hello.start(); // 启动hello线程 try { hello.join(); // 等待hello线程结束 } catch (InterruptedException e) { log.info("MyThread interrupted!"); } hello.interrupt(); } } @Slf4j class HelloThread extends Thread { public void run() { int n = 0; while (!isInterrupted()) { n++; log.info(n + " hello!"); try { Thread.sleep(100); } catch (InterruptedException e) { log.info("HelloThread interrupted!"); break; } } } }
main
线程经过调用t.interrupt()
从而通知t
线程中断,而此时t
线程正位于hello.join()
的等待中,此方法会马上结束等待并抛出InterruptedException
。因为咱们在t
线程中捕获了InterruptedException
,所以,就能够准备结束该线程。在t
线程结束前,对hello
线程也进行了interrupt()
调用通知其中断。若是去掉这一行代码,能够发现hello
线程仍然会继续运行,且JVM不会退出。
另外一个经常使用的中断线程的方法是设置标志位。咱们一般会用一个running
标志位来标识线程是否应该继续运行,在外部线程中,经过把HelloThread.running
置为false
,就可让线程结束:
public class Main { public static void main(String[] args) throws InterruptedException { HelloThread t = new HelloThread(); t.start(); Thread.sleep(1); t.running = false; // 标志位置为false } } class HelloThread extends Thread { public volatile boolean running = true; public void run() { int n = 0; while (running) { n++; System.out.println(n + " hello!"); } System.out.println("end!"); } }
注意到HelloThread
的标志位boolean running
是一个线程间共享的变量。线程间共享变量须要使用volatile
关键字标记,确保每一个线程都能读取到更新后的变量值。
为何要对线程间共享的变量用关键字volatile
声明?这涉及到Java的内存模型。在Java虚拟机中,变量的值保存在主内存中,可是,当线程访问变量时,它会先获取一个副本,并保存在本身的工做内存中。若是线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,可是,这个时间是不肯定的!
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ Main Memory │ │ ┌───────┐┌───────┐┌───────┐ │ │ var A ││ var B ││ var C │ │ └───────┘└───────┘└───────┘ │ │ ▲ │ ▲ │ ─ ─ ─│─│─ ─ ─ ─ ─ ─ ─ ─│─│─ ─ ─ │ │ │ │ ┌ ─ ─ ┼ ┼ ─ ─ ┐ ┌ ─ ─ ┼ ┼ ─ ─ ┐ ▼ │ ▼ │ │ ┌───────┐ │ │ ┌───────┐ │ │ var A │ │ var C │ │ └───────┘ │ │ └───────┘ │ Thread 1 Thread 2 └ ─ ─ ─ ─ ─ ─ ┘ └ ─ ─ ─ ─ ─ ─ ┘
这会致使若是一个线程更新了某个变量,另外一个线程读取的值可能仍是更新前的。例如,主内存的变量a = true
,线程1执行a = false
时,它在此刻仅仅是把变量a
的副本变成了false
,主内存的变量a
仍是true
,在JVM把修改后的a
回写到主内存以前,其余线程读取到的a
的值仍然是true
,这就形成了多线程之间共享的变量不一致。
所以,volatile
关键字的目的是告诉虚拟机:
volatile
关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其余线程可以马上看到修改后的值。
若是咱们去掉volatile
关键字,运行上述程序,发现效果和带volatile
差很少,这是由于在x86的架构下,JVM回写主内存的速度很是快,可是,换成ARM的架构,就会有显著的延迟。
对目标线程调用interrupt()
方法能够请求中断一个线程,目标线程经过检测isInterrupted()
标志获取自身是否已中断。若是目标线程处于等待状态,该线程会捕获到InterruptedException
;
目标线程检测到isInterrupted()
为true
或者捕获了InterruptedException
都应该马上结束自身线程;
经过标志位判断须要正确使用volatile
关键字;
volatile
关键字解决了共享变量在线程间的可见性问题。
若是有一个线程没有退出,JVM进程就不会退出。因此,必须保证全部线程都能及时结束。
然而这类线程常常没有负责人来负责结束它们。可是,当其余线程结束时,JVM进程又必需要结束,怎么办?
答案是使用守护线程(Daemon Thread)。
守护线程是指为其余线程服务的线程。在JVM中,全部非守护线程都执行完毕后,不管有没有守护线程,虚拟机都会自动退出。
所以,JVM退出时,没必要关心守护线程是否已结束。
如何建立守护线程呢?方法和普通线程同样,只是在调用start()
方法前,调用setDaemon(true)
把该线程标记为守护线程:
Thread t = new MyThread(); t.setDaemon(true); t.start();
在守护线程中,编写代码要注意:守护线程不能持有任何须要关闭的资源,例如打开文件等,由于虚拟机退出时,守护线程没有任何机会来关闭文件,这会致使数据丢失。
守护线程是为其余线程服务的线程;
全部非守护线程都执行完毕后,虚拟机退出;
守护线程不能持有须要关闭的资源(如打开文件等)。
当多个线程同时运行时,线程的调度由操做系统决定,程序自己没法决定。所以,任何一个线程都有可能在任何指令处被操做系统暂停,而后在某个时间段后继续执行。
这个时候,有个单线程模型下不存在的问题就来了:若是多个线程同时读写共享变量,会出现数据不一致的问题。
咱们来看一个例子:
public class Main { public static void main(String[] args) throws Exception { AddThread add = new AddThread(); DecThread dec = new DecThread(); add.start(); dec.start(); add.join(); dec.join(); System.out.println(Counter.count); } } class Counter { public static int count = 0; } class AddThread extends Thread { public void run() { for (int i = 0; i < 10000; i++) { Counter.count += 1; } } } class DecThread extends Thread { public void run() { for (int i = 0; i < 10000; i++) { Counter.count -= 1; } } }
上面的代码很简单,两个线程同时对一个int
变量进行操做,一个加10000次,一个减10000次,最后结果应该是0,可是,每次运行,结果实际上都是不同的。
这是由于对变量进行读取和写入时,结果要正确,必须保证是原子操做。原子操做是指不能被中断的一个或一系列操做。
例如,对于语句:
n = n + 1;
看上去是一行语句,实际上对应了3条指令:
ILOAD IADD ISTORE
咱们假设n
的值是100
,若是两个线程同时执行n = n + 1
,获得的结果极可能不是102
,而是101
,缘由在于:
┌───────┐ ┌───────┐ │Thread1│ │Thread2│ └───┬───┘ └───┬───┘ │ │ │ILOAD (100) │ │ │ILOAD (100) │ │IADD │ │ISTORE (101) │IADD │ │ISTORE (101)│ ▼ ▼
若是线程1在执行ILOAD
后被操做系统中断,此刻若是线程2被调度执行,它执行ILOAD
后获取的值仍然是100
,最终结果被两个线程的ISTORE
写入后变成了101
,而不是期待的102
。
这说明多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其余线程必须等待:
┌───────┐ ┌───────┐ │Thread1│ │Thread2│ └───┬───┘ └───┬───┘ │ │ │-- lock -- │ │ILOAD (100) │ │IADD │ │ISTORE (101) │ │-- unlock -- │ │ │-- lock -- │ │ILOAD (101) │ │IADD │ │ISTORE (102) │ │-- unlock -- ▼ ▼
经过加锁和解锁的操做,就能保证3条指令老是在一个线程执行期间,不会有其余线程会进入此指令区间。即便在执行期线程被操做系统中断执行,其余线程也会由于没法得到锁致使没法进入此指令区间。只有执行线程将锁释放后,其余线程才有机会得到锁并执行。这种加锁和解锁之间的代码块咱们称之为临界区(Critical Section),任什么时候候临界区最多只有一个线程能执行。
可见,保证一段代码的原子性就是经过加锁和解锁实现的。Java程序使用synchronized
关键字对一个对象进行加锁:
synchronized(lock) { n = n + 1; }
synchronized
保证了代码块在任意时刻最多只有一个线程能执行。咱们把上面的代码用synchronized
改写以下:
public class Main { public static void main(String[] args) throws Exception { AddThread add = new AddThread(); DecThread dec = new DecThread(); add.start(); dec.start(); add.join(); dec.join(); System.out.println(Counter.count); } } class Counter { public static final Object lock = new Object(); public static int count = 0; } class AddThread extends Thread { public void run() { for (int i = 0; i < 10000; i++) { synchronized (Counter.lock) { Counter.count += 1; } } } } class DecThread extends Thread { public void run() { for (int i = 0; i < 10000; i++) { synchronized (Counter.lock) { Counter.count -= 1; } } } }
注意到代码:
synchronized(Counter.lock) { // 获取锁 ... } // 释放锁
它表示用Counter.lock
实例做为锁,两个线程在执行各自的synchronized(Counter.lock) { ... }
代码块时,必须先得到锁,才能进入代码块进行。执行结束后,在synchronized
语句块结束会自动释放锁。这样一来,对Counter.count
变量进行读写就不可能同时进行。上述代码不管运行多少次,最终结果都是0。
使用synchronized
解决了多线程同步访问共享变量的正确性问题。可是,它的缺点是带来了性能降低。由于synchronized
代码块没法并发执行。此外,加锁和解锁须要消耗必定的时间,因此,synchronized
会下降程序的执行效率。
咱们来归纳一下如何使用synchronized
:
synchronized(lockObject) { ... }
。在使用synchronized
的时候,没必要担忧抛出异常。由于不管是否有异常,都会在synchronized
结束处正确释放锁:
public void add(int m) { synchronized (obj) { if (m < 0) { throw new RuntimeException(); } this.value += m; } // 不管有无异常,都会在此释放锁 }
咱们再来看一个错误使用synchronized
的例子:
public class Main { public static void main(String[] args) throws Exception { AddThread add = new AddThread(); DecThread dec = new DecThread(); add.start(); dec.start(); add.join(); dec.join(); System.out.println(Counter.count); } } class Counter { public static final Object lock1 = new Object(); public static final Object lock2 = new Object(); public static int count = 0; } class AddThread extends Thread { public void run() { for (int i=0; i<10000; i++) { synchronized(Counter.lock1) { Counter.count += 1; } } } } class DecThread extends Thread { public void run() { for (int i=0; i<10000; i++) { synchronized(Counter.lock2) { Counter.count -= 1; } } } }
结果并非0,这是由于两个线程各自的synchronized
锁住的不是同一个对象!这使得两个线程各自均可以同时得到锁:由于JVM只保证同一个锁在任意时刻只能被一个线程获取,但两个不一样的锁在同一时刻能够被两个线程分别获取。
所以,使用synchronized
的时候,获取到的是哪一个锁很是重要。锁对象若是不对,代码逻辑就不对。
咱们再看一个例子:
public class Main { public static void main(String[] args) throws Exception { Thread[] ts = new Thread[]{new AddStudentThread(), new DecStudentThread(), new AddTeacherThread(), new DecTeacherThread()}; for (Thread t : ts) { t.start(); } for (Thread t : ts) { t.join(); } System.out.println(Counter.studentCount); System.out.println(Counter.teacherCount); } } class Counter { public static final Object lock = new Object(); public static int studentCount = 0; public static int teacherCount = 0; } class AddStudentThread extends Thread { public void run() { for (int i = 0; i < 10000; i++) { synchronized (Counter.lock) { Counter.studentCount += 1; } } } } class DecStudentThread extends Thread { public void run() { for (int i = 0; i < 10000; i++) { synchronized (Counter.lock) { Counter.studentCount -= 1; } } } } class AddTeacherThread extends Thread { public void run() { for (int i = 0; i < 10000; i++) { synchronized (Counter.lock) { Counter.teacherCount += 1; } } } } class DecTeacherThread extends Thread { public void run() { for (int i = 0; i < 10000; i++) { synchronized (Counter.lock) { Counter.teacherCount -= 1; } } } }
上述代码的4个线程对两个共享变量分别进行读写操做,可是使用的锁都是Counter.lock
这一个对象,这就形成了本来能够并发执行的Counter.studentCount += 1
和Counter.teacherCount += 1
,如今没法并发执行了,执行效率大大下降。实际上,须要同步的线程能够分红两组:AddStudentThread
和DecStudentThread
,AddTeacherThread
和DecTeacherThread
,组之间不存在竞争,所以,应该使用两个不一样的锁,即:
AddStudentThread
和DecStudentThread
使用lockStudent
锁:
synchronized(Counter.lockStudent) { ... }
AddTeacherThread
和DecTeacherThread
使用lockTeacher
锁:
synchronized(Counter.lockTeacher) { ... }
这样才能最大化地提升执行效率。
JVM规范定义了几种原子操做:
long
和double
除外)赋值,例如:int n = m
;List list = anotherList
。long
和double
是64位数据,JVM没有明确规定64位赋值操做是否是一个原子操做,不过在x64平台的JVM是把long
和double
的赋值做为原子操做实现的。
单条原子操做的语句不须要同步。例如:
public void set(int m) { synchronized(lock) { this.value = m; } }
就不须要同步。
对引用也是相似。例如:
public void set(String s) { this.value = s; }
上述赋值语句并不须要同步。
可是,若是是多行赋值语句,就必须保证是同步操做,例如:
class Pair { int first; int last; public void set(int first, int last) { synchronized(this) { this.first = first; this.last = last; } } }
有些时候,经过一些巧妙的转换,能够把非原子操做变为原子操做。例如,上述代码若是改形成:
class Pair { int[] pair; public void set(int first, int last) { int[] ps = new int[] { first, last }; this.pair = ps; } }
就再也不须要同步,由于this.pair = ps
是引用赋值的原子操做。而语句:
int[] ps = new int[] { first, last };
这里的ps
是方法内部定义的局部变量,每一个线程都会有各自的局部变量,互不影响,而且互不可见,并不须要同步。
多线程同时读写共享变量时,会形成逻辑错误,所以须要经过synchronized
同步;
同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;
注意加锁对象必须是同一个实例;
对JVM定义的单个原子操做不须要同步。
若是一个类被设计为容许多线程正确访问,咱们就说这个类就是“线程安全”的(thread-safe),上面的Counter
类就是线程安全的。Java标准库的java.lang.StringBuffer
也是线程安全的。
还有一些不变类,例如String
,Integer
,LocalDate
,它们的全部成员变量都是final
,多线程同时访问时只能读不能写,这些不变类也是线程安全的。
最后,相似Math
这些只提供静态方法,没有成员变量的类,也是线程安全的。
除了上述几种少数状况,大部分类,例如ArrayList
,都是非线程安全的类,咱们不能在多线程中修改它们。可是,若是全部线程都只读取,不写入,那么ArrayList
是能够安全地在线程间共享的。
没有特殊说明时,一个类默认是非线程安全的。
下面两种写法是等价的:
public void add(int n) { synchronized(this) { // 锁住this count += n; } // 解锁 } public synchronized void add(int n) { // 锁住this count += n; } // 解锁
所以,用synchronized
修饰的方法就是同步方法,它表示整个方法都必须用this
实例加锁。
对于static
方法,是没有this
实例的,由于static
方法是针对类而不是实例。可是咱们注意到任何一个类都有一个由JVM自动建立的Class
实例,所以,对static
方法添加synchronized
,锁住的是该类的class
实例。下面两种写法是等价的:
public class Counter { public synchronized static void test(int n) { ... } } public class Counter { public static void test(int n) { synchronized(Counter.class) { ... } } }
考察Counter
的get()
方法:
public class Counter { private int count; public int get() { return count; } ... }
它没有同步,由于读一个int
变量不须要同步。
然而,若是咱们把代码稍微改一下,返回一个包含两个int
的对象:
public class Counter { private int first; private int last; public Pair get() { Pair p = new Pair(); p.first = first; p.last = last; return p; } ... }
就必需要同步了。
用synchronized
修饰方法能够把整个方法变为同步代码块,synchronized
方法加锁对象是this
;
经过合理的设计和数据封装可让一个类变为“线程安全”;
一个类没有特殊说明,默认不是thread-safe;
多线程可否安全访问某个非线程安全的实例,须要具体问题具体分析。
Java的线程锁是可重入的锁。
什么是可重入的锁?咱们仍是来看例子:
public class Counter { private int count = 0; public synchronized void add(int n) { if (n < 0) { dec(-n); } else { count += n; } } public synchronized void dec(int n) { count += n; } }
观察synchronized
修饰的add()
方法,一旦线程执行到add()
方法内部,说明它已经获取了当前实例的this
锁。若是传入的n < 0
,将在add()
方法内部调用dec()
方法。因为dec()
方法也须要获取this
锁,如今问题来了:
对同一个线程,可否在获取到锁之后继续获取同一个锁?
答案是确定的。JVM容许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫作可重入锁。
因为Java的线程锁是可重入锁,因此,获取锁的时候,不但要判断是不是第一次获取,还要记录这是第几回获取。每获取一次锁,记录+1,每退出synchronized
块,记录-1,减到0的时候,才会真正释放锁。
一个线程能够获取一个锁后,再继续获取另外一个锁。例如:
public void add(int m) { synchronized(lockA) { // 得到lockA的锁 this.value += m; synchronized(lockB) { // 得到lockB的锁 this.another += m; } // 释放lockB的锁 } // 释放lockA的锁 } public void dec(int m) { synchronized(lockB) { // 得到lockB的锁 this.another -= m; synchronized(lockA) { // 得到lockA的锁 this.value -= m; } // 释放lockA的锁 } // 释放lockB的锁 }
在获取多个锁的时候,不一样线程获取多个不一样对象的锁可能致使死锁。对于上述代码,线程1和线程2若是分别执行add()
和dec()
方法时:
add()
,得到lockA
;dec()
,得到lockB
。随后:
lockB
,失败,等待中;lockA
,失败,等待中。此时,两个线程各自持有不一样的锁,而后各自试图获取对方手里的锁,形成了双方无限等待下去,这就是死锁。
死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。
所以,在编写多线程应用时,要特别注意防止死锁。由于死锁一旦造成,就只能强制结束进程。
那么咱们应该如何避免死锁呢?答案是:线程获取锁的顺序要一致。即严格按照先获取lockA
,再获取lockB
的顺序,改写dec()
方法以下:
public void dec(int m) { synchronized(lockA) { // 得到lockA的锁 this.value -= m; synchronized(lockB) { // 得到lockB的锁 this.another -= m; } // 释放lockB的锁 } // 释放lockA的锁 }
Java的synchronized
锁是可重入锁;
死锁产生的条件是多线程各自持有不一样的锁,并互相试图获取对方已持有的锁,致使无限等待;
避免死锁的方法是多线程获取锁的顺序要一致。