内容摘自我的学习网站:topjavaer.cn
分享50道Java并发高频面试题。
线程池
线程池:一个治理线程的池子。
为什么平时都是应用线程池创立线程,间接new一个线程不好吗?
嗯,手动创立线程有两个毛病
- 不受控危险
- 频繁创立开销大
为什么不受控?
系统资源无限,每个人针对不同业务都能够手动创立线程,并且创立线程没有统一标准,比方创立的线程有没有名字等。当零碎运行起来,所有线程都在抢占资源,毫无规定,凌乱局面可想而知,不好管控。最全面的Java面试网站
频繁手动创立线程为什么开销会大?跟new Object() 有什么差异?
尽管Java中万物皆对象,然而new Thread() 创立一个线程和 new Object()还是有区别的。
new Object()过程如下:
- JVM调配一块内存 M
- 在内存 M 上初始化该对象
- 将内存 M 的地址赋值给援用变量 obj
创立线程的过程如下:
- JVM为一个线程栈分配内存,该栈为每个线程办法调用保留一个栈帧
- 每一栈帧由一个局部变量数组、返回值、操作数堆栈和常量池组成
- 每个线程取得一个程序计数器,用于记录以后虚拟机正在执行的线程指令地址
- 零碎创立一个与Java线程对应的本机线程
- 将与线程相干的描述符增加到JVM外部数据结构中
- 线程共享堆和办法区域
创立一个线程大略须要1M左右的空间(Java8,机器规格2c8G)。可见,频繁手动创立/销毁线程的代价是十分大的。
为什么应用线程池?
- 升高资源耗费。通过反复利用已创立的线程升高线程创立和销毁造成的耗费。
- 进步响应速度。当工作达到时,能够不须要等到线程创立就能立刻执行。
- 进步线程的可管理性。对立治理线程,防止零碎创立大量同类线程而导致耗费完内存。
线程池执行原理?
- 当线程池里存活的线程数小于外围线程数
corePoolSize
时,这时对于一个新提交的工作,线程池会创立一个线程去解决工作。当线程池外面存活的线程数小于等于外围线程数corePoolSize
时,线程池外面的线程会始终存活着,就算闲暇工夫超过了keepAliveTime
,线程也不会被销毁,而是始终阻塞在那里始终期待工作队列的工作来执行。 - 当线程池外面存活的线程数曾经等于corePoolSize了,这是对于一个新提交的工作,会被放进工作队列workQueue排队期待执行。
- 当线程池外面存活的线程数曾经等于
corePoolSize
了,并且工作队列也满了,假如maximumPoolSize>corePoolSize
,这时如果再来新的工作,线程池就会持续创立新的线程来解决新的工作,晓得线程数达到maximumPoolSize
,就不会再创立了。 - 如果以后的线程数达到了
maximumPoolSize
,并且工作队列也满了,如果还有新的工作过去,那就间接采纳回绝策略进行解决。默认的回绝策略是抛出一个RejectedExecutionException异样。
本文曾经收录到Github仓库,该仓库蕴含计算机根底、Java根底、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享等外围知识点,欢送star~
Github地址
如果拜访不了Github,能够拜访gitee地址。
gitee地址
线程池参数有哪些?
ThreadPoolExecutor 的通用构造函数:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);
1、corePoolSize
:当有新工作时,如果线程池中线程数没有达到线程池的根本大小,则会创立新的线程执行工作,否则将工作放入阻塞队列。当线程池中存活的线程数总是大于 corePoolSize 时,应该思考调大 corePoolSize。
2、maximumPoolSize
:当阻塞队列填满时,如果线程池中线程数没有超过最大线程数,则会创立新的线程运行工作。否则依据回绝策略解决新工作。非核心线程相似于长期借来的资源,这些线程在闲暇工夫超过 keepAliveTime 之后,就应该退出,防止资源节约。
3、BlockingQueue
:存储期待运行的工作。
4、keepAliveTime
:非核心线程闲暇后,放弃存活的工夫,此参数只对非核心线程无效。设置为0,示意多余的闲暇线程会被立刻终止。
5、TimeUnit
:工夫单位
TimeUnit.DAYSTimeUnit.HOURSTimeUnit.MINUTESTimeUnit.SECONDSTimeUnit.MILLISECONDSTimeUnit.MICROSECONDSTimeUnit.NANOSECONDS
6、ThreadFactory
:每当线程池创立一个新的线程时,都是通过线程工厂办法来实现的。在 ThreadFactory 中只定义了一个办法 newThread,每当线程池须要创立新线程就会调用它。
public class MyThreadFactory implements ThreadFactory { private final String poolName; public MyThreadFactory(String poolName) { this.poolName = poolName; } public Thread newThread(Runnable runnable) { return new MyAppThread(runnable, poolName);//将线程池名字传递给构造函数,用于辨别不同线程池的线程 }}
7、RejectedExecutionHandler
:当队列和线程池都满了的时候,依据回绝策略解决新工作。
AbortPolicy:默认的策略,间接抛出RejectedExecutionExceptionDiscardPolicy:不解决,间接抛弃DiscardOldestPolicy:将期待队列队首的工作抛弃,并执行当前任务CallerRunsPolicy:由调用线程解决该工作
线程池大小怎么设置?
如果线程池线程数量太小,当有大量申请须要解决,零碎响应比较慢,会影响用户体验,甚至会呈现工作队列大量沉积工作导致OOM。
如果线程池线程数量过大,大量线程可能会同时抢占 CPU 资源,这样会导致大量的上下文切换,从而减少线程的执行工夫,影响了执行效率。
CPU 密集型工作(N+1): 这种工作耗费的次要是 CPU 资源,能够将线程数设置为 N(CPU 外围数)+1
,多进去的一个线程是为了避免某些起因导致的线程阻塞(如IO操作,线程sleep,期待锁)而带来的影响。一旦某个线程被阻塞,开释了CPU资源,而在这种状况下多进去的一个线程就能够充分利用 CPU 的闲暇工夫。
I/O 密集型工作(2N): 零碎的大部分工夫都在解决 IO 操作,此时线程可能会被阻塞,开释CPU资源,这时就能够将 CPU 交出给其它线程应用。因而在 IO 密集型工作的利用中,能够多配置一些线程,具体的计算方法:最佳线程数 = CPU外围数 * (1/CPU利用率) = CPU外围数 * (1 + (IO耗时/CPU耗时))
,个别可设置为2N。
最全面的Java面试网站
线程池的类型有哪些?实用场景?
常见的线程池有 FixedThreadPool
、SingleThreadExecutor
、CachedThreadPool
和 ScheduledThreadPool
。这几个都是 ExecutorService
线程池实例。
FixedThreadPool
固定线程数的线程池。任何工夫点,最多只有 nThreads 个线程处于活动状态执行工作。
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());}
应用无界队列 LinkedBlockingQueue(队列容量为 Integer.MAX_VALUE),运行中的线程池不会回绝工作,即不会调用RejectedExecutionHandler.rejectedExecution()办法。
maxThreadPoolSize 是有效参数,故将它的值设置为与 coreThreadPoolSize 统一。
keepAliveTime 也是有效参数,设置为0L,因为此线程池里所有线程都是外围线程,外围线程不会被回收(除非设置了executor.allowCoreThreadTimeOut(true))。
实用场景:实用于解决CPU密集型的工作,确保CPU在长期被工作线程应用的状况下,尽可能的少的调配线程,即实用执行长期的工作。须要留神的是,FixedThreadPool 不会回绝工作,在工作比拟多的时候会导致 OOM。
SingleThreadExecutor
只有一个线程的线程池。
public static ExecutionService newSingleThreadExecutor() { return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());}
应用无界队列 LinkedBlockingQueue。线程池只有一个运行的线程,新来的工作放入工作队列,线程解决完工作就循环从队列里获取工作执行。保障程序的执行各个工作。
实用场景:实用于串行执行工作的场景,一个工作一个工作地执行。在工作比拟多的时候也是会导致 OOM。
CachedThreadPool
依据须要创立新线程的线程池。
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());}
如果主线程提交工作的速度高于线程解决工作的速度时,CachedThreadPool
会一直创立新的线程。极其状况下,这样会导致耗尽 cpu 和内存资源。
应用没有容量的SynchronousQueue作为线程池工作队列,当线程池有闲暇线程时,SynchronousQueue.offer(Runnable task)
提交的工作会被闲暇线程解决,否则会创立新的线程解决工作。
实用场景:用于并发执行大量短期的小工作。CachedThreadPool
容许创立的线程数量为 Integer.MAX_VALUE ,可能会创立大量线程,从而导致 OOM。
ScheduledThreadPoolExecutor
在给定的提早后运行工作,或者定期执行工作。在理论我的项目中根本不会被用到,因为有其余计划抉择比方quartz
。
应用的工作队列 DelayQueue
封装了一个 PriorityQueue
,PriorityQueue
会对队列中的工作进行排序,工夫早的工作先被执行(即ScheduledFutureTask
的 time
变量小的先执行),如果time雷同则先提交的工作会被先执行(ScheduledFutureTask
的 squenceNumber
变量小的先执行)。
执行周期工作步骤:
- 线程从
DelayQueue
中获取已到期的ScheduledFutureTask(DelayQueue.take())
。到期工作是指ScheduledFutureTask
的 time 大于等于以后零碎的工夫; - 执行这个
ScheduledFutureTask
; - 批改
ScheduledFutureTask
的 time 变量为下次将要被执行的工夫; - 把这个批改 time 之后的
ScheduledFutureTask
放回DelayQueue
中(DelayQueue.add()
)。
实用场景:周期性执行工作的场景,须要限度线程数量的场景。
一个我的项目应用多个线程池还是一个线程池?
我的项目中如果有多个场景须要应用线程池,那么最好的形式是:每一个业务场景应用独立的线程池。不要让所有的场景共用一个线程池。
1)独立的线城池之间相互不影响彼此的工作作业,更有利于保障本工作的独立性和完整性,更合乎低耦合的设计思维
2)如果所有的场景共用一个线程池,可能会呈现问题,比方有工作A、工作B、工作C 这三个工作场景共用一个线程池。当工作A申请量激烈减少的时候就会导致工作B和工作C,没有可用的线程,可能呈现迟迟获取不到资源的状况。比方工作A同时有3000个线程申请,此时就可能会导致 工作B和工作C调配不到资源或者调配到很少的线程资源。
注:
1.JDK自带的类应用了很多的线程池;
2.很多开源框架应用了大量的线程池;
3.本人的利用也会创立多个线程池;
4.多少个线程池,每个线程池提供多少线程,必须通过具体的测试
过程线程
过程是指一个内存中运行的应用程序,每个过程都有本人独立的一块内存空间。
线程是比过程更小的执行单位,它是在一个过程中独立的控制流,一个过程能够启动多个线程,每条线程并行执行不同的工作。
线程的生命周期
初始(NEW):线程被构建,还没有调用 start()。
运行(RUNNABLE):包含操作系统的就绪和运行两种状态。
阻塞(BLOCKED):个别是被动的,在抢占资源中得不到资源,被动的挂起在内存,期待资源开释将其唤醒。线程被阻塞会开释CPU,不开释内存。
期待(WAITING):进入该状态的线程须要期待其余线程做出一些特定动作(告诉或中断)。
超时期待(TIMED_WAITING):该状态不同于WAITING,它能够在指定的工夫后自行返回。
终止(TERMINATED):示意该线程曾经执行结束。
图片起源:Java并发编程的艺术
讲讲线程中断?
线程中断即线程运行过程中被其余线程给打断了,它与 stop 最大的区别是:stop 是由零碎强制终止线程,而线程中断则是给指标线程发送一个中断信号,如果指标线程没有接管线程中断的信号并完结线程,线程则不会终止,具体是否退出或者执行其余逻辑取决于指标线程。
线程中断三个重要的办法:
1、java.lang.Thread#interrupt
调用指标线程的interrupt()
办法,给指标线程发一个中断信号,线程被打上中断标记。
2、java.lang.Thread#isInterrupted()
判断指标线程是否被中断,不会革除中断标记。
3、java.lang.Thread#interrupted
判断指标线程是否被中断,会革除中断标记。
private static void test2() { Thread thread = new Thread(() -> { while (true) { Thread.yield(); // 响应中断 if (Thread.currentThread().isInterrupted()) { System.out.println("Java技术栈线程被中断,程序退出。"); return; } } }); thread.start(); thread.interrupt();}
创立线程有哪几种形式?
- 通过扩大
Thread
类来创立多线程 - 通过实现
Runnable
接口来创立多线程 - 实现
Callable
接口,通过FutureTask
接口创立线程。 - 应用
Executor
框架来创立线程池。
继承 Thread 创立线程代码如下。run()办法是由jvm创立完操作系统级线程后回调的办法,不能够手动调用,手动调用相当于调用一般办法。
/** * @author: 程序员大彬 * @time: 2021-09-11 10:15 */public class MyThread extends Thread { public MyThread() { } @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread() + ":" + i); } } public static void main(String[] args) { MyThread mThread1 = new MyThread(); MyThread mThread2 = new MyThread(); MyThread myThread3 = new MyThread(); mThread1.start(); mThread2.start(); myThread3.start(); }}
Runnable 创立线程代码:
/** * @author: 程序员大彬 * @time: 2021-09-11 10:04 */public class RunnableTest { public static void main(String[] args){ Runnable1 r = new Runnable1(); Thread thread = new Thread(r); thread.start(); System.out.println("主线程:["+Thread.currentThread().getName()+"]"); }}class Runnable1 implements Runnable{ @Override public void run() { System.out.println("以后线程:"+Thread.currentThread().getName()); }}
实现Runnable接口比继承Thread类所具备的劣势:
- 能够防止java中的单继承的限度
- 线程池只能放入实现Runable或Callable类线程,不能间接放入继承Thread的类
Callable 创立线程代码:
/** * @author: 程序员大彬 * @time: 2021-09-11 10:21 */public class CallableTest { public static void main(String[] args) { Callable1 c = new Callable1(); //异步计算的后果 FutureTask<Integer> result = new FutureTask<>(c); new Thread(result).start(); try { //期待工作实现,返回后果 int sum = result.get(); System.out.println(sum); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } }}class Callable1 implements Callable<Integer> { @Override public Integer call() throws Exception { int sum = 0; for (int i = 0; i <= 100; i++) { sum += i; } return sum; }}
应用 Executor 创立线程代码:
/** * @author: 程序员大彬 * @time: 2021-09-11 10:44 */public class ExecutorsTest { public static void main(String[] args) { //获取ExecutorService实例,生产禁用,须要手动创立线程池 ExecutorService executorService = Executors.newCachedThreadPool(); //提交工作 executorService.submit(new RunnableDemo()); }}class RunnableDemo implements Runnable { @Override public void run() { System.out.println("大彬"); }}
什么是线程死锁?
线程死锁是指两个或两个以上的线程在执行过程中,因抢夺资源而造成的一种相互期待的景象。若无外力作用,它们都将无奈推动上来。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方持有的资源,所以这两个线程就会相互期待而进入死锁状态。
上面通过例子阐明线程死锁,代码来自并发编程之美。
public class DeadLockDemo { private static Object resource1 = new Object();//资源 1 private static Object resource2 = new Object();//资源 2 public static void main(String[] args) { new Thread(() -> { synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource2"); synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); } } }, "线程 1").start(); new Thread(() -> { synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource1"); synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); } } }, "线程 2").start(); }}
代码输入如下:
Thread[线程 1,5,main]get resource1Thread[线程 2,5,main]get resource2Thread[线程 1,5,main]waiting get resource2Thread[线程 2,5,main]waiting get resource1
线程 A 通过 synchronized
(resource1) 取得 resource1 的监视器锁,而后通过 Thread.sleep(1000)
。让线程 A 休眠 1s 为的是让线程 B 失去执行而后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠完结了都开始希图申请获取对方的资源,而后这两个线程就会陷入相互期待的状态,这也就产生了死锁。
线程死锁怎么产生?怎么防止?
死锁产生的四个必要条件:
- 互斥:一个资源每次只能被一个过程应用
- 申请与放弃:一个过程因申请资源而阻塞时,不开释取得的资源
- 不剥夺:过程已取得的资源,在未应用之前,不能强行剥夺
- 循环期待:过程之间循环期待着资源
防止死锁的办法:
- 互斥条件不能毁坏,因为加锁就是为了保障互斥
- 一次性申请所有的资源,防止线程占有资源而且在期待其余资源
- 占有局部资源的线程进一步申请其余资源时,如果申请不到,被动开释它占有的资源
- 按序申请资源
线程run和start的区别?
- 当程序调用
start()
办法,将会创立一个新线程去执行run()
办法中的代码。run()
就像一个一般办法一样,间接调用run()
的话,不会创立新线程。 - 一个线程的
start()
办法只能调用一次,屡次调用会抛出 java.lang.IllegalThreadStateException 异样。run()
办法则没有限度。
线程都有哪些办法?
start
用于启动线程。
getPriority
获取线程优先级,默认是5,线程默认优先级为5,如果不手动指定,那么线程优先级具备继承性,比方线程A启动线程B,那么线程B的优先级和线程A的优先级雷同
setPriority
设置线程优先级。CPU会尽量将执行资源让给优先级比拟高的线程。
interrupt
通知线程,你应该中断了,具体到底中断还是持续运行,由被告诉的线程本人解决。
当对一个线程调用 interrupt() 时,有两种状况:
- 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立刻退出被阻塞状态,并抛出一个InterruptedException异样。
- 如果线程处于失常活动状态,那么会将该线程的中断标记设置为 true。不过,被设置中断标记的线程能够持续失常运行,不受影响。
interrupt() 并不能真正的中断线程,须要被调用的线程本人进行配合才行。
join
期待其余线程终止。在以后线程中调用另一个线程的join()办法,则以后线程转入阻塞状态,直到另一个过程运行完结,以后线程再由阻塞转为就绪状态。
yield
暂停以后正在执行的线程对象,把执行机会让给雷同或者更高优先级的线程。
sleep
使线程转到阻塞状态。millis参数设定睡眠的工夫,以毫秒为单位。当睡眠完结后,线程主动转为Runnable状态。
volatile底层原理
volatile
是轻量级的同步机制,volatile
保障变量对所有线程的可见性,不保障原子性。
- 当对
volatile
变量进行写操作的时候,JVM会向处理器发送一条LOCK
前缀的指令,将该变量所在缓存行的数据写回零碎内存。 - 因为缓存一致性协定,每个处理器通过嗅探在总线上流传的数据来查看本人的缓存是不是过期了,当处理器发现自己缓存行对应的内存地址被批改,就会将以后处理器的缓存行置为有效状态,当处理器对这个数据进行批改操作的时候,会从新从零碎内存中把数据读到处理器缓存中。
来看看缓存一致性协定是什么。
缓存一致性协定:当CPU写数据时,如果发现操作的变量是共享变量,即在其余CPU中也存在该变量的正本,会发出信号告诉其余CPU将该变量的缓存行置为有效状态,因而当其余CPU须要读取这个变量时,就会从内存从新读取。
volatile
关键字的两个作用:
- 保障了不同线程对共享变量进行操作时的可见性,即一个线程批改了某个变量的值,这新值对其余线程来说是立刻可见的。
- 禁止进行指令重排序。
指令重排序是JVM为了优化指令,进步程序运行效率,在不影响单线程程序执行后果的前提下,尽可能地进步并行度。Java编译器会在生成指令系列时在适当的地位会插入内存屏障
指令来禁止处理器重排序。插入一个内存屏障,相当于通知CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。对一个volatile字段进行写操作,Java内存模型将在写操作后插入一个写屏障指令,这个指令会把之前的写入值都刷新到内存。
synchronized的用法有哪些?
- 润饰一般办法:作用于以后对象实例,进入同步代码前要取得以后对象实例的锁
- 润饰静态方法:作用于以后类,进入同步代码前要取得以后类对象的锁,synchronized关键字加到static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁
- 润饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要取得给定对象的锁
synchronized的作用有哪些?
原子性:确保线程互斥的拜访同步代码;
可见性:保障共享变量的批改可能及时可见;
有序性:无效解决重排序问题。
synchronized 底层实现原理?
synchronized 同步代码块的实现是通过 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始地位,monitorexit
指令则指明同步代码块的完结地位。当执行 monitorenter
指令时,线程试图获取锁也就是获取 monitor
的持有权(monitor对象存在于每个Java对象的对象头中, synchronized 锁便是通过这种形式获取锁的,也是为什么Java中任意对象能够作为锁的起因)。
其外部蕴含一个计数器,当计数器为0则能够胜利获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit
指令后,将锁计数器设为0
,表明锁被开释。如果获取对象锁失败,那以后线程就要阻塞期待,直到锁被另外一个线程开释为止
synchronized 润饰的办法并没有 monitorenter
指令和 monitorexit
指令,获得代之的的确是ACC_SYNCHRONIZED
标识,该标识指明了该办法是一个同步办法,JVM 通过该 ACC_SYNCHRONIZED
拜访标记来分别一个办法是否申明为同步办法,从而执行相应的同步调用。
volatile和synchronized的区别是什么?
volatile
只能应用在变量上;而synchronized
能够在类,变量,办法和代码块上。volatile
至保障可见性;synchronized
保障原子性与可见性。volatile
禁用指令重排序;synchronized
不会。volatile
不会造成阻塞;synchronized
会。
ReentrantLock和synchronized区别
- 应用synchronized关键字实现同步,线程执行完同步代码块会主动开释锁,而ReentrantLock须要手动开释锁。
- synchronized是非偏心锁,ReentrantLock能够设置为偏心锁。
- ReentrantLock上期待获取锁的线程是可中断的,线程能够放弃期待锁。而synchonized会无限期期待上来。
- ReentrantLock 能够设置超时获取锁。在指定的截止工夫之前获取锁,如果截止工夫到了还没有获取到锁,则返回。
- ReentrantLock 的 tryLock() 办法能够尝试非阻塞的获取锁,调用该办法后立即返回,如果可能获取则返回true,否则返回false。
wait()和sleep()的异同点?
相同点:
- 它们都能够使以后线程暂停运行,把机会交给其余线程
- 任何线程在调用wait()和sleep()之后,在期待期间被中断都会抛出
InterruptedException
不同点:
wait()
是Object超类中的办法;而sleep()
是线程Thread类中的办法- 对锁的持有不同,
wait()
会开释锁,而sleep()
并不开释锁 - 唤醒办法不完全相同,
wait()
依附notify
或者notifyAll
、中断、达到指定工夫来唤醒;而sleep()
达到指定工夫被唤醒 - 调用
wait()
须要先获取对象的锁,而Thread.sleep()
不必
Runnable和Callable有什么区别?
- Callable接口办法是
call()
,Runnable的办法是run()
; - Callable接口call办法有返回值,反对泛型,Runnable接口run办法无返回值。
- Callable接口
call()
办法容许抛出异样;而Runnable接口run()
办法不能持续上抛异样。
线程执行程序怎么管制?
假如有T1、T2、T3三个线程,你怎么保障T2在T1执行完后执行,T3在T2执行完后执行?
能够应用join办法解决这个问题。比方在线程A中,调用线程B的join办法示意的意思就是:A期待B线程执行结束后(开释CPU执行权),在继续执行。
代码如下:
public class ThreadTest { public static void main(String[] args) { Thread spring = new Thread(new SeasonThreadTask("春天")); Thread summer = new Thread(new SeasonThreadTask("夏天")); Thread autumn = new Thread(new SeasonThreadTask("秋天")); try { //春天线程先启动 spring.start(); //主线程期待线程spring执行完,再往下执行 spring.join(); //夏天线程再启动 summer.start(); //主线程期待线程summer执行完,再往下执行 summer.join(); //秋天线程最初启动 autumn.start(); //主线程期待线程autumn执行完,再往下执行 autumn.join(); } catch (InterruptedException e) { e.printStackTrace(); } }}class SeasonThreadTask implements Runnable{ private String name; public SeasonThreadTask(String name){ this.name = name; } @Override public void run() { for (int i = 1; i <4; i++) { System.out.println(this.name + "来了: " + i + "次"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } }}
运行后果:
春天来了: 1次春天来了: 2次春天来了: 3次夏天来了: 1次夏天来了: 2次夏天来了: 3次秋天来了: 1次秋天来了: 2次秋天来了: 3次
守护线程是什么?
守护线程是运行在后盾的一种非凡过程。它独立于管制终端并且周期性地执行某种工作或期待解决某些产生的事件。在 Java 中垃圾回收线程就是非凡的守护线程。
线程间通信形式
1、应用 Object 类的 wait()/notify()。Object 类提供了线程间通信的办法:wait()
、notify()
、notifyAll()
,它们是多线程通信的根底。其中,wait/notify
必须配合 synchronized
应用,wait 办法开释锁,notify 办法不开释锁。wait 是指在一个曾经进入了同步锁的线程内,让本人临时让出同步锁,以便其余正在期待此锁的线程能够失去同步锁并运行,只有其余线程调用了notify()
,notify并不开释锁,只是通知调用过wait()
的线程能够去参加取得锁的竞争了,但不是马上失去锁,因为锁还在他人手里,他人还没开释,调用 wait()
的一个或多个线程就会解除 wait 状态,从新参加竞争对象锁,程序如果能够再次失去锁,就能够持续向下运行。
2、应用 volatile 关键字。基于volatile关键字实现线程间互相通信,其底层应用了共享内存。简略来说,就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程可能感知并执行相应的业务。
3、应用JUC工具类 CountDownLatch。jdk1.5 之后在java.util.concurrent
包下提供了很多并发编程相干的工具类,简化了并发编程开发,CountDownLatch
基于 AQS 框架,相当于也是保护了一个线程间共享变量 state。
4、基于 LockSupport 实现线程间的阻塞和唤醒。LockSupport
是一种非常灵活的实现线程间阻塞和唤醒的工具,应用它不必关注是期待线程先进行还是唤醒线程先运行,然而得晓得线程的名字。
ThreadLocal
线程本地变量。当应用ThreadLocal
保护变量时,ThreadLocal
为每个应用该变量的线程提供独立的变量正本,所以每一个线程都能够独立地扭转本人的正本,而不会影响其它线程。
ThreadLocal原理
每个线程都有一个ThreadLocalMap
(ThreadLocal
外部类),Map中元素的键为ThreadLocal
,而值对应线程的变量正本。
调用threadLocal.set()
-->调用getMap(Thread)
-->返回以后线程的ThreadLocalMap<ThreadLocal, value>
-->map.set(this, value)
,this是threadLocal
自身。源码如下:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value);}ThreadLocalMap getMap(Thread t) { return t.threadLocals;}void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue);}
调用get()
-->调用getMap(Thread)
-->返回以后线程的ThreadLocalMap<ThreadLocal, value>
-->map.getEntry(this)
,返回value
。源码如下:
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
threadLocals
的类型ThreadLocalMap
的键为ThreadLocal
对象,因为每个线程中可有多个threadLocal
变量,如longLocal
和stringLocal
。
public class ThreadLocalDemo { ThreadLocal<Long> longLocal = new ThreadLocal<>(); public void set() { longLocal.set(Thread.currentThread().getId()); } public Long get() { return longLocal.get(); } public static void main(String[] args) throws InterruptedException { ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo(); threadLocalDemo.set(); System.out.println(threadLocalDemo.get()); Thread thread = new Thread(() -> { threadLocalDemo.set(); System.out.println(threadLocalDemo.get()); } ); thread.start(); thread.join(); System.out.println(threadLocalDemo.get()); }}
ThreadLocal
并不是用来解决共享资源的多线程拜访问题,因为每个线程中的资源只是正本,不会共享。因而ThreadLocal
适宜作为线程上下文变量,简化线程内传参。
ThreadLocal内存透露的起因?
每个线程都有⼀个ThreadLocalMap
的外部属性,map的key是ThreaLocal
,定义为弱援用,value是强援用类型。垃圾回收的时候会⾃动回收key,而value的回收取决于Thread对象的生命周期。个别会通过线程池的形式复用线程节俭资源,这也就导致了线程对象的生命周期比拟长,这样便始终存在一条强援用链的关系:Thread
--> ThreadLocalMap
-->Entry
-->Value
,随着工作的执行,value就有可能越来越多且无奈开释,最终导致内存透露。
解决⽅法:每次使⽤完ThreadLocal
就调⽤它的remove()
⽅法,手动将对应的键值对删除,从⽽防止内存透露。
ThreadLocal应用场景有哪些?
场景1
ThreadLocal 用作保留每个线程独享的对象,为每个线程都创立一个正本,这样每个线程都能够批改本人所领有的正本, 而不会影响其余线程的正本,确保了线程平安。
这种场景通常用于保留线程不平安的工具类,典型的应用的类就是 SimpleDateFormat。
如果需要为500个线程都要用到 SimpleDateFormat,应用线程池来实现线程的复用,否则会耗费过多的内存等资源,如果咱们每个工作都创立了一个 simpleDateFormat 对象,也就是说,500个工作对应500个 simpleDateFormat 对象。然而这么多对象的创立是有开销的,而且这么多对象同时存在在内存中也是一种内存的节约。能够将simpleDateFormat 对象给提取了进去,变成动态变量,然而这样一来就会有线程不平安的问题。咱们想要的成果是,既不节约过多的内存,同时又想保障线程平安。此时,能够应用 ThreadLocal来达到这个目标,每个线程都领有一个本人的 simpleDateFormat 对象。
场景2
ThreadLocal 用作每个线程内须要独立保存信息,以便供其余办法更不便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,后面执行的办法保留了信息后,后续办法能够通过 ThreadLocal 间接获取到,防止了传参,相似于全局变量的概念。
比方Java web利用中,每个线程有本人独自的Session
实例,就能够应用ThreadLocal
来实现。
AQS原理
AQS,AbstractQueuedSynchronizer
,形象队列同步器,定义了一套多线程访问共享资源的同步器框架,许多并发工具的实现都依赖于它,如罕用的ReentrantLock/Semaphore/CountDownLatch
。
AQS应用一个volatile
的int类型的成员变量state
来示意同步状态,通过CAS批改同步状态的值。当线程调用 lock 办法时 ,如果 state
=0,阐明没有任何线程占有共享资源的锁,能够取得锁并将 state
加1。如果 state
不为0,则阐明有线程目前正在应用共享变量,其余线程必须退出同步队列进行期待。
private volatile int state;//共享变量,应用volatile润饰保障线程可见性
同步器依赖外部的同步队列(一个FIFO双向队列)来实现同步状态的治理,以后线程获取同步状态失败时,同步器会将以后线程以及期待状态(独占或共享 )结构成为一个节点(Node)并将其退出同步队列并进行自旋,当同步状态开释时,会把首节点中的后继节点对应的线程唤醒,使其再次尝试获取同步状态。
ReentrantLock 是如何实现可重入性的?
ReentrantLock
外部自定义了同步器sync,在加锁的时候通过CAS算法,将线程对象放到一个双向链表中,每次获取锁的时候,查看以后保护的那个线程ID和以后申请的线程ID是否 统一,如果统一,同步状态加1,示意锁被以后线程获取了屡次。
源码如下:
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false;}
锁的分类
偏心锁与非偏心锁
依照线程拜访程序获取对象锁。synchronized
是非偏心锁,Lock
默认是非偏心锁,能够设置为偏心锁,偏心锁会影响性能。
public ReentrantLock() { sync = new NonfairSync();}public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();}
共享式与独占式锁
共享式与独占式的最次要区别在于:同一时刻独占式只能有一个线程获取同步状态,而共享式在同一时刻能够有多个线程获取同步状态。例如读操作能够有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其余操作都会被阻塞。
乐观锁与乐观锁
乐观锁,每次拜访资源都会加锁,执行完同步代码开释锁,synchronized
和ReentrantLock
属于乐观锁。
乐观锁,不会锁定资源,所有的线程都能拜访并批改同一个资源,如果没有抵触就批改胜利并退出,否则就会持续循环尝试。乐观锁最常见的实现就是CAS
。
实用场景:
- 乐观锁适宜写操作多的场景。
- 乐观锁适宜读操作多的场景,不加锁能够晋升读操作的性能。
乐观锁有什么问题?
乐观锁防止了乐观锁独占对象的问题,进步了并发性能,但它也有毛病:
- 乐观锁只能保障一个共享变量的原子操作。
- 长时间自旋可能导致开销大。如果CAS长时间不胜利而始终自旋,会给CPU带来很大的开销。
- ABA问题。CAS的原理是通过比对内存值与预期值是否一样而判断内存值是否被改过,然而会有以下问题:如果内存值原来是A, 起初被一条线程改为B,最初又被改成了A,则CAS认为此内存值并没有产生扭转。能够引入版本号解决这个问题,每次变量更新都把版本号加一。
什么是CAS?
CAS全称Compare And Swap
,比拟与替换,是乐观锁的次要实现形式。CAS在不应用锁的状况下实现多线程之间的变量同步。ReentrantLock
外部的AQS和原子类外部都应用了CAS。
CAS算法波及到三个操作数:
- 须要读写的内存值V。
- 进行比拟的值A。
- 要写入的新值B。
只有当V的值等于A时,才会应用原子形式用新值B来更新V的值,否则会持续重试直到胜利更新值。
以AtomicInteger
为例,AtomicInteger
的getAndIncrement()
办法底层就是CAS实现,要害代码是 compareAndSwapInt(obj, offset, expect, update)
,其含意就是,如果obj
内的value
和expect
相等,就证实没有其余线程扭转过这个变量,那么就更新它为update
,如果不相等,那就会持续重试直到胜利更新值。
CAS存在的问题?
CAS 三大问题:
ABA问题。CAS须要在操作值的时候查看内存值是否发生变化,没有发生变化才会更新内存值。然而如果内存值原来是A,起初变成了B,而后又变成了A,那么CAS进行查看时会发现值没有发生变化,然而实际上是有变动的。ABA问题的解决思路就是在变量后面增加版本号,每次变量更新的时候都把版本号加一,这样变动过程就从
A-B-A
变成了1A-2B-3A
。JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,原子更新带有版本号的援用类型。
- 循环工夫长开销大。CAS操作如果长时间不胜利,会导致其始终自旋,给CPU带来十分大的开销。
只能保障一个共享变量的原子操作。对一个共享变量执行操作时,CAS可能保障原子操作,然而对多个共享变量操作时,CAS是无奈保障操作的原子性的。
Java从1.5开始JDK提供了AtomicReference类来保障援用对象之间的原子性,能够把多个变量放在一个对象里来进行CAS操作。
原子类
根本类型原子类
应用原子的形式更新根本类型
- AtomicInteger:整型原子类
- AtomicLong:长整型原子类
- AtomicBoolean :布尔型原子类
AtomicInteger 类罕用的办法:
public final int get() //获取以后的值public final int getAndSet(int newValue)//获取以后的值,并设置新的值public final int getAndIncrement()//获取以后的值,并自增public final int getAndDecrement() //获取以后的值,并自减public final int getAndAdd(int delta) //获取以后的值,并加上预期的值boolean compareAndSet(int expect, int update) //如果输出的数值等于预期值,则以原子形式将该值设置为输出值(update)public final void lazySet(int newValue)//最终设置为newValue,应用 lazySet 设置之后可能导致其余线程在之后的一小段时间内还是能够读到旧的值。
AtomicInteger 类次要利用 CAS (compare and swap) 保障原子操作,从而防止加锁的高开销。
数组类型原子类
应用原子的形式更新数组里的某个元素
- AtomicIntegerArray:整形数组原子类
- AtomicLongArray:长整形数组原子类
- AtomicReferenceArray :援用类型数组原子类
AtomicIntegerArray 类罕用办法:
public final int get(int i) //获取 index=i 地位元素的值public final int getAndSet(int i, int newValue)//返回 index=i 地位的以后的值,并将其设置为新值:newValuepublic final int getAndIncrement(int i)//获取 index=i 地位元素的值,并让该地位的元素自增public final int getAndDecrement(int i) //获取 index=i 地位元素的值,并让该地位的元素自减public final int getAndAdd(int i, int delta) //获取 index=i 地位元素的值,并加上预期的值boolean compareAndSet(int i, int expect, int update) //如果输出的数值等于预期值,则以原子形式将 index=i 地位的元素值设置为输出值(update)public final void lazySet(int i, int newValue)//最终 将index=i 地位的元素设置为newValue,应用 lazySet 设置之后可能导致其余线程在之后的一小段时间内还是能够读到旧的值。
援用类型原子类
- AtomicReference:援用类型原子类
- AtomicStampedReference:带有版本号的援用类型原子类。该类将整数值与援用关联起来,可用于解决原子的更新数据和数据的版本号,能够解决应用 CAS 进行原子更新时可能呈现的 ABA 问题。
- AtomicMarkableReference :原子更新带有标记的援用类型。该类将 boolean 标记与援用关联起来
为什么要应用Executor线程池框架呢?
- 每次执行工作都通过new Thread()去创立线程,比拟耗费性能,创立一个线程是比拟耗时、耗资源的
- 调用new Thread()创立的线程不足治理,能够无限度的创立,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪
- 间接应用new Thread()启动的线程不利于扩大,比方定时执行、定期执行、定时定期执行、线程中断等都不好实现
如何进行一个正在运行的线程?
- 应用共享变量的形式。共享变量能够被多个执行雷同工作的线程用来作为是否进行的信号,告诉进行线程的执行。
- 应用interrupt办法终止线程。当一个线程被阻塞,处于不可运行状态时,即便主程序中将该线程的共享变量设置为true,但该线程此时根本无法查看循环标记,当然也就无奈立刻中断。这时候能够应用Thread提供的interrupt()办法,因为该办法尽管不会中断一个正在运行的线程,然而它能够使一个被阻塞的线程抛出一个中断异样,从而使线程提前结束阻塞状态。
什么是Daemon线程?
后盾(daemon)线程,是指在程序运行的时候在后盾提供一种通用服务的线程,并且这个线程并不属于程序中不可或缺的局部。因而,当所有的非后盾线程完结时,程序也就终止了,同时会杀死过程中的所有后盾线程。反过来说,只有有任何非后盾线程还在运行,程序就不会终止。必须在线程启动之前调用setDaemon()办法,能力把它设置为后盾线程。
留神:后盾过程在不执行finally子句的状况下就会终止其run()办法。
比方:JVM的垃圾回收线程就是Daemon线程,Finalizer也是守护线程。
SynchronizedMap和ConcurrentHashMap有什么区别?
SynchronizedMap一次锁住整张表来保障线程平安,所以每次只能有一个线程来拜访map。
JDK1.8 ConcurrentHashMap采纳CAS和synchronized来保障并发平安。数据结构采纳数组+链表/红黑二叉树。synchronized只锁定以后链表或红黑二叉树的首节点,反对并发拜访、批改。
另外ConcurrentHashMap应用了一种不同的迭代形式。当iterator被创立后汇合再产生扭转就不再是抛出ConcurrentModificationException,取而代之的是在扭转时new新的数据从而不影响原有的数据 ,iterator实现后再将头指针替换为新的数据 ,这样iterator线程能够应用原来老的数据,而写线程也能够并发的实现扭转。
怎么判断线程池的工作是不是执行完了?
有几种办法:
1、应用线程池的原生函数isTerminated();
executor提供一个原生函数isTerminated()来判断线程池中的工作是否全副实现。如果全副实现返回true,否则返回false。
2、应用重入锁,维持一个公共计数。
所有的一般工作维持一个计数器,当工作实现时计数器加一(这里要加锁),当计数器的值等于工作数时,这时所有的工作曾经执行结束了。
3、应用CountDownLatch。
它的原理跟第二种办法相似,给CountDownLatch一个计数值,工作执行结束后,调用countDown()执行计数值减一。最初执行的工作在调用办法的开始调用await()办法,这样整个工作会阻塞,直到这个计数值为零,才会继续执行。
这种形式的毛病就是须要提前晓得工作的数量。
4、submit向线程池提交工作,应用Future判断工作执行状态。
应用submit向线程池提交工作与execute提交不同,submit会有Future类型的返回值。通过future.isDone()办法能够晓得工作是否执行实现。
什么是Future?
在并发编程中,不论是继承thread类还是实现runnable接口,都无奈保障获取到之前的执行后果。通过实现Callback接口,并用Future能够来接管多线程的执行后果。
Future示意一个可能还没有实现的异步工作的后果,针对这个后果能够增加Callback以便在工作执行胜利或失败后作出相应的操作。
举个例子:比方去吃早点时,点了包子和凉菜,包子须要等3分钟,凉菜只需1分钟,如果是串行的一个执行,在吃上早点的时候须要期待4分钟,然而因为你在等包子的时候,能够同时筹备凉菜,所以在筹备凉菜的过程中,能够同时筹备包子,这样只须要期待3分钟。Future就是前面这种执行模式。
Future接口次要包含5个办法:
- get()办法能够当工作完结后返回一个后果,如果调用时,工作还没有完结,则会阻塞线程,直到工作执行结束
- get(long timeout,TimeUnit unit)做多期待timeout的工夫就会返回后果
- cancel(boolean mayInterruptIfRunning)办法能够用来进行一个工作,如果工作能够进行(通过mayInterruptIfRunning来进行判断),则能够返回true,如果工作曾经实现或者曾经进行,或者这个工作无奈进行,则会返回false。
- isDone()办法判断以后办法是否实现
- isCancel()办法判断以后办法是否勾销
最初给大家分享一个Github仓库,下面有大彬整顿的300多本经典的计算机书籍PDF,包含C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生等,能够star一下,下次找书间接在下面搜寻,仓库继续更新中~
Github地址