Java并发编程01基础知识
进程、线程与管程
进程是程序的一次执行过程,是资源分配的最小单元。
线程是任务调度的最小单元。
线程的生命周期与状态
线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。
当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的run()方法之后将会进入到 TERMINATED(终止) 状态。
线程的创建
继承Thread类创建
package thread.create;
/**
* @program: draft
* @description: 通过继承Thread类创建线程
* @author: atong
* @create: 2021-12-18 14:50
*/
public class Extends {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("I am a child thread");
}
}
}
实现Runnable接口创建
package thread.create;
/**
* @program: draft
* @description: 通过实现Runable接口创建线程
* @author: atong
* @create: 2021-12-18 14:55
*/
public class Implements {
public static void main(String[] args) {
Runnable runnable = new RunableTask();
new Thread(runnable).start();
new Thread(runnable).start();
}
public static class RunableTask implements Runnable {
@Override
public void run() {
System.out.println("I am a child thread");
}
}
}
通过Callable和Future创建
package thread.create;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @program: draft
* @description: 通过实现Callable接口
* @author: atong
* @create: 2021-12-18 15:00
*/
public class CallableTask {
public static void main(String[] args) {
//创建异步任务
FutureTask<String> futureTask = new FutureTask<>(new CallTask());
//启动线程
new Thread(futureTask).start();
try {
//任务执行完毕, 返回结果
String result = futureTask.get();
System.out.println(result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
public static class CallTask implements Callable<String> {
@Override
public String call() throws Exception {
return "hello world";
}
}
}
线程死锁
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力的情况下,这些线程会一直等待而无法继续运行下去。
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
package thread.lock.dead;
/**
*认识线程死锁
*
*产生死锁的四个必备条件:
* 1.互斥条件:该资源任意一个时刻只由一个线程占用。
* 2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
* 3.不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
* 4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
*
* 如何避免线程死锁:
* 1.破坏互斥条件 :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
* 2.破坏请求与保持条件 :一次性申请所有的资源。
* 3.破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
* 4.破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
*/
public class DeadLock {
//资源 1
private static Object resource1 = new Object();
//资源 2
private static Object resource2 = new Object();
public static void main(String[] args) {
//线程1在持有resource1未释放的前提下,去锁resource2
//线程1通过 synchronized(resource1)获得resource1的监视器锁
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
//让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
//线程1请求获取resource2,但resource2已被线程2持有且未释放。
//导致死锁
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
//线程2在持有resource2未释放的前提下,去锁resource1
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
//让线程 B 休眠 1s 为的是让线程 A 得到执行然后获取到 resource1 的监视器锁
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
//线程2请求获取resource1,但resource1已被线程1持有且未释放。
//导致死锁
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
产生死锁的四个必备条件
互斥条件:该资源同时只能由一个线程占用。
请求与持有条件:一个线程已经持有至少一个资源,但又请求已被其他线程占有的资源而阻塞,同时对已持有的资源保持不放。
不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
如何避免线程死锁
只要破坏产生死锁的四个条件中的其中一个就可以了。但是,目前只有请求与持有和环路等待是可以被破坏的。
破坏互斥条件
这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
破坏请求与保持条件
一次性申请所有的资源。
破坏不剥夺条件
对于持有部分资源的进程进一步申请其他资源时,如果申请不到,可以主动释放它所持有的资源。
破坏循环等待条件
靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
package thread.lock.dead;
/**
* @program: draft
* @description: 打破线程死锁
* 如何避免线程死锁:
* 破坏互斥条件
* 这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
* 破坏请求与保持条件
* 一次性申请所有的资源。
* 破坏不剥夺条件
* 对于持有部分资源的进程进一步申请其他资源时,如果申请不到,可以主动释放它所持有的资源。
* 破坏循环等待条件
* 靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
* @author: atong
* @create: 2021-12-18 15:13
*/
public class BreakDeadLock {
//资源 1
private static Object resource1 = new Object();
//资源 2
private static Object resource2 = new Object();
public static void main(String[] args) {
//线程1在持有resource1未释放的前提下,去锁resource2
new Thread(() -> {
//线程1通过 synchronized(resource1)获得resource1的监视器锁
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
//让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
//线程1请求获取resource2,但resource2已被线程2持有且未释放。
//导致死锁
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
/**
* 造成死锁的原因和申请资源的顺序有很大关系,
* 使用资源申请的有序性原则就可以避免死锁
* 该类与src/main/java/thread/lock/dead/DeadLock.java 不同之处,
* 在于:该类 线程2 先去获取resource1, 后去获取 resource2, 跟线程1资源顺序保持一致
*/
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource2");
try {
//让线程 B 休眠 1s 为的是让线程 A 得到执行然后获取到 resource1 的监视器锁
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
//线程2请求获取resource1,但resource1已被线程1持有且未释放。
//导致死锁
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
如何检测死锁
第一步,使用 Java 中的 jps 命令,查看进程ID。
第二步,使用 jstack,查看线程状态。
wait方法与sleep方法的区别
sleep是Thread的静态方法。
wait是Object 的方法,任何对象实例都能调用。
sleep 通常被用于暂停执行。
wait 通常被用于线程间交互/通信。
sleep不会释放锁,它也不需要占用锁。
wait 会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized 中)。
它们都可以被interrupted方法中断。
用户线程与守护线程
用户线程
平时用到的普通线程均是用户线程,当在Java程序中创建一个线程,它就被称为用户线程。
守护线程
守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者。当最后一个非守护线程结束时候,JVM会正常退出,同时会杀死进程中的所有守护线程。
管程
管程(monitor)是操作系统中的一个重要概念。它也可用于Java同步中。
所谓管程,指的是管理共享变量以及对其的操作过程,让它们支持并发访问。
常见问题
为什么要使用多线程呢?
单核时代: 在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率。举个例子:当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我们可以简单地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了,当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在理想情况下达到 100%了。
多核时代: 多核时代多线程主要是为了提高 CPU 利用率。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。
为什么调用 start() 方法时会执行 run() 方法,为什么不能直接调用 run() 方法?
调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
Runnable和Callable有什么不同?
Runnable从JDK1.0开始就有了,Callable是在JDK1.5增加的。
它们的主要区别是Callable的 call() 方法可以返回值和抛出异常,而Runnable的run()方法没有这些功能。
Callable可以返回装载有计算结果的Future对象。