java并发基础(1) 介绍了并发的理论基础,接下来继续看下并发的编程基础
并发编程基础
1. 线程的生命周期
生命周期 | 解释 |
---|---|
1. 新建状态
|
通过new创建了线程对象
|
2. 就绪状态(可运行状态)
|
调用了start()方法,就进入就绪状态,等待jvm的线程调度器的调度
|
3. 运行状态
|
当获取了CPU资源就执行run()方法,就是运行状态
|
4. 阻塞状态
|
a. 等待阻塞:执行了wait(),进入等待池
b. 同步阻塞:获取synchronized失败,进入到锁池中
c. 调用sleep()或是join():未超时,进入阻塞,超时后进入就绪等待CPU调度。
|
5. 死亡状态
|
a. run()执行结束,或异常退出后的状态,此状态不可逆转。
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()
|
当获取synchronized的线程,调用 Object.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()
|
java 并发包中的锁,都是基于它实现的。
调用 LockSupport.park() 方法,当前线程会阻塞,线程的状态会从 RUNNABLE 转换到 WAITING。
调用 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
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/65396.html