天天看点

四.多线程使用及其通信

并发复习笔记之第四章(多线程使用及其通信)

想看后续请持续关注

以下来源有书籍 深入理解 JVM 虚拟机,java 并发编程的艺术,深入浅出多线程,阿里巴巴技术手册以及一些公众号 CS-Notes,JavaGuide,以及一些大厂高频面试题吐血总结,以及狂神说视频笔记,目的在于通过问题来复习整个多线程,已下是全部章节,觉得不错点个赞评论收藏三连一下,您的鼓励就是我继续创作的最大动力!!!!

文章目录

    • 4.多线程的使用
      • 4.1 多线程的四种创建方法
        • 4.1.1 实现接口和直接继承Thread类哪个好?
        • 4.1.2 Runnable实现和 Callable 实现有什么区别
      • 4.2 线程间的通信
        • 4.2.1 线程通信之 Join()
        • 4.2.2 线程通信之wait()/notify()/notifyAll
          • 4.2.2.1 wait 为什么会释放锁
          • 4.2.2.2 经典的等待通知范例
          • 4.2.2.3 虚假唤醒是什么,如何避免虚假唤醒?
          • 4.2.2.4 使用 notify 注意?
        • 4.2.3 简单介绍下 ThreadLocal

4.多线程的使用

4.1 多线程的四种创建方法

  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() 方法来启动线程。

  1. 实现 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 内部

  1. 继承 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() 方法。

  1. 线程池的方法

转线程池章节

4.1.1 实现接口和直接继承Thread类哪个好?

  1. 实现接口好,因为 java 只有单继承,如果继承了之后就无法继承其它类,但可以实现多个接口
  2. 类可能只要求可执行就行,继承整个 Thread 类开销过大。

4.1.2 Runnable实现和 Callable 实现有什么区别

  1. Runnable执行方法是run(),Callable是call()
  2. 实现Runnable接口的任务线程无返回值;实现Callable接口的任务线程能返回执行结果
  3. 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()会出现虚假唤醒

  1. 线程被唤醒后,会从 wait() 处开始继续往下执行;
  2. while 被掉换成 if 后出现了虚假唤醒,出现了我们不想要的结果;
  3. 因为if只会执行一次,执行完会接着向下执行if()外边的而while不会,直到条件满足才会向下执行while()外边的
4.2.2.4 使用 notify 注意?
  1. notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或 notifAll()的线程释放锁之后,等待线程才有机会从wait()返回。注意与 wait 方法

4.2.3 简单介绍下 ThreadLocal

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK 中提供的

ThreadLocal

类正是为了解决这样的问题。

ThreadLocal

类主要解决的就是让每个线程绑定自己的值,可以将

ThreadLocal

类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

如果你创建了一个

ThreadLocal

变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是

ThreadLocal

变量名的由来。他们可以使用

get()

set()

方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。