【廖雪峰官方网站/Java教程】多线程(1)

news/2024/5/20 0:50:47/文章来源:https://blog.csdn.net/Allenlzcoder/article/details/105313823

多线程是Java最基本的一种并发模型,本章我们将详细介绍Java多线程编程。

1.多线程基础

1.1.进程

在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。
某些进程内部还需要同时执行多个子任务。例如,我们在使用Word时,Word可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程
进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程
在这里插入图片描述
操作系统调度的最小任务单位其实不是进程,而是线程
因为同一个应用程序,既可以有多个进程,也可以有多个线程,因此,实现多任务的方法,有以下几种:

1.1.1.多进程模式(每个进程只有一个线程)

在这里插入图片描述

1.1.2.多线程模式(一个进程有多个线程)

在这里插入图片描述

1.1.3.多进程+多线程模式(复杂度最高)

在这里插入图片描述

1.2.进程 vs 线程

进程和线程是包含关系,但是多任务既可以由多进程实现,也可以由单进程内的多线程实现,还可以混合多进程+多线程。
具体采用哪种方式,要考虑到进程和线程的特点。
和多线程相比,多进程的缺点在于:

  • 创建进程比创建线程开销大,尤其是在Windows系统上;
  • 进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。

多进程的优点在于:

  • 多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。

1.3.多线程

Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程等。
因此,对于大多数Java程序来说,我们说多任务,实际上是说如何使用多线程实现多任务
和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。
Java多线程编程的特点又在于:

  • 多线程模型是Java程序最基本的并发模型;
  • 后续读写网络、数据库、Web开发等都依赖Java多线程模型。

因此,必须掌握Java多线程编程才能继续深入学习其他内容。

2.创建新线程

Java语言内置了多线程支持。当Java程序启动的时候,实际上是启动了一个JVM进程,然后,JVM启动主线程来执行main()方法。在main()方法中,我们又可以启动其他线程。
要创建一个新线程非常容易,我们需要实例化一个Thread实例,然后调用它的start()方法:

// 多线程
public class Main {public static void main(String[] args) {Thread t = new Thread();t.start(); // 启动新线程}
}

但是这个线程启动后实际上什么也不做就立刻结束了。我们希望新线程能执行指定的代码,有以下几种方法。

2.1.新线程执行指定代码的方法

2.1.1.在Thread的派生类中,覆写run()

// 多线程
public class Main {public static void main(String[] args) {Thread t = new MyThread();t.start(); // 启动新线程}
}class MyThread extends Thread {@Overridepublic void run() {System.out.println("start new thread!");}
}

执行上述代码,注意到start()方法会在内部自动调用实例的run()方法。

2.1.2.创建Thread实例,传入Runnable实例

public class Main {public static void main(String[] args) {Thread t = new Thread(new MyRunnable());t.start(); // 启动新线程}
}class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("start new thread!");}
}

或者用Java8引入的lambda语法进一步简写为:

public class Main {public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println("start new thread!");});t.start(); // 启动新线程}
}

2.2.线程执行语句和main()方法执行的区别

有童鞋会问,使用线程执行的打印语句,和直接在main()方法执行有区别吗?
区别大了去了。我们看以下代码:

public class Main {public static void main(String[] args) {System.out.println("main start..."); // main线程Thread t = new Thread() { // main线程public void run() {System.out.println("thread run..."); // t线程System.out.println("thread end."); // t线程}};t.start(); // main线程System.out.println("main end..."); // main线程}
}

我们用蓝色表示主线程,也就是main线程,main线程执行的代码有4行,首先打印main start,然后创建Thread对象,紧接着调用start()启动新线程。当start()方法被调用时,JVM就创建了一个新线程,我们通过实例变量t来表示这个新线程对象,并开始执行。
接着,main线程继续执行打印main end语句,而t线程在main线程执行的同时会并发执行,打印thread run和thread end语句。
当run()方法结束时,新线程就结束了。而main()方法结束时,主线程也结束了。
我们再来看线程的执行顺序:

  1. main线程肯定是先打印main start,再打印main end;
  2. t线程肯定是先打印thread run,再打印thread end。

但是,除了可以肯定,main start会先打印外,main end打印在thread run之前、thread end之后或者之间,都无法确定。因为从t线程开始运行以后,两个线程就开始同时运行了,并且由操作系统调度,程序本身无法确定线程的调度顺序。
要模拟并发执行的效果,我们可以在线程中调用Thread.sleep(),强迫当前线程暂停一段时间:

public class Main {public static void main(String[] args) {System.out.println("main start...");Thread t = new Thread() {public void run() {System.out.println("thread run...");try {Thread.sleep(10);} catch (InterruptedException e) {}System.out.println("thread end.");}};t.start();try {Thread.sleep(20);} catch (InterruptedException e) {}System.out.println("main end...");}
}

sleep()传入的参数是毫秒。调整暂停时间的大小,我们可以看到main线程和t线程执行的先后顺序。
要特别注意:直接调用Thread实例的run()方法是无效的

public class Main {public static void main(String[] args) {Thread t = new MyThread();t.run();}
}class MyThread extends Thread {public void run() {System.out.println("hello");}
}

直接调用run()方法,相当于调用了一个普通的Java方法,当前线程并没有任何改变,也不会启动新线程。上述代码实际上是在main()方法内部又调用了run()方法,打印hello语句是在main线程中执行的,没有任何新线程被创建。
必须调用Thread实例的start()方法才能启动新线程,如果我们查看Thread类的源代码,会看到start()方法内部调用了一个private native void start0()方法,native修饰符表示这个方法是由JVM虚拟机内部的C代码实现的,不是由Java代码实现的。

2.3.线程的优先级

可以对线程设定优先级,设定优先级的方法是:

Thread.setPriority(int n) // 1~10, 默认值5

优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。

3.线程的状态

3.1.线程状态

在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:

  • New:新创建的线程,尚未执行;
  • Runnable:运行中的线程,正在执行run()方法的Java代码;
  • Blocked:运行中的线程,因为某些操作被阻塞而挂起;
  • Waiting:运行中的线程,因为某些操作在等待中;
  • Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
  • Terminated:线程已终止,因为run()方法执行完毕。

用一个状态转移图表示如下:
在这里插入图片描述
当线程启动后,它可以在Runnable、Blocked、Waiting和Timed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。

3.2.线程终止的原因

  • 线程正常终止: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)的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。

3.3.小结

  1. Java线程对象Thread的状态包括:New、Runnable、Blocked、Waiting、Timed Waiting和Terminated;
  2. 通过对另一个线程对象调用join()方法可以等待其执行结束;
  3. 可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;
  4. 对已经运行结束的线程调用join()方法会立刻返回。

4.中断线程

中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束正在执行的run()方法,使得自身线程能立刻结束运行。

4.1.调用interrupt()方法

中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。
我们还是看示例代码:

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线程结束System.out.println("end");}
}class MyThread extends Thread {public void run() {Thread hello = new HelloThread();hello.start(); // 启动hello线程try {hello.join(); // 等待hello线程结束} catch (InterruptedException e) {System.out.println("interrupted!");}hello.interrupt();}
}class HelloThread extends Thread {public void run() {int n = 0;while (!isInterrupted()) {n++;System.out.println(n + " hello!");try {Thread.sleep(100);} catch (InterruptedException e) {break;}}}
}

main线程通过调用t.interrupt()从而通知t线程中断,而此时t线程正位于hello.join()的等待中,此方法会立刻结束等待并抛出InterruptedException。由于我们在t线程中捕获了InterruptedException,因此,就可以准备结束该线程。在t线程结束前,对hello线程也进行了interrupt()调用通知其中断。如果去掉这一行代码,可以发现hello线程仍然会继续运行,且JVM不会退出。

4.2.设置running标志位

另一个常用的中断线程的方法是设置标志位。我们通常会用一个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虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!
在这里插入图片描述
这会导致如果一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。例如,主内存的变量a = true,线程1执行a = false时,它在此刻仅仅是把变量a的副本变成了false,主内存的变量a还是true,在JVM把修改后的a回写到主内存之前,其他线程读取到的a的值仍然是true,这就造成了多线程之间共享的变量不一致。
因此,volatile关键字的目的是告诉虚拟机:

  • 每次访问变量时,总是获取主内存的最新值;
  • 每次修改变量后,立刻回写到主内存。

volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。
如果我们去掉volatile关键字,运行上述程序,发现效果和带volatile差不多,这是因为在x86的架构下,JVM回写主内存的速度非常快,但是,换成ARM的架构,就会有显著的延迟。

4.3.小结

  1. 对目标线程调用interrupt()方法可以请求中断一个线程,目标线程通过检测isInterrupted()标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到InterruptedException;

  2. 目标线程检测到isInterrupted()为true或者捕获了InterruptedException都应该立刻结束自身线程;

  3. 通过标志位判断需要正确使用volatile关键字

  4. volatile关键字解决了共享变量在线程间的可见性问题

5.守护线程

5.1.为何需要守护线程

Java程序入口就是由JVM启动main线程,main线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。
如果有一个线程没有退出,JVM进程就不会退出。所以,必须保证所有线程都能及时结束。
但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程:

class TimerThread extends Thread {@Overridepublic void run() {while (true) {System.out.println(LocalTime.now());try {Thread.sleep(1000);} catch (InterruptedException e) {break;}}}
}

如果这个线程不结束,JVM进程就无法结束。问题是,由谁负责结束这个线程?
然而这类线程经常没有负责人来负责结束它们。但是,当其他线程结束时,JVM进程又必须要结束,怎么办?
答案是使用守护线程(Daemon Thread)。

5.2.守护线程的概念及创建

守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出
因此,JVM退出时,不必关心守护线程是否已结束。
如何创建守护线程呢?方法和普通线程一样,只是在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程

Thread t = new MyThread();
t.setDaemon(true);
t.start();

在守护线程中,编写代码要注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失

5.3.小结

  1. 守护线程是为其他线程服务的线程;
  2. 所有非守护线程都执行完毕后,虚拟机退出;
  3. 守护线程不能持有需要关闭的资源(如打开文件等)。

6.线程同步

6.1.线程同步背景

当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。
这个时候,有个单线程模型下不存在的问题就来了:如果多个线程同时读写共享变量,会出现数据不一致的问题。
看个例子:

public class Main {public static void main(String[] args) throws Exception {var add = new AddThread();var 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,但是,每次运行,结果实际上都是不一样的。
这是因为对变量进行读取和写入时,结果要正确,必须保证是原子操作。原子操作是指不能被中断的一个或一系列操作

6.2.synchronized关键字

例如,对于语句:

n = n + 1;

看上去是一行语句,实际上对应了3条指令:

ILOAD
IADD
ISTORE

我们假设n的值是100,如果两个线程同时执行n = n + 1,得到的结果很可能不是102,而是101,原因在于:
在这里插入图片描述
如果线程1在执行ILOAD后被操作系统中断,此刻如果线程2被调度执行,它执行ILOAD后获取的值仍然是100,最终结果被两个线程的ISTORE写入后变成了101,而不是期待的102。
这说明多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待:
在这里插入图片描述
通过加锁解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行
可见,保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用synchronized关键字对一个对象进行加锁:

synchronized(lock) {n = n + 1;
}

synchronized保证了代码块在任意时刻最多只有一个线程能执行。我们把上面的代码用synchronized改写如下:

public class Main {public static void main(String[] args) throws Exception {var add = new AddThread();var 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会降低程序的执行效率。

6.3.如何使用synchronized

  1. 找出修改共享变量的线程代码块;
  2. 选择一个共享实例作为锁;
  3. 使用synchronized(lockObject) { … };

在使用synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁:

public void add(int m) {synchronized (obj) {if (m < 0) {throw new RuntimeException();}this.value += m;} // 无论有无异常,都会在此释放锁
}

因此,使用synchronized的时候,获取到的是哪个锁非常重要。锁对象如果不对,代码逻辑就不对。

6.4.不需要synchronized的操作

JVM规范定义了几种原子操作:

  1. 基本类型(long和double除外)赋值,例如:int n = m;
  2. 引用类型赋值,例如:List<String> 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是方法内部定义的局部变量,每个线程都会有各自的局部变量,互不影响,并且互不可见,并不需要同步。

6.5.小结

  1. 多线程同时读写共享变量时,会造成逻辑错误,因此需要通过synchronized同步;
  2. 同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;
  3. 注意加锁对象必须是同一个实例;
  4. 对JVM定义的单个原子操作不需要同步。

7.同步方法

7.1.对synchronized的逻辑封装

我们知道Java程序依靠synchronized对线程进行同步,使用synchronized的时候,锁住的是哪个对象非常重要。
让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把synchronized逻辑封装起来。例如,我们编写一个计数器如下:

public class Counter {private int count = 0;public void add(int n) {synchronized(this) {count += n;}}public void dec(int n) {synchronized(this) {count -= n;}}public int get() {return count;}
}

这样一来,线程调用add()、dec()方法时,它不必关心同步逻辑,因为synchronized代码块在add()、dec()方法内部。并且,我们注意到,synchronized锁住的对象是this,即当前实例,这又使得创建多个Counter实例的时候,它们之间互不影响,可以并发执行:

var c1 = Counter();
var c2 = Counter();// 对c1进行操作的线程:
new Thread(() -> {c1.add();
}).start();
new Thread(() -> {c1.dec();
}).start();// 对c2进行操作的线程:
new Thread(() -> {c2.add();
}).start();
new Thread(() -> {c2.dec();
}).start();

现在,对于Counter类,多线程可以正确调用。

7.2.线程安全(thread-safe)

如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的(thread-safe),上面的Counter类就是线程安全的。Java标准库的java.lang.StringBuffer也是线程安全的。
还有一些不变类,例如String,Integer,LocalDate,它们的所有成员变量都是final多线程同时访问时只能读不能写,这些不变类也是线程安全的。
最后,类似Math这些只提供静态方法,没有成员变量的类,也是线程安全的。
除了上述几种少数情况,大部分类,例如ArrayList,都是非线程安全的类,我们不能在多线程中修改它们。但是,如果所有线程都只读取,不写入,那么ArrayList是可以安全地在线程间共享的。
没有特殊说明时,一个类默认是非线程安全的
我们再观察Counter的代码:

public class Counter {public void add(int n) {synchronized(this) {count += n;}}...
}

当我们锁住的是this实例时,实际上可以用synchronized修饰这个方法。下面两种写法是等价的:

public void add(int n) {synchronized(this) { // 锁住thiscount += n;} // 解锁
}
public synchronized void add(int n) { // 锁住thiscount += n;
} // 解锁

因此,用synchronized修饰的方法就是同步方法,它表示整个方法都必须用this实例加锁。

7.3.synchronized修饰的static方法

我们再思考一下,如果对一个静态方法添加synchronized修饰符,它锁住的是哪个对象?

public synchronized static void test(int n) {...
}

对于static方法,是没有this实例的,因为static方法是针对类而不是实例。但是我们注意到任何一个类都有一个由JVM自动创建的Class实例,因此,对static方法添加synchronized,锁住的是该类的class实例。上述synchronized static方法实际上相当于:

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;}...
}

就必须要同步了。

7.4.小结

  1. 用synchronized修饰方法可以把整个方法变为同步代码块,synchronized方法加锁对象是this;
  2. 通过合理的设计和数据封装可以让一个类变为“线程安全”;
  3. 一个类没有特殊说明,默认不是thread-safe;
  4. 多线程能否安全访问某个非线程安全的实例,需要具体问题具体分析。

8.死锁

8.1.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的时候,才会真正释放锁。

8.2.死锁

一个线程可以获取一个锁后,再继续获取另一个锁。例如:

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()方法时:

  • 线程1:进入add(),获得lockA;
  • 线程2:进入dec(),获得lockB。

随后:

  • 线程1:准备获得lockB,失败,等待中;
  • 线程2:准备获得lockA,失败,等待中。

此时,两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁。
死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。
因此,在编写多线程应用时,要特别注意防止死锁。因为死锁一旦形成,就只能强制结束进程。
那么我们应该如何避免死锁呢?答案是:线程获取锁的顺序要一致。即严格按照先获取lockA,再获取lockB的顺序,改写dec()方法如下:

public void dec(int m) {synchronized(lockA) { // 获得lockA的锁this.value -= m;synchronized(lockB) { // 获得lockB的锁this.another -= m;} // 释放lockB的锁} // 释放lockA的锁
}

8.3.小结

  1. Java的synchronized锁是可重入锁;
  2. 死锁产生的条件是多线程各自持有不同的锁,并互相试图获取对方已持有的锁,导致无限等待;
  3. 避免死锁的方法是多线程获取锁的顺序要一致。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_712919.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【廖雪峰官方网站/Java教程】多线程(2)

1.使用wait和notify 1.1.多线程协调 在Java程序中&#xff0c;synchronized解决了多线程竞争的问题。例如&#xff0c;对于一个任务管理器&#xff0c;多个线程同时往队列中添加任务&#xff0c;可以用synchronized加锁&#xff1a; class TaskQueue {Queue<String> q…

【廖雪峰官方网站/Java教程】多线程(3)

1.使用线程池 1.1.ExecutorService介绍 Java语言虽然内置了多线程支持&#xff0c;启动一个新线程非常方便&#xff0c;但是&#xff0c;创建线程需要操作系统资源&#xff08;线程资源&#xff0c;栈空间等&#xff09;&#xff0c;频繁创建和销毁大量线程需要消耗大量时间。…

【廖雪峰官方网站/Java教程】Maven基础

Maven是一个Java项目管理和构建工具&#xff0c;它可以定义项目结构、项目依赖&#xff0c;并使用统一的方式进行自动化构建&#xff0c;是Java项目不可缺少的工具。 1.Maven介绍 1.1.Maven功能及项目结构 1.1.1.Maven主要功能 Maven就是是专门为Java项目打造的管理和构建工…

【廖雪峰官方网站/Java教程】设计模式(一)

0.概述.设计模式的基本概念及原则 设计模式&#xff0c;即Design Patterns&#xff0c;是指在软件设计中&#xff0c;被反复使用的一种代码设计经验。使用设计模式的目的是为了可重用代码&#xff0c;提高代码的可扩展性和可维护性。 为什么要使用设计模式&#xff1f;根本原因…

[转]上海新东方vs新东方,SEO实战

引用前言&#xff1a;半夜无聊上网&#xff0c;看到了这篇文章&#xff0c;觉得还不错&#xff0c;看了收获不小&#xff0c;所以就转过来了。来源 非常郑重地声明一下&#xff08;文章发表约20小时后补充&#xff09; 我不得不承认&#xff0c;这篇文章有点“软”&#xff0c;…

前端web:响应式网站开发的现状你了解吗?

当企业对网络营销有了更深的认识时&#xff0c;不管是大企业还是小企业&#xff0c;都已经建立了自己的个性化响应网站&#xff0c;都希望利用互联网这一新形态的市场&#xff0c;以及网络营销的新型营销模式。如今建立响应式网站也是一种趋势&#xff0c;利用互联网的优势&…

学用MVC4做网站五:5.4删除文章

前几天把添加、修改功能都做了&#xff0c;今天开始写删除功能。删除文章既要删除文章本身同时也要在公共模型中删除对应项。 首先写从数据库中删除文章的函数。打开ArticleRepository修改Delete的函数。有上次的教训这次明白了传递的id应该是公共模型id。 /// <summary>…

三分钟免费搞定网站在线客服,利用PowerTalkBox控件制作而成,为大家提供比较好的示例...

下载地址:http://download.csdn.net/source/1876659 必须安装.net2.0才可以支持网站服务端 内带完整的安装流程,支持飞信功能,使您不在电脑前时也可以用手机交流. 可以利用以下的js代码实现浮动窗口的功能. <script languagejavascript>var cao_x,cao_y; function cao888…

amazon s3_在Amazon S3上托管静态网站

amazon s3Static website hosting on Amazon S3 is one of the very popular use cases of Amazon S3. It allows you to host an entire static website and on very low cost. Amazon S3 is a highly available and scalable hosting solution.Amazon S3上的静态网站托管是Am…

一个可以实时查相关电子产品价格的网站

香港价格网&#xff0c;里面的价格和香港的百老汇、丰泽等的价格几乎同步&#xff0c;相差不大&#xff0c;有很大的参考价值&#xff0c;对于准备去香港买电子产品的网友来说&#xff0c;是个非常好的网站&#xff0c;特别分享&#xff1a; http://www.price.com.hk/转载于:ht…

简单高效!25个漂亮的简约风格网站设计作品

在过去几年里&#xff0c;网站设计领域发生了巨大变化。除了 RWD&#xff08;响应式网页设计&#xff09;和 Web 字体的革命&#xff0c;现代设计的发展趋势迅速流行扁平化的配色方案&#xff0c;网页排版变得更加重要&#xff0c;重点已放在内容第一。最后&#xff0c;页面加载…

增城seo搜索引擎优化_搜索引擎seo优化主要从哪里入手?

首先我们应该了解什么是搜索引擎优化以及网站搜索引擎seo优化的价值&#xff0c;从基础开始逐步深入&#xff0c;下面拓王朝所要讲的都是一些理论知识&#xff0c;很好理解&#xff0c;有不同见解欢迎评论。SEO优化SEO搜索引擎优化&#xff0c;是指通过采用易于搜索引擎索引和排…

[转]使用ThinkPHP框架快速开发网站(多图)

本文转自&#xff1a;http://blog.csdn.net/ruby97/article/details/7574851 这一周一直忙于做实验室的网站&#xff0c;基本功能算是完成了。比较有收获的是大概了解了ThinkPHP框架。写一些东西留作纪念吧。如果对于同样是Web方面新手的你有一丝丝帮助&#xff0c;那就更好了挖…

《大型网站技术架构》读书笔记[3] - 架构核心五要素

架构设计中要考虑的核心五要素&#xff1b; 性能、可用性、扩展性、伸缩性、安全性 性能 性能的测试指标 响应时间 应用执行一个操作需要的时间&#xff0c;包括从发出请求开始到收到最后响应数据所需要的时间。响应时间是系统最重要的性能指标&#xff0c;直观地反映了系统的“…

java抓取网页数据_Golang丨Java丨Python爬虫实战—Boss直聘网站数据抓取

我们分别通过Golang、Python、Java三门语言&#xff0c;分别实现对Boss直聘网站的招聘数据进行爬取。首先打开Boss直聘网站&#xff1a;然后我们在职位类型中输入Go或者Golang关键字&#xff1a;然后我们可以看到一个列表&#xff0c;和Go语言相关的各种招聘职位&#xff0c;还…

我的网站被黑了,关键词被劫持,总结一下是怎么解决的。

1、发现被黑&#xff0c;网站被黑的症状 两年前自己用wordpress搭了一个网站&#xff0c;平时没事写写文章玩玩。但是前些日子&#xff0c;突然发现网站的流量突然变小&#xff0c;site了一下百度收录&#xff0c;发现出了大问题&#xff0c;网站被黑了。大多数百度抓取收录的页…

一个大图切成几个小图加载速度更快_谷歌SEO页面速度的重要性

什么是页面速度&#xff1f;页面速度是指网页加载所需的时间。一个页面的加载速度是由几个不同的因素决定的&#xff0c;包括网站的服务器、页面文件大小和图片压缩。也就是说&#xff0c;"页面速度 "并不像 "网页速度 "那么重要。"页面速度 "并…

大学计算机思维导图_3款免费在线思维导图网站,你一定要收藏一个!

1&#xff1a;迅捷画图https://www.liuchengtu.com/迅捷画图是一个专业的思维导图、流程图制作网站。支持在线创作流程图、思维导图、组织结构、ER图、网络拓扑图、UML图等等。接下来说说特色&#xff1a;l 支持导出多种格式&#xff0c;如JPG、PNG、PDF文件、txt文本等格式l 提…

python中data.find_all爬取网站为空列表_利用Golang快速爬取盗版网站的整套音频

01前言最近因为 Zigma 帮我写了个推广 Catcher 小程序软文的原因&#xff0c;答应了他帮他爬了一个盗版音频网站的整套 《李淼谈奇案》 。在制作爬虫脚本的过程中&#xff0c;也是遇到了一些有趣的问题&#xff0c;所以特此写了这篇 Blog 用于记录脚本的整一个实现与问题解决。…

java web 项目伪静态_【Java Web】使用URLRewrite实现网站伪静态

大部分搜索引擎都会优先考虑收录静态的HTML页面&#xff0c;而不是动态的*.jsp、*.php页面。但实际上绝大部分网站都是动态的&#xff0c;不可能全部是静态的HTML页面&#xff0c;因此互联网上大部分网站都会考虑伪静态——就是将*.jsp、*.php这种动态URL伪装成静态的HTML页面。…