Java 多线程 【春招面试高频考点】【总结背诵版】
之前分享:
高频Java基础面试题
高频Java集合面试题
单列模式的6种实现方式
轻松理解工厂模式
今天分享一些Java 多线程高频面试题。
整理分享不易,先点赞评论收藏支持一波再看鸭~
文章目录:
1. 什么是进程?什么是线程?进程与线程的区别与联系?
概念: 进程是操作系统资源分配和管理的基本单位,而线程是处理器任务调度和执行的基本单位;一个进程里有一个或多个线程,线程也被称为轻量级进程。
区别:
- 进程之间的地址空间和资源是独立的,同一进程的线程共享本进程的地址空间和资源。
- 一个进程崩溃之后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃之后,整个进程都会死掉。所以多进程要比多线程健壮。
- 进程可以独立执行,且每个独立的进程都有一个程序运行的入口、顺序执行序列和程序入口。
线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。进程和线程都可以并发执行。
2. 什么是并行?什么是并发?
并发:同一个时间段,多个任务都在执行,但不一定同时执行。
并行:多个任务在单位时间内同时执行
3. 什么是线程安全性?
线程安全性的含义是:某个类行为与其规范完全一致。
也就是当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
(摘自《深入理解Java虚拟机》 还是 《Java并发编程的艺术》 上的,很久之前看书的时候记得比较,忘记是哪本书了。。。)
4. 线程的状态?
线程状态 | 说明 |
---|---|
NEW | 初始状态。线程被创建了,但还没有调用 start() 方法启动。 |
RUNNABLE | 可运行状态。又可细分为就绪状态和运行中状态。线程调用了 start() 方法启动之后, |
BLOCKED | 阻塞状态。一个线程请求一个锁时,这个锁已经被其它线程占用了,这个线程就会被锁阻塞。阻塞是被动的。 |
WAITING | 等待状态。表示线程进入等待状态,需要等待其它线程的显式唤醒。等待是主动的。 |
TIMED_WAITING | 限时等待状态。相较于等待状态,限时等待会在等待了一定时间之后会自动退出等待状态。 |
TERMINATED | 终止状态。表示当前线程已经执行完毕结束了,或者发生了异常结束了。 |
5. 线程的创建方式?
线程的创建方式有三种:
- 继承 Thread 类
- 实现 Runnable 接口
- 实现 Callable 接口
不过项目中一般不会直接使用这些方式来创建,而是基于这些创建方式使用线程池来创建和管理。
5.1 继承 Thread 类
- 创建一个类去继承Thread类。
- 重写Thread类的run()方法 -->将此线程执行的操作声明在run()中。
- 创建Thread类的子类的对象,相当于创建了一个还未启动的线程。
- 通过此对象调用start(),启动线程。
//1.创建一个继承于Thread类的子类 class MyThread extends Thread{ //2.重写Thread的run()方法 @Override public void run() { //do something... } } public class ThreaadTest { public static void main(String[] args) { //3.创建Thread类的子类的对象 MyThread t1 = new MyThread(); //可创建多个对象 MyThread t2 = new MyThread(); //4.通过此对象调用start(): ①启动当前线程 ②调用当前线程的run()。 //两个线程并发执行。 t1.start(); t2.start(); } }
5.2 实现 Runnable 接口
- 创建类去实现Runnable接口。
- 然后该类去实现Runnable中的抽象方法:run();(将此线程执行的操作声明在run()中。)
- 创建实现类的对象。
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象,相当于创建了一个还未启动的线程。
- 通过刚刚创建的Thread类的对象去调用start(),启动线程。
//1.创建一个类实现Runnable接口 class MyThread implements Runnable{ //2.去实现Runnable中的抽象方法:run() @Override public void run() { //do something... } } public class ThreadTest1 { public static void main(String[] args) { //3.创建实现类的对象 MyThread myThread = new MyThread(); //4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的实列,相当于是未启动的线程,可创建多个。 Thread t1 = new Thread(myThread); Thread t2 = new Thread(myThread); //5.通过Thread类的实列调用start(),也就是启动线程。 t1.start(); t2.start(); } }
5.3 实现 Callable 接口
- 创建一个类去实现Callable接口。
- 实现call方法,将此线程需要执行的操作声明在call()中
- 创建Callable接口实现类的对象
- 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
- 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
- 如果需要返回值,可以调用 get() 方法 获取Callable中call方法的返回值
public class MyThread implements Callable<Integer> { public Integer call() { return 123; } } public static void main(String[] args) throws ExecutionException, InterruptedException { MyThread myThread = new MyThread(); FutureTask<Integer> ft = new FutureTask<>(myThread); Thread t1= new Thread(ft); t1.start(); System.out.println(ft.get()); }
5.4 三种创建方式的比较
1.实现接口的方法好?还是继承 Thread 类的方法好?
实现接口的方式比继承的方式好,因为 Java 单继承多实现,如果继承了 Thread 类就无法继承其它类;并且继承整个Thread 类开销过大。
2.实现 Runnable 接口方式与 实现 Callable 接口方式区别?
- Callable 有返回值,Runnable 无返回值。
- Callable 能够抛出 checked exception ,而 Runnable 不行。
再次注意:项目使用中,一般不会直接这样创建线程,而是使用线程池来创建和管理;线程池的内容后面会具体说。
6. 线程间的同步问题?(如何控制多线程对共享资源的访问?)
6.1 synchronized 和 Lock
线程间的同步可以使用 synchronized 锁 或者 Lock 锁。
二者区别:
- 首先 synchronized 是 java 内置关键字,在jvm层面,而 Lock 是个 java 类;
- synchronized 无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
- synchronized 会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手动释放锁(unlock()方法释放锁),否则容易造成线程死锁;
- 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
- synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可中断、可公平(两者皆可)
- Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
6.2 synchronized 底层实现
本质是是对对象监视器 monitor 的获取。
synchronized 使用在语句块时:底层其实就是在这个需要同步的语句块前后分别加了monitorenter 和 monitorexit 指令。
在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。执行 monitorexit指令时,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
synchronized 使用在方法上时:底层会在这个方法上加一个 ACC_SYNCHRONIZED 标识,会告诉 JVM 这是个同步方法,然后本质也是对于对象监视器 monitor 的获取与释放。
6.3 Lock (ReentrantLock )的底层实现
Lock底层是基于AQS实现的,采用线程独占的方式,在硬件层面依赖特殊的CPU指令(CAS)。
然后 ReenTrantLock 的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。
6.4 什么是 CAS?有什么缺点和问题?
CAS 是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。整个比较并替换的操作是一个原子操作。
CAS的缺点:
- 循环时间长开销很大。
- 只能保证一个变量的原子操作。
- ABA问题。
ABA问题:
内存地址V初次读取的值是A,准备赋值的时候读取再读取,发现还是A,但这可能并不能说明它的值没被修改过。
因为在这中间,A可能被改成了B,然后又被改成了A。
ABA解决办法:
加版本号,每次改动都会增加版本号,这样就知道有没有修改过了。
7. 线程间的通信
如果使用 synchronized 锁,那么线程间的通信可以使用 wait、notify、notifyAll 。
如果是使用Lock中的ReentrantLock 锁,那么线程间的通信可以使用 Condition 中的await、signal、signalAll。
7.1 synchronized — wait、notify、notifyAll
等待 *使用 *wait **,唤醒** 使用 notifyAll / notify *。
*案例:
现在两个线程,轮流操作初始值为零的一个变量,
实现一个线程对该变量加1,一个线程对该变量减1,交替进行。
也就是一直 1、0、1、0、1、0、1、0 .....1、0
代码演示:
//资源类 class ShareDataOne { //初始值为零的一个变量 private int number = 0; public synchronized void increment() throws InterruptedException { //1 判断是否可以干活 while (number != 0) { //如果不能干活则等待,直到有其他线程通知可以干活 this.wait(); } //2 干活 ++number; System.out.println(Thread.currentThread().getName() + "\t" + number); //3 通知其他线程可以干活了 this.notifyAll(); } public synchronized void decrement() throws InterruptedException { // 1判断 while (number == 0) { this.wait(); } // 2干活 --number; System.out.println(Thread.currentThread().getName() + "\t" + number); // 3通知 this.notifyAll(); } } public class NotifyWaitDemoOne { public static void main(String[] args) { ShareDataOne sd = new ShareDataOne(); new Thread(() -> { for (int i = 1; i < 1000; i++) { try { sd.increment(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }, "A").start(); new Thread(() -> { for (int i = 1; i < 1000; i++) { try { sd.decrement(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }, "B").start(); } }
7.2 Lock (ReentrantLock )—Condition(await、signal、signalAll)
Condition 中的 await、signal、signalAll 需要和 ReentrantLock 一起使用。
或者对标一下 synchronized 更好理解:
- synchronized、wait、notify、notifyAll
- ReentrantLock、await、signal、signalAll
看代码更好理解:
class ShareDate { private int number = 0; private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void increment() { lock.lock(); try { //1. 判断 while (number != 0) { condition.await(); } //2. 干活 number++; System.out.println(Thread.currentThread().getName() + "\t" + number); //3. 通知 condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void decremnet() { lock.lock(); try { while (number != 1) { condition.await(); } number--; System.out.println(Thread.currentThread().getName() + "\t" + number); condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } public class AwaitSignalAllDemoOne { public static void main(String[] args) { ShareDate sd = new ShareDate(); new Thread(() -> { for (int i = 0; i < 10; i++) { sd.increment(); } },"A").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { sd.decremnet(); } },"B").start(); }
7.3 多线程通信时,判断为什么用While而不用if?(虚假唤醒问题)
多线程通信中,判断时要用 while ,不要用 if !
上面的两个代码演示中(7.1、7.2),我们是两个线程,判断时用的是 while,如果换成 if ,有没有问题?没有问题,依旧可以实现 生产一个,消费一个。
但是如果换成四个线程,或者更多线程,然后换成 if ,会不会有问题? 会有问题! 会出现,生产一个之后,还没来得及消费,又生产了一个。
这其实是因为产生了虚假唤醒问题。
虚假唤醒案例:
- 有四个线程, A、B 是生产线程,C、D 是消费线程。
- A 抢到了线程, 生产了一个资源后,
- B 抢到了线程,进来判断,发现还有未消费的资源,便会 等待。
- 结果 A 又抢到了线程,进来判断,也发现还有未消费的资源,便也会 等待。
- 此时 C 或者 D 进来将这个资源消费掉了,就会通知生产线程去生产。
- 因为此时 A、B 都已经在等待了,并且没有再去判断,所以 A、B 都去生产了资源,就出现了问题。
解决办法: 将判断语句 if 改成 while ,while中的等待线程被通知唤醒时,还会再去判断一次,就避免了虚假唤醒。
所以切记:多线程通信中,判断时要用 while ,不要用 if !
7.4 经典多线程通信的面试/笔试代码题-1
面试题:
两个线程,一个线程打印1-52,另一个打印字母A-Z,打印顺序为12A34B...5152Z,
要求用线程间的通信。
参考实现:
class NumberAndLetter { //标记,用于判断是要输出数字还是字母 private boolean flag = false; private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void SoutNumber(int i) { lock.lock(); try { while (flag == true){ condition.await(); } //一次输出两个数字 System.out.print( i + "" + (i + 1)); flag = true; condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void SoutLetter(int i) { lock.lock(); try { while (flag == false){ condition.await(); } System.out.print((char) ('A' + i)); flag = false; condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } public class ThreadPractice { public static void main(String[] args) { NumberAndLetter nl = new NumberAndLetter(); //输出数字 new Thread(() -> { for (int i = 1; i < 52; i += 2) { nl.SoutNumber(i); } }, "number").start(); //输出字母 new Thread(() -> { for (int i = 0; i < 26; i++) { nl.SoutLetter(i); } },"Letter").start(); } }
7.5 经典多线程通信的面试/笔试代码题-2
面试题:
三个线程 A、B、C 之间按顺序调用,要求
AA打印5次,BB打印10次,CC打印15次;
AA打印5次,BB打印10次,CC打印15次 ;
... ...
AA打印5次,BB打印10次,CC打印15次 。
这样打印10轮。
可以使用线程间的定制化调用通信来实现。
参考实现:
class numbers { //标记, 1-A , 2-B , 3-C private int flag = 1 ; private Lock lock = new ReentrantLock(); private Condition conditionA = lock.newCondition(); private Condition conditionB = lock.newCondition(); private Condition conditionC = lock.newCondition(); public void print5AA() { lock.lock(); try { //判断 while (flag != 1) { conditionA.await(); } //干活 for (int i = 0; i < 5; i++) { System.out.println("AA"); } //修改标记 flag = 2; //通知下一个 conditionB.signal(); }catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void print10BB() { lock.lock(); try { while (flag != 2) { conditionB.await(); } for (int i = 0; i < 10; i++) { System.out.println("BB"); } flag = 3; conditionC.signal(); }catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void print15CC() { lock.lock(); try { while (flag != 3) { conditionC.await(); } for (int i = 0; i < 15; i++) { System.out.println("CC"); } flag = 1; conditionA.signal(); }catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } } public class ThreadPractice2 { public static void main(String[] args) { numbers numbers = new numbers(); new Thread(() -> { for (int i = 0; i < 10; i++) { numbers.print5AA(); } },"A").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { numbers.print10BB(); } },"B").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { numbers.print15CC(); } },"C").start(); } }
8. 集合类的线程安全问题和解决办法?
这个之前集合里写过了,看这里:高频Java集合面试题
9.Java并发工具包(JUC)中的几个辅助类相关的。
9.1 CountDownLatch (减少计数)
描述: JUC 中的 CountDownLatch 的作用是让一些线程阻塞,直到另一些线程完成了各自的操作才被唤醒。
CountDownLatch 中主要有两个方法:await() 和 countDown()。
解释: 当我们实例化一个 CountDownLatch 的时候,需要给他一个 int 类型的值,相当于计数器的初始值;当一个线程调用 await() 时会被阻塞,当一个线程调用 countDown() 时,计数器的值会减 1 ;当计数器的值减到 0 时,被 await 阻塞的那些线程会被唤醒,继续执行。
来个例子:某个蛋糕店老板,每天限量只卖 8 个蛋糕,卖完就关门,没卖完就一直等着,直到卖完再关门。
实现:
public class CountDownLatchDemo { public static void main(String[] args) { //限量 8 个蛋糕 CountDownLatch countDownLatch = new CountDownLatch(8); //蛋糕店老板 new Thread(() -> { try { //这个线程会被阻塞,直到计数器的值减为 0 。 countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":卖完了每天限量的8个蛋糕,关门休息了。"); },"蛋糕店老板").start(); for (int i = 1; i <= 8; i++) { //顾客线程 new Thread(() -> { System.out.println("第 " + Thread.currentThread().getName() + "个蛋糕卖掉了"); //使计数器的值减 1 。 countDownLatch.countDown(); },String.valueOf(i)).start(); } } }
运行结果:
9.2 CyclicBarrier (循环屏障)
描述: JUC 中的 CyclicBarrier 的作用是等待直到规定个数的线程都到达屏障后,才会打开屏障,进行下一步操作。
CyclicBarrier 中主要有两个方法:await() 和 CyclicBarrier()。
解释: 当我们实例化一个 CyclicBarrier 的时候,需要给他一个 int 类型的值,相当于屏障的最大承受值;当一个线程调用 await() 时会被屏障阻塞,当屏障阻塞的线程个数到了屏障最大承受值,屏障会消失,所有被阻塞的线程会继续执行各自后面的操作。
来个例子:集齐七颗龙珠才能召唤神龙,召唤神龙后,七颗龙珠又各自消散了。
实现:
public class CyclicBarrierDemo { public static void main(String[] args) { CyclicBarrier cyclicBarrier = new CyclicBarrier(7); for (int i = 1; i <= 7; i++) { new Thread(() -> { try { System.out.println("找到了 " + Thread.currentThread().getName() + " 号龙珠。"); cyclicBarrier.await(); System.out.println(Thread.currentThread().getName() + "号龙珠消散了。"); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } },String.valueOf(i)).start(); } } }
运行结果:
不过,有的同学可能会说:啊?我还没来得及许愿望呢!龙珠怎么就消散了??
不要急,其实创建一个 CyclicBarrier 对象实例的时候,也可以传两个参数,一个参数依然是屏障值,另一个参数是一个线程,这个线程会在屏障消失的时候执行。
实现看看:
public class CyclicBarrierDemo { public static void main(String[] args) { CyclicBarrier cyclicBarrier = new CyclicBarrier(7,() -> { System.out.println("集齐了 7 颗龙珠,召唤出了猿兄!实现了一个愿望:所有程序员加薪不加班!"); }); for (int i = 1; i <= 7; i++) { new Thread(() -> { try { System.out.println("找到了 " + Thread.currentThread().getName() + " 号龙珠。"); cyclicBarrier.await(); System.out.println(Thread.currentThread().getName() + "号龙珠消散了。"); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } },String.valueOf(i)).start(); } } }
运行结果:
9.3 Semaphore (信号灯/信号量)
描述: JUC 中的 Semaphore 的主要是用于多个共享资源的互斥使用,以及并发线程数的控制。
Semaphore 中主要有两个方法:acquire() 和 release()。
解释:
- 当我们实例化一个 Semaphore 的时候,需要给他一个 int 类型的值,代表可用信号量;
- 当一个线程调用 acquire() 时,如果此时还有可用信号,就会成功获取一个信号量,并使可用信号量减 1,如果此时已经没有可用信号了,那么这个线程会一直等待,直到有可用信号,或者超时。
- 当一个线程调用 release(),会释放出信号,使可用信号量加 1 。
来个例子:有 3 个吹风机,有 9 位同学要使用这个吹风机。
实现:
public class SemaphoreDemo { public static void main(String[] args) { // 有 3 个吹风机。 Semaphore semaphore = new Semaphore(3); //9个同学要使用吹风机 for (int i = 1; i <= 9; i++) { new Thread(() -> { try { semaphore.acquire(); System.out.println(Thread.currentThread().getName() + " 号同学拿到了吹风机,使用中...呼呼呼..."); Thread.sleep(3); System.out.println(Thread.currentThread().getName() + " 号同学用完了吹风机"); } catch (InterruptedException e) { e.printStackTrace(); }finally { semaphore.release(); } },String.valueOf(i)).start(); } } }
运行结果:
10. 线程池
这个内容比较多,另开个帖子。[等下发上来]