摘要:多线程能够了解为在同一个程序中可能同时运行多个不同的线程来执行不同的工作,这些线程能够同时利用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://www.gaodaima.com/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里蕴含来源gao@!dai!ma.com搞$$代^@码网的援用。
- 经典用法:
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://www.gaodaima.com/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://www.gaodaima.com/qq_4307…
https://www.gaodaima.com/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线程,那么就什么也不解决。
点击关注,第一工夫理解华为云陈腐技术~