java并发基础(1) 介绍了并发的理论基础,接下来继续看下并发的编程基础
并发编程基础
- 1. 线程的生命周期
- 2. 线程状态的转换
- 2.1. runnable<=>blocked
- 2.2. RUNNABLE <=> WAITING
- 2.3. RUNNABLE 到 TERMINATED
- stop() 方法
- interrupt() 方法
- 3. 使用线程的方式
- 3.1. 实现 Runnable 接口
- 3.2. 实现 Callable 接口
- 3.3. 继承 Thread 类
- 3.4 实现接口还是继承 Thread
- 3.5. Executor
- 4. 线程互斥同步
- 4.1. synchronized
- 4.2. ReentrantLock
- 4.3. 使用哪一个
1. 线程的生命周期
生命周期 | 解释 |
1. 新建状态 | 通过new创建了线程对象 |
2. 就绪状态(可运行状态) | 调用了start()方法,就进入就绪状态,等待jvm的线程调度器的调度 |
3. 运行状态 | 当获取了CPU资源就执行run()方法,就是运行状态 |
4. 阻塞状态 | c. 调用sleep()或是join():未超时,进入阻塞,超时后进入就绪等待CPU调度。 |
5. 死亡状态 | b.强制死亡:执行了stop;destroy |
2. 线程状态的转换
2.1. runnable<=>blocked
两种状态的相互转换的场景,只有synchronized这一种场景。
没有获取synchronized的线程会处于blocked状态,获取了synchronized之后,就会处于Runnable,等待CPU的调度。
线程调用阻塞式 API 时,是否会转换到 BLOCKED 状态呢
不会。线程会阻塞,指的是操作系统线程的状态,并不是 Java 线程的状态。
JVM 层面并不关心操作系统调度相关的状态,在 JVM 看来,等待 CPU 使用权(操作系统层面此时处于可执行状态)与等待 I/O(操作系统层面此时处于休眠状态)没有区别,都是在等待某个资源,所以都归入了 RUNNABLE 状态。
2.2. RUNNABLE <=> WAITING
操作 | 解释 |
wait() | 需其它线程唤醒(Object.notify/ Object.notifyAll),否则会一直等待堵塞。 |
Thread.sleep(mills) | 线程调用 Thread.sleep(timeOut) 时,会堵塞自己。当到达timeOut时,会进入RUNNABLE。 |
join() | 例如有一个线程A,当线程 B 调用 A.join() 时,B进入waiting状态,(线程 B )等待 A 执行完,A执行完之后,B切换到RUNNABLE状态。 |
LockSupport.park() | 调用 LockSupport.unpark(Thread thread) 可唤醒目标线程,目标线程的状态又会从 WAITING 状态转换到 RUNNABLE。 |
2.3. RUNNABLE 到 TERMINATED
当程序执行完run,或者执行run内的方法抛出异常时,线程会变成TERMINATED。
可以通过stop() 和 interrupt()强制中断run的执行。
stop() 方法
会杀死线程,而不释放锁(调用unlock() )。如果线程持有 ReentrantLock 锁,执行stop之后,因为没有释放锁,其他线程再也获取不到ReentrantLock 锁。
interrupt() 方法
仅仅是通知线程,线程有机会执行一些后续操作,同时也可以无视这个通知。
被 interrupt 的线程,是怎么收到通知的呢?
当线程 A 处于 WAITING(调用了类似 wait/join/sleep)时,如果线程B调用线程 A 的 interrupt() 方法,会使线程 A 返回到 RUNNABLE状态,同时线程 A 的代码会触发 InterruptedException 异常。
当线程 A 处于 RUNNABLE 状态时,如果线程B调用A 的 interrupt() 方法,那么线程 A 可以通过 isInterrupted() 方法,检测是不是自己被中断了,拿到被中断的信号之后,可以忽视也可以做相应的操作。
3. 使用线程的方式
有三种使用线程的方法 : 实现 Runnable 接口、实现 Callable 接口、继承 Thread 类。
实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此还需要通过Thread 来调用。
3.1. 实现 Runnable 接口
//需要实现 run() 方法。
public class MyRunnable implements Runnable {
public void run() {
// ...
}
}
//通过 Thread 调用 start() 方法来启动线程。
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Thread thread = new Thread(instance);
thread.start();
}
3.2. 实现 Callable 接口
与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。
public class MyCallable implements Callable<Integer> {
public Integer call() {
return 123;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
3.3. 继承 Thread 类
也是需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。
当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。
public class MyThread extends Thread {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
3.4 实现接口还是继承 Thread
实现接口会更好一些:
- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
- 类可能只要求可执行就行,继承整个Thread 类开销过大。
3.5. Executor
Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。
主要有三种 Executor:
- CachedThreadPool: 一个任务创建一个线程;
- FixedThreadPool: 所有任务只能使用固定大小的线程;
- SingleThreadExecutor: 相当于大小为 1 的 FixedThreadPool
一般建议使用线程池的方式去创建线程
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
executorService.execute(new MyRunnable());
}
//会等待线程都执行完毕之后再关闭
executorService.shutdown();
//调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。
}
如果只想中断 Executor 中的一个线程,可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future<?> 对象,通过调用该对象的 cancel(true) 方法就可以中断线程。
Future<?> future = executorService.submit(() -> {
// ..
});
future.cancel(true);
4. 线程互斥同步
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,1. 是 JVM 实现的 synchronized,2. 是 JDK 实现的 ReentrantLock。
4.1. synchronized
可以同步一个代码块、方法、类。看一个例子:
public class SynchronizedExample {
public void func1() {
synchronized (this) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
}
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func1());
executorService.execute(() -> e1.func1());
}
//两个线程由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。
//0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
//两个线程调用了不同对象的同步代码块,因此这两个线程就不需要同步。从输出结果可以看出,两个线程交叉执行。
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
SynchronizedExample e2 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func1());
executorService.execute(() -> e2.func1());
}
//0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
4.2. ReentrantLock
public class LockExample {
private Lock lock = new ReentrantLock();
public void func() {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
} finally {
lock.unlock(); // 确保释放锁,从而避免发生死锁。
}
}
}
public static void main(String[] args) {
LockExample lockExample = new LockExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> lockExample.func());
executorService.execute(() -> lockExample.func());
}
//0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
4.3. 使用哪一个
比较 | 解释 |
锁的实现 | synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。 |
等待可中断 | ReentrantLock 可中断,而 synchronized 不行。 |
公平锁 | synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。 |
锁绑定多个条件 | 一个 ReentrantLock 可以同时绑定多个 Condition 对象。 |
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。
因为 synchronized JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。
并且使用 synchronized不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
参考:
https://pdai.tech/md/java/thread/java-thread-x-overview.html