881 lines
33 KiB
Markdown
881 lines
33 KiB
Markdown
---
|
||
title: Java多线程
|
||
date: 2020-10-24 16:23:00
|
||
tags:
|
||
- Java
|
||
- 多线程
|
||
categories:
|
||
- Java基础
|
||
---
|
||
|
||
## 多线程概述
|
||
|
||
==多线程:栈空间独立,堆内存共享==
|
||
|
||
多线程是实现并发机制的一种有效手段。进程和线程一样,都是实现并发的一个基本单位。线程是比进程更小的执行单位,线程是在进程的基础上进行的进一步划分。所谓多线程是指一个进程在执行过程中可以产生多个线程,这些线程可以同时存在、同时运行,一个进程可能包含了多个同时执行的线程。
|
||
|
||
<!-- more -->
|
||
|
||
### 进程与线程
|
||
|
||
* **进程**:
|
||
* 正在运行的应用程序:是指一个内存中运行的应用程序,**每个进程都有一个独立的内存空间**,即每个进程都有着自己的堆、栈等且是互不共享的。
|
||
* **线程**:
|
||
* 进程中的一个**执行路径**(一段程序从执行到结束的整个过程),共享一个内存空间,线程之间可以自由切换,并发执行,<font color=red>一个进程最少有一个线程</font>
|
||
* 线程实际上是在进程的基础上进一步划分的,一个进程执行后,里面的若干执行路径又可以划分为若干个线程
|
||
|
||
### 线程调度
|
||
|
||
1. **分时调度**
|
||
|
||
* 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
|
||
|
||
2. **抢占式调度**
|
||
|
||
* 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),**Java使用的为抢占式调度**。
|
||
* CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核心而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉很快,看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
|
||
|
||
### 同步与异步&并发与并行
|
||
|
||
> **同步**:排队执行,效率低但安全
|
||
>
|
||
> **异步**:同时执行,效率高但数据不安全
|
||
|
||
> **并发**:指两个或多个事件在<font color=red>同一个时间段内</font>发生。
|
||
>
|
||
> **并行**:指两个或多个事件在<font color=red>同一时刻</font>发生(同时发生)。
|
||
|
||
## 多线程的实现方式
|
||
|
||
### 继承Thread类
|
||
|
||
步骤:
|
||
|
||
1. 创建一个自定义类并继承Thread类;
|
||
|
||
2. 重写run()方法,创建新的执行任务(通过thread对象的start()方法启动任务,一般不直接调用run()方法)
|
||
|
||
3. 创建自定义类对象实例,调用start(),让线程执行
|
||
|
||
代码如下:
|
||
|
||
```java
|
||
//MyThread.java
|
||
public class MyThread extends Thread{
|
||
@Override
|
||
public void run() { //run()方法就是线程要执行的任务的方法
|
||
for (int i = 0; i < 10; i++) {
|
||
System.out.println("MyThread" + i);
|
||
}
|
||
}
|
||
}
|
||
//ThreadTest.java
|
||
public class ThreadTest {
|
||
public static void main(String[] args) {
|
||
MyThread mt = new MyThread();
|
||
mt.start(); //启动线程任务
|
||
for (int i = 0; i < 5; i++) {
|
||
System.out.println("MainThread" + i);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
运行结果:
|
||
|
||
可以看到顺序并不统一,两个线程在交替执行而且各自所占的时间不完全相同,这是线程在抢时间片,谁先抢到谁就执行。
|
||
|
||

|
||
|
||
> **时序图:**
|
||
>
|
||
> 
|
||
>
|
||
> 运行过程中子线程任务中调用的方法都在子线程中运行
|
||
|
||
> 在上述代码中。如果Thread对象只需要调用1次,也可以通过使用匿名内部类的方式进行简化:
|
||
>
|
||
> ```java
|
||
> public class ThreadTest {
|
||
> public static void main(String[] args) {
|
||
> new Thread(){
|
||
> public void run() {
|
||
> for (int i = 0; i < 5; i++) {
|
||
> System.out.println("MyRunnable" + i);
|
||
> }
|
||
> }
|
||
> }.start();
|
||
> for (int i = 0; i < 5; i++) {
|
||
> System.out.println("MainThread" + i);
|
||
> }
|
||
> }
|
||
> }
|
||
> ```
|
||
|
||
### 实现Runnable接口
|
||
|
||
Runnable接口代码:
|
||
|
||
```java
|
||
public interface Runnable {
|
||
public abstract void run();
|
||
}
|
||
```
|
||
|
||
步骤:
|
||
|
||
1. 创建一个自定义类实现Runnable接口,并实现其抽象方法run(),编写线程要执行的任务
|
||
2. 创建自定义类对象实例
|
||
3. 用Thread类创建一个对象实例,并将第二步中的自定义类对象实例作为参数传给其构造函数
|
||
4. 调用Thread类实例的start()方法执行线程。
|
||
|
||
```java
|
||
//MyRunnable.java
|
||
public class MyRunnable implements Runnable {
|
||
@Override
|
||
public void run() {
|
||
for (int i = 0; i < 5; i++) {
|
||
System.out.println("MyRunnable" + i);
|
||
}
|
||
}
|
||
}
|
||
//RunnableTest.java
|
||
public class RunnableTest {
|
||
public static void main(String[] args) {
|
||
MyRunnable mr = new MyRunnable();
|
||
Thread t = new Thread(mr);
|
||
t.start();
|
||
for (int i = 0; i < 5; i++) {
|
||
System.out.println("MainRunnable" + i);
|
||
}
|
||
}
|
||
}
|
||
//运行效果应该跟上面继承Thread类实现多线程效果差不多。
|
||
```
|
||
|
||
> 上述代码也可以通过使用匿名内部类的方式进行简化:
|
||
>
|
||
> ```java
|
||
> public class RunnableTest {
|
||
> public static void main(String[] args) {
|
||
> new Thread(new Runnable() {
|
||
> public void run() {
|
||
> for (int i = 0; i < 5; i++) {
|
||
> System.out.println("MyRunnable" + i);
|
||
> }
|
||
> }
|
||
> }).start();
|
||
> for (int i = 0; i < 5; i++) {
|
||
> System.out.println("MainRunnable" + i);
|
||
> }
|
||
> }
|
||
> }
|
||
> ```
|
||
|
||
> **==上面两种方式的比较==**
|
||
>
|
||
> **继承Thread类**:
|
||
>
|
||
> * 优点:直接使用Thread类中的方法,代码简单
|
||
> * 弊端:如果已有父类,不可用(Java不可多继承)
|
||
>
|
||
> **实现Runnable接口(更常用)**:
|
||
>
|
||
> 与继承Threadl类相比具有以下优势:
|
||
>
|
||
> * 通过创建任务,给线程分配任务实现多线程,更适合多个线程同时执行相同任务的情况
|
||
> * 可以避免单继承带来的局限性(Java允许实现多个接口,但不能继承多个父类)
|
||
> * 任务和线程分离,提高程序健壮性
|
||
> * 后续学到的线程池技术,它只接收Runnable类型任务,不接收Thread类型线程
|
||
|
||
> **==Thread类API==**
|
||
>
|
||
> 1. 常用构造方法
|
||
>
|
||
> | 构造器 | 描述 |
|
||
> | -------------------------------------- | ----------------------- |
|
||
> | `Thread()` | 分配新的 `Thread`对象。 |
|
||
> | `Thread(Runnable target)` | 分配新的 `Thread`对象。 |
|
||
> | `Thread(Runnable target, String name)` | 分配新的 `Thread`对象。 |
|
||
> | `Thread(String name)` | 分配新的 `Thread`对象。 |
|
||
>
|
||
> 2. 常用其他方法
|
||
>
|
||
> | 变量和类型 | 方法 | 描述 |
|
||
> | :-------------- | :------------------------------ | :----------------------------------------------------------- |
|
||
> | `long` | `getId()` | 返回此Thread的标识符。 |
|
||
> | `String` | `getName()` | 返回此线程的名称。 |
|
||
> | `int` | `getPriority()` | 返回此线程的优先级。 |
|
||
> | `void` | `setPriority(int newPriority)` | 更改此线程的优先级。 |
|
||
> | `Thread.State` | `getState()` | 返回此线程的状态。 |
|
||
> | `static Thread` | `currentThread()` | 返回对当前正在执行的线程对象的引用。 |
|
||
> | `void` | `start()` | 导致此线程开始执行; Java虚拟机调用此线程的`run`方法。 |
|
||
> | `static void` | `sleep(long millis)` | 导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数,具体取决于系统计时器和调度程序的精度和准确性。 |
|
||
> | `static void` | `sleep(long millis, int nanos)` | 导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数加上指定的纳秒数,具体取决于系统定时器和调度程序的精度和准确性。 |
|
||
> | `void` | `setDaemon(boolean on)` | 将此线程标记为 daemon线程或用户线程。 |
|
||
>
|
||
> 3. 特殊字段:控制线程抢到时间片的几率
|
||
>
|
||
> | 变量和类型 | 字段 | 描述 |
|
||
> | ------------ | --------------- | -------------------------- |
|
||
> | `static int` | `MAX_PRIORITY` | 线程可以拥有的最大优先级。 |
|
||
> | `static int` | `MIN_PRIORITY` | 线程可以拥有的最低优先级。 |
|
||
> | `static int` | `NORM_PRIORITY` | 分配给线程的默认优先级。 |
|
||
>
|
||
> 其他的可以参考Java的API手册
|
||
|
||
### 实现Callable接口
|
||
|
||
Callable接口代码:
|
||
|
||
```java
|
||
public interface Callable<V> {
|
||
V call() throws Exception;
|
||
}
|
||
```
|
||
|
||
步骤:
|
||
|
||
1. 创建一个自定义类实现Callable接口,并实现其抽象方法call(),编写线程要执行的任务
|
||
|
||
```java
|
||
class XXX implements Callable<T> {
|
||
@Override
|
||
public <T> call() throws Exception {
|
||
return T;
|
||
}
|
||
}
|
||
```
|
||
|
||
2. 创建FutureTask对象 , 并传入第一步编写的Callable类对象
|
||
|
||
```java
|
||
FutureTask<Integer> future = new FutureTask<>(callable);
|
||
```
|
||
|
||
3. 通过Thread,启动线程
|
||
|
||
```java
|
||
new Thread(future).start();
|
||
```
|
||
|
||
```java
|
||
//MyCallable.java
|
||
import java.util.concurrent.Callable;
|
||
public class MyCallable<T> implements Callable<T> {
|
||
@Override
|
||
public T call() throws Exception {
|
||
for (int i = 0; i < 5; i++) {
|
||
System.out.println("MyCallable:" + i);
|
||
}
|
||
return null;
|
||
}
|
||
}
|
||
//CallableTest.java
|
||
import java.util.concurrent.FutureTask;
|
||
public class CallableTest {
|
||
public static void main(String[] args) {
|
||
MyCallable<String> mc = new MyCallable<> ();
|
||
FutureTask<String> future = new FutureTask<> (mc);
|
||
new Thread(future).start();
|
||
for (int i = 0; i < 5; i++) {
|
||
System.out.println("main" + i);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
> 上述代码也可以通过使用匿名内部类的方式进行简化:
|
||
>
|
||
> ```java
|
||
> import java.util.concurrent.Callable;
|
||
> import java.util.concurrent.FutureTask;
|
||
> public class CallableTest {
|
||
> public static void main(String[] args) {
|
||
> new Thread(new FutureTask<>(new Callable<String>() {
|
||
> @Override
|
||
> public String call() throws Exception {
|
||
> for (int i = 0; i < 5; i++) {
|
||
> System.out.println("MyCallable:" + i);
|
||
> }
|
||
> return null;
|
||
> }
|
||
> })).start();
|
||
> for (int i = 0; i < 5; i++) {
|
||
> System.out.println("main" + i);
|
||
> }
|
||
> }
|
||
> }
|
||
> ```
|
||
>
|
||
>
|
||
|
||
> **Runnable 与 Callable比较**
|
||
>
|
||
> * 相同点:
|
||
> * 都是接口
|
||
> * 都可以编写多线程程序
|
||
> * 都采用Thread.start()启动线程
|
||
>
|
||
> * 不同点
|
||
> * Runnable没有返回值;Callable可以返回执行结果
|
||
> * Callable接口的call()允许抛出异常;Runnable的run()不能抛出
|
||
>
|
||
> Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执
|
||
> 行,如果不调用不会阻塞。
|
||
|
||
## 多线程的应用实例
|
||
|
||
### 设置和获取线程名称
|
||
|
||
`currentThread()` 可以获取当前正在执行的线程对象
|
||
|
||
```java
|
||
//MyRunnable.java
|
||
public class MyRunnable implements Runnable{
|
||
@Override
|
||
public void run() {
|
||
System.out.println(Thread.currentThread().getName());//获取当前线程对象的名称
|
||
}
|
||
}
|
||
//GetThread.java
|
||
public class GetThread {
|
||
public static void main(String[] args) {
|
||
System.out.println(Thread.currentThread().getName());//获取当前线程对象的名称
|
||
new Thread(new MyRunnable()).start();
|
||
new Thread(new MyRunnable()).start();
|
||
new Thread(new MyRunnable(),"answer").start(); //给线程指定一个名称 (方法一)
|
||
Thread t = new Thread(new MyRunnable());
|
||
t.setName("anotherWay"); //给线程指定一个名称 (方法二)
|
||
t.start();
|
||
}
|
||
}
|
||
```
|
||
|
||
执行结果:
|
||
|
||

|
||
|
||
### 线程休眠sleep
|
||
|
||
`sleep(long millis)`是Thread类的静态方法,类名直接调用即可,单位ms。
|
||
|
||
```java
|
||
public class Demo1 {
|
||
public static void main(String[] args) throws InterruptedException {
|
||
for (int i = 0; i < 10; i++) {
|
||
System.out.print(i + " ");
|
||
Thread.sleep(1000);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
运行结果:每隔1秒打印一个数字。
|
||
|
||

|
||
|
||
> **线程阻塞**:所有较耗时的操作都能称为阻塞。也叫耗时操作。
|
||
|
||
### 线程的中断
|
||
|
||
一个线程是一个独立的执行路径,它是否应该结束,**由其自身决定**。
|
||
|
||
因为线程执行过程会有很多资源需要使用或释放,如果干涉它的结束,很可能导致资源没能来得及释放,一直占用,从而产生无法回收的内存垃圾。
|
||
|
||
Java以前提供stop()方法可以结束线程,现在已经过时(不再使用)。现在出了新的方法,**给线程打中断标记**(`interrupt`)来控制它的结束。
|
||
|
||
具体方法就是 调用`interrupt()`方法,子线程执行时捕获中断异常,并在catch块中,添加处理释放资源的代码。
|
||
|
||
如下代码所示:main线程执行完后不管子线程是否执行完都中断掉它
|
||
|
||
```java
|
||
//MyRunnable.java
|
||
public class MyRunnable implements Runnable{
|
||
@Override
|
||
public void run() {
|
||
//线程任务:打印1-10
|
||
for (int i = 1; i <= 10; i++) {
|
||
System.out.println(Thread.currentThread().getName() + ":" + i);
|
||
try {
|
||
Thread.sleep(1000);
|
||
} catch (InterruptedException e) {//发现中断标记,进入catch块中,进行释放资源处理
|
||
System.out.println(Thread.currentThread().getName()+":发现中断标记,我自杀了");
|
||
return; //为了演示,直接结束
|
||
}
|
||
}
|
||
}
|
||
}
|
||
//InterruptTest.java
|
||
public class InterruptTest {
|
||
public static void main(String[] args) {
|
||
Thread t1 = new Thread(new MyRunnable());
|
||
t1.setName("myThread");
|
||
t1.start();
|
||
//main线程 打印1-5
|
||
for (int i = 1; i <= 5; i++) {
|
||
System.out.println(Thread.currentThread().getName() + ":" + i);
|
||
try {
|
||
Thread.sleep(1000);
|
||
} catch (InterruptedException e) {
|
||
e.printStackTrace();
|
||
}
|
||
}
|
||
t1.interrupt(); //给线程t1添加中断标记
|
||
}
|
||
}
|
||
```
|
||
|
||
运行结果:
|
||
|
||

|
||
|
||
### 守护线程
|
||
|
||
线程分为**守护线程**和**用户线程**
|
||
|
||
- **用户线程**:当一个进程不包含任何存活的用户线程时,进行结束。
|
||
- **守护线程**:守护用户线程,当最后一个用户线程结束时,所有守护线程自动死亡。
|
||
|
||
直接创建的都是用户线程,
|
||
|
||
设置线程为守护线程:在启动之前设置 ,语法为:`线程对象.setDaemon(true);`。
|
||
|
||
```java
|
||
//MyRunnable.java
|
||
public class MyRunnable implements Runnable{
|
||
@Override
|
||
public void run() {
|
||
//线程任务:打印1-10
|
||
for (int i = 1; i <= 10; i++) {
|
||
System.out.println(Thread.currentThread().getName() + ":" + i);
|
||
try {
|
||
Thread.sleep(1000);
|
||
} catch (InterruptedException e) {
|
||
e.printStackTrace();
|
||
return; //为了演示,直接结束
|
||
}
|
||
}
|
||
}
|
||
}
|
||
//InterruptTest.java
|
||
public class InterruptTest {
|
||
public static void main(String[] args) {
|
||
Thread t1 = new Thread(new MyRunnable());
|
||
t1.setName("myThread");
|
||
t1.setDaemon(true);//设置t1为守护线程
|
||
t1.start();
|
||
//main线程 打印1-5
|
||
for (int i = 1; i <= 5; i++) {
|
||
System.out.println(Thread.currentThread().getName() + ":" + i);
|
||
try {
|
||
Thread.sleep(1000);
|
||
} catch (InterruptedException e) {
|
||
e.printStackTrace();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
运行结果
|
||
|
||

|
||
|
||
### 线程安全问题
|
||
|
||
#### 问题引入
|
||
|
||
我们先来看个例子:三个窗口(线程)同时卖5张票。
|
||
|
||
```java
|
||
public class Demo1 {
|
||
public static void main(String[] args) {
|
||
Runnable run = new Ticket();
|
||
new Thread(run).start();
|
||
new Thread(run).start();
|
||
new Thread(run).start();
|
||
}
|
||
static class Ticket implements Runnable{
|
||
private int count = 5; //票数
|
||
@Override
|
||
public void run() {
|
||
while (count > 0) {
|
||
//卖票
|
||
System.out.println(Thread.currentThread().getName()+"正在卖票");
|
||
try {
|
||
Thread.sleep(1000);
|
||
} catch (InterruptedException e) {
|
||
e.printStackTrace();
|
||
}
|
||
count--;
|
||
System.out.println(Thread.currentThread().getName()+"出票成功,余票:"+count);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
运行结果部分截图:
|
||
|
||

|
||
|
||
我们看到余票出现了负数,显然这是不合理的,这就是线程不安全导致的。出现这种情况的原因:线程争抢,导致线程不安全。 多线程在进行同一卖票任务时,没人干涉,各个窗口疯狂买票,最终导致卖的票超出总票数,余票出现负数。
|
||
|
||
<font color=red>**线程不安全的原因:**</font>
|
||
|
||
当多线程并发访问**临界资源**时,如果破坏**原子操作**,可能会造成数据不一致。
|
||
|
||
- 临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。
|
||
- 原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。
|
||
|
||
多个线程争抢同一个数据,使得数据在判断和使用时出现不一致的情况。那如何解决呢?
|
||
|
||
解决方法:保证一段数据同时只能被一个线程使用(排队使用),也就是线程同步,给线程加锁(synchronized)
|
||
|
||
我们有以下三种方法解决线程不安全的问题:同步代码块、同步方法、显示锁
|
||
|
||
#### 同步代码块
|
||
|
||
使用synchronized关键字加上一个锁对象来定义一段代码, 这就叫同步代码块
|
||
|
||
多个同步代码块如果使用**相同的锁对象**, 那么他们就是同步的
|
||
|
||
语法格式:`synchronized(锁对象) {}`
|
||
|
||
任何对象都可以作为**锁对象**存在。
|
||
|
||
还以上面卖票的代码为例,给卖票的线程加锁
|
||
|
||

|
||
|
||
#### 同步方法
|
||
|
||
以方法为单位进行加锁。把synchronized关键字修饰在方法中。
|
||
|
||
还以上面卖票的代码为例,写一个synchronized修饰的方法sale()执行卖票任务,
|
||
|
||

|
||
|
||
#### 显式锁
|
||
|
||
> 上面三种方法中,同步代码块和同步代码都是隐式锁
|
||
|
||
`Lock l = new ReentrantLock()`:自己创建一把锁
|
||
|
||
`lock()`:加锁 `unlock()`:解锁
|
||
|
||
还以上面卖票的代码为例
|
||
|
||

|
||
|
||
> **显式锁和隐式锁的区别:**
|
||
>
|
||
> | 区别 | synchronized | lock |
|
||
> | ------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
|
||
> | 原始构成 | Java关键字,由JVM维护,是JVM层面的锁 | JDK1.5之后的类,使用lock是在调用API,是API层面的锁 |
|
||
> | 使用方式 | 隐式锁,不需要手动获取和释放锁,只需要写synchronized,不用进行其他操作 | 显式锁,需要手动获取和释放锁,如果没有释放锁,可能会出现死锁 |
|
||
> | 等待中断 | 不会中断,除非抛出异常或正常运行完成 | 可以中断,1:调用设置超时方法tryLock(long timeout ,timeUnit unit);2:调用lockInterruptibly()放到代码块中,然后调用interrupt()方法可以中断 |
|
||
> | 加锁公平 | **非公平锁** | 可以是**公平锁**也可以是**非公平锁**,默认是非公平锁。可以在其构造方法传入Boolean值,true公平锁,false非公平锁 |
|
||
> | 绑定多个条件 | 没有。不能精确唤醒线程,要么随机唤醒一个线程,要么唤醒所有等待线程 | 用来实现分组唤醒需要唤醒的线程,可以精确唤醒线程 |
|
||
> | 性能 | JDK1.5时性能较低,JDK1.6时性能优化,与lock相较无异 | JDK1.5时性能更高,JDK1.6时synchronized优化赶上lock |
|
||
> | 加锁方式 | 线程获取独占锁(CPU悲观锁机制),只能依靠阻塞等待线程释放锁。在CPU转换线程阻塞时会引起线程上下文切换,当竞争锁的线程过多时,会引起CPU频繁上下文切换导致效率低下 | 使用乐观锁机制(CAS操作 Computer and Swap),假设不会发生冲突,一旦发生冲突失败就重试,直到成功为止。 |
|
||
>
|
||
> **公平锁**:先来先得,排队执行
|
||
>
|
||
> **非公平锁**:抢占式的,谁抢到是谁的
|
||
|
||
> 更多关于线程安全的问题可以看下面这篇文章
|
||
>
|
||
> [如果你这样回答“什么是线程安全”,面试官都会对你刮目相看(建议珍藏)](https://mp.weixin.qq.com/s/WDeewsvWUEBIuabvVVhweA)
|
||
|
||
### <font color=red>线程死锁</font>
|
||
|
||
#### 概述
|
||
|
||
当两个或两个以上的线程在执行过程中,因为争夺资源而造成的一种相互等待的状态,由于存在一种环路的锁依赖关系而永远地等待下去,如果没有外部干涉,他们将永远等待下去,此时的这个状态称之为死锁。
|
||
|
||
多个线程相互占用对方的资源的锁,而又相互等对方释放锁,此时若无外力干预,这些线程则一直处于阻塞的假死状态,形成死锁。
|
||
|
||
举个例子,如下图所示,在线程A持有锁L并想获得锁M的同时,线程B持有锁M并尝试获得锁L,那么这两个线程将永远地等待下去,这种情况就是死锁形式。
|
||
|
||

|
||
|
||
**死锁产生的条件:**
|
||
|
||
- **互斥条件**:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用完释放。
|
||
- **请求和保持条件**:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
|
||
- **不剥夺条件**:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
|
||
- **环路等待条件**:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{A,B,C,···,Z} 中的A正在等待一个B占用的资源;B正在等待C占用的资源,……,Z正在等待已被A占用的资源。
|
||
|
||
#### 如何避免死锁
|
||
|
||
1. 按顺序加锁:如果每个线程都按同一个的加锁顺序这样就不会出现死锁。
|
||
|
||
2. 给锁加时限:每个线程获取锁的时候加上个时限,如果超过某个时间就放弃锁。
|
||
|
||
3. 死锁检测:按线程间获取锁的关系检测线程间是否发生死锁,如果发生死锁就执行一定的策略,如终断线程或回滚操作等。
|
||
|
||
> 更多关于线程死锁的问题可以看下面这篇文章,以上内容也是来自这篇文章:
|
||
>
|
||
> [多线程 死锁详解](https://mp.weixin.qq.com/s/kvvJ9_xKaOMobaX7PZ5VCQ)
|
||
|
||
### 多线程通信
|
||
|
||
Object方法中提供了一些线程间相互通信的方法
|
||
|
||
| 变量和类型 | 方法 | 描述 |
|
||
| ---------- | -------------------------------------- | ------------------------------------------------------------ |
|
||
| `void` | `notify()` | 唤醒正在此对象监视器上等待的单个线程。 |
|
||
| `void` | `notifyAll()` | 唤醒等待此对象监视器的所有线程。 |
|
||
| `void` | `wait()` | 导致当前线程等待它被唤醒,通常是 通知或 中断 。 |
|
||
| `void` | `wait(long timeoutMillis)` | 导致当前线程等待它被唤醒,通常是 通知或 中断 ,或者直到经过一定量的实时。 |
|
||
| `void` | `wait(long timeoutMillis, int nanos)` | 导致当前线程等待它被唤醒,通常是 通知或 中断 ,或者直到经过一定量的实时。 |
|
||
|
||
**什么时候需要通信**
|
||
|
||
多个线程并发执行时, 在默认情况下CPU是随机切换线程的,如果我们希望他们有规律的执行, 就可以使用通信。
|
||
|
||
#### 生产者与消费者
|
||
|
||
> 看下面代码,有Cooker类,Waiter类,Food类
|
||
>
|
||
> 厨师cooker为生产者线程,服务员waiter为消费者线程,食物food为生产与消费的物品;
|
||
>
|
||
> 假设目前只有一个厨师,一个服务员,一个盘子。理想状态是:厨师生产一份饭菜,服务员端走一份,且饭菜的属性未发生错乱;
|
||
>
|
||
> 厨师可以制作两种口味的饭菜,制作100次;
|
||
>
|
||
> 服务员可以端走饭菜100次;
|
||
|
||
```java
|
||
public class Demo {
|
||
public static void main(String[] args) {
|
||
Food f = new Food();
|
||
new Cooker(f).start();
|
||
new Waiter(f).start();
|
||
}
|
||
static class Cooker extends Thread{ //生产者线程
|
||
private Food f;
|
||
public Cooker(Food f){
|
||
this.f = f;
|
||
}
|
||
public void run() {
|
||
//生产100个菜
|
||
for (int i = 0; i < 100; i++) {
|
||
if (i % 2 == 0){
|
||
f.setNameAndTaste("菜1","味道1");
|
||
} else {
|
||
f.setNameAndTaste("菜2","味道2");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
static class Waiter extends Thread { //消费者线程
|
||
private Food f;
|
||
public Waiter(Food f){
|
||
this.f = f;
|
||
}
|
||
public void run() {
|
||
for (int i = 0; i < 100; i++) {
|
||
try {
|
||
Thread.sleep(100);
|
||
} catch (InterruptedException e) {
|
||
e.printStackTrace();
|
||
}
|
||
f.get();
|
||
}
|
||
}
|
||
}
|
||
static class Food {
|
||
private String name;
|
||
private String taste;
|
||
public void setNameAndTaste(String name,String taste){ //生产
|
||
this.name = name; //先设定名称
|
||
try {
|
||
Thread.sleep(100); //为使线效果明显,中间休眠一段时间
|
||
} catch (InterruptedException e) {
|
||
e.printStackTrace();
|
||
}
|
||
this.taste = taste; //休眠后设定味道
|
||
}
|
||
public void get(){ //消费
|
||
System.out.println("服务员端走的菜名称是:" + name + ",味道:" + taste);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
运行结果
|
||
|
||

|
||
|
||
原因:我们在设定菜名和味道的`setNameAndTaste`方法中,先设定名称,然后休眠一段时间,再设定的味道,中间休眠的那段时间很可能发生时间片丢失,使得菜属性产生混乱。
|
||
|
||
**解决方式一**:
|
||
|
||
为了防止在生产过程中setNameAndTaste出现时间片切换,可以用synchronized修饰此方法;
|
||
|
||
```java
|
||
public synchronized void setNameAndTaste(String name,String taste){
|
||
//...
|
||
}
|
||
public synchronized void get(){ // 消费
|
||
//...
|
||
}
|
||
```
|
||
|
||
运行结果
|
||
|
||

|
||
|
||
可以看出,依然不符合实际情况,这是因为synchronized只是确保了方法内部不会发生线程切换,但并不能保证生产一个消费一个的逻辑关系
|
||
|
||
**解决方式二**:
|
||
|
||
在解决方案一的基础上,进行线程之间的通信
|
||
|
||
```java
|
||
private boolean flag = true; //默认为true,表示可以做饭
|
||
```
|
||
|
||
厨师做完饭后喊醒服务员,自己睡着。服务员送完饭后喊醒厨师,自己睡着;将Food类左如下修改
|
||
|
||

|
||
|
||
运行结果,做一道菜,端走一道。
|
||
|
||

|
||
|
||
## 线程的六种状态
|
||
|
||
Enum Thread.State描述了六种线程的状态,如下表所示
|
||
|
||
| Enum Constant | 描述 |
|
||
| --------------- | -------------------------------------------------- |
|
||
| `BLOCKED` | 线程的线程状态被阻塞等待监视器锁定。(阻塞) |
|
||
| `NEW` | 尚未启动的线程的线程状态。(创建) |
|
||
| `RUNNABLE` | 可运行线程的线程状态。(就绪和运行) |
|
||
| `TERMINATED` | 终止线程的线程状态。(消亡) |
|
||
| `TIMED_WAITING` | 具有指定等待时间的等待线程的线程状态。(有限期等待) |
|
||
| `WAITING` | 等待线程的线程状态。(无限期等待) |
|
||
|
||
## 线程池Executors
|
||
|
||
> 普通线程的执行流程:
|
||
>
|
||
> 创建线程 → 创建任务 → 执行任务 → 关闭线程
|
||
>
|
||
> 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间。 线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建和销毁线程对象的操作,节省了大量的时间和资源。
|
||
|
||
线程池的好处
|
||
|
||
* 降低资源消耗。
|
||
* 提高响应速度。
|
||
* 提高线程的可管理性。
|
||
|
||
Java中有四种线程池(ExecutorService):缓存线程池、定长线程池、单线程线程池、周期性任务定长线程池
|
||
|
||
### 缓存线程池
|
||
|
||
长度无限制
|
||
|
||
执行流程:
|
||
|
||
1. 判断线程池是否存在空闲线程
|
||
|
||
2. 存在则使用
|
||
|
||
3. 不存在,则创建线程 并放入线程池, 然后使用
|
||
|
||
```java
|
||
ExecutorService service = Executors.newCachedThreadPool(); //获取缓存线程池对象
|
||
//向线程池中 加入 新的任务
|
||
service.execute(new Runnable() {
|
||
@Override
|
||
public void run() {
|
||
//线程任务代码
|
||
}
|
||
});
|
||
```
|
||
|
||
### 定长线程池
|
||
|
||
长度是指定的数值
|
||
|
||
步骤:
|
||
|
||
1. 判断线程池是否存在空闲线程
|
||
2. 存在则使用
|
||
3. 不存在空闲线程,线程池未满的情况下,则创建线程 并放入线程池, 然后使用
|
||
4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
|
||
|
||
```java
|
||
ExecutorService service = Executors.newFixedThreadPool(2);
|
||
service.execute(new Runnable() {
|
||
public void run() {
|
||
//线程任务代码
|
||
}
|
||
});
|
||
```
|
||
|
||
### 单线程线程池
|
||
|
||
步骤:
|
||
|
||
1. 判断线程池的那个线程是否空闲
|
||
2. 空闲则使用
|
||
3. 不空闲则等待池中的单个线程空闲后使用
|
||
|
||
```java
|
||
ExecutorService service = Executors.newSingleThreadExecutor();
|
||
service.execute(new Runnable() {
|
||
public void run() {
|
||
//线程任务代码
|
||
}
|
||
});
|
||
```
|
||
|
||
### 周期性任务定长线程池
|
||
|
||
步骤:
|
||
|
||
1. 判断线程池是否存在空闲线程
|
||
2. 存在则使用
|
||
3. 不存在空闲线程,且线程池未满的情况下,则创建线程,并放入线程池后使用
|
||
4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
|
||
|
||
周期性任务执行时:定时执行, 当某个时机触发时, 自动执行某任务
|
||
|
||
```java
|
||
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
|
||
/**
|
||
* 定时执行
|
||
* 参数1. runnable类型的任务
|
||
* 参数2. 时长数字 5
|
||
* 参数3. 时长数字的单位 TimeUnit.SECONDS
|
||
*/
|
||
service.schedule(new Runnable() {
|
||
public void run() {
|
||
//线程任务代码
|
||
}
|
||
},5,TimeUnit.SECONDS);
|
||
/**
|
||
* 周期执行
|
||
* 参数1. runnable类型的任务
|
||
* 参数2. 时长数字(延迟执行的时长) 5
|
||
* 参数3. 周期时长(每次执行的间隔时间) 2
|
||
* 参数4. 时长数字的单位 TimeUnit.SECONDS
|
||
*/
|
||
service.scheduleAtFixedRate(new Runnable() {
|
||
public void run() {
|
||
//线程任务代码
|
||
}
|
||
},5,2,TimeUnit.SECONDS);
|
||
```
|
||
|
||
|
||
|
||
|
||
|