并发复习笔记之第四章(多线程使用及其通信)
想看后续请持续关注
以下来源有书籍 深入理解 JVM 虚拟机,java 并发编程的艺术,深入浅出多线程,阿里巴巴技术手册以及一些公众号 CS-Notes,JavaGuide,以及一些大厂高频面试题吐血总结,以及狂神说视频笔记,目的在于通过问题来复习整个多线程,已下是全部章节,觉得不错点个赞评论收藏三连一下,您的鼓励就是我继续创作的最大动力!!!!
4.多线程的使用
4.1 多线程的四种创建方法
- Runnable 接口
public class MyRunnable implements Runnable {
@Override
public void run() {
// ...
}
}
需要实现接口中的 run() 方法。
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Thread thread = new Thread(instance);
thread.start();
}
使用 Runnable 实例再创建一个 Thread 实例,然后调用 Thread 实例的 start() 方法来启动线程。
- 实现 Callable 接口
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());
}
一个类实现 callable 接口 -> 创建一个实例 送入 ->FutureTask implements RunnableFuture extends Runnable 放入 Thread 内部
- 继承 Thread 类
public class MyThread extends Thread {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
同样也是需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。
当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。
- 线程池的方法
转线程池章节
4.1.1 实现接口和直接继承Thread类哪个好?
- 实现接口好,因为 java 只有单继承,如果继承了之后就无法继承其它类,但可以实现多个接口
- 类可能只要求可执行就行,继承整个 Thread 类开销过大。
4.1.2 Runnable实现和 Callable 实现有什么区别
- Runnable执行方法是run(),Callable是call()
- 实现Runnable接口的任务线程无返回值;实现Callable接口的任务线程能返回执行结果
- call方法可以抛出异常,run方法若有异常只能在内部消化
4.2 线程间的通信
4.2.1 线程通信之 Join()
如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才 从thread.join()返回。
public class Juc {
public static void main(String[] args) throws Exception {
Thread previous = Thread.currentThread();
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new Domino(previous), String.valueOf(i));
thread.start();
previous = thread;
}
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + " terminate.");
}
static class Domino implements Runnable {
private Thread thread; public Domino(Thread thread) {
this.thread = thread;
}
public void run() {
try {
thread.join();
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + " terminate.");
}
}
}
每个线程终止的前提是前驱线程的终止,每个线程等待前驱线程 终止后,才从join()方法返回,这里涉及了等待/通知机制(等待前驱线程结束,接收前驱线程结 束通知)。
我们可以看 Join()源码
public final synchronized void join(final long millis)
throws InterruptedException {
if (millis > 0) {
if (isAlive()) {
final long startTime = System.nanoTime();
long delay = millis;
do {
wait(delay);
} while (isAlive() && (delay = millis -
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
}
} else if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
throw new IllegalArgumentException("timeout value is negative");
}
}
由源码我们可以看出,调用 join 方法的线程死去之后,才会从 join()方法处返回
4.2.2 线程通信之wait()/notify()/notifyAll
方法 | 描述 |
---|---|
wait() | 调用该方法进入 wating 状态,需要被通知或者中断后会返回,注意 wait 方法会释放锁 |
notify() | 通知一个在对象等待的线程,从 wait() 方法返回,特别注意不会立刻释放锁,执行完代码块才会释放 |
notifyAll() | 通知所有等待队列的线程 |
wait() | 超时等待一会,如果超时了,就返回 |
在图中,WaitThread首先获取了对象的锁,然后调用对象的wait()方法,从而放弃了锁 并进入了对象的等待队列WaitQueue中,进入等待状态。由于WaitThread释放了对象的锁, NotifyThread随后获取了对象的锁,并调用对象的notify()方法,将WaitThread从WaitQueue移到 SynchronizedQueue中,此时WaitThread的状态变为阻塞状态。NotifyThread释放了锁之后, WaitThread再次获取到锁并从wait()方法返回继续执行。
4.2.2.1 wait 为什么会释放锁
这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。
4.2.2.2 经典的等待通知范例
synchronized(对象) {
while(条件不满足) {
对象.wait();
}
对应的处理逻辑
}
synchronized(对象) {
改变条件 对象.notifyAll();
}
4.2.2.3 虚假唤醒是什么,如何避免虚假唤醒?
虚假唤醒:在线程的 等待/唤醒 的过程中,等待的线程被唤醒后,在条件不满足的情况依然继续向下运行了。
为什么使用 if()会出现虚假唤醒
- 线程被唤醒后,会从 wait() 处开始继续往下执行;
- while 被掉换成 if 后出现了虚假唤醒,出现了我们不想要的结果;
- 因为if只会执行一次,执行完会接着向下执行if()外边的而while不会,直到条件满足才会向下执行while()外边的
4.2.2.4 使用 notify 注意?
- notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或 notifAll()的线程释放锁之后,等待线程才有机会从wait()返回。注意与 wait 方法
4.2.3 简单介绍下 ThreadLocal
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK 中提供的ThreadLocal
类正是为了解决这样的问题。 ThreadLocal
类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal
类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
如果你创建了一个ThreadLocal
变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal
变量名的由来。他们可以使用 get()
和 set()
方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。