摘要:多线程能够了解为在同一个程序中可能同时运行多个不同的线程来执行不同的工作,这些线程能够同时利用 CPU 的多个外围运行。
本文分享自华为云社区《对 Java 多线程的用法感到一头乱麻?40 个问题让你疾速把握多线程的精华》,原文作者:breakDraw。
多线程能够了解为在同一个程序中可能同时运行多个不同的线程来执行不同的工作,这些线程能够同时利用 CPU 的多个外围运行。多线程编程可能最大限度的利用 CPU 的资源。本文将通过以下几个方向为大家解说多线程的用法。
- 1.Thread 类根底
- 2.synchronized 关键字
- 3. 其余的同步工具
- CountDownLatch
- FutureTask
- Semaphore
- CyclicBarrier
- Exchanger
- 原子类 AtomicXXX
- 4. 线程池
- 5.Thread 状态转换
- 6.Volatile
- 7. 线程群组
一、Thread 类根底
Q:Thread 的 deprecated 过期办法是哪 3 个?作用是啥
A:
- stop(),终止线程的执行。
- suspend(),暂停线程执行。
- resume(),复原线程执行。
Q:废除 stop 的起因是啥?
A:调用 stop 时,会间接终止线程并开释线程上已锁定的锁,线程外部无奈感知,并且不会做线程内的 catch 操作!即线程外部不会解决 stop 后的烂摊子。如果其余线程等在等着下面的锁去取数据,那么拿到的可能是 1 个半成品。
变成题目的话应该是上面这样,问会输入什么?
public class Test {public static void main(String[] args) throws InterruptedException {System.out.println("start");
Thread thread = new MyThread();
thread.start();
Thread.sleep(1000);
thread.stop();
// thread.interrupt();}
}
class MyThread extends Thread {public void run() {
try {System.out.println("run");
Thread.sleep(5000);
} catch (Exception e) {
// 解决烂摊子, 清理资源
System.out.println("clear resource!");
}
}
}
答案是输入 start 和 run,然而不会输入 clear resource
Q:stop 的代替办法是什么?
A:interrupt()。
调用 thread.interrupt()终止时,不会间接开释锁,可通过调用 interrupt()或者捕获 sleep 产生的中断异样,来判断是否被终止,并解决烂摊子。
上题把 thread.stop()改成 thread.interrupt(),在 Thread.sleep()过程中就会抛出 interrupException(留神,InterrupExcetpion 是 sleep 抛出的)因而就会输入 clear resource。如果没有做 sleep 操作,能够用 isInterrupted()来判断本人这个线程是否被终止了,来做清理。
另外留神一下 interrupt 和 isInterrupted 的区别:
Q:suspend/resume 的废除起因是什么?
A::调用 suspend 不会开释锁。
如果线程 A 暂停后,他的 resume 是由线程 B 来调用的,然而线程 B 又依赖 A 里的某个锁,那么就死锁了。例如上面这个例子,就要晓得会引发死锁:
public class Test {public static Object lockObject = new Object();
public static void main(String[] args) throws InterruptedException {System.out.println("start");
Thread thread = new MyThread();
thread.start();
Thread.sleep(1000);
System.out.println("主线程试图占用 lockObject 锁资源");
synchronized (Test.lockObject) {
// 用 Test.lockObject 做一些事
System.out.println("做一些事");
}
System.out.println("复原");
thread.resume();}
}
class MyThread extends Thread {public void run() {
try {synchronized (Test.lockObject) {System.out.println("占用 Test.lockObject");
suspend();}
System.out.println("MyThread 开释 TestlockObject 锁资源");
}
catch (Exception e){}}
}
答案输入
MyThread 外部暂停后,内部的 main 因为没法拿到锁,所以无奈执行前面的 resume 操作。
Q:上题的 suspend 和 resume 能够怎么替换,来解决死锁问题?
A:能够用 wait 和 noitfy 来解决(不过尽量不要这样设计,个别都是用 run 外部带 1 个 while 循环的)
public class Test {public static Object lockObject = new Object(); // 拿来做长期锁对象
public static void main(String[] args) throws InterruptedException {Thread thread = new MyThread();
thread.start();
Thread.sleep(1000);
System.out.println("主线程试图占用 lockObject 锁资源");
synchronized (Test.lockObject) {
// 用 Test.lockObject 做一些事
System.out.println("做一些事");
}
System.out.println("复原");
synchronized (Test.lockObject) {Test.lockObject.notify();
}
}
}
class MyThread extends Thread {public void run() {
try {synchronized (Test.lockObject) {System.out.println("占用 Test.lockObject");
Test.lockObject.wait();}
System.out.println("MyThread 开释 TestlockObject 锁资源");
}
catch (Exception e){}}
}
如此执行,后果失常:
Q:上面这例子为什么会运行异样,抛出 IllegalMonitorStateException 谬误?
public static void main(String[] args) throws InterruptedException {Thread thread = new MyThread();
thread.start();
thread.notify();}
A:notify 和 wait 的应用前提是必须持有这个对象的锁,即 main 代码块 须要先持有 thread 对象的锁,能力应用 notify 去唤醒(wait 同理)。
改成上面就行了:
Thread thread = new MyThread();
thread.start();
synchronized (thread) {thread.notify();
}
Q:Thread.sleep()和 Object.wait()的区别
A:sleep 不会开释对象锁,而 wait 会开释对象锁。
Q:Runnable 接口和 Callable 的区别。
A:Callable 能够和 Futrue 配合,并且启动线程时用的时 call,可能拿到线程完结后的返回值,call 办法还能抛出异样。
Q:thread.alive()示意线程以后是否处于沉闷 / 可用状态。
沉闷状态:线程曾经启动且尚未终止。线程处于正在运行或筹备开始运行的状态,就认为线程是“存活的
thread.start()后,是否 alive()肯定返回 true?public class Main {public static void main(String[] args) {TestThread tt = new TestThread();
System.out.println("Begin ==" + tt.isAlive());
tt.start();
System.out.println("end ==" + tt.isAlive());
}
A: 不肯定,有可能在打印时,线程曾经运行完结了,或者 start 后,还未真正启动起来(就是还没进入到 run 中)
Q: 线程 A 如下:
public class A extends Thread {
@Override
public void run() {System.out.println("this.isAlive()=" + this.isAlive());
}
}
把线程 A 作为结构参数,传给线程 B
A a = new A();
Thread b = new Thread(a);
b.start()
此时会打印什么?
A:此时会打印 false!
因为把 a 作为结构参数传入 b 中,b 执行 start 时,实际上是在 B 线程中去调用了 A 对象的 run 办法,而不是启用了 A 线程。
如果改成
A a = new A();
a.start()
那么就会打印 true 了
Q:把 FutureTask 放进 Thread 中,并 start 后,会失常执行 callable 里的内容吗?
public static void main(String[] args) throws Exception {Callable<Integer> callable = () -> {System.out.println("call 100");
return 100;
};
FutureTask<Integer> task = new FutureTask<>(callable);
Thread thread = new Thread(task);
thread.start();}
A:能失常打印
二、synchronized 关键字
- 即可作为办法的修饰符,也能够作为代码块的修饰符
- 留神润饰办法时,并不是这个办法上有锁,而是调用该办法时,须要取该办法所在对象上的锁。
class A{synchroized f(){}}
即调用这个 f(),并不是说 f 同一时刻只能进入一次,而是说进入 f 时,须要取到 A 上的锁。
Q:调用上面的 f()时,会呈现死锁吗?
class A{synchroized f(){t()
}
synchroized t(){}
}
A:不会。
1 个线程内,能够反复进入 1 个对象的 synchroized 块。
- 原理:
当线程申请本人的锁时。JVM 会记下锁的持有者,并且给这个锁计数为 1。如果该线程再次申请本人的锁,则能够再次进入,计数为 2。退出时计数 -1,直到全副退出时才会开释锁。
Q:2 个线程同时调用 f1 和 f2 会产生同步吗?
class A{private static synchronized void f1(){};
private synchronized void f2(){};
}
A: 不会产生同步。二者不是 1 个锁。
f1 是类锁,等同于 synchronized(A.class)
f2 是对象锁。
三、其余的同步工具
CountDownLatch
final CountDownLatch latch = new CountDownLatch(2);
2 是计数器初始值。
而后执行 latch.await()时,就会阻塞,直到其余线程中把这个 latch 进行 latch.countDown(),并且计数器升高至 0。
- 和 join 的区别:
join 阻塞时,是只期待单个线程的实现
而 CountDownLatch 可能是为了期待多个线程
Q:countDownLatch 的外部计数值能被重置吗?
A:不能重置了。如果要从新计数必须从新 new 一个。毕竟他的类名就叫 DownLatch
FutureTask
能够了解为一个反对有返回值的线程
FutureTask<Integer> task = new FutureTask<>(runable);
当调用 task.get()时,就能能达到线程里的返回值
Q:调用 futrueTask.get()时,这个是阻塞办法吗?如果是阻塞,什么时候会完结?
A:是阻塞办法。
- 线程跑完并返回后果
- 阻塞工夫达到 futrueTask.get(xxx)里设定的 xxx 工夫
- 线程出现异常 InterruptedException 或者 ExecutionException
- 线程被勾销,抛出 CancellationException
Semaphore
信号量:就是操作系统里常见的那个概念,java 实现,用于各线程间进行资源协调。
用 Semaphore(permits)结构一个蕴含 permits 个资源的信号量,而后某线程做了生产动作,则执行 semaphore.acquire(),则会生产一个资源,如果某线程做了生产动作,则执行 semaphore.release(),则会开释一个资源(即新增一个资源)
更具体的信号量办法阐明:https://blog.csdn.net/hanchao…
Q:信号量中,偏心模式和非偏心模式的区别?上面设成 true 就是偏心模式
//new Semaphore(permits,fair): 初始化许可证数量和是否偏心模式的构造函数
semaphore = new Semaphore(5, true);
A:其实就是应用哪种偏心锁还是非偏心锁。
Java 并发中的 fairSync 和 NonfairSync 次要区别为:
- 如果以后线程不是锁的占有者, 则 NonfairSync 并不判断是否有期待队列, 间接应用 compareAndSwap 去进行锁的占用, 即谁正好抢到,就给谁用!
- 如果以后线程不是锁的占有者, 则 FairSync 则会判断以后是否有期待队列, 如果有则将本人加到期待队列尾,即严格的先到先得!
CyclicBarrier
栅栏,个别是在线程中去调用的。它的结构须要指定 1 个线程数量,和栅栏被毁坏前要执行的操作,每当有 1 个线程调用 barrier.await(),就会进入阻塞,同时 barrier 里的线程计数 -1。
当线程计数为 0 时,调用栅栏里指定的那个操作后,而后毁坏栅栏,所有被阻塞在 await 上的线程持续往下走。
Exchanger
我了解为两方栅栏,用于替换数据。
简略说就是一个线程在实现肯定的事务后,想与另一个线程替换数据,则第一个先拿出数据的线程会始终期待第二个线程,直到第二个线程拿着数据到来时能力彼此替换对应数据。
原子类 AtomicXXX
就是外部已实现了原子同步机制
Q:上面输入什么?(考查 getAndAdd 的用法)
AtomicInteger num = new AtomicInteger(1);
System.out.println(num.getAndAdd(1));
System.out.println(num.get());
A:输入 1、2
顾名思义,getAndAdd(), 那么就是先 get,再加,相似于 num++。
如果是 addAndGet(),那么就是 ++num
Q:AtomicReference 和 AtomicInteger 的区别?
A:AtomicInteger 是对整数的封装,而 AtomicReference 则对应一般的对象援用。也就是它能够保障你在批改对象援用时的线程安全性。即可能会有多个线程批改 atomicReference 里蕴含的援用。
- 经典用法:
boolean exchanged = atomicStringReference.compareAndSet(initialReference, newReference)就是经典的 CAS 同步法
compreAndSet 它会将将援用与预期值(援用)进行比拟,如果它们相等,则在 AtomicReference 对象内设置一个新的援用。相似于一个非负责的自旋锁。
- AtomicReferenceArray 是原子数组,能够进行一些原子的数组操作例如 set(index, value),
java 中已实现的全副原子类:
留神,没有 float,没有 short 和 byte。
四、线程池
Q: ThreadPoolExecutor 线程池结构参数中,corePoolSize 和 maximumPoolSize 有什么区别?
A:当提交新线程到池中时
- 如果以后线程数 < corePoolSize,则会创立新线程
- 如果以后线程数 =corePoolSize,则新线程被塞进一个队列中期待。
- 如果队列也被塞满了,那么又会开始新建线程来运行工作,防止工作阻塞或者抛弃
- 如果队列满了的状况下,线程总数超过了 maxinumPoolSize,那么就抛异样或者阻塞(取决于队列性质)。
- 调用 prestartCoreThread()可提前开启一个闲暇的外围线程
- 调用 prestartAllCoreThreads(),可提前创立 corePoolSize 个外围线程。
Q:线程池的 keepalive 参数是干嘛的?
A:当线程数量在 corePoolSize 到 maxinumPoolSize 之间时,如果有线程已跑完,且闲暇工夫超过 keepalive 时,则会被革除(留神只限于 corePoolSize 到 maxinumPoolsize 之间的线程)
Q:线程池有哪三种队列策略?
A:
- 1. 握手队列
相当于不排队的队列。可能造成线程数量有限增长直到超过 maxinumPoolSize(相当于 corePoolSize 没什么用了,只以 maxinumPoolSize 做下限) - 2. 无界队列
队列队长有限,即线程数量达到 corePoolSize 时,前面的线程只会在队列中期待。(相当于 maxinumPoolSize 没什么用了)
缺点:可能造成队列有限增长以至于 OOM - 3. 有界队列
Q:线程池队列已满且 maxinumPoolSize 已满时,有哪些回绝策略?
A: - AbortPolicy 默认策略:间接抛出 RejectedExecutionException 异样
- DiscardPolicy 抛弃策略:间接丢了,什么谬误也不报
- DiscardOldestPolicy 抛弃队头策略:即把最先入队的人从队头扔出去,再尝试让该工作进入队尾(队头工作心田:不偏心。。。。)
- CallerRunsPolicy 调用者解决策略:交给调用者所在线程本人去跑工作(即谁调用的 submit 或者 execute,他就本人去跑)
- 也能够用实现自定义新的 RejectedExecutionHandler
Q:有以下五种 Executor 提供的线程池,留神记忆一下他们的用处,就能了解外部的原理了。
- newCachedThreadPool:缓存线程池
- corePoolSize=0, maxinumPoolSize=+∞,队列长度 =0,因而线程数量会在 corePoolSize 到 maxinumPoolSize 之间始终灵便缓存和变动,且不存在队列期待的状况,一来工作我就创立,用完了会开释。
- newFixedThreadPool:定长线程池
corePoolSize= maxinumPoolSize= 结构参数值,队列长度 =+∞。因而不存在线程不够时裁减的状况 - newScheduledThreadPool : 定时器线程池
提交定时工作用的,结构参数里会带定时器的距离和单位。其余和 FixedThreadPool 雷同,属于定长线程池。 - newSingleThreadExecutor : 单线程池
corePoolSize=maxinumPoolSize=1,队列长度 =+∞,只会跑一个工作,所以其余的工作都会在队列中期待,因而会严格依照 FIFO 执行 - newWorkStealingPool(继承自 ForkJoinPool):并行线程池
如果你的工作执行工夫很长,并且外面的工作运行并行跑的,那么他会把你的线程工作再细分到其余的线程来分治。ForkJoinPool 介绍:https://blog.csdn.net/m0_3754…
Q:submit 和 execute 的区别是什么?
A:
- execute 只能接管 Runnable 类型的工作,而 submit 除了 Runnable,还能接管 Callable(Callable 类型工作反对返回值)
- execute 办法返回 void,submit 办法返回 FutureTask。
- 异样方面,submit 办法因为返回了 futureTask 对象,而当进行 future.get()时,会把线程中的异样抛出,因而调用者能够不便地解决异样。(如果是 execute,只能用外部捕获或者设置 catchHandler)
Q:线程池中,shutdown、shutdownNow、awaitTermination 的区别?
A:
- shutdown: 进行接管新工作,期待所有池中已存在工作实现(包含期待队列中的线程)。异步办法,即调用后马上返回。
- shutdownNow: 进行接管新工作,并 进行所有正执行的 task,返回还在队列中的 task 列表。
- awaitTermination:仅仅是一个判断办法,判断以后线程池工作是否全副完结。个别用在 shutdown 前面,因为 shutdown 是异步办法,你须要晓得什么时候才真正完结。
五、Thread 状态转换
Q:线程的 6 种状态是:
A:
- New:新建了线程,然而还没调用 start
- RUNNABLE:运行,就绪状态包含在运行态中
- BLOCKED:阻塞,个别是因为想拿锁拿不到
- WAITING:期待,个别是 wait 或者 join 之后
- TIMED_WAITING: 定时期待,即固定工夫后可返回,个别是调用 sleep 或者 wait(工夫)的。
- TERMINATED:终止状态。
观赏一幅好图,能理解调用哪些办法会进入哪些状态。
原图链接
Q:java 线程什么时候会进入阻塞(可能按多选题考):
A:
- sleep
- wati()挂起,期待取得别的线程发送的 Notify()音讯
- 期待 IO
- 期待锁
六、Volatile
用 volatile 润饰成员变量时,一旦有线程批改了变量,其余线程可立刻看到扭转。
Q:不必 volatile 润饰成员变量时,为什么其余线程会无奈立刻看到扭转?
A:线程能够把变量保留在本地内存(比方机器的寄存器)中,而不是间接在主存中进行读写。
这就可能造成一个线程在主存中批改了一个变量的值,而另外一个线程还持续应用它在寄存器中的变量值。
Q:用了 volatile 是不是就能够不必加锁啦?
A:不行。
- 锁并不是只保障 1 个变量的互斥,有时候是要保障几个成员在间断变动时,让其余线程无奈烦扰、读取。
- 而 volatile 保障 1 个变量可变,保障不了几个变量同时变动时的原子性。
Q: 展现一段《Java 并发编程实战》书里的一个经典例子,在科目二考试里也呈现了,只是例子换了个皮。为什么上面这个例子可能会死循环,或者输入 0?
A: 首先了解一下 java 重排序,能够看一下这篇博文:https://www.cnblogs.com/cosha…
而后剖析前面那 2 个奇怪的状况是怎么产生的。
- 永远不输入:
通过程序的指令排序,呈现了这种状况:
- ReaderThread 在 while 里读取 ready 值,此时是 false,于是存入了 ReaderThread 的寄存器。
- 主线程批改 ready 和 number。
- ReaderThread 没有感知到 ready 的批改(对于 ReaderThread 线程,感知不到相干的指令,来让他更新 ready 寄存器的值),因而进入死循环。
- 输入 0
通过程序的指令排序,呈现了这种状况:
1)主线程设置 ready 为 true
2)ReaderThread 在 while 里读取 ready 值,是 true,于是退出 while 循环
- ReaderThread 读取到 number 值,此时 number 还是初始化的值为 0,于是输入 0
- 主线程这时候才批改 number=42,此时 ReaderThread 曾经完结了!
下面这个问题,能够用 volatile 或者加锁。当你加了锁时,如果变量被写了,会有指令去更新另一个寄存器的值,因而就可见了。
七、线程群组
为了方便管理一批线程,咱们应用 ThreadGroup 来示意线程组,通过它对一批线程进行分类管理
应用办法:
Thread group = new ThreadGroup("group");
Thread thread = new Thread(gourp, ()->{..});
即 thread 除了 Thread(Runable)这个构造方法外,还有个 Thread(ThreadGroup, Runnable)构造方法
Q:在线程 A 中创立线程 B,他们属于同一个线程组吗
A:是的
线程组的一大作用是对同一个组线程进行对立的异样捕获解决,防止每次新建线程时都要从新去
setUncaghtExceptionHandler。即线程组本身能够实现一个 uncaughtException 办法。ThreadGroup group = new ThreadGroup("group") {
@Override
public void uncaughtException(Thread thread, Throwable throwable) {System.out.println(thread.getName() + throwable.getMessage());
}
};
}
线程如果抛出异样,且没有在线程外部被捕获,那么此时线程异样的解决程序是什么?置信很多人都看过上面这段话,好多讲线程组的博客里都这样写:
(1)首先看看以后线程组(ThreadGroup)有没有父类的线程组,如果有,则应用父类的 UncaughtException()办法。
(2)如果没有,就看线程是不是调用 setUncaughtExceptionHandler()办法建设 Thread.setUncaughtExceptionHandler 实例。如果建设,间接应用它的 UncaughtException()办法解决异样。
(3)如果上述都不成立就看这个异样是不是 ThreadDead 实例,如果是,什么都不做,如果不是,输入堆栈追踪信息(printStackTrace)。
起源:
https://blog.csdn.net/qq_4307…
https://blog.csdn.net/qq_4307…
好,别急着记,先看一下上面的题目,问输入什么:
Q:
// 父类线程组
static class GroupFather extends ThreadGroup {public GroupFather(String name) {super(name);
}
@Override
public void uncaughtException(Thread thread, Throwable throwable) {System.out.println("groupFather=" + throwable.getMessage());
}
}
public static void main(String[] args) {
// 子类线程组
GroupFather groupSon = new GroupFather("groupSon") {
@Override
public void uncaughtException(Thread thread, Throwable throwable) {System.out.println("groupSon=" + throwable.getMessage());
}
};
Thread thread1 = new Thread(groupSon, ()->{throw new RuntimeException("我异样了");
});
thread1.start();}
A:一看(1),那是不是应该输入 groupFather?
错错错,输入的是 groupSon 这句话在很多中央能看到,但没有去实际过看过源码的人就会这句话被误导。实际上父线程组不是指类继承关系上的线程组,而是指上面这样的:
即指的是结构关系的有父子关系。如果子类的 threadGroup 没有去实现 uncaughtException 办法,那么就会去结构参数里指定的父线程组去调用办法。
Q:那我改成结构关系上的父子关系,上面输入什么?
public static void main(String[] args) {
// 父线程组
ThreadGroup groupFather = new ThreadGroup("groupFather") {
@Override
public void uncaughtException(Thread thread, Throwable throwable) {System.out.println("groupFather=" + throwable.getMessage());
}
};
// 子线程组, 把 groupFather 作为 parent 参数
ThreadGroup groupSon = new ThreadGroup(groupFather, "groupSon") {
@Override
public void uncaughtException(Thread thread, Throwable throwable) {System.out.println("groupSon=" + throwable.getMessage());
}
};
Thread thread1 = new Thread(groupSon, ()->{throw new RuntimeException("我异样了");
});
thread1.start();}
A:答案输入
即只有子线程组有实现过,则会用子线程组里的办法,而不是间接去找的父线程组!
Q:如果我让本人做 set 捕获器的操作呢?那上面这个输入什么?
public static void main(String[] args) {
// 父线程组
ThreadGroup group = new ThreadGroup("group") {
@Override
public void uncaughtException(Thread thread, Throwable throwable) {System.out.println("group=" + throwable.getMessage());
}
};
// 建一个线程,在线程组内
Thread thread1 = new Thread(group, () -> {throw new RuntimeException("我异样了");
});
// 本人设置 setUncaughtExceptionHandler 办法
thread1.setUncaughtExceptionHandler((t, e) -> {System.out.println("no gourp:" + e.getMessage());
});
thread1.start();}
A: 看之前的论断里,仿佛是应该输入线程组的异样?
然而后果却输入的是:
也就是说,如果线程对本人顺便执行过 setUncaughtExceptionHandler,那么有优先对本人设置过的 UncaughtExceptionHandler 做解决。
那难道第(2)点这个是错的吗?的确错了,实际上第二点应该指的是全局 Thread 的默认捕获器,留神是全局的。实际上那段话出自 ThreadGroup 里 uncaughtException 的源码:
这里就解释了之前的那三点,然而该代码中没思考线程本身设置了捕获器
所以批改一下之前的总结一下线程的理论异样抛出判断逻辑:
- 如果线程本身有进行过 setUncaughtExceptionHandler, 则应用本人设置的按个。
- 如果没设置过,则看一下没有线程组。并依照以下逻辑判断:
- 如果线程组有覆写过 uncaughtException,则用覆写过的 uncaughtException
- 如果线程组没有覆写过,则去找父线程组(留神是结构体上的概念)的 uncaughtException 办法。
- 如果线程组以及父类都没覆写过 uncaughtException,则判断是否用 Thread.setDefaultUncaughtExceptionHandler(xxx)去设置全局的默认捕获器,有的话则用全局默认
- 如果不是 ThreadDeath 线程,则只打印堆栈。
- 如果是 ThreadDeath 线程,那么就什么也不解决。
点击关注,第一工夫理解华为云陈腐技术~