安琪拉教百里守约学并发编程之多线程基础

Kamaria ·
更新时间:2024-09-21
· 588 次阅读

《安琪拉与面试官二三事》系列文章
一个HashMap能跟面试官扯上半个小时
一个synchronized跟面试官扯了半个小时

《安琪拉教鲁班学算法》系列文章

安琪拉教鲁班学算法之动态规划

安琪拉教鲁班学算法之BFS和DFS

安琪拉教鲁班学算法之堆排序
《安琪拉教妲己学分布式》系列文章
安琪拉教妲己分布式限流
《安琪拉教百里守约学并发编程》系列文章
安琪拉教百里守约学并发编程之多线程基础

本文是来自读者群里@凯的建议,决定开一个多线程的专栏,尊重读者安琪拉是认真的,为什么是教百里守约,也是因为读者群里@百里守约是安琪拉的忠实读者,每期必读,经常草丛里定点蹲安琪拉,然后抢板凳(放大招)。这期是《安琪拉教百里守约学并发线程》系列文章第一集多线程基础。

前言

并发编程应该是Java 后端工程必备的技能,在日常开发中用的好能提升系统吞吐量,提升业务逻辑执行效率,提高系统的响应性,简化程序结构,当然这把青龙偃月刀也不是随随便便就能耍的好,需要些内力。先放一张Java 并发工具包JUC的知识脑图,后面 Wx公众号【安琪拉的博客】《安琪拉教百里守约学并发线程》会按以下思维脑图详细介绍 JUC 的各部分组件实际使用场景以及组件特性:

开场

百里守约:安琪拉,你熟悉线程(Thread)吗?和进程(Process)有什么区别?

安琪拉:熟悉啊!一个应用就是一个进程,一个进程可以包含多个线程,从操作系统层面看,同一个进程中的线程共享该进程的资源,例如内存空间和文件句柄。Linux 操作系统中线程是轻量级进程。

百里守约:在Java 中怎么创建一个线程呢?

安琪拉:线程的创建有2 种方式,如下,很多网上的文章还写了通过线程池的方式创建,其本质也是这二种中的一种:

继承 Thread 类; 实现 Runnable 接口;

百里守约:能不能用实际的代码举例说一下?

安琪拉:可以,如下所示:

public static void main(String[] args) { new Seller("笔").start(); new Thread(new Seller02("书")).start(); } //第一种方式 继承 Thread public static class Seller extends Thread{ String product; public Seller(String product){ this.product = product; } @Override public void run() { System.out.println("继承 Thread类 卖 " + product); } } //第二种方式 实现 Runnable public static class Seller02 implements Runnable{ String product; public Seller02(String product){ this.product = product; } @Override public void run() { System.out.println("实现 Runnable接口 卖 " + product); } }

百里守约:如果我直接使用 new Seller("笔").run() 执行和start() 有什么区别?

安琪拉start() 方法是native 方法,JVM 会另起一个线程执行,而直接执行run() 方法是本地线程执行,我们可以使用示例程序对比一下,如下:

public static void main(String[] args) { new Seller("笔").run(); //没有另起一个线程 new Seller("笔").start(); //在新线程中执行 run 函数 } //第一种方式 继承 Thread public static class Seller extends Thread{ String product; public Seller(String product){ this.product = product; } @Override public void run() { System.out.println(String.format("当前线程: %s 卖%s", Thread.currentThread().getName(), product)); } }

看下控制台输出如下:

当前线程: main 卖笔 当前线程: Thread-1 卖笔

因为调用start() 方法后,JVM 会新建一个线程来执行run() 方法内容。

百里守约:我理解了,Thread 对象是Java 中普通的对象,和其他对象一样,只是在调用 start() 这个native 方法时变得不一样了,JVM 会根据Thread 对象来创建线程。

安琪拉:你说的非常对,new Thread() 创建Thread 对象时,JVM 还没有实际创造线程,调用start() 方法后JVM 才会通过 pthread_create 方法(Linux系统)创建线程。因此一定要将Thread 对象和真实的线程区分开。

百里守约:那JVM 又是如何创建线程的呢?

安琪拉:你这个问的有点深了,我可以大致讲讲,因为今天是基础篇,因此不展开聊,想深入了解的可以关注【安琪拉的博客】公众号,有源代码层的详细的讲解,先丢个源代码地址:Hotspot1.8 jvm.cpp,推荐先将本篇文章整体看完,然后回过头来再看实现原理。担心很多同学没学过c++,或者源码太多无从下嘴,后面会出一期JVM 创建线程源代码解析。

今天先丢个大致原理:Java 种新建的Thread 对象只是操作系统线程运行的载体,Thread类的作用主要有二点:

Thread 对象内的属性提供了创建新线程时所需要的线程描述信息,例如线程名、线程id、线程组、是否为守护线程; Thread 对象内的方法提供了Java 程序可以跟操作系统线程打交道的手段,例如wait、sleep、join、interrupt等。

前面说到JVM new Thread对象时其实还没有真实创建线程,调用start() 方法时才开始正式创建。

百里守约:那线程是怎么从创建到执行,最后销毁的啊?

安琪拉:那你就要看Java 中线程的生命周期了,如下图所示:

线程状态转换图

在 Thread 类中有个State 枚举类型标识线程状态,如下。

public static enum State { NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED; }

同时可以使用Thread.currentThread().getState()获取当前线程的状态。

解释一下每种状态:

New: 刚创建而未启动的线程就是这个状态。由于一个线程只能被启动一次,因此一个线程只可能有一次在这个状态。 Runnable:如上图,这个状态实际是个复合状态,包含二个子状态:Ready 和 Running。Ready是就绪状态,可以被JVM 线程调度器(Scheduler) 进行调度,如果是单核CPU,同一时刻只有一个线程处于Running 状态,可能有多个线程处于 Ready 状态,Running 表示当前线程正在被CPU 执行,在Java 中就是Thread 对象只 run() 方法正在被执行。当 yield() 方法被调用,或者线程时间片被用完,线程就会从 Running 状态转为 Ready 状态。另外有个小姿势点,CPU 的一个时间片时间是多久呢? 这个展开来讲又可以单独写篇文章,这里只说一个结论:CPU时间片和主机时钟频率有关系,一般是10 ~ 20 ms。 Blocked:一个线程发生一个阻塞式I/0 (文件读写I/O, 网络读写I/O)时,或者试图获取其他线程持有的锁时,线程会进入此状态,例如:获取别的线程已经持有的 synchronized 修饰的对象锁。如果大家对synchronized 关键字感兴趣,可以看我这篇文章 一个synchronized跟面试官扯了半个小时,建议看完这篇再回过头看,顺便还可以点个赞。在Blocked 状态的线程不会占用CPU 资源,但是程序如果出现大量处于这个状态的线程,需要警惕了,可以考虑优化一下程序性能。 Waiting: 一个线程执行了Object.wait( )、 Thread.join( ) 、LockSupport.park( ) 后会进入这个状态,这个状态是处于无限等待状态,没有指定等待时间,可以和Timed_Waiting 对比,Timed_Waiting是有等待时间的。这个状态的线程如果要恢复到Runnable 状态需要通过别的线程调用Object.notify( )、Object.notifyAll( )、LockSupport.unpark( thread )。 Timed_Waiting: 带时间限制的Waiting。 Terminated: 已经执行结束的线程处于此状态。Thread 的 run( ) 方法执行结束,或者由于异常而提前终止都会让线程处于这个状态。

百里守约:你刚才上面讲了wait( )、sleep( )、join( )、yield( ) 、notify()、notifyAll( ) 都是做什么的?什么区别?

安琪拉:这些方法都是线程控制方法,JAVA 通过这些方法跟它创建的操作系统线程进行交互,具体如下:

wait:线程等待,调用该方法会让线程进入 Waiting 状态,同时很重要的一点,线程会释放对象锁,所以wait 方法一般用在同步方法或同步代码块中;

sleep: 线程休眠,调用该方法会让线程进入Time_Waiting 状态,调sleep 方法需要传入一个参数标识线程需要休眠的时间;

yield:线程让步,yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争CPU 时间片,一般来说,优先级高的线程有更大的可能性成功竞争到CPU 时间片,但不是绝对的,有的系统对优先级不敏感。

join:在当前线程中调用另一个线程的join 方法,则当前线程转为阻塞状态,等到另一线程执行结束,当前线程才会从阻塞状态变为就绪状态,等待CPU 的调度。写个代码一看就明白:

public static void main(String[] args) { System.out.println(String.format("主线程%s 开始运行...", Thread.currentThread().getName())); Thread threadA = new Thread(new ThreadA()); threadA.start(); try { // 主线程 wait(0) 释放 thread 对象锁,主线程进入 waiting 状态 threadA.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(String.format("主线程%s 运行结束...", Thread.currentThread().getName())); } private static class ThreadA implements Runnable{ @Override public void run() { System.out.println(String.format("子线程%s 开始运行...", Thread.currentThread().getName())); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(String.format("子线程%s 准备结束运行...", Thread.currentThread().getName())); } }

控制台输出如下:

主线程main 开始运行... 子线程Thread-0 开始运行... 子线程Thread-0 准备结束运行... 主线程main 运行结束...

主线程调用threadA.join() 导致主线程等Thread-0 线程执行结束才开始继续执行。

join() 函数的内部实现如下:

public final void join() throws InterruptedException { join(0); } /** * Waits at most {@code millis} milliseconds for this thread to * die. A timeout of {@code 0} means to wait forever. * *

This implementation uses a loop of {@code this.wait} calls * conditioned on {@code this.isAlive}. As a thread terminates the * {@code this.notifyAll} method is invoked. It is recommended that * applications not use {@code wait}, {@code notify}, or * {@code notifyAll} on {@code Thread} instances. */ public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { //如果当前Thread对象关联的线程还是存活的,当前正在执行的线程进入 Waitting状态,如果当前Thread对象关联的线程执行结束,会调用notifyAll() 唤醒进入 Waitting状态的线程。 while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } } //wait 属于 Object 对象方法 public class Object{ //线程进入 Time_Watting 或 Waiting 状态 public final native void wait(long timeout) throws InterruptedException; }

为了便于大家理解,我画了图(一言不合就上图),大家对照着代码和图看,上面代码主要有二个线程,主线程和 ThreadA 线程,主线程创建ThreadA并启动ThreadA线程,然后调用threadA.join() 会导致主线程阻塞,直到ThreadA 线程执行结束 isActive 变为 false,主线程恢复继续执行。

join()

interrupt:线程中断,调用interrupt 方法中断一个线程,是希望给这个线程一个通知信号,会改变线程内部的一个中断标识位,线程本身并不会因为中断而改变状态(如阻塞、终止等)。调用interrupt 方法有二种情况:

如果当前线程正处于 Running 状态,interrupt( ) 只会改变中断标识位,不会真的中断正在运行的线程; 如果线程当前处于 Timed_Waiting 状态,interrupt( ) 会让线程抛出 InterruptedException。

所以我们在编写多线程程序时,优雅关闭线程需要同时处理这二种情况,常规写法是:

public static class ThreadInterrupt implements Runnable{ @Override public void run() { //1. 非阻塞状态,通过检查中断标识位退出 while(!Thread.currentThread().isInterrupted()){ try{ //doSomething() Thread.sleep(1000); } catch (InterruptedException e) { //2. 阻塞状态,捕获中断异常,break 退出 e.printStackTrace(); break; } } } }

notify:notify方法和wait方法一样,也是Object 类中的方法,notify方法用于唤醒在此对象监视器上等待的单个线程,如果有多个线程在此对象监视器上等待,选择其中一个进行唤醒。另外要注意一点的是,当前线程唤醒等待线程后不会立即释放锁,而是当前线程执行结束才会释放锁,因此被唤醒的线程不是说唤醒之后立即就可以开始执行,而是要等到唤醒的线程执行结束,获得对象锁之后开始执行。上代码吧。

public static void main(String[] args) { new Thread(new ThreadA()).start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(new ThreadB()).start(); } private static final Object lock = new Object(); private static class ThreadA implements Runnable{ @Override public void run() { synchronized (lock){ System.out.println("Thread-A 进入状态 running..."); try { System.out.println("Thread-A 进入状态 waiting..."); lock.wait(); System.out.println("Thread-A 进入状态 running..."); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("Thread-A 执行完毕, 进入状态 terminated..."); } } private static class ThreadB implements Runnable{ @Override public void run() { synchronized (lock){ System.out.println("Thread-B 进入状态 running..."); try { System.out.println("Thread-B 进入状态 time_waiting..."); Thread.sleep(3000); System.out.println("Thread-B 进入状态 running..."); lock.notify(); System.out.println("Thread-B 进入状态 time_waiting..."); Thread.sleep(5000); System.out.println("Thread-B 进入状态 running..."); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("Thread-B 执行完毕, 进入状态 terminated..."); } }

控制台输出:

Thread-A 进入状态 running... Thread-A 进入状态 waiting... Thread-B 进入状态 running... Thread-B 进入状态 time_waiting... Thread-B 进入状态 running... Thread-B 进入状态 time_waiting... Thread-B 进入状态 running... Thread-B 执行完毕, 进入状态 terminated... Thread-A 进入状态 running... Thread-A 执行完毕, 进入状态 terminated...

可以看到B 线程调用 lock.notify() 之后A 线程没有立即开始执行,而是等到B 线程执行结束后才开始执行,所以lock.notify() 唤醒 A 线程只是让 A 线程进入预备执行的状态,而不是直接进 Running 状态,B 线程调 notify 没有立即释放对象锁。

鉴于篇幅原因,此篇也是基础篇,知识部分就到此为止,接下来是一些常规的线程面试题。

第一题:关闭线程的方式有哪几种?哪种方式最可取?(美团一面面试题)

使用退出标识位;

public class ThreadSafe extends Thread { public volatile boolean exit = false; public void run() { while (!exit){ //do something } } }

调用 interrupt 方法,这种是最可取的,但是要考虑到处理二种情况;

stop 方法,这种属于强行终止,非常危险。就像直接给线程断电,调用thread.stop() 方法时,会释放子线程持有的所有锁,这种突然的释放可能会导致数据不一致,因此不推荐使用这种方式终止线程。

第二题:很多面试会问wait 和sleep 的区别?(比心一面面试题)

主要有以下3点:

sleep 方法让线程进入 Timed_Waiting 状态,sleep 方法必须传入时间参数,会让当前线程挂起一段时间,过了这个时间会恢复到runnable 状态(取决于系统计时器和调度程序的精度和准确性)。而wait 方法会让当前线程进入Waiting 状态,会一直阻塞,直到别的线程调用 notify 或者 notifyAll 方法唤醒。 wait 是Object 类中的方法,sleep 是Thread 类中的方法,理解这点很重要,wait方法跟对象绑定的,调用wait方法会释放wait 关联的对象锁; 如果在同步代码块,当前线程持有锁,执行到wait 方法会释放对象锁,sleep 只是单纯休眠,不会释放锁;

我们看个代码巩固一下:

public static void main(String[] args) { new Thread(new ThreadA()).start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(new ThreadB()).start(); } private static final Object lock = new Object(); private static class ThreadA implements Runnable{ @Override public void run() { synchronized (lock){ System.out.println("Thread-A 进入状态 running..."); try { System.out.println("Thread-A 进入状态 waiting..."); lock.wait(); System.out.println("Thread-A 进入状态 running..."); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("Thread-A 执行完毕, 进入状态 terminated..."); } } private static class ThreadB implements Runnable{ @Override public void run() { synchronized (lock){ System.out.println("Thread-B 进入状态 running..."); try { System.out.println("Thread-B 进入状态 time_waiting..."); Thread.sleep(3000); System.out.println("Thread-B 进入状态 running..."); lock.notify(); System.out.println("Thread-B 进入状态 time_waiting..."); Thread.sleep(5000); System.out.println("Thread-B 进入状态 running..."); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("Thread-B 执行完毕, 进入状态 terminated..."); } }

这里我建议大家先不急着看控制台输出,根据自己经验猜测一下输出应该是怎样的,然后对比输出,这样对比能看是否有偏差。另外我建议大家有条件,把本篇文章的示例程序拷贝到本地,实际看下运行。

控制台输出如下:

Thread-A 进入状态 running... Thread-A 进入状态 waiting... Thread-B 进入状态 running... Thread-B 进入状态 time_waiting... Thread-B 进入状态 running... Thread-B 进入状态 time_waiting... Thread-B 进入状态 running... Thread-B 执行完毕, 进入状态 terminated... Thread-A 进入状态 running... Thread-A 执行完毕, 进入状态 terminated... 第三题:手写一个死锁的例子?(美团二面面试题) private static final Object lock1 = new Object(); private static final Object lock2 = new Object(); public static class DeadLockSample implements Runnable{ Object[] locks; public DeadLockSample(Object lock1, Object lock2){ locks = new Object[2]; locks[0] = lock1; locks[1] = lock2; } @Override public void run() { synchronized (lock1) { try { Thread.sleep(3000); synchronized (lock2) { System.out.println(String.format("%s come in...", Thread.currentThread().getName())); } } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) { Thread a = new Thread(new DeadLockSample(lock1, lock2)); Thread b = new Thread(new DeadLockSample(lock2, lock1)); a.start(); b.start(); } 第四题:写一个通过线程wait / notify通信的生产者消费者代码?(声网四面面试题) static class MangoIce{ int counter; public MangoIce(int counter) { this.counter = counter; } } static class Producer implements Runnable { private final List barCounter; private final int MAX_CAPACITY; public Producer(List sharedQueue, int size) { this.barCounter = sharedQueue; this.MAX_CAPACITY = size; } @Override public void run() { int counter = 1; while (!Thread.currentThread().isInterrupted()) { try { produce(counter++); } catch (InterruptedException ex) { ex.printStackTrace(); break; } } } private void produce(int i) throws InterruptedException { synchronized (barCounter) { while (barCounter.size() == MAX_CAPACITY) { System.out.println("吧台满了,冰沙放不下 " + Thread.currentThread().getName() + " 线程等待,当前吧台冰沙数: " + barCounter.size()); barCounter.wait(); } Thread.sleep(1000); barCounter.add(new MangoIce(i)); System.out.println("生产第: " + i + "杯冰沙..."); barCounter.notifyAll(); } } } static class Consumer implements Runnable { private final List barCounter; public Consumer(List sharedQueue) { this.barCounter = sharedQueue; } @Override public void run() { while (!Thread.currentThread().isInterrupted()) { try { consume(); } catch (InterruptedException ex) { ex.printStackTrace(); break; } } } private void consume() throws InterruptedException { synchronized (barCounter) { while (barCounter.isEmpty()) { System.out.println("吧台空的,没有冰沙 " + Thread.currentThread().getName() + " 消费者线程等待,当前吧台冰沙数: " + barCounter.size()); barCounter.wait(); } Thread.sleep(1000); MangoIce i = barCounter.remove(0); System.out.println("消费第: " + i.counter + "杯冰沙..."); barCounter.notifyAll(); } } } public static void main(String[] args) { List taskQueue = new ArrayList(); int MAX_CAPACITY = 5; Thread tProducer = new Thread(new Producer(taskQueue, MAX_CAPACITY), "生产者"); Thread tConsumer = new Thread(new Consumer(taskQueue), "消费者"); tProducer.start(); tConsumer.start(); }

控制台输出

生产第: 1杯冰沙... 生产第: 2杯冰沙... 生产第: 3杯冰沙... 生产第: 4杯冰沙... 生产第: 5杯冰沙... 吧台满了,冰沙放不下 生产者 线程等待,当前吧台冰沙数: 5 消费第: 1杯冰沙... 消费第: 2杯冰沙... 消费第: 3杯冰沙... 消费第: 4杯冰沙... 消费第: 5杯冰沙... 吧台空的,没有冰沙 消费者 消费者线程等待,当前吧台冰沙数: 0 生产第: 6杯冰沙... 生产第: 7杯冰沙... 生产第: 8杯冰沙... 生产第: 9杯冰沙... 生产第: 10杯冰沙... 吧台满了,冰沙放不下 生产者 线程等待,当前吧台冰沙数: 5 消费第: 6杯冰沙... 消费第: 7杯冰沙...

后面几期会分别讲以下内容,顺序还没定,大概会按照读者群的反馈来。

线程上下文切换、JAVA锁 线程池实战、Fork / Join 并发工具 CyclicBarrier、CountDownLatch、Semaphore的实际使用场景 synchronized、volatile、Atomic*** 涉及的原子性、内存可见性和指令重排序原理 AQS、ReentrantLock原理、以及和synchronized区别、CAS原理 阻塞队列、线程调度
欢迎关注Wx 公众号【安琪拉的博客】查看后续内容更新

参考:How to work with wait(), notify() and notifyAll() in Java?


作者:安琪拉的博客



程之 并发编程 并发 多线程 线程

需要 登录 后方可回复, 如果你还没有账号请 注册新账号