并发
为什么要使用并发编程(并发编程的优点)
- 并发编程可以提升 CPU 的计算能力的利用率,通过并发编程的形式可以将多核CPU 的计算能力发挥到极致
- 提升程序的性能,如:响应时间、吞吐量、计算机资源使用率等。
- 并发程序可以更好地处理复杂业务,对复杂业务进行多任务拆分,简化任务调度,同步执行任务,提升系统并发能力和性能
并发编程有什么缺点
并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、线程安全、死锁等问题。
频繁的上下文切换消耗性能
时间片是CPU分配给各个线程的时间,因为时间非常短,所以CPU不断通过切换线程,让我们觉得多个线程是同时执行的,时间片一般是几十毫秒。而每次切换时,需要保存当前的状态起来,以便能够进行恢复先前状态,而这个切换时非常损耗性能,过于频繁反而无法发挥出多线程编程的优势。
减少上下文切换的解决方案
- 无锁并发编程:可以参照concurrentHashMap锁分段的思想,不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间。
- CAS算法:利用Atomic下使用CAS算法来更新数据,使用了乐观锁,可以有效的减少一部分不必要的锁竞争带来的上下文切换。
- 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多的线程,这样会造成大量的线程都处于等待状态。
- 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
线程安全容易导致死锁
多线程编程中最难以把握的就是临界区线程安全问题,稍微不注意就会出现死锁的情况,一旦产生死锁就会造成系统功能不可用。
public class DeadLockDemo {
private static String resource_a = "A";
private static String resource_b = "B";
public static void main(String[] args) {
deadLock();
}
public static void deadLock() {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resource_a) {
System.out.println("get resource a");
try {
Thread.sleep(3000);
synchronized (resource_b) {
System.out.println("get resource b");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resource_b) {
System.out.println("get resource b");
synchronized (resource_a) {
System.out.println("get resource a");
}
}
}
});
threadA.start();
threadB.start();
}
}
在上面的这个demo中,开启了两个线程threadA, threadB,其中threadA占用了resource_a, 并等待被threadB释放的resource _b。threadB占用了resource _b并等待被threadA释放的resource _a。因此threadA,threadB出现线程安全的问题,形成死锁。
如上所述,完全可以看出当前死锁的情况。
通常可以用如下方式避免死锁的情况:
- 避免一个线程同时获得多个锁;
- 避免一个线程在锁内部占有多个资源,尽量保证每个锁只占用一个资源;
- 尝试使用定时锁,使用lock.tryLock(timeOut),当超时等待时当前线程不会阻塞;
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况
内存泄露
内存泄漏也称作"存储渗漏",用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。(其实说白了就是该内存空间使用完毕之后未回收)即所谓内存泄漏。
并发编程三要素是什么?在 Java 程序中怎么保证多线程的运行安全?
并发编程三要素(线程的安全性问题体现在)
- 原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
- 可见性:一个线程对共享变量的修改,其他线程能够立刻看到。
- 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
出现线程安全问题的原因
- 线程切换带来的原子性问题
- 缓存导致的可见性问题
- 编译优化带来的有序性问题
解决办法
- JDK Atomic开头的原子类、synchronized、LOCK可以解决原子性问题
- synchronized、volatile、LOCK,可以解决可见性问题
- Happens-Before 规则可以解决有序性问题
并行和并发有什么区别?
- 并发的多个任务之间是互相抢占资源的。 并行的多个任务之间是不互相抢占资源的
- 并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生,并行是真正意义上的同时进行,而并发只是让用户看起来是同时进行的,实际上还是一个一个的进行的
- 并发是指单个CPU处理多个任务,并行是指多个CPU处理多个任务
并发
并发 指单个cpu同时处理多个线程任务,cpu在反复切换任务线程,实际还是串行化的(串行化是指有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题);其实就是通过一种算法将 CPU 资源合理地分配给多个任务,当一个任务执行 I/O 操作时,CPU 可以转而执行其它的任务,等到 I/O 操作完成以后,或者新的任务遇到 I/O 操作时,CPU 再回到原来的任务继续执行。它只是用户看起来是同时进行的,实际上不上同时进行,而是一个个的的进行(多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行)
并行
并行是指多个处理器或者是多核的处理器同时处理多个不同的任务,例如当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行
这里面有一个很重要的点,那就是系统要有多个CPU才会出现并行。在有多个CPU的情况下,才会出现真正意义上的『同时进行』。
就像上面这张图,只有一个咖啡机的时候,一台咖啡机其实是在并发被使用的。而有多个咖啡机的时候,多个咖啡机之间才是并行被使用的。
多线程
多线程环境下为什么要引入同步的机制
当时用多线程访问同一个资源时,非常容易出现线程安全的问题,引用同步机制保证在多线程并发的情况下共享资源只能被一个线程所持有,从而避免了多线程冲突导致数据混乱
多线程什么场景下会发生死锁
当两个线程(进程)相互持有对方所需要的的资源,又不主动释放,导致所有线程(进程)都无法继续前进,导致程序陷入无尽的阻塞
什么是多线程?多线程的优劣?
多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务。
多线程的好处:
可以提高 CPU 的利用率,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
多线程的劣势:
线程也是程序,所以线程需要占用内存,线程越多占用内存也越多; 多线程需要协调和管理,所以需要 CPU 时间跟踪线程;线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。
线程池
什么是线程池?
创建线程和销毁线程的花销是比较大的,这些时间有可能比处理业务的时间还要长。这样频繁的创建线程和销毁线程,再加上业务工作线程,消耗系统资源的时间,可能导致系统资源不足。所以提高服务程序效率的一个手段就是尽可能减少线程创建和销毁的次数, 这就是”池化资源”技术产生的原因,池化技术主要是为了减少每次获取资源的消耗,提高对资源的利用率。
线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中, 从而减少创建和销毁线程对象的开销。
JDK 中线程池框架的继承关系
线程池有什么作用/为什么要使用线程池/优点?
- 提高响应速度。同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行 ,创建好一定数量的线程放在池中,等需要使用的时候就从池中拿一个,这要比需要的时候创建一个线程对象要快的多。
- 提高线程的可管理性。可有效的控制最大并发线程数。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。比如说启动时有该程序创建100个线程,每当有请求的时候,就分配一个线程去工作,如果刚好并发有101个请求,那多出的这一个请求可以排队等候,避免因无休止的创建线程导致系统崩溃。
- 提高系统资源的使用率,重用存在的线程,减少线程创建销毁的开销。
- 附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。
综上所述使用线程池框架 Executor 能更好的管理线程、提供系统资源使用率。
线程池创建的参数含义?
为了彻底了解线程池的时候,我们需要弄清楚线程池创建的几个参数
- corepoolsize : 核心线程数,默认情况下,在创建线程池后,每当有新的任务来的时候,如果此时线程池中的线程数小于核心线程数,就会去创建一个线程执行(就算有空线程也不复用),当创建的线程数达到核心线程数之后,再有任务进来就会放入任务缓存队列中。当任务缓存队列也满了的时候,就会继续创建线程,知道达到最大线程数。如果达到最大线程数之后再有任务过来,那么就会采取拒绝服务策略。
- Maximumpoolsize : 线程池中最多可以创建的线程数
- keeplivetime : 线程空闲状态时,最多保持多久的时间会终止。默认情况下,当线程池中的线程数大于corepollsize 时,才会起作用 ,直到线程数不大于 corepollsize 。
- workQueue: 阻塞队列,用来存放等待的任务
- rejectedExecutionHandler :任务拒绝处理器(这个注意一下),有四种
-
- abortpolicy 丢弃任务,抛出异常
- discardpolicy 拒绝执行,不抛异常
- discardoldestpolicy 丢弃任务缓存队列中最老的任务
- CallerRunsPolicy 线程池不执行这个任务,主线程自己执行。
线程池哪几种?分别说一下
CachedThreadPool(可缓冲线程池)
线程池中数量没有固定,可达到最大值(Interger. MAX_VALUE)(只有非核心线程,最大线程数很大),每新来一个任务,当没有空余线程的时候就会重新创建一个线程,这边有一个超时机制,当空闲的线程超过60s内没有用到的话就会被回收,它可以一定程度减少频繁创建/销毁线程,减少系统开销,适用于执行时间短并且数量多的任务场景。
Executors.newCachedThreadPool();
特征
(1)线程池中数量没有固定,可达到最大值(Interger. MAX_VALUE)
(2)线程池中的线程可进行缓存重复利用和回收(回收默认时间为1分钟)
(3)当线程池中,没有可用线程,会重新创建一个线程
FixedThreadPool(定长线程池)
一个有指定的线程数的线程池,线程数量是固定的,响应的速度快。正规的并发线程,多用于服务器。固定的线程数由系统资源设置。核心线程是没有超时机制的,队列大小没有限制,除非线程池关闭了核心线程才会被回收。
(1)Executors.newFixedThreadPool(int nThreads);//nThreads为线程的数量
(2)Executors.newFixedThreadPool(int nThreads,ThreadFactory threadFactory);//nThreads为线程的数量,threadFactory创建线程的工厂方式
特征
(1)线程池中的线程处于一定的量,可以很好的控制线程的并发量
(2)线程可以重复被使用,在显示关闭之前,都将一直存在
(3)超出一定量的线程被提交时候需在队列中等待
SingleThreadExecutor(单任务线程池)
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行,每次任务到来后都会进入阻塞队列,然后按指定顺序执行。
注意:如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务
(1)Executors.newSingleThreadExecutor() ;
(2)Executors.newSingleThreadExecutor(ThreadFactory threadFactory);// threadFactory创建线程的工厂方式
特征
(1)线程池中最多执行1个线程,之后提交的线程活动将会排在队列中以此执行
ScheduleThreadPool(周期线程池)
创建一个定长线程池,支持定时及周期性任务执行,通过过schedule(s gei4 酒)方法可以设置任务的周期执行
(1)Executors.newScheduledThreadPool(int corePoolSize);// corePoolSize线程的个数
(2)newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory);
// corePoolSize线程的个数,threadFactory创建线程的工厂
特征
(1)线程池中具有指定数量的线程,即便是空线程也将保留
(2)可定时或者延迟执行线程活动
一个任务进来后,线程池是怎么处理的(线程池的实现原理)?
任务被提交到线程池,先看看有没有空闲的线程可以用,有就直接用,没有就会先判断当前线程数量是否小于corePoolSize(线程池核心线程数量),如果小于则创建线程来执行提交的任务,否则将任务放入workQueue(线程堵塞队列)缓存队列,如果workQueue缓存队列满了,则判断当前线程数量是否小于maximumPoolSize(线程池最大线程数量),如果小于则创建线程执行任务,否则就会调用handler(拒绝策略)来拒绝接收任务。
线程池的拒绝策略有哪几种?
当线程池中正在运行的线程已经达到了指定的最大线程数量maximumPoolSize且线程池的阻塞队列也已经满了时,向线程池提交任务将触发拒绝处理逻辑。提供了四种拒绝策略,它们分别是AbortPolicy,CallerRunsPolicy,DiscardOldestPolicy和DiscardPolicy
1. AbortPolicy(a 不 t)
拒绝策略在拒绝任务时,会直接抛出一个类型为 RejectedExecutionException 的 RuntimeException,让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
/**
* The default rejected execution handler
*/
private static final RejectedExecutionHandler defaultHandler =
new AbortPolicy();
可以看出线程池的默认拒绝策略为AbortPolicy
2.DiscardPolicy
这种拒绝策略直接把被提交的新任务直接被丢弃掉,也不会报错,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。安静的扔掉多余的新任务。
3.DiscardOldestPolicy
丢弃最早未处理请求策略,丢弃最先进入阻塞队列的任务以腾出空间让新的任务入队列。
4.CallerRunsPolicy
当有新任务提交后把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。
这样做主要有两点好处
- 新提交的任务不会被丢弃,这样也就不会造成业务损失。
- 由于谁提交任务谁就要负责执行任务,那提交任务的线程就会一直被占着,线程池的其他线程在此期间也会执行掉一部分任务,那就腾出一定的空间,相当于是给了线程池一定的缓冲期(其实就是提交任务的线程执行提交的新任务时,其他线程可能就有的执行完任务了,原本是没有多余的线程,但是现在就有了多余空闲的线程)
使用也很容易,在配置的线程池中指定对应的拒绝策略就行
源码好文参考:java线程池的四种拒绝策略_TTODS.的博客-CSDN博客
如果你提交任务时,线程池队列已满,这时会发生什么
- 如果使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务
- 如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,则判断当前线程数量是否小于maximumPoolSize(线程池最大线程数量),如果小于则创建线程执行任务,否则就会调用handler(拒绝策略)来拒绝接收任务,默认是 AbortPolicy策略
线程池都有哪些状态?
- RUNNING:线程池一旦被创建,就处于 RUNNING 状态,任务数为 0,能够接收新任务,对已排队的任务进行处理。
- SHUTDOWN:不接收新任务,但能处理已排队的任务。调用线程池的 shutdown() 方法,线程池由 RUNNING 转变为 SHUTDOWN 状态。
- STOP:不接收新任务,不处理已排队的任务,并且会中断正在处理的任务。调用线程池的 shutdownNow() 方法,线程池由(RUNNING 或 SHUTDOWN ) 转变为 STOP 状态。
- TIDYING(太顶):
-
- SHUTDOWN 状态下,当已经排队的所有任务都被处理完了,任务数为 0, 线程池会变为 TIDYING 状态,会执行 terminated() 方法。线程池中的 terminated() 方法是空实现,可以重写该方法进行相应的处理。
- 线程池在 STOP 状态,线程池中执行中任务为空时,就会由 STOP 转变为 TIDYING 状态。
- TERMINATED(te1 米 nei3 ti3 d):线程池彻底终止。线程池在 TIDYING 状态执行完 terminated() 方法就会由 TIDYING 转变为 TERMINATED 状态
Executor 框架
什么是 Executor线程池框架?
Executor框架是指JDK 1.5中引入的一系列并发库中与线程池相关的功能类,包括Executor、Executors、ExecutorService、Future、Callable等。可以通过该框架来控制线程的启动,执行,关闭,简化并发编程。Executor框架把任务提交和执行解耦,要执行任务的人只需要把任务提交即可,任务的执行不需要去关心。通过Executor框架来启动线程比使用Thread更好,更易管理,效率高,避免this逃逸问题。
Executor的实现还提供了对生命周期的支持,以及统计信息收集,应用程序管理机制和性能监视等机制。简单来说,Executor框架可以非常方便的创建一个线程池
Executor框架结构
Executor框架包括3大部分
(1)任务。也就是工作单元,包括被执行任务需要实现的接口:Runnable接口或者Callable接口;
(2)任务的执行。也就是把任务分派给多个线程的执行机制,包括Executor接口及继承自Executor接口的ExecutorService接口。
(3)异步计算的结果。包括Future接口及实现了Future接口的FutureTask类。
Executor框架的成员及其关系可以用一下的关系图表示
还是蛮好理解的,正如Java优秀框架的一贯设计思路,顶级接口-次级接口-虚拟实现类-实现类。
Executor:执行者,java线程池框架的最上层父接口,地位类似于spring的BeanFactry、集合框架的Collection接口,在Executor这个接口中只有一个execute方法,该方法的作用是向线程池提交任务并执行。
ExecutorService:该接口继承自Executor接口,添加了shutdown、shutdownAll、submit、invokeAll等一系列对线程的操作方法,该接口比较重要,在使用线程池框架的时候,经常用到该接口。
ExecutorService的生命周期包括三种状态:运行,关闭,终止。创建后便进入运行状态,当调用了shutdown()方法时,进入关闭状态,此时不再接受新任务,但是它还在执行已经提交了的任务,当所有的任务执行完后,便达到了终止状态。shutdownNow()方法关闭当前服务,尚未执行的任务,不再执行;正在执行的任务,通过线程中断thread.interrupt。方法返回等待执行的任务列表。
AbstractExecutorService:这是一个抽象类,实现ExecuotrService接口,
ThreadPoolExecutor:这是Java线程池最核心的一个类,该类继承自AbstractExecutorService,主要功能是创建线程池,给任务分配线程资源,执行任务。
ScheduledExecutorSerivce 和 ScheduledThreadPoolExecutor :延迟执行和周期性执行的线程池。
Executors:这是一个静态工厂类,该类定义了一系列静态工厂方法,通过这些工厂方法可以返回各种不同的线程池。
Executor框架的使用示意图
为什么要使用Executor线程池框架
- 每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的。
- 调用 new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。
- 直接使用new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不便实现。
使用Executor线程池框架的优点
- 能复用已存在并空闲的线程从而减少线程对象的创建从而减少了消亡线程的开销。
- 可有效控制最大并发线程数,提高系统资源使用率,同时避免过多资源竞争。
- 框架中已经有定时、定期、单线程、并发数控制等功能。
- 综上所述使用线程池框架Executor能更好的管理线程、提供系统资源使用率。
在 Java 中 Executor 和 Executors 的区别?
- Executor它是"执行者",java线程池框架的最上层父接口,在Executor这个接口中只有一个execute方法,该方法的作用是向线程池提交任务并执行,Executor 接口对象能执行我们的线程任务。准确的说,Executor提供了execute()接口来执行已提交的 Runnable 任务的对象。ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。
- Executors是个静态工厂类,是个工具类,可以通过这个工具类按照我们的需求创建了不同的线程池,来满足业务的需求。它通过静态工厂方法返回ExecutorService、ScheduledExecutorService、ThreadFactory 和 Callable 等类的对象。
线程池中 submit() 和 execute() 方法有什么区别?
- 接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行Runnable 和 Callable 类型的任务。
- 返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()没有异常处理:submit()方便Exception处理
什么是线程组?为什么在 Java 中不推荐使用?
什么是线程组?
线程组(ThreadGroup)简单来说就是多个线程的集合。线程组的出现是为了更方便地管理线程。Java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制,默认情况下,所有的线程都属于主线程组(main线程组)。
线程组是父子结构的,一个线程组可以集成其他线程组,同时也可以拥有其他子线程组。从结构上看,线程组是一个树形结构,每个线程都隶属于一个线程组,线程组又有父线程组,这样追溯下去,可以追溯到一个根线程组——System线程组。
下面介绍一下线程组树的结构:
- JVM创建的system线程组是用来处理JVM的系统任务的线程组,例如对象的销毁等。
- system线程组的直接子线程组是main线程组,这个线程组至少包含一个main线程,用于执行main方法。
- main线程组的子线程组就是应用程序创建的线程组。
线程组和线程池是两个不同的概念,他们的作用完全不同,前者是为了方便线程的管理,后者是为了管理线程的生命周期,复用线程,减少创建销毁线程的开销。
为什么不推荐使用线程组?
虽然线程组看上去很有用处,实际上现在的程序开发中已经不推荐使用它了,主要有两个原因:
- 线程组ThreadGroup对象中比较有用的方法是stop、resume、suspend等方法,由于这几个方法会导致线程的安全问题(主要是死锁问题),已经被官方废弃掉了,所以线程组本身的应用价值就大打折扣了。
- 线程组ThreadGroup不是线程安全的,这在使用过程中获取的信息并不全是及时有效的,这就降低了它的统计使用价值。
虽然线程组现在已经不被推荐使用了,但是它在线程的异常处理方面还是做出了一定的贡献。
当线程运行过程中出现异常情况时,在某些情况下JVM会把线程的控制权交到线程关联的线程组对象上来进行处理。
线程池之ThreadPoolExecutor详解
最原始的创建线程池的方式,它包含了 7 个参数可供设置。
public static void myThreadPoolExecutor() {
// 创建线程池
ThreadPoolExecutor threadPool =
new ThreadPoolExecutor(5, 10, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
// 执行任务
for (int i = 0; i < 10; i++) {
final int index = i;
threadPool.execute(() -> {
System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
执行结果如下:
ThreadPoolExecutor 参数介绍
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
}
7 个参数代表的含义如下:
参数 1:corePoolSize
核心线程数,线程池中始终存活的线程数。
参数 2:maximumPoolSize
最大线程数,线程池中允许的最大线程数,当线程池的任务队列满了之后可以创建的最大线程数。
参数 3:keepAliveTime
最大线程数可以存活的时间,当线程中没有任务执行时,最大线程就会销毁一部分,最终保持核心线程数量的线程。
参数 4:unit
单位是和参数 3 存活时间配合使用的,合在一起用于设定线程的存活时间 ,参数 keepAliveTime 的时间单位有以下 7 种可选:
- TimeUnit.DAYS:天
- TimeUnit.HOURS:小时
- TimeUnit.MINUTES:分
- TimeUnit.SECONDS:秒
- TimeUnit.MILLISECONDS:毫秒
- TimeUnit.MICROSECONDS:微妙
- TimeUnit.NANOSECONDS:纳秒
参数 5:workQueue
一个阻塞队列,用来存储线程池等待执行的任务,均为线程安全,它包含以下 7 种类型:
- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue:一个由链表结构组成的无界阻塞队列(默认)。
- SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。
- PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
- DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
较常用的是 LinkedBlockingQueue 和 Synchronous,线程池的排队策略与 BlockingQueue 有关。
参数 6:threadFactory
线程工厂,主要用来创建线程,默认为正常优先级、非守护线程。
参数 7:handler
拒绝策略,拒绝处理任务时的策略,系统提供了 4 种可选:
AbortPolicy:拒绝并抛出异常。
CallerRunsPolicy:使用当前调用的线程来执行此任务。
DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。
DiscardPolicy:忽略并抛弃当前任务。
默认策略为 AbortPolicy。
线程池的执行流程
ThreadPoolExecutor 关键节点的执行流程如下:
- 当线程数小于核心线程数时,创建线程。
- 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
- 当线程数大于等于核心线程数,且任务队列已满:若线程数小于最大线程数,创建线程;若线程数等于最大线程数,抛出异常,拒绝任务。
线程池的执行流程如下图所示:
自定义拒绝策略演示
public static void main(String[] args) {
// 任务的具体方法
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("当前任务被执行,执行时间:" + new Date() +
" 执行线程:" + Thread.currentThread().getName());
try {
// 等待 1s
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 创建线程,线程的任务队列的长度为 1
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,
100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1),
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 执行自定义拒绝策略的相关操作
System.out.println("我是自定义拒绝策略~");
}
});
// 添加并执行 4 个任务
threadPool.execute(runnable);
threadPool.execute(runnable);
threadPool.execute(runnable);
threadPool.execute(runnable);
}
总结
最推荐使用的是 ThreadPoolExecutor 的方式进行线程池的创建,ThreadPoolExecutor 最多可以设置 7 个参数,当然设置 5 个参数也可以正常使用,ThreadPoolExecutor 当任务过多(处理不过来)时提供了 4 种拒绝策略,当然我们也可以自定义拒绝策略
你知道怎么创建线程池吗?
线程池的创建方式总共包含以下 7 种(其中 6 种是通过 Executors 创建的,1 种是通过 ThreadPoolExecutor 创建的):
- Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
- Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
- Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
- Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
- Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
- Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
- ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置,后面会详细讲。
简单来说就两种创建线程池的方法,一种是用Executors工具类来创建,一种是用ThreadPoolExecutor来创建,推荐使用ThreadPoolExecutor创建线程池
为什么不推荐使用Executors创建线程池,而是推荐使用ThreadPoolExecutor来创建线程池?
因为JDK 自带工具类创建的线程池Executors存在的问题,最大的两个问题如下所示
- 有的线程池可以无限添加任务或线程,容易导致 OOM;
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
可见其任务队列用的是LinkedBlockingQueue,且没有指定容量,相当于无界队列,这种情况下就可以添加大量的任务,甚至达到Integer.MAX_VALUE的数量级,如果任务大量堆积,可能会导致 OOM。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
这个虽然使用了有界队列SynchronousQueue,但是最大线程数设置成了Integer.MAX_VALUE,这就意味着可以创建大量的线程,也有可能导致 OOM。
- 还有一个问题就是这些线程池的线程都是使用 JDK 自带的线程工厂 (ThreadFactory)创建的,线程名称都是类似pool-1-thread-1的形式,第一个数字是线程池编号,第二个数字是线程编号,这样很不利于系统异常时排查问题。
所以推荐使用ThreadPoolExecutor方法来创建线程池
Executors和ThreaPoolExecutor创建线程池的区别
Executors类和ThreadPoolExecutor都是util.concurrent并发包下面的类, Executos下面的newFixedThreadPool、newScheduledThreadPool、newSingleThreadExecutor、newCachedThreadPool底层的实现都是用的ThreadPoolExecutor实现的,所以ThreadPoolExecutor更加灵活。
我觉得最大的区别就是Executors使用起来很方便,但是容易出现问题,例如像OOM,这个里面的线程池都是固定类型的,可能所有的类型在某个场合都不合适使用,所以这个时候还是要ThreadPoolExecutor来,而ThreadPoolExecutor因为是最原始的,所以使用起来更灵活,可以定制线程池,ThreaPoolExecutor创建线程池方式只有一种,就是走它的构造函数,参数自己指定。
《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险Executors 各个方法的弊端:
- newFixedThreadPool 和 newSingleThreadExecutor:
- 主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。newCachedThreadPool 和 newScheduledThreadPool:
- 主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
线程池之ScheduledThreadPoolExecutor详解
线程池之ScheduledThreadPoolExecutor详解_ThinkWon的博客-CSDN博客_scheduledthreadpoolexecutor
ScheduledThreadPoolExecutor详解 - 知乎
什么是 FutureTask
FutureTask是一种可以取消的异步的计算任务,FutureTask实现了Runnable和Future这两个接口,FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作,只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。由于实现了Runnable接口,所以 FutureTask 也可以放入线程池中。
FutureTask实现了RunnableFuture接口,而RunnableFuture接口扩展自Future和Runnable接口,在创建FutureTask时可以使用Callable接口的实例或者Lambda表达式,也可以使用Runnable的实例,但内部还是会使用适配器模式转换成Callable实例类型。
使用FutureTask的优势有
- 可以获取线程执行后的返回结果;
- 提供了超时控制功能。
- 它实现了Runnable接口和Future接口
什么是异步计算呢?
异步计算是指在让该任务执行时,不需要一直等待其运行结束返回结果,而是可以先去处理其他的事情,然后再获取返回结果。例如你想下载一个很大的文件,这时很耗时的操作,没必要一直等待着文件下载完,你可以先去吃个饭,然后再回来看下文件是否下载完成,如果下载完成就可以使用了,否则还需要继续等待。异步计算实现原理就是因为FutureTask实现了Runnable和Future这两个接口
创建FutureTask实例的两种方式
第一种:使用Callable创建一个FutureTask实例:
FutureTask<Boolean> future = new FutureTask<>(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return true;
}
});
通过new一个对象的方法可以直接创建一个FutureTask实例,如果直接调用run方法将直接在当前线程中运行,不会开启新线程。
第二种:使用ExecutorService或者线程可以让FutureTask进行托管进行,示例如下:
//托管给线程池处理
Future<?> futureResult = Executors.newCachedThreadPool().submit(future);
//托管给单独线程处理
new Thread(future).start();
因为FutureTask继承了Runnable接口,所以它可以通过new Thread()的方式进行运行,再由future变量来检索结果值或者取消任务等操作,通过线程池托管的方式也可以适用。
取消任务
遇到特殊情况时需要对没有运行的或者已经运行的任务进行取消操作,这时可以调用cancel()方法,取消方法有一个布尔类型的参数mayInterruptIfRunning;当值为ture时将尝试中断托管的线程(调用托管线程的interrupt尝试中断)。
取消任务使用的是线程的中断操作,如果任务是可取消的,在任务中存在阻塞线程的地方需要加上InterruptedException的异常捕获来处理中断异常或者在任务可取消点使用Thread.currentThread().isInterrupted()方法来判断任务是否已经发送的中断请求以正常取消任务。
检索结果值
使用FutureTask的一个重要目的是为了能获取到任务的结果值,使用Callable使用一个任务调用点时可以在任务中返回一个引用类型。
在FutureTask内部使用outcome变量存储Callable的结果,调用FutureTask.get()方法将检索结果值,但get()方法也会阻塞调用线程直到任务执行完成或者取消。
get()方法还可以通过设置超时时间来指定等待的时长,超过等待时间后将会抛出TimeoutException异常。
总结
使用FutureTask可以完成任务的取消、检查结果值,这两项也是FutureTask的特色,但FutureTask的底层还是托管给Thread完成;相对于Thread检查结果值会更新的方便,不再需要管理线程执行的状态与值。
在使用cancel方法需要注意任务是否可以取消,在任务内部需要使用Thread.currentThread().isInterrupted()检查中断状态并在Thread.sleep() 、condition.wait()、 Thread.join() 、Thread.wait()使用线程进行阻塞以及可中断的I/O操作方法中捕获InterruptedException异常以避免不必要的情况发生,在大多数任务中是不应该被中断的,所以最好在可中断的任务中设置好检查点;在任务线程会被阻塞点捕获InterruptedException异常,根据情况判断是否需要取消任务。
什么是 Callable 和 Future?
Callable 是类似于 Runnable的接口,Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值和抛异常
Future 接口就是可以拿到Callable的返回值,Future 可以拿到异步执行任务的返回值。表示异步任务,是一个可能还没有完成的异步任务的结果。所以说Callable用于产生结果,Future 用于获取结果。
public class CallableAndFuture {
public static void main(String[] args) {
Callable<Integer> callable = new Callable<Integer>() {
public Integer call() throws Exception {
return new Random().nextInt(100);
}
};
FutureTask<Integer> future = new FutureTask<Integer>(callable);
new Thread(future).start();
try {
Thread.sleep(5000);// 可能做一些事情
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
线程
什么是线程和进程?
进程
进程是操作系统资源分配的基本单位,进程是一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。
线程
线程是处理器任务调度和执行的基本单位,线程是进程中的一个控制单元,负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
进程与线程的区别
线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。
1、根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
2、资源开销:进程之间的切换会有较大的开销,线程之间切换的开销小。
3、包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线
(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
4、内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
5、影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
6、执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行
一般怎么样才能做到线程安全?
1. 无状态
我们都知道只有多个线程访问公共资源的时候,才可能出现数据安全问题,那么如果我们没有公共资源,是不是就没有这个问题呢?
public class NoStatusService {
public void add(String status) {
System.out.println("add status:" + status);
}
public void update(String status) {
System.out.println("update status:" + status);
}
}
这个例子中NoStatusService没有定义公共资源,换句话说是无状态的。
这种场景中,NoStatusService类肯定是线程安全的。
2. 不可变
如果多个线程访问的公共资源是不可变的,也不会出现数据的安全性问题。
public class NoChangeService {
public static final String DEFAULT_NAME = "abc";
public void add(String status) {
System.out.println(DEFAULT_NAME);
}
}
DEFAULT_NAME被定义成了static final的常量,在多线程中环境中不会被修改,所以这种情况,也不会出现线程安全问题。
3. 无修改权限
有时候,我们定义了公共资源,但是该资源只暴露了读取的权限,没有暴露修改的权限,这样也是线程安全的。
public class SafePublishService {
private String name;
public String getName() {
return name;
}
public void add(String status) {
System.out.println("add status:" + status);
}
}
这个例子中,没有对外暴露修改name字段的入口,所以不存在线程安全问题。
4. synchronized
使用JDK内部提供的同步机制,这也是使用比较多的手段,分为:同步方法 和 同步代码块。我们优先使用同步代码块,因为同步方法的粒度是整个方法,范围太大,相对来说,更消耗代码的性能。
其实,每个对象内部都有一把锁,只有抢到那把锁的线程,才被允许进入对应的代码块执行相应的代码。当代码块执行完之后,JVM底层会自动释放那把锁。
public class SyncService {
private int age = 1;
private Object object = new Object();
//同步方法
public synchronized void add(int i) {
age = age + i;
System.out.println("age:" + age);
}
public void update(int i) {
//同步代码块,对象锁
synchronized (object) {
age = age + i;
System.out.println("age:" + age);
}
}
public void update(int i) {
//同步代码块,类锁
synchronized (SyncService.class) {
age = age + i;
System.out.println("age:" + age);
}
}
}
5. Lock
除了使用synchronized关键字实现同步功能之外,JDK还提供了Lock接口,这种显示锁的方式。
通常我们会使用Lock接口的实现类:ReentrantLock,它包含了:公平锁、非公平锁、可重入锁、读写锁 等更多更强大的功能。
public class LockService {
private ReentrantLock reentrantLock = new ReentrantLock();
public int age = 1;
public void add(int i) {
try {
reentrantLock.lock();
age = age + i;
System.out.println("age:" + age);
} finally {
reentrantLock.unlock();
}
}
}
但如果使用ReentrantLock,它也带来了有个小问题就是:需要在finally代码块中手动释放锁。
不过说句实话,在使用Lock显示锁的方式,解决线程安全问题,给开发人员提供了更多的灵活性。
6. 分布式锁
如果是在单机的情况下,使用synchronized和Lock保证线程安全是没有问题的。
但如果在分布式的环境中,即某个应用如果部署了多个节点,每一个节点使用可以synchronized和Lock保证线程安全,但不同的节点之间,没法保证线程安全。
这就需要使用:分布式锁了。
分布式锁有很多种,比如:数据库分布式锁,zookeeper分布式锁,redis分布式锁等。
其中我个人更推荐使用redis分布式锁,其效率相对来说更高一些。
try{
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
return true;
}
return false;
} finally {
unlock(lockKey);
}
同样需要在finally代码块中释放锁。
7. volatile
有时候,我们有这样的需求:如果在多个线程中,有任意一个线程,把某个开关的状态设置为false,则整个功能停止。
简单的需求分析之后发现:只要求多个线程间的可见性,不要求原子性。
如果一个线程修改了状态,其他的所有线程都能获取到最新的状态值。
这样一分析这就好办了,使用volatile就能快速满足需求。
@Service
public CanalService {
private volatile boolean running = false;
private Thread thread;
@Autowired
private CanalConnector canalConnector;
public void handle() {
//连接canal
while(running) {
//业务处理
}
}
public void start() {
thread = new Thread(this::handle, "name");
running = true;
thread.start();
}
public void stop() {
if(!running) {
return;
}
running = false;
}
}
需要特别注意的地方是:volatile不能用于计数和统计等业务场景。因为volatile不能保证操作的原子性,可能会导致数据异常。
8. ThreadLocal
除了上面几种解决思路之外,JDK还提供了另外一种用空间换时间的新思路:ThreadLocal。
当然ThreadLocal并不能完全取代锁,特别是在一些秒杀更新库存中,必须使用锁。
ThreadLocal的核心思想是:共享变量在每个线程都有一个副本,每个线程操作的都是自己的副本,对另外的线程没有影响。
温馨提醒一下:我们平常在使用ThreadLocal时,如果使用完之后,一定要记得在finally代码块中,调用它的remove方法清空数据,不然可能会出现内存泄露问题。
public class ThreadLocalService {
private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public void add(int i) {
Integer integer = threadLocal.get();
threadLocal.set(integer == null ? 0 : integer + i);
}
}
9. 线程安全集合
有时候,我们需要使用的公共资源放在某个集合当中,比如:ArrayList、HashMap、HashSet等。
如果在多线程环境中,有线程往这些集合中写数据,另外的线程从集合中读数据,就可能会出现线程安全问题。
为了解决集合的线程安全问题,JDK专门给我们提供了能够保证线程安全的集合。
比如:CopyOnWriteArrayList、ConcurrentHashMap、CopyOnWriteArraySet、ArrayBlockingQueue等等。
public class HashMapTest {
private static ConcurrentHashMap<String, Object> hashMap = new ConcurrentHashMap<>();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
hashMap.put("key1", "value1");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
hashMap.put("key2", "value2");
}
}).start();
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(hashMap);
}
}
在JDK底层,或者spring框架当中,使用ConcurrentHashMap保存加载配置参数的场景非常多。
比较出名的是spring的refresh方法中,会读取配置文件,把配置放到很多的ConcurrentHashMap缓存起来。
10. CAS
JDK除了使用锁的机制解决多线程情况下数据安全问题之外,还提供了CAS机制。
这种机制是使用CPU中比较和交换指令的原子性,JDK里面是通过Unsafe类实现的。
CAS内部包含了3个值:旧数据、期望数据、新数据 ,比较旧数据 和 期望的数据,如果一样的话,就把旧数据改成新数据。如果不一样的话,当前线程不断自旋,一直到成功为止。
不过,使用CAS保证线程安全,可能会出现ABA问题,需要使用AtomicStampedReference增加版本号解决。
其实,实际工作中很少直接使用Unsafe类的,一般用atomic包下面的类即可。
public class AtomicService {
private AtomicInteger atomicInteger = new AtomicInteger();
public int add(int i) {
return atomicInteger.getAndAdd(i);
}
}
11. 数据隔离
有时候,我们在操作集合数据时,可以通过数据隔离,来保证线程安全。
public class ThreadPoolTest {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(8, //corePoolSize线程池中核心线程数
10, //maximumPoolSize 线程池中最大线程数
60, //线程池中线程的最大空闲时间,超过这个时间空闲线程将被回收
TimeUnit.SECONDS,//时间单位
new ArrayBlockingQueue(500), //队列
new ThreadPoolExecutor.CallerRunsPolicy()); //拒绝策略
List<User> userList = Lists.newArrayList(
new User(1L, "苏三", 18, "成都"),
new User(2L, "苏三说技术", 20, "四川"),
new User(3L, "技术", 25, "云南"));
for (User user : userList) {
threadPool.submit(new Work(user));
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(userList);
}
static class Work implements Runnable {
private User user;
public Work(User user) {
this.user = user;
}
@Override
public void run() {
user.setName(user.getName() + "测试");
}
}
}
这个例子中,使用线程池处理用户信息。
每个用户只被线程池中的一个线程处理,不存在多个线程同时处理一个用户的情况。所以这种人为的数据隔离机制,也能保证线程安全。
数据隔离还有另外一种场景:kafka生产者把同一个订单的消息,发送到同一个partion中。每一个partion都部署一个消费者,在kafka消费者中,使用单线程接收消息,并且做业务处理。
这种场景下,从整体上看,不同的partion是用多线程处理数据的,但同一个partion则是用单线程处理的,所以也能解决线程安全问题。
创建线程有哪几种方式?
创建线程有3种方式:
1、继承 Thread 类;
2、实现 Runnable 接口;
3、 实现 Callable 接口;
1、继承 Thread 类
- 定义一个Thread类的子类,重写run方法,将相关逻辑实现,run()方法就是线程要执行的业务逻辑方法
- 创建自定义的线程子类对象
- 调用子类实例的star()方法来启动线程
运行结果
2、实现 Runnable 接口步骤
- 定义Runnable接口实现类MyRunnable,并重写run()方法
- 创建MyRunnable实例myRunnable,以myRunnable作为target创建Thead对象,该Thread对象才是真正的线程对象
- 调用线程对象的start()方法
执行结果
3、实现 Callable 接口步骤
- 创建实现Callable接口的类myCallable
- 以myCallable为参数创建FutureTask对象
- 将FutureTask作为参数创建Thread对象
- 调用线程对象的start()方法
执行结果
Runnable和Callable创建线程有什么区别?
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
由源码就能够看出它们的区别:
- callable的核心是call方法,允许返回值,runnable的核心是run方法,没有返回值
- call方法可以抛出异常,但是run方法不行
- callable和runnable都可以应用于executors。而thread类只支持runnable
它们的相同点
- 两者都是接口
- 两者都需要调用Thread.start启动线程
- 都可以编写多线程程序
什么是线程安全?为什么不安全?
1、什么是线程安全?
《Java并发编程实践》中对线程安全的定义:
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。
《深入理解Java虚拟机》的作者也认可这个观点。本人也认为这是一个恰当的定义,因为线程安全的主体是什么?是方法还是代码块?这里给出的主体是对象,这是非常恰当的,因为Java是纯面向对象的,Java中一切为对象。因此通过对象定义线程安全是恰当的。
简单讲就是线程安全是指在多线程环境下,多线程每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的。
2、为什么线程会出现不安全?
- 不满足原子性:一个或者多个操作在 CPU 执行的过程中被中断
- 不满足可见性:一个线程对共享变量的修改,另外一个线程不能立刻看到
- 不满足有序性:程序执行的顺序没有按照代码的先后顺序执行
原子性
即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。原子性就像数据库里面的事务一样,他们是一个团队,同生共死。
可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性
有序性即程序执行的顺序按照代码的先后顺序执行
什么是上下文切换?
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
概括来说就是:当前线程在执行完 CPU 时间片切换到另一个线程之前会先保存自己当前的状态,以便下次得到时间片后继续执行这个任务,这个过程就属于一次上下文切换。
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
守护线程和用户线程有什么区别呢?
区别就是守护线程是依赖于用户线程,当一个程序中的全部用户线程都结束执行之后,那么无论守护线程是否还在工作都会随着用户线程一块结束,整个程序也会随之结束运行,用户线程是独立存在的,不会因为其他用户线程退出而退出。
用户 (User) 线程
运行在前台,执行具体的任务,在 Java 语言中线程分为两类:用户线程和守护线程,默认情况下我们创建的线程或线程池都是用户线程,Java 语言中无论是线程还是线程池,默认都是用户线程,因此用户线程也被称为普通线程,main 函数所在的线程就是一个用户线程
守护线程(Daemon Thread)
守护线程是为用户线程服务的,当一个程序中的所有用户线程都执行完成之后程序就会结束运行,程序结束运行时不会管守护线程是否正在运行,由此我们可以看出守护线程在 Java 体系中权重是比较低的,这就是守护线程和用户线程的区别
守护线程注意事项
- setDaemon(true)必须在start()方法前执行,否则会抛出IllegalThreadStateException 异常(可以通过 setDaemon(true) 方法将线程类型更改为守护线程)
- 在守护线程中产生的新线程也是守护线程
- 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑
- 守护 (Daemon) 线程中不能依靠 finally 块的内容来确保执行关闭或清理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守护 线程会随 JVM 一起结束工作,所以守护 (Daemon) 线程中的 finally 语句块可能无法被执行。
守护线程深入了解:额!Java中用户线程和守护线程区别这么大? - 知乎
如何在 Windows 和 Linux 上查找哪个线程cpu利用率最高?
Linux中
- top找到占用高的进程的pid
- top -Hp ${pid}直接查看这个进程的所有线程运行情况,找到占用CPU资源多的线程的tid
- 直接打印该线程(jstack ${pid} | grep ${tid} -A 30)
- 详细内容:在Linux下查询占用CPU较高的线程信息_Alecor的博客-CSDN博客
Windows 中
任务管理器直接看呗
什么是线程死锁
死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态,这些永远在互相等待的进程(线程)称为死锁进程(线程)
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
如下图所示,线程 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 resource1 Thread[线程 2,5,main]get resource2 Thread[线程 1,5,main]waiting get resource2 Thread[线程 2,5,main]waiting get resource1
线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过Thread.sleep(1000);让线程 A 休眠 1s 为的是让线程 B 得到CPU执行权,然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。
形成死锁的四个必要条件是什么
- 互斥条件:线程(进程)对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放
- 请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程(进程)已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环路
(类似于死循环),造成永久阻塞
如何避免线程死锁
我们只要破坏产生死锁的四个条件中的其中一个就可以了。
1、破坏互斥条件
这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
2、破坏请求与保持条件
一次性申请所有的资源
3、破坏不剥夺条件
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
4、破坏循环等待条件
按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
线程的 run()和 start()有什么区别?
线程的 run()和 start()有什么区别?
- start方法是启动线程,但是并没有真正运行,只是把线程放到等待队列中等得到cpu的时间片,而run方法才是真正运行线程
- start方法是由代码调用的,而run方法是由java虚拟机调用的
- 先调用start,后调用run
start()
用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体中的代码执行完毕而直接继续执行后续的代码。通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里的run()方法称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。
run()
run()方法只是类的一个普通方法而已。run方法相当于线程的任务处理逻辑的入口方法,就是线程体,它由java虚拟机在运行相应线程时直接调用,而不是由代码进行调用。
多线程原理相当于玩游戏机,只有一个游戏机(CPU),start是排队,等CPU轮到你,你就run。
总结
调用start后,线程会被放入到等待队列中,也就是上面说的就绪状态,等待CPU调用,并不是马上运行。一旦得到cpu时间片就通过java虚拟机在运行相应线程时直接调用run方法来执行本线程的线程体,先调用start,后调用run。
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就自动执行 run() 方法的内容,这是真正的多线程工作。
而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是thread 的一个普通方法调用,还是在主线程里执行,简单说就是为了实现多线程
说说线程的生命周期(五种基本状态)?
线程的生命周期分为新建(new)、就绪(Runnable)、运行(running)、阻塞(Blocked)、死亡(Dead)五种状态
新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
- 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
- 同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
- 其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期
Java 中用到的线程调度算法是什么?
Java 中用到的线程调度算法是什么?
Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。
线程调度器选择优先级最高的线程运行,但如果发生以下情况就会让出CPU
- 线程体中调用了 yield 方法让出了对 cpu 的占用权利
- 线程体中调用了 sleep 方法使线程进入睡眠状态
- 线程由于 IO 操作受到阻塞
- 另外一个更高优先级线程出现
- 在支持时间片的系统中,该线程的时间片用完
有几种线程调度算法?
有两种调度模型:分时调度模型和抢占式调度模型。
分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。
抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。
线程调度
计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU 的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看, 各个线程轮流获得 CPU 的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待 CPU,JAVA 虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权。
请说出与线程同步以及线程调度相关的方法?
线程同步就是当一个线程对内存进行操作时,其他线程不可以对这个内存地址进行操作,其他线程只能是等待状态。
线程调度是按照特定的机制为多个线程分配cpu使用权;
- wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
- sleep():使一个正在运行的线程处于睡眠状态(等待状态),是一个静态方法,调用此方法要处理 InterruptedException 异常,并且不释放所持有的对象的锁;
- notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
- notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
yield()方法详解/yield 方法有什么作用?
①yield()方法只是提出申请释放CPU资源,至于能否成功释放由JVM决定。
由于这个特性,一般编程中用不到此方法,但在很多并发工具包中,yield()方法被使用,如AQS、ConcurrentHashMap、FutureTask等。
②调用了yield()方法后,线程依然处于就绪状态,线程不会进入堵塞状态,线程就保留了随时被调度的权利。
sleep()方法详解
sleep() 方法的作用是让当前“正在执行的线程”休眠(暂停执行),Sleep方法会释放CPU资源,sleep()方法调用后线程处于阻塞状态
yield()方法和sleep()方法有什么区别
- sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
- 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;所以yield()方法调用后线程只是暂时的将调度权让给别人,但可以立刻回到竞争线程锁的状态
- sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;
- sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行
sleep() 和 wait() 有什么区别?
两者都可以暂停线程的执行
- 类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
- 是否释放锁:sleep() 不释放锁;wait() 释放锁。
- 用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
- 用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个
对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。 或者可以使用wait(long timeout)超时后线程会自动苏醒。
注意这两个方法都要处理 InterruptedException异常
什么是线程调度器和时间分片?
什么是线程调度器(Thread Scheduler)?
线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。 线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是更好的选择
(也就是说不要让你的程序依赖于线程的优先级)。
什么是时间分片(Thread Scheduler)
时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配CPU 时间可以基于线程优先级或者线程等待的时间。
使用 if 块还是while循环调用 wait() 方法的?为什么?
使用 if 块还是while循环调用 wait() 方法的?
使用while循环来调用wait方法
为什么?
因为用if判断的话,唤醒后线程会从 wait 之后的代码开始运行,但是不会重新判断if条件,直接继续运行 if 代码块之后的代码,而如果使用 while 的话,也会从 wait 之后的代码运行,但是唤醒后会重新判断循环条件,如果不成立再执行 while 代码块之后的代码块,成立的话继续wait。
为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在Object 类里?
原因是JAVA提供的锁是对象级的而不是线程级的,在Java中,任意对象都可以当作锁来使用,而线程是可以获得这个对象的。例如线程需要等待某些锁,那么只要调用对象中的wait()方法便可以了,wait,notify,notifyAll跟对象锁之间是有一个绑定关系的,比如你用对象锁Object调用的wait()方法,那么你只能通过Object.notify()或者Object.notifyAll()来唤醒这个线程,这样jvm很容易就知道应该从哪个对象锁的等待池中去唤醒线程,假如用Thread.wait(),Thread.notify(),Thread.notifyAll()来调用,虚拟机根本就不知道需要操作的对象锁是哪一个,而Object类是所有类的一个父类,把这些方法放在Object中,则java中的所有对象都可以去调用这些方法了。
为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用?
当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁, 接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的notify()方法。同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁才可以进行操作,所以需要用同步方法或同步块来保证这个对象锁只能被一个线程占有
所谓的同步的作用就是让这个对象的锁只能有一个线程拥有,其他线程只能等这个线程释放这个对象的锁,正是因为上面的三个方法都需要线程拥有这个对象的的锁,而对象的锁只有一个,只能被一个线程拥有,这就需要同步来帮忙实现,不然的话这个锁就可能被多个线程拥有,那这个锁就没用了
为什么 Thread 类的 sleep()和 yield ()方法是静态的?
sleep作为Thread 父类的静态方法,在jvm 加载时只加载一次,也就是说jvm 里面只存在一个sleep 方法,根据jvm 动态加载规则,jvm 对当前线程子类是有标记的,所以,sleep 方法调用时,必然是通过jvm 识别了调用的线程,结果就是谁调用,谁有效。
sleep之所以写成静态方法,是告诉程序员,你在哪里调用,调用的都是同一个方法,并非某个线程独有的实例方法,它只对当前运行的线程有效
通俗点讲
如果sleep和yield是静态方法,那么不管哪个线程,只要一调用就把自己给sleep、yield了。
如果sleep和yield是实例方法,那就热闹了。一个线程可以获取其他线程对象的引用,然后通过引用调要其他线程的sleep和yield方法,让其他线程让出CPU使用权。试想,如果每个线程都可以通过sleep、yield其他线程的方式来让自己获得CPU使用权,那不是世界大乱了。线程之间可以互相sleep,互相yield。
来啊,互相伤害啊,TMD谁都别想用CPU!
如何停止一个正在运行的线程?
停止线程是在多线程开发中很重要的技术点,比如在多线程持续处理业务代码时,由于处理逻辑中有第三方接口异常,我们就假设发送短信接口挂了吧,那么此时多线程调用短信接口是没有任何意义的,我们希望接口恢复后再对接口进行处理,那么此时怎么办呢,如何中止已经启动的线程呢?
其实在Java中有3种方式可以终止正在运行的线程
- 使用stop方法强制退出:使用stop()方法强制终止线程,注意,强烈不推荐这种方式,并且该方法已经被标记为过期方法了。
- 使用interrupt方法中断线程,该方法只是告诉线程要终止,但最终何时终止取决于计算机;
- 设置标志位使用了volatile关键字共享变量方式,通过改变共享变量+抛异常的方式来暂停线程,注意是暂停线程
暴力停止线程的stop()方法「禁止使用」
public class UserModel {
/**
* 给定userName+password默认值
* 用于模拟上一个线程给赋的旧值
*/
private String userName = "张三";
private String password = "hahahha";
/**
* 用于复制的方法
* 为防止多线程数据错乱,加上synchronized关键字
* @param userName
* @param password
*/
synchronized public void setValue(String userName, String password){
try {
this.userName = userName;
Thread.sleep(3000);
this.password = password;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
省略get\set方法...
}
然后我们再在ThreadDemo中使用这个实体:
public class ThreadDemo extends Thread{
private UserModel userModel;
public ThreadDemo(UserModel userModel){
this.userModel = userModel;
}
@Override
public void run() {
/**
* 重新设置用户名+密码
* 用户名:niceyoo
* 密码:123456
*/
userModel.setValue("niceyoo","123456");
}
}
然后在MyTest中创建并启动线程,然后调用stop()方法:
public class MyTest {
public static void main(String[] args) {
try {
/**创建用户实体**/
UserModel userModel = new UserModel();
/**创建线程**/
ThreadDemo demo = new ThreadDemo(userModel);
/**开启线程**/
demo.start();
/**线程休眠**/
Thread.sleep(1000);
/**停止线程**/
demo.stop();
/**输出用户实体**/
System.out.println(userModel.getUserName() + " :" + userModel.getPassword());
} catch (ThreadDeath | InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果如下:
niceyoo :hahahha
显然跟我们预期的输出结果niceyoo\123456不一致,使用stop()释放锁,对锁定的对象进行了解锁,导致数据得不到同步的处理,出现数据不一致的情况,所以这样就会导致数据安全问题,这也是现在为何 stop() 方法被标注为 “作废、过期”。
interrupt()方法「只告诉要停止,不知道何时停」
使用interrupt()方法就不像是stop()方法那样简单粗暴了,调用该方法仅仅是在当前线程中打了一个停止的标记,并不是真的停止线程,就好比,我打电话告诉你不要玩游戏了,但是你什么时候停止玩游戏就是你的事了。
public class MyTest {
public static void main(String[] args) {
try {
/**创建线程**/
ThreadDemo2 demo = new ThreadDemo2();
/**开启线程**/
demo.start();
/**线程休眠**/
Thread.sleep(2000);
/**停止线程**/
demo.interrupt();
} catch (InterruptedException e) {
System.out.println("线程已经暂停");
e.printStackTrace();
}
}
}
public class ThreadDemo2 extends Thread{
@Override
public void run() {
try {
for (int i = 0; i < 1800000; i++) {
if(!this.isInterrupted()){
System.out.println("输出i:"+i++ + " - 线程未停止 ");
}else{
System.out.println("输出i:"+i++ + " - 线程已停止 - 抛出异常");
throw new InterruptedException();
}
}
}catch (InterruptedException e){
System.out.println("线程已结束...");
}
}
}
输出结果:
输出i:1499992 - 线程未停止
...
输出i:1700624 - 线程未停止
输出i:1700626 - 线程未停止
输出i:1700628 - 线程已停止 - 抛出异常
线程已结束...
简单说一下上方代码,首先我们创建了一个for循环输出i++的线程,启动线程后调用 interrupt() 方法停止线程,但是啥时候停止是不可控的,虽然不可控但是还是有方法知道线程是否是停止的,我们在ThreadDemo2线程类中看到 if 判断 — this.isInterrupted() 「等价于Thread.currentThread().isInterrupt() 」,这是用来判断当前线程是否被终止,通过这个判断我们可以做一些业务逻辑处理,通常如果this.isInterrupted被判定为true后,我们会抛一个中断异常,然后通过try-catch捕获。
再额外说一下,有的小伙伴设置的 for 循环变量的最大值比较小,测试执行过程中并没有重现线程被终止,然后就怀疑这个 interrupt() 到底能不能停止线程呀, 不用纠结,这正是线程的自主权,我们无法像 stop() 方法一样直接停止线程的。
设置标志位
设置标志位使用了volatile关键字共享变量方式,通过改变共享变量+抛异常的方式来暂停线程,注意是暂停线程。当共享标志位位false的的时候就停止线程,我们了解线程对于变量的操作都是操作的变量副本,而一旦使用volatile关键字修饰后,因为其可见性,变量变更始将终从主存中获取最新值。
public class MyTest {
public static void main(String[] args) {
/**创建2个线程**/
ThreadDemo3 demo1 = new ThreadDemo3();
ThreadDemo3 demo2 = new ThreadDemo3();
demo1.setName("线程1");
demo2.setName("线程2");
/**开启线程**/
demo1.start();
demo2.start();
/**让线程先运行5s**/
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**修改线程的变量**/
demo1.heartbeat = false;
demo2.heartbeat = false;
System.out.println("----暂停线程----");
/**让线程再运行5s**/
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**再将标志为置为true**/
demo1.heartbeat = true;
demo2.heartbeat = true;
System.out.println("----从新开启线程----");
}
}
public class ThreadDemo3 extends Thread{
/**共享变量**/
volatile Boolean heartbeat = true;
@Override
public void run() {
while (true){
/**判断标志是否为true**/
if (heartbeat){
System.out.println("当前运行线程为:" +Thread.currentThread().getName() + " - 运行");
}else{
System.out.println("当前运行线程为:" +Thread.currentThread().getName() + " - 非运行");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
输出结果:
省略ing...
当前运行线程为:线程1 - 运行
当前运行线程为:线程2 - 运行
----暂停线程----
省略ing...
当前运行线程为:线程1 - 非运行
当前运行线程为:线程2 - 非运行
----从新开启线程----
当前运行线程为:线程1 - 运行
当前运行线程为:线程2 - 运行
省略ing...
来看一下上方代码,我们在线程类里创建了共享变量heartbeat,因为要监听这个共享变量的状态,肯定是要用while循环体了,为了演示状态的变更,所以在while循环体代码中没有throw抛出 InterruptedException 异常,正常情况下在判断共享变量为fasle时,也是要手动抛出异常的,ok,这就是设置标志位了。
总结
stop()方法在这就不提了,肯定是行不通的,至于为何不能使用大家可以再仔细看看上方那个例子。
然后是interrupt()方法+抛异常处理,看完上边那个例子,大家可能会觉得这个方法有点问题,暂停线程完全靠线程自身决定,即便调用了也不能快速的停止线程,但是我要告诉你,这是目前最为正确的方式… 咳咳,别着急,咱先把设置标志位说了。
设置标志位使用了volatile关键字共享变量方式,通过改变共享变量+抛异常的方式来暂停线程,这个看起来最有效,最正确的方式,其实有一点点问题,而这一点点问题就是为什么让 interrupt() 成为最正确的方式。
volatile标记共享变量方式,在线程发生阻塞时是无法完成响应的。
这个所谓的阻塞指的是什么呢?
其实发生阻塞的情况是比较常见的,比如调用 Thread.join() 方法「当前线程陷入无限期的阻塞,join() 所属的线程对象正常运行run()方法,对join()方法不了解的小伙伴可以去百度了」,或者是 Thread.sleep() 方法,再或者是线程需要等待键盘输入而被阻塞,还有socket网络编程中的 ServerSocket.accept() 方法等等等,总之,在这些种种情况下让线程处于不可运行状态时,即便是主线程修改了共享变量的值,该线程此时根本无法检查循环标志,所以也就无法实现线程中断。
所以,interrupt() + 手动抛异常的方式是目前中断一个正在运行的线程最为正确的方式了。
什么是阻塞方法?
线程阻塞和阻塞方法
当一个线程因为某些原因没有正常执行,而是处理等待阻塞状态,这就是线程阻塞,例如等待I/O操作完成,等待某个锁可用或等待一个外部计算结束。而可以导致线程阻塞的方法,我们称为 阻塞方法。
在 Java 中,能抛出 InterruptedException 的方法一定是阻塞方法,这个异常会在处于阻塞状态的线程被中断时抛出。Java 中,每个线程有一个中断状态,Thread 提供了 interrupt 方法用以中断线程(设置中断状态)。
需要注意的是,中断是一种协作机制。一个线程不能强制另一个线程停止正在执行的操作而去执行其他操作。当线程 A 去中断线程 B 时,A 仅仅设置了 B 的中断状态,B 可以检查到这个中断状态然后在可以停止的地方自愿停下当前操作。
中断处理方法
若自己的方法中调用了可能抛出 InterruptedException 异常的方法时,自己的方法也将变成阻塞方法。调用阻塞方法时必须要处理对中断的响应。常见的处理方式有传递、恢复中断。绝对不要捕获它但不做任何处理,这会导致线程中断的证据丢失,调用栈上更高层的代码无法对中断采取措施。
传递:不做处理,将 InterruptedExcption 传递给调用者。
恢复中断:有时候方法无法抛出异常,如代码是 Runnable 的一部分时。可以通过调用当前线程上的 interrupt 方法恢复中断状态。
public class TaskRunnable implements Runnable {
BlockingQueue<Object> queue;
@Override
public void run() {
try {
queue.take();
} catch (InterruptedException e) {
// 恢复中断状态
Thread.currentThread().interrupt();
}
}
}
Java 中 interrupted 和 isInterrupted 方法的区别?
interrupted 和isInterrupted的两个主要区别
- interrupted 是作用于当前线程,isInterrupted 是作用于调用该方法的线程对象所对应的线程(线程对象对应的线程不一定是当前运行的线程。例如我们可以在A线程中去调用B线程对象的isInterrupted方法,所以isInterrupted方法是作用于A线程的)
- 这两个方法最终都测试线程(调用该方法的线程)是否已经中断,interrupted 清除中断信号,isInterrupted 不清除中断信号
interrupt
interrupt方法是用于中断线程的,调用该方法的线程的状态将被置为"中断"状态。注意:线程中断仅仅是设置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出InterruptedException的方法,比如这里的sleep,以及Object.wait等方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”并且自己中断了就会抛出中断异常。
interrupted
是静态方法,测试当前线程(当前线程是指运行interrupted()方法的线程)是否已经中断(查看当前中断信号是true还是false)并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了,因为中断信号标记被清除了
isInterrupted
测试线程(调用该方法的线程)是否已经中断,不清除中断信号。
Java 中你怎样唤醒一个阻塞的线程?
- 调用notify()方法则将随机解除该对象阻塞的线程
- 调用notifyAll方法全部唤醒阻塞的线程
首先 ,wait()、notify() 方法是针对对象的,调用 wait()方法都将导 致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用notify()方法则将随机解除该对象阻塞的线程或者调用notifyAll方法全部唤醒阻塞的线程,无论是哪个方法都需要重新获取该对象的锁,直到获取成功才能往下执行;
其次,wait、notify 、notifyAll方法必须在 synchronized 块或方法中被调用,并且要保证同步块或方法的锁对象与调用 wait、notify 、notifyAll方法的对象是同一个,如此一来在调用 wait 之前当前线程就已经成功获取某对象的锁,执行 wait 阻塞后当前线程就将之前获取的对象锁释放。然后notify、notifyAll又去获取那个原来wait的对象锁,只有获取了那个对象锁才可以唤醒对象
notify() 和 notifyAll() 有什么区别?
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
notify() 和 notifyAll() 有什么区别?
- notifyAll() 会唤醒所有的线程,notify() 只会唤醒一个线程。
- notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争(这个锁是指的对象锁),竞争成功则状态变就绪状态,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制(注意: 实际上, hotspot是顺序唤醒的!! )
如何在两个线程间共享数据?
在理想情况下,我们希望做到同步和互斥来实现数据在多线程环境下的一致性和安全性。
如果一个类继承Thread,则不适合资源共享(不是不可以实现资源共享)。但是如果实现了Runable接口的话,则很容易的实现资源共享。实现Runnable接口或callable接口,适合多个相同或不同的程序代码的线程去共享同一个资源。
多个线程共享数据分两种情况
- 如果多个线程执行同一个Runnable实现类中的代码,此时共享的数据放在Runnable实现类中;
- 如果多个线程执行不同的Runnable实现类中的代码,此时共享数据和操作共享数据的方法封装到一个对象中,在不同的Runnable实现类中调用操作共享数据的方法。
相同程序代码的多个线程共享一个资源
如果有多个线程在同时运行同一段段代码,可以使用同一个Runnable实现类,多个线程可以共享一个实现类对象,共享数据作为这个Runnable实现类的全局变量
public class Demo08 {
public static void main(String[] args) {
//创建线程任务对象
Ticket ticket = new Ticket();
//创建三个窗口对象
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
//卖票
t1.start();
t2.start();
t3.start();
}
static class Ticket implements Runnable {
//Object lock = new Object();
ReentrantLock lock = new ReentrantLock();
private int ticket = 10;
public void run() {
String name = Thread.currentThread().getName();
while (true) {
sell(name);
if (ticket <= 0) {
break;
}
}
}
private synchronized void sell(String name) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}if (ticket > 0) {
System.out.println(name + "卖票:" + ticket);
ticket--;
}
}
}
}
ticket就是全局变量,作为三个线程的共享数据。
不同程序代码的多个线程共享一个资源
如果每个线程执行的代码不同,将共享数据和操作共享数据的方法封装在一个对象中,在不同的Runnable实现类调用操作共享数据的方法。
public class Bank {
private volatile int count =0;//账户余额
//存钱
public void addMoney(int money){
count +=money;
System.out.println(System.currentTimeMillis()+"存进:"+money);
}
//取钱
public void subMoney(int money){
if(count-money < 0){
System.out.println("余额不足");
return;
}
count -=money;
System.out.println(+System.currentTimeMillis()+"取出:"+money);
}
//查询
public void lookMoney(){
System.out.println("账户余额:"+count);
}
}
public class SyncThreadTest {
public static void main(String args[]){
final Bank bank=new Bank();
Thread tadd=new Thread(new Runnable() {
public void run() {
// TODO Auto-generated method stub
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
bank.addMoney(100);
bank.lookMoney();
System.out.println("\n");
}
}
});
Thread tsub = new Thread(new Runnable() {
public void run() {
// TODO Auto-generated method stub
while(true){
bank.subMoney(100);
bank.lookMoney();
System.out.println("\n");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
});
tsub.start();
tadd.start();
}
}
如果一个类继承Thread,则不适合资源共享,并不是不可以实现资源共享
包子铺线程生产包子,吃货线程消费包子。当包子没有时(包子状态为false),吃货线程等待,包子铺线程生产包子(即包子状态为true),并通知吃货线程(解除吃货的等待状态),因为已经有包子了,那么包子铺线程进入等待状态。接下来,吃货线程能否进一步执行则取决于锁的获取情况。如果吃货获取到锁,那么就执行吃包子动作,包子吃完(包子状态为false),并通知包子铺线程(解除包子铺的等待状态),吃货线程进入等待。包子铺线程能否进一步执行则取决于锁的获取情况。
包子资源类
吃货线程类
包子铺线程类
实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
Java 如何实现多线程之间的通讯和协作?
多线程通信最经典的例子就是生产者和消费者
举个例子,我作为消费者去肯德基买鸡块吃,正常情况下,如果还有鸡块的话就直接卖给我了,如果没有的话,前台就会通知后面的大厨进行制作,那么大厨就相当于是生产者。大厨做好之后会给到前台,然后前台通知我(消费者)来取餐。
在线程中,生产者是一条线程,消费者是一条线程,中间有个产品,如果有产品的话就进行通知,没有的话就进行等待。这样一来,两条线程之间就有了依赖及协作。
应用场景:
- 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费
- 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止
- 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待直到仓库中再次放入产品为止
分析:这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件
对于生产者,没有生产产品之前,要通知消费者等待.而生产了产品之后,又需要马上通知消费者消费
对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费
在生产者消费者问题中,仅有 synchronized是不够的
- synchronized可阻止并发更新同一个共享资源,实现了同步
- synchronized不能用来实现不同线程之间的消息传递(通信)
也就是说synchronized只能解决线程同步问题,不能解决通信问题
方法1:Java提供了几个方法解决线程之间的通信问题
方法名 | 作用 |
wait() | 表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁 |
wait(long timeout) | 指定等待的亳秒数 |
notify | 唤醒一个处于等待状态的线程 |
notifyAll() | 唤醒同一个对象上所有调用wa(方法的线程,优先级别高的线程优先调度 |
注意:均是 Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常 llegalMonitorStateException
import java.util.ArrayList;
import java.util.List;
public class MyList {
private static List<String> list = new ArrayList<String>();
public static void add() {
list.add("anyString");
}
public static int size() {
return list.size();
}
}
public class ThreadA extends Thread {
private Object lock;
public ThreadA(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
if (MyList.size() != 5) {
System.out.println("wait begin "
+ System.currentTimeMillis());
lock.wait();
System.out.println("wait end "
+ System.currentTimeMillis());
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ThreadB extends Thread {
private Object lock;
public ThreadB(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
for (int i = 0; i < 10; i++) {
MyList.add();
if (MyList.size() == 5) {
lock.notify();
System.out.println("已经发出了通知");
}
System.out.println("添加了" + (i + 1) + "个元素!");
Thread.sleep(1000);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Run {
public static void main(String[] args) {
try {
Object lock = new Object();
ThreadA a = new ThreadA(lock);
a.start();
Thread.sleep(50);
ThreadB b = new ThreadB(lock);
b.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
线程A要等待某个条件满足时(list.size()==5),才执行操作。线程B则向list中添加元素,改变list 的size。
A,B之间如何通信的呢?也就是说,线程A如何知道 list.size() 已经为5了呢?
这里用到了Object类的 wait() 和 notify() 方法。
当条件未满足时(list.size() !=5),线程A调用wait() 放弃CPU,并进入阻塞状态。---不像②while轮询那样占用CPU
当条件满足时,线程B调用 notify()通知 线程A,所谓通知线程A,就是唤醒线程A,并让它进入可运行状态。
这种方式的一个好处就是CPU的利用率提高了。
但是也有一些缺点:比如,线程B先执行,一下子添加了5个元素并调用了notify()发送了通知,而此时线程A还执行;当线程A执行并调用wait()时,那它永远就不可能被唤醒了。因为,线程B已经发了通知了,以后不再发通知了。这说明:通知过早,会打乱程序的执行逻辑。
方法2:管程法
- 生产者:负责生产数据的模块(可能是方法,对象,线程,进程)
- 消费者:负责处理数据的模块(可能是方法,对象,线程,进程)
- 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
用代码模拟一个场景:生产者只顾生产,消费者只顾消费,利用一个缓冲区,容量大小为10;
缓冲区有两个方法
- 放入产品:当产品丢过来的时候,先判断一下缓冲区有没有满,如果满了,生产者就要进行等待;如果没满,就把产品放进去,有了产品之后,就通知消费者进行消费。
- 消费产品:消费之前先判断有没有产品,如果有就直接进行消费,消费完了通知生产者;如果没有,就进行等待,等待生产者的通知。
public class ThreadDemo {
public static void main(String[] args) {
SynContainer synContainer = new SynContainer();
new Productor(synContainer).start();
new Consumer(synContainer).start();
}
}
// 生产者
class Productor extends Thread {
SynContainer synContainer;
public Productor(SynContainer synContainer) {
this.synContainer = synContainer;
}
// 生产
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synContainer.push(new Chicken(i));
System.out.println("生产了第" + i + "只鸡");
}
}
}
// 消费者
class Consumer extends Thread {
SynContainer synContainer;
public Consumer(SynContainer synContainer) {
this.synContainer = synContainer;
}
// 消费
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费了第--->" + synContainer.pop().id + "只鸡");
}
}
}
// 产品
class Chicken {
// 产品编号
int id;
public Chicken(int id) {
this.id = id;
}
}
// 缓冲区
class SynContainer {
// 需要一个容器
Chicken[] chickens = new Chicken[10];
// 容器计数器
int count = 0;
// 生产者放入产品
public synchronized void push(Chicken chicken) {
// 如果容器满了,就需要等待消费者消费
if (count == chickens.length) {
// 通知消费者消费,生产者进行等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果容器没满,就需要放入产品
chickens[count] = chicken;
count++;
// 通知消费者可以消费了
this.notifyAll();
}
// 消费者消费产品
public synchronized Chicken pop() {
// 判断能否消费
if (count == 0) {
// 等待生产者生产,消费者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果可以消费
count--;
// 消费完了,通知生产者生产
this.notifyAll();
// 返回消费的是哪只鸡
return chickens[count];
}
}
可以看到,生产者生产完了,消费者立马消费,消费者消费完了,生产者又立马生产;这是一个轮流的过程,一直保持着生产者跟消费者之间的互相等待。
方法3:信号灯法
并发协作模型“生产者/消费者模式” —> 信号灯法
使用一个标志位,如果标志位为true,就等待,如果为false,就通知另一个线程。
代码模拟场景:
演员(生产者)表演节目,观众(消费者)观看节目。
用一个标志位控制线程什么时候等待,什么时候通知。
public class ThreadDemo2 {
public static void main(String[] args) {
TV tv = new TV();
new Player(tv).start();
new Watcher(tv).start();
}
}
// 生产者 -> 演员
class Player extends Thread {
TV tv;
public Player(TV tv) {
this.tv = tv;
}
// 生产
@Override
public void run() {
for (int i = 0; i < 20; i++) {
this.tv.play("电视剧第" + i + "段");
}
}
}
// 消费者 -> 观众
class Watcher extends Thread {
TV tv;
public Watcher(TV tv) {
this.tv = tv;
}
// 消费
@Override
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}
// 产品 -> 节目
class TV {
// 演员表演,观众等待
// 观众观看,演员等待
// 表演的节目
String voice;
// 标志位
boolean flag = true;
// 表演
public synchronized void play(String voice) {
if (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.voice = voice;
System.out.println("演员表演了:" + voice);
this.flag = !this.flag;
// 通知观众观看
this.notifyAll();
}
// 表演
public synchronized void watch() {
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观众观看了:" + voice);
this.flag = !this.flag;
// 通知演员表演
this.notifyAll();
}
}
演员演一段,观众看一段
方法四:while轮询的方式
import java.util.ArrayList;
import java.util.List;
public class MyList {
private List<String> list = new ArrayList<String>();
public void add() {
list.add("elements");
}
public int size() {
return list.size();
}
}
import mylist.MyList;
public class ThreadA extends Thread {
private MyList list;
public ThreadA(MyList list) {
super();
this.list = list;
}
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
list.add();
System.out.println("添加了" + (i + 1) + "个元素");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
import mylist.MyList;
public class ThreadB extends Thread {
private MyList list;
public ThreadB(MyList list) {
super();
this.list = list;
}
@Override
public void run() {
try {
while (true) {
if (list.size() == 5) {
System.out.println("==5, 线程b准备退出了");
throw new InterruptedException();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
import mylist.MyList;
import extthread.ThreadA;
import extthread.ThreadB;
public class Test {
public static void main(String[] args) {
MyList service = new MyList();
ThreadA a = new ThreadA(service);
a.setName("A");
a.start();
ThreadB b = new ThreadB(service);
b.setName("B");
b.start();
}
}
在这种方式下,线程A不断地改变条件,线程ThreadB不停地通过while语句检测这个条件(list.size()==5)是否成立 ,从而实现了线程间的通信。但是这种方式会浪费CPU资源。之所以说它浪费资源,是因为JVM调度器将CPU交给线程B执行时,它没做啥“有用”的工作,只是在不断地测试 某个条件是否成立。就类似于现实生活中,某个人一直看着手机屏幕是否有电话来了,而不是: 在干别的事情,当有电话来时,响铃通知TA电话来了。
notify()/notifyAll()/sleep()/wait()的区别
- 我们通过对这些方法分析,sleep()方法属于Thread类,而wait()/notify()/notifyAll()属于Object基础类,也就是说每个对象都有wait()/notify()/notifyAll()的功能。
- sleep()不会释放锁,而wait()会释放锁。
- sleep()必须捕获异常,而wait()/notify()/notifyAll()不需要捕获异常。
- sleep()可以在任何地方使用,而wait()/notify()/notifyAll()只能在同步控制方法或者同步控制块里面使用。
介绍
- notify():随机唤醒一个等待该对象同步锁的线程,进入就绪队列等待CPU的调度;这里的唤醒是由JVM确定唤醒哪个线程,而且不是按优先级决定。
- notifyAll():唤醒所有的等待该对象同步锁的线程,进入就绪队列等待CPU调度;注意唤醒的是notify之前wait的线程,对于notify之后的wait线程是没有效果的。
- wait():调用时需要先获得该Object的锁,调用后,会把当前的锁释放掉同时阻塞住;但可以通过调用该Object的notify()或者notifyAll()来重新获得锁。
- sleep():在指定的时间内让正在执行的线程暂停执行,但不会释放锁。
同步方法和同步块,哪个是更好的选择?
同步块是更好的选择,同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。
请知道一条原则:同步的范围越小越好。
借着这一条,我额外提一点,虽说同步的范围越少越好,但是在 Java 虚拟机中还是存在着一种叫做锁粗化的优化方法,这种方法就是把同步范围变大。这是有用的,比方说 StringBuffer ,它是一个线程安全的类,自然最常用的 append() 方法是一个同步方法,我们写代码的时候会反复 append 字符串,这意味着要进行反复的加锁 -> 解锁,这对性能不利,因为这意味着 Java 虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此 Java 虚拟机会将多次 调用append 方法的代码进行一个锁粗化的操作,将多
次的 append 的操作扩展到 append 方法的头尾,变成一个大的同步块,这样就减少了加锁 --> 解锁的次数,有效地提升了代码执行的效率。
什么是线程同步和线程互斥?线程同步和互斥的区别?同步有哪几种实现方式?
线程同步
同步,又称直接制约关系,是指多个线程彼此合作,通过一定的逻辑关系来共同完成一个任务,必须严格按照规定的某种先后顺序来运行。一般来说,同步关系中往往包含互斥,同时对临界区的资源会按照某种逻辑顺序进行访问,如先生产后使用
另一种说法
互斥解决了「多进程/线程」对临界区使用的问题,但是它没有解决「多进程/线程」协同工作的问题
我们都知道在多线程里,每个线程一定是顺序执行的,它们各自独立,以不可预知的速度向前推进,但有时候我们希望多个线程能密切合作,以实现一个共同的任务。
所谓同步,就是「多进程/线程间」在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为「进程/线程」同步。
简单说:同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。同步其实已经实现了互斥,所以同步是一种更为复杂的互斥
线程互斥
当有若干个线程都要使用某一共享资源(共享的数据和硬件资源)时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。
简单说:互斥是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的,互斥是一种特殊的同步。
线程同步和互斥的区别
- 互斥是通过竞争对资源的独占使用,彼此之间不需要知道对方的存在,执行顺序是一个乱序。
- 同步是协调多个相互关联线程合作完成任务,彼此之间知道对方存在,执行顺序往往是有序的。
实现线程同步的方法
- 同步代码方法:sychronized 关键字修饰的方法同步代码块:sychronized 关键字修饰的代码块
- 使用特殊变量域volatile实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制
- 使用重入锁实现线程同步:reentrantlock类是可冲入、互斥、实现了lock接口的锁他与sychronized方法具有相同的基本行为和语义
- 使用信号量首先我们要清楚临界资源和临界区的概念,临界资源就是同一时刻只允许一个线程(或进程)访问的资源,临界区就是访问临界资源的代码段。信号量是一种特殊的变量,用来控制对临界资源的使用,在多个进程或线程都要访问临界资源的时候,就需要控制多个进行或线程对临界资源的使用。信号量机制通过p、v操作实现。p操作:原子减1,申请资源,当信号量为0时,p操作阻塞;v操作:原子加1,释放资源。
在监视器(Monitor)内部,是如何做线程同步的?
在 java 虚拟机中,监视器和锁在Java虚拟机中是一块使用的,监视器监视一块同步代码块,每一个监视器都和一个对象引用相关联,为了确保一次只有一个线程执行同步代码块(线程同步),每个对象都关联着一把锁。一旦方法或者代码块被 synchronized 修饰,那么这个部分就放入了监视器的监视区域,线程在获取锁之前不允许执行该部分的代码,确保一次只能有一个线程执行该部分的代码,另外 java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案
servlet和Struts2的action是线程安全吗?SpringMVC 的 Controller 是线程安全的吗?
servlet是线程安全吗?
Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。
Struts2的action是线程安全吗?
Struts2 的 action 是多实例多线程的,是线程安全的,每个请求过来都会 new 一个新的 action 分配给这个请求,请求完成后销毁。
SpringMVC 的 Controller 是线程安全的吗?
不是的
总结
Struts2 好处是不用考虑线程安全问题;Servlet 和 SpringMVC 需要考虑线程安全问题,但是性能可以提升不用处理太多的 gc,可以使用 ThreadLocal 来处理多线程的问题。
线程的安全问题体现和导致原因和解决方案?
线程的安全性问题体现
- 原子性:一个或者多个操作在CPU执行的过程中不被中断的特性。
- 可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。
- 有序性:程序执行的顺序按照代码的先后顺序执行。
导致原因
- 线程切换带来的原子性问题
- 缓存导致的可见性问题
- 编译优化带来的有序性问题
解决办法
- JDK的Atomic开头的原子类,synchronized、Lock,可以解决原子性问题
- synchronized、volatile、Lock,可以解决可见性问题
- Happens-Before规则可以解决有序性问题
在 Java 程序中怎么保证多线程的运行安全?
方法一:使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
方法二:使用自动锁 synchronized。
方法三:使用手动锁 Lock。
手动锁 Java 示例代码如下:
你对线程优先级的理解是什么?
每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。理论上,优先级高的线程比优先级低的线程获得更多的CPU时间。实际上,线程获得的CPU时间通常由包括优先级在内的多个因素决定。线程优先级是一个 int 变量(从 1-10),1 代表最低优先级,10 代表最高优先级。
Java 的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级。
进阶了解:Java基础篇:什么是线程优先级?_kaikeba的博客-CSDN博客
线程类的构造方法和静态块是被哪个线程调用的?run方法呢?
这是一个非常刁钻和狡猾的问题。
请记住:线程类的构造方法、静态块是被 new 这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自身所调用的。
如果说上面的说法让你感到困惑,那么我举个例子,假设 Thread2 中 new 了Thread1,main 函数中 new 了 Thread2,那么:
- Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run()方法是Thread2 自己调用的
- Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run()方法是Thread1 自己调用的
什么是 dump 文件?你如何在 Java 中获取线程堆栈?
什么是线程dump?
线程dump是非常有用的诊断java应用问题的工具,每一个java虚拟机都有及时生成显示所有线程在某一点状态的线程dump能力。虽然每个java虚拟机线程dump打印输出格式上略微有一些不同,但是线程dump的信息包含线程基本信息、线程的运行状态、标识、调用栈;调用的堆栈包含完整的类名,所执行的方法,如果可能的话还有源代码的行数。
其中:
- 线程的一些基本信息:名称、优先级及id
- 线程状态:waiting on condition
- 线程的调用栈
- 线程锁住的资源:locked <0x3f63d600>
JVM中的许多问题都可以使用线程dump文件来进行诊断,其中比较典型的包括线程阻塞,CPU使用率过高,JVM Crash,堆内存不足和类装载等问题。
你如何在 Java 中获取线程堆栈(dump文件)?
在 windows环境中
在启动程序的控制台里敲: Ctrl +Break,线程的 dump会产生在标准输出中(缺省标准输出就是控制台,如果对输出进行了重定向,则要查看输出文件)
在 linux环境中
- 通过使用 jps 命令获取到线进程的 pid
- 通过使用 jstack pid 命令打印线程堆栈
jstack [option] pid
--参数
1. -F 强制打印堆栈
2. -m 打印java 和 native(C++) 堆栈信息
3. -l 打印额外的信息,包括锁信息
另外提一点, Thread 类提供了一个 getStackTrace() 方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈
注意:jps 和ps的作用差不多,都是显示当前系统的java进程情况及进程id,top主要看cpu,内存使用情况及占用资源最多的进程由高到低排序,关注点在于资源占用情况
使用jps [-l]命令查看本机所有java进程pid
使用top查看目前正在运行的进程使用系统资源情况
当前占用cpu最高26.5%的进程为27796的java程序
使用jstack将所有线程信息导出到指定文件中
使用jstack [-l] pid > xxx.log命令将所有线程信息输入到指定文件中
当'jstack [-l] pid'没有响应,使用jstack -F [-m] [-l] pid >xxx.log命令强制导出堆栈dump
一个线程运行时发生异常会怎样?
如果异常没有被捕获该线程将会停止执行,如果该异常被捕获或抛出,则程序继续运行。
Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候,JVM 会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler 并将线程和异常作为参数传递给 handler 的uncaughtException()方法进行处理。
Java 线程数过多会造成什么异常?
(1)线程的生命周期开销非常高
(2)消耗过多的CPU 资源
如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU 资源时还将产生其他性能的开销。
(3)降低稳定性
JVM 在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError 异常。
为什么代码会重排序?
在执行程序时,为了提高性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:
- 编译器和处理器都必须遵守 as-if-serial 语义,as-if-serial 语义的意思指:不管编译器和处理器为了提高并行度怎么重排序,(单线程)程序的执行结果不能被改变。
- 存在数据依赖关系的不允许重排序
深入了解:https://blog.csdn.net/weixin_43820556/article/details/124245439
as-if-serial规则和happens-before规则是什么?两者的区别?
as-if-serial规则(语义)
as-if-serial 语义的意思指:不管编译器和处理器为了提高并行度怎么重排序,(单线程)程序的执行结果不能被改变。
Happens-Before规则
1、程序次序规则:在一个线程内,按照程序控制流顺序,书写在前面的操作先行发生于书写在后面的操作。
2、管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。
3、volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
4、线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
5、线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测。
6、线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
7、对象终结原则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()垃圾回收方法的开始。
- as-if-serial语义保证单线程内程序的执行结果不被改变,happens- before关系保证正确同步的多线程程序的执行结果不被改变。
- as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens- before指定的顺序来执行的。
- as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。
LockSupport详解
LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,当然阻塞之后肯定得有唤醒的方法。每个使用LockSupport的线程都会与一个许可关联,如果该许可可用,那这个线程调用park()将会立即返回,否则阻塞。如果许可不可用,则可以调用 unpark 使其可用(许可只有一个,不可累加)
LockSupport有常用的方法如下,主要有两类方法:park和unpark
public static void park(Object blocker); // 暂停当前线程
public static void parkNanos(Object blocker, long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(Object blocker, long deadline); // 暂停当前线程,直到某个时间
public static void park(); // 无期限暂停当前线程
public static void parkNanos(long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(long deadline); // 暂停当前线程,直到某个时间
public static void unpark(Thread thread); // 恢复当前线程
public static Object getBlocker(Thread t);
为什么叫park呢,park英文意思为停车。我们如果把Thread看成一辆车的话,park就是让车停下,unpark就是让车启动然后跑起来。
public class LockSupportDemo {
public static Object u = new Object();
static ChangeObjectThread t1 = new ChangeObjectThread("t1");
static ChangeObjectThread t2 = new ChangeObjectThread("t2");
public static class ChangeObjectThread extends Thread {
public ChangeObjectThread(String name) {
super(name);
}
@Override public void run() {
synchronized (u) {
System.out.println("in " + getName());
LockSupport.park();
if (Thread.currentThread().isInterrupted()) {
System.out.println("被中断了");
}
System.out.println("继续执行");
}
}
}
public static void main(String[] args) throws InterruptedException {
t1.start();
Thread.sleep(1000L);
t2.start();
Thread.sleep(3000L);
t1.interrupt();
LockSupport.unpark(t2);
t1.join();
t2.join();
}
}
in t1
被中断了
继续执行
in t2
继续执行
这儿park和unpark其实实现了wait和notify的功能,不过还是有一些差别的。
- park不需要获取某个对象的锁
- 因为中断的时候park不会抛出InterruptedException异常,所以需要在park之后自行判断中断状态,然后做额外的处理
我们再来看看Object blocker,这是个什么东西呢?这其实就是方便在线程dump的时候看到具体的阻塞对象的信息。
"t1" #10 prio=5 os_prio=31 tid=0x00007f95030cc800 nid=0x4e03 waiting on condition [0x00007000011c9000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304)
// `下面的这个信息`
at com.wtuoblist.beyond.concurrent.demo.chapter3.LockSupportDemo$ChangeObjectThread.run(LockSupportDemo.java:23) //
- locked <0x0000000795830950> (a java.lang.Object)
park和unpark的使用不会出现死锁的情况,这是因为park和unpark会对每个线程维持一个许可(boolean值)
- unpark调用时,如果当前线程还未进入park,则许可为true
- park调用时,判断许可是否为true,如果是true,则继续往下执行;如果是false,则等待,直到许可为true
总结一下
- park和unpark可以实现类似wait和notify的功能,但是并不和wait和notify交叉,也就是说unpark不会对wait起作用,notify也不会对park起作用。
- park和unpark的使用不会出现死锁的情况
- blocker的作用是在dump线程的时候看到阻塞对象的信息
ThreadLocal 是什么?原理是什么?有哪些使用场景?
什么是ThreadLocal?
ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
package com.javaBase.LineDistance;
/**
* 〈一句话功能简述〉;
* 〈功能详细描述〉
*
* @author jxx
* @see [相关类/方法](可选)
* @since [产品/模块版本] (可选)
*/
public class TestThreadLocal {
public static void main(String[] args) {
ThreadLocal<Integer> threadLocal = new MyThreadLocal();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
threadLocal.set(threadLocal.get() + 1);
System.out.println("线程1:" + threadLocal.get());
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
threadLocal.set(threadLocal.get() + 1);
System.out.println("线程2:" + threadLocal.get());
}
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
threadLocal.set(threadLocal.get() + 1);
System.out.println("线程3:" + threadLocal.get());
}
}
});
t1.start();
t2.start();
t3.start();
}
private static class MyThreadLocal extends ThreadLocal<Integer> {
@Override
protected Integer initialValue() {
return 0;
}
}
}
线程2:1
线程1:1
线程2:2
线程3:1
线程1:2
线程3:2
线程2:3
线程3:3
线程1:3
可知个线程之间对ThreadLocal的操作互不影响。
ThreadLocal原理
ThreadLocal中的几个主要方法:
- void set(Object value)设置当前线程的线程局部变量的值。
- public Object get()该方法返回当前线程所对应的线程局部变量。
- public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
- protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
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();
}
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
实现原理:ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本来实现线程隔离,ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。
ThreadLocal的应用场景
1、方便同一个线程使用某一对象,避免不必要的参数传递;
2、线程间数据隔离(每个线程在自己线程里使用自己的局部变量,各线程间的ThreadLocal对象互不影响);
3、获取数据库连接、Session、关联ID(比如日志的uniqueID,方便串起多个日志);
其中spring中的事务管理器就是使用的ThreadLocal:
Spring的事务管理器通过AOP切入业务代码,在进入业务代码前,会依据相应的事务管理器提取出相应的事务对象,假如事务管理器是DataSourceTransactionManager,
就会从DataSource中获取一个连接对象,通过一定的包装后将其保存在ThreadLocal中。而且Spring也将DataSource进行了包装,重写了当中的getConnection()方法,或者说
该方法的返回将由Spring来控制,这样Spring就能让线程内多次获取到的Connection对象是同一个。
什么是线程局部变量?
线程局部变量是线程内部的变量,属于线程自身所有,不在多个线程间共享。Java 提供 ThreadLocal 类来支持线程局部变量,是一种实现线程安全的方式。
但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程局部变量的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。
ThreadLocal内存泄漏分析与解决方案
ThreadLocal造成内存泄漏的原因?
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key 为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。
ThreadLocal内存泄漏解决方案?
解决方案就是每次使用完ThreadLocal,都调用它的remove()方法,清除数据
注意事项
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal 就跟加锁完要解锁一样,用完就清理。
锁
Java里面的锁有哪些?
1、乐观锁和悲观锁
i、乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
ii、悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程),Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
2、独享锁(独占锁)和共享锁
i、共享锁
共享锁是一种思想: 可以有多个线程获取读锁,以共享的方式持有锁。该锁可被多个线程共有,典型的就是ReentrantReadWriteLock里的读锁,它的读锁是可以被共享的,但是它的写锁确每次只能被独占。另外读锁的共享可保证并发读是非常高效的,但是读写和写写,写读都是互斥的。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
Java中用到的共享锁: ReentrantReadWriteLock的读锁。
ii、独享锁(独占锁)
独占锁是一种思想: 只能有一个线程获取锁,以独占的方式持有锁。对于Synchronized而言,当然是独享锁。
Java中用到的独占锁: synchronized,ReentrantLock
3、互斥锁和读写锁
上面讲的独享锁/共享锁就是一种概念,互斥锁/读写锁是具体的实现
i、互斥锁(独占锁的实现)
在访问共享资源之前对进行加锁操作,在访问完成之后进行解锁操作。 加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。
如果解锁时有一个以上的线程阻塞,那么所有该锁上的线程都被编程就绪状态, 第一个变为就绪状态的线程又执行加锁操作,那么其他的线程又会进入等待。 在这种方式下,只有一个线程能够访问被互斥锁保护的资源
互斥锁的具体实现就是synchronized、ReentrantLock。ReentrantLock是JDK1.5的新特性,采用ReentrantLock可以完全替代替换synchronized传统的锁机制,更加灵活。
ii、读写锁(共享锁的实现)
读写锁是一种技术: 通过ReentrantReadWriteLock类来实现。为了提高性能, Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的。
读锁: 允许多个线程获取读锁,同时访问同一个资源。
写锁: 只允许一个线程获取写锁,不允许同时访问同一个资源。
读写锁既是互斥锁,又是共享锁,read模式是共享,write是互斥(排它锁)的。
读写锁有三种状态:读加锁状态、写加锁状态和不加锁状态
读写锁在Java中的具体实现就是ReadWriteLock
一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
只有一个线程可以占有写状态的锁,但可以有多个线程同时占有读状态锁,这也是它可以实现高并发的原因。当其处于写状态锁下,任何想要尝试获得锁的线程都会被阻塞,直到写状态锁被释放;如果是处于读状态锁下,允许其它线程获得它的读状态锁,但是不允许获得它的写状态锁,直到所有线程的读状态锁被释放;为了避免想要尝试写操作的线程一直得不到写状态锁,当读写锁感知到有线程想要获得写状态锁时,便会阻塞其后所有想要获得读状态锁的线程。所以读写锁非常适合资源的读操作远多于写操作的情况。
4、可重入锁(递归锁)
广义上的可重入锁指的是可重复可递归调用的锁,任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,例如在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。可重入锁的作用是避免死锁,ReentrantLock和synchronized都是可重入锁
实现原理
每一个可重入锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功或获取锁后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增1;当线程退出同步代码块时,计数器会递减1,如果计数器为 0,则释放该锁。
5、分段锁
分段锁是一种机制: 最好的例子来说明分段锁是ConcurrentHashMap。ConcurrentHashMap原理:它内部细分了若干个小的 HashMap,称之为段(Segment)。默认情况下一个 ConcurrentHashMap 被进一步细分为 16 个段,既就是锁的并发度。如果需要在 ConcurrentHashMap 添加一项key-value,并不是将整个 HashMap 加锁,而是首先根据 hashcode 得到该key-value应该存放在哪个段中,然后对该段加锁,并完成 put 操作。在多线程环境中,如果多个线程同时进行put操作,只要被加入的key-value不存放在同一个段中,则线程间可以做到真正的并行。
线程安全:ConcurrentHashMap 是一个 Segment 数组, Segment 通过继承ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全
容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
比如:在ConcurrentHashMap中使用了一个包含16个锁的数组,每个锁保护所有散列桶的1/16,其中第N个散列桶由第(N mod 16)个锁来保护。假设使用合理的散列算法使关键字能够均匀的分部,那么这大约能使对锁的请求减少到越来的1/16。也正是这项技术使得ConcurrentHashMap支持多达16个并发的写入线程。
6、偏向锁 & 轻量级锁 & 重量级锁
JDK 1.6 为了减少获得锁和释放锁所带来的性能消耗,在JDK 1.6里引入了4种锁的状态:无锁、偏向锁、轻量级锁和重量级锁,它会随着多线程的竞争情况逐渐升级,但不能降级。
i、重量级锁
重量级锁是一种称谓: 这种依赖于操作系统 Mutex Lock来实现的锁称为重量级锁。synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本身依赖底层的操作系统的 Mutex Lock来实现。为了优化synchonized,引入了轻量级锁,偏向锁。
Java中的重量级锁: synchronized
iii、轻量级锁
轻量级锁是JDK6时加入的一种锁优化机制: 轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量。轻量级是相对于使用操作系统互斥量来实现的重量级锁而言的。轻量级锁在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。如果出现两条以上的线程争用同一个锁的情况,那轻量级锁将不会有效,必须膨胀为重量级锁。
优点: 如果没有竞争,通过CAS操作成功避免了使用互斥量的开销。
缺点: 如果存在竞争,除了互斥量本身的开销外,还额外产生了CAS操作的开销,因此在有竞争的情况下,轻量级锁比传统的重量级锁更慢。
iii、偏向锁
研究发现大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了不让这个线程每次获得锁都需要CAS操作的性能消耗,就引入了偏向锁。当一个线程访问对象并获取锁时,会在对象头里存储锁偏向的这个线程的ID,以后该线程再访问该对象时只需判断对象头的Mark Word里是否有这个线程的ID,如果有就不需要进行CAS操作,这就是偏向锁。当线程竞争更激烈时,偏向锁就会升级为轻量级锁,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待一会儿上一个线程就会释放锁,但是当自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁,重量级锁就是Synchronized,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。
优点: 把整个同步都消除掉,连CAS操作都不去做了,优于轻量级锁。
缺点: 如果程序中大多数的锁都总是被多个不同的线程访问,那偏向锁就是多余的。
volatile 关键字的作用,volatile 关键字是什么原理?
1、volatile 的主要作用
- 保证变量的内存可见性
- 禁止指令重排序
2、volatile 主要作用的实现原理
i、保证变量的内存可见性的实现原理
当对volatile修饰的共享变量执行写操作后,JMM会把工作内存(本地内存)中的最新变量值强制刷新到主内存,并且通过 CPU 总线嗅探机制( CPU 总线嗅探机制其实就是一种用来实现缓存一致性的常见机制)告知其他线程该变量副本已经失效,需要重新从主内存中读取。这样volatile就保证了不同线程对共享变量操作的可见性
嗅探机制工作原理:每个处理器通过监听在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中。
可见性
多个线程共同访问共享变量时,某个线程修改了此变量,其它线程总是能知道这个变量变化。也就是说,如果线程 A 修改了共享变量 V 的值,那么线程 B 在使用 V 的值时,能立即读到 V 的最新值(要看懂这句话就要知道JMM模型)
Java 内存模型
JMM(Java Memory Model):Java 内存模型,是 Java 虚拟机规范中所定义的一种内存模型,Java 内存模型是标准化的,屏蔽掉了底层不同计算机的区别。也就是说,JMM 是 JVM 中定义的一种并发编程的底层模型机制。
JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存(也叫工作内存),本地内存中存储了该线程以读/写共享变量的副本。
JMM 的规定
所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
- 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
- 线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
- 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。
JMM 的抽象示意图
然而,JMM 这样的规定可能会导致线程对共享变量的修改没有即时更新到主内存,或者线程没能够即时将共享变量的最新值同步到工作内存中,从而使得线程在使用共享变量的值时,该值并不是最新的。正因为 JMM 这样的机制,就出现了可见性问题。
ii、禁止指令重排序的实现原理
使用 volatile 修饰变量时,根据 volatile 重排序规则表,Java 编译器在生成字节码时,会在指令序列中插入内存屏障指令来禁止特定类型的处理器重排序。说白了就是靠这个内存屏障来实现volatile 禁止指令重排序
什么是指令重排序?
为了提高性能,编译器和处理器常常会在不改变数据依赖的情况下对指令重新排序,也就是不一定会根据编译时的指令顺序(可以理解为不会按照你写代码的顺序从上到下的依次执行),而是它自己会重新排序以达到提高性能的目的
什么是volatile 重排序规则表?
JMM 针对编译器制定了 volatile 重排序规则表,JMM 会限制特定类型的编译器和处理器重排序。如下所示:
其实就是JMM为了实现volatile 禁止指令重排序这个功能针对编译器和处理器制定的规则来限制特定类型的编译器和处理器重排序
什么是内存屏障指令?
内存屏障指令是一组处理器指令,它的作用是禁止指令重排序和解决内存可见性的问题。
JMM 把内存屏障指令分为下列四类:
StoreLoad 屏障是一个全能型的屏障,它同时具有其他三个屏障的效果。所以执行该屏障开销会很大,因为它使处理器要把缓存中的数据全部刷新到内存中。
下面我们来看看 volatile 读 / 写时是如何插入内存屏障的,见下图:
从上图,我们可以知道 volatile 读 / 写插入内存屏障规则:
- 在每个 volatile 读操作的后面插入 LoadLoad 屏障和 LoadStore 屏障。
- 在每个 volatile 写操作的前后分别插入一个 StoreStore 屏障和一个 StoreLoad 屏障。
也就是说volatile 禁止指令重排序就是靠上面两条内存屏障规则来实现的,这样编译器不会对 volatile 读与 volatile 读后面的任意内存操作重排序;编译器不会对 volatile 写与 volatile 写前面的任意内存操作重排序。
好文参考:volatile 关键字,你真的理解吗? - 知乎
java内部有哪些同步的机制?
(1)synchronized关键字实现的重量级同步机制,synchronized是一种同步锁,可以修饰代码块、方法(包括静态方法)、类
synchronized详细内容看这篇:Java同步机制 - 简书
(2)Lock接口及其实现类,如ReentrantLock.ReadLock和ReentrantReadWriteLock.WriteLock。
(3)volatile关键字实现的轻量级同步机制
什么是可重入锁?为什么需要可重入锁? 可重入锁的实现原理?
什么是可重入锁?
广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。可重入锁的作用就是为了避免死锁,java中synchronized和ReentrantLock都是可重入锁
package com.test.reen;
// 演示可重入锁是什么意思,可重入,就是可以重复获取相同的锁,synchronized和ReentrantLock都是可重入的
// 可重入降低了编程复杂性
public class WhatReentrant {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (this) {
System.out.println("第1次获取锁,这个锁是:" + this);
int index = 1;
while (true) {
synchronized (this) {
System.out.println("第" + (++index) + "次获取锁,这个锁是:" + this);
}
if (index == 10) {
break;
}
}
}
}
}).start();
}
}
我的理解就是,某个线程已经获得某个锁,可以无需等待而再次获取锁,并且不会出现死锁(不同线程当然不能多次获得锁,需要等待)。
简单的说,就是某个线程获得锁,之后可以不用等待而再次获取锁且不会出现死锁。
public class Demo1 {
public synchronized void functionA(){
System.out.println("iAmFunctionA");
functionB();
}
public synchronized void functionB(){
System.out.println("iAmFunctionB");
}
}
代码解释
functionA()和functionB()都是同步方法(就是要获取类的对象锁才可以执行这个方法),当线程进入funcitonA()会获得该类的对象锁,然后又在functionA()对方法functionB()做了调用,但是functionB()也是同步的,funcitonA()并没有释放这个类的对象锁,但是因为synchronized是可重入锁,然后funcitonB()又是同一个class的方法(也就是同一个线程),所以这个funcitonB()是可以执行的,但是如果synchronized不是可重入锁,那就会发生死锁,因为funcitonA()并没有释放锁,而funcitonB()又需要这个锁才可以执行,但是由于funcitonA()没有执行完funcitonB()所以就一直不释放这个锁,所以就死锁了。这也就是为啥能够防止死锁的原因
可重入锁的实现原理?
每一个可重入锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功或获取锁后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增1;当线程退出同步代码块时,计数器会递减1,如果计数器为 0,则释放该锁。
synchronized关键字的底层实现原理
synchronized关键字修饰的地方
- 修饰实例方法上,锁对象是当前的 this 对象。
- 修饰代码块,也就是synchronized(object){},锁对象是()中的对象,一般为this或明确的对象。也就是锁住的是你指定的对象
- 修饰静态方法上,锁对象是方法区中的类对象,是一个全局锁。
- 修饰类,即直接作用一个类。
底层实现原理
synchronized修饰的地方不同,实现的原理不同
- synchronized用来修饰方法和静态方法时,是通过ACC_SYNCHRONIZED标识符来保持线程同步的。ACC_SYNCHRONIZE:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取 monitor对象,获取成功之后才能执行方法体,方法执行完后再释放monitor对象。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
- 用来修饰代码块和类时,是通过monitorenter和monitorexit指令来完成。monitorenter: 该指令表示获取锁对象的 monitor 对象,这时 monitor 对象中的 count 会加+1,如果 monitor 已经被其他线程所获取,该线程会被阻塞住,直到 count = 0,再重新尝试获取monitor对象。
monitorexit: 该指令表示该线程释放锁对象的 monitor 对象,这时monitor对象的count便会-1变成0,其他被阻塞的线程可以重新尝试获取锁对象的monitor对象。
synchronized用来修饰方法和静态方法时
synchronized用来修饰方法和静态方法时,是通过ACC_SYNCHRONIZED标识符来保持线程同步的。
public class SyncTest {
public synchronized void sync(){
}
}
通过javap -verbose xxx.class查看反编译结果:
从反编译的结果来看,我们可以看到sync()方法中多了一个标识符。JVM就是根据该ACC_SYNCHRONIZED标识符来实现方法的同步,即:
当方法被执行时,JVM 调用指令会去检查方法上是否设置了ACC_SYNCHRONIZED标识符,如果设置了ACC_SYNCHRONIZED标识符,则会获取锁对象的 monitor 对象,线程执行完方法体后,又会释放锁对象的 monitor对象。在此期间,其他线程无法获得锁对象的 monitor 对象。
修饰代码块和类时,是通过monitorenter和monitorexit指令来完成
public class SyncTest {
private static int count;
public SyncTest() {
count = 0;
}
public void sync() {
synchronized (this) {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
SyncTest s = new SyncTest();
Thread t0 = new Thread(new Runnable() {
@Override
public void run() {
s.sync();
}
});
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
s.sync();
}
});
t0.start();
t1.start();
}
}
查看字节码信息:
我们可以看到sync()字节码指令中会有两个monitorenter和monitorexit指令:
monitorenter: 该指令表示获取锁对象的 monitor 对象,这时 monitor 对象中的 count 会加+1,如果 monitor 已经被其他线程所获取,该线程会被阻塞住,直到 count = 0,再重新尝试获取monitor对象。
monitorexit: 该指令表示该线程释放锁对象的 monitor 对象,这时monitor对象的count便会-1变成0,其他被阻塞的线程可以重新尝试获取锁对象的monitor对象。
实现原理的核心
monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
- 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;
monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。注:monitorexit指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁;
ACC_SYNCHRONIZE:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取 monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。而两个指令的执行是JVM通过调用操作系统的互斥原语mutex(谬 tei4 g s)来实现,被阻塞的线程会被挂起、等待重新调度,会导致用户态和内核态两个态之间来回切换,对性能有较大影响。
通过上面两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
synchronized 的作用?说说自己是怎么使用 synchronized 关键字
synchronized 的作用?
在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。
另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
说说自己是怎么使用 synchronized 关键字?
synchronized关键字最主要的三种使用方式:
1、修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
2、修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
3、修饰代码块: 指定加锁对象,对指定对象加锁,进入同步代码块前要获得指定对
象的锁。
总结: synchronized 关键字加到 static 静态方法是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!
volatile 修饰符的有过什么实践?单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理?
volatile 的实践最经典的就是单例模式
双重校验锁实现对象单例(线程安全)
另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要,因为使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行
uniqueInstance = new Singleton();这段代码其实是分为三步执行:
1、为 uniqueInstance 分配内存空间
2、初始化 uniqueInstance
3、将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回uniqueInstance,但此时 uniqueInstance 还未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
什么是自旋?什么是自旋锁?
什么是自旋?
我们了解什么叫自旋?“自旋”可以理解为“自我旋转”,这里的“旋转”指“循环”,比如 while 循环或者 for 循环。“自旋”就是自己在这里不停地循环,直到目标达成。而不像普通的锁那样,如果获取不到锁就进入阻塞。
对比自旋和非自旋的获取锁的流程
我们用这样一张流程图来对比一下自旋锁和非自旋锁的获取锁的过程。
我们来看自旋锁,它并不会放弃 CPU 时间片,而是通过自旋等待锁的释放,也就是说,它会不停地再次地尝试获取锁,如果失败就再次尝试,直到成功为止。
我们再来看下非自旋锁,非自旋锁和自旋锁是完全不一样的,如果它发现此时获取不到锁,它就把自己的线程切换状态,让线程休眠,然后 CPU 就可以在这段时间去做很多其他的事情,直到之前持有这把锁的线程释放了锁,于是 CPU 再把之前的线程恢复回来,让这个线程再去尝试获取这把锁。如果再次失败,就再次让线程休眠,如果成功,一样可以成功获取到同步资源的锁。
可以看出,非自旋锁和自旋锁最大的区别,就是如果它遇到拿不到锁的情况,它会把线程阻塞,直到被唤醒。而自旋锁会不停地尝试。
自旋锁的好处
阻塞和唤醒线程都是需要高昂的开销的,如果同步代码块中的内容不复杂,那么可能转换线程带来的开销比实际业务代码执行的开销还要大。
在很多场景下,可能我们的同步代码块的内容并不多,所以需要的执行时间也很短,如果我们仅仅为了这点时间就去切换线程状态,那么其实不如让线程不切换状态,而是让它自旋地尝试获取锁,等待其他线程释放锁,有时我只需要稍等一下,就可以避免上下文切换等开销,提高了效率。
用一句话总结自旋锁的好处,那就是自旋锁用循环去不停地尝试获取锁,让线程始终处于 Runnable 状态,节省了线程状态切换带来的开销。
缺点
那么自旋锁有没有缺点呢?
其实自旋锁是有缺点的。它最大的缺点就在于虽然避免了线程切换的开销,但是它在避免线程切换开销的同时也带来了新的开销,因为它需要不停得去尝试获取锁。如果这把锁一直不能被释放,那么这种尝试只是无用的尝试,会白白浪费处理器资源。也就是说,虽然一开始自旋锁的开销低于线程切换,但是随着时间的增加,这种开销也是水涨船高,后期甚至会超过线程切换的开销,得不偿失。
适用场景
所以我们就要看一下自旋锁的适用场景。首先,自旋锁适用于并发度不是特别高的场景,以及临界区比较短小的情况,这样我们可以利用避免线程切换来提高效率。
可是如果临界区很大,线程一旦拿到锁,很久才会释放的话,那就不合适用自旋锁,因为自旋会一直占用 CPU 却无法拿到锁,白白消耗资源。
多线程中 synchronized 锁升级的原理是什么?
在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。
骚戴理解:刚开始如果没有synchronized锁没有线程占用那就是无状态锁,然后有个A线程占用了这个锁就会升级为偏向锁,然后如果这个时候A线程又来尝试获取这个锁,那就可以直接获取这个锁,如果是B线程来获取这个锁,那就会从偏向锁升级为轻量级锁, 轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待一会儿上一个线程就会释放锁,B线程就会自旋等待A线程释放锁,这个时候如果又来了个C线程,那就会从轻量级锁升级为重量级锁,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。简单来说就是一个线程占有锁那就是偏向锁,两个就是轻量级锁,三个及以上就是重量级锁!
synchronized 锁升级原理
在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁(就是先让它是一个偏向锁),并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,偏向锁就会升级为轻量级锁,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待一会儿上一个线程就会释放锁,但是当自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁,重量级锁就是Synchronized,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。
i、重量级锁
重量级锁是一种称谓: 这种依赖于操作系统 Mutex Lock来实现的锁称为重量级锁。synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本身依赖底层的操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致用户态和内核态两个态之间来回切换,对性能有较大影响。。为了优化synchonized,引入了轻量级锁,偏向锁。
Java中的重量级锁: synchronized
iii、轻量级锁
轻量级锁是JDK6时加入的一种锁优化机制: 轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待一会儿上一个线程就会释放锁。轻量级锁是使用CAS操作去消除同步使用的互斥量,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,轻量级是相对于使用操作系统互斥量来实现的重量级锁而言的。如果出现两个以上的线程争用同一个锁的情况(这里注意是两个以上!不是两个!),那轻量级锁将不会有效,必须膨胀为重量级锁。
优点: 如果竞争的程度很低(小于等于两个线程竞争锁),通过CAS操作成功避免了使用互斥量的开销。
缺点: 如果两个以上的线程竞争锁就会失效,除了互斥量本身的开销外,还额外产生了CAS操作的开销,因此在有竞争的情况下,轻量级锁比传统的重量级锁更慢。
iii、偏向锁
研究发现大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了不让这个线程每次获得锁都需要CAS操作的性能消耗,就引入了偏向锁。当一个线程访问对象并获取锁时,会在对象头里存储锁偏向的这个线程的ID,以后该线程再访问该对象时只需判断对象头的Mark Word里是否有这个线程的ID,如果有就不需要进行CAS操作,这就是偏向锁。
优点: 把整个同步都消除掉,连CAS操作都不去做了,优于轻量级锁。
缺点: 如果程序中大多数的锁都总是被多个不同的线程访问,那偏向锁就是多余的。
锁的升级的目的
锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
线程 B 怎么知道线程 A 修改了变量
- volatile 修饰变量
- synchronized 修饰修改变量的方法
- wait/notify
- while 轮询
当一个线程进入一个对象的 synchronized 方法 A 之后,其它线程是否可进入此对象的 synchronized 方法 B?为什么?
不能
为什么?
其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的 synchronized 修饰符要求执行方法时要获得对象的锁,如果已经进入A 方法说明对象锁已经被取走,那么试图进入 B 方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。
synchronized、volatile、CAS、Lock 比较?
- synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
- volatile 提供多线程共享变量可见性和禁止指令重排序优化。
- CAS 是基于冲突检测的乐观锁(非阻塞)
- Lock锁,使用时需要手动获取锁和释放锁,比synchronized更加灵活;
synchronized、volatile、CAS、Lock 比较?
场景
- CAS:单个变量支持比较替换操作,如果实际值与期望值一致时才进行修改
- volatile:单个变量并发操作,直接修改为我们的目标值
- synchronized:一般性代码级别的并发
- Lock:代码级别的并发,需要使用锁实现提供的独特机制,例如:读写分离、分段、中断、共享、重入等 synchronized 不支持的机制。
原子性
- CAS:保证原子性
- volatile:单个操作保证原子性,组合操作(例如:++)不保证原子性
- synchronized:保证原子性
- Lock:保证原子性
并发粒度
- CAS:单个变量值
- volatile:单个变量值
- synchronized:静态、非静态方法、代码块
- Lock:代码块
编码操作性
- CAS:调用 JDK 方法
- volatile:使用关键字,系统通过屏障指令保证并发性
- synchronized:使用关键字,加锁解锁操作系统默认通过指令控制
- Lock:手动加锁解锁
线程阻塞
- CAS:不会
- volatile:不会
- synchronized:可能会
- Lock:可能会
性能
- CAS:主要表现在 CPU 资源占用
- volatile:性能较好
- synchronized:性能一般(JDK 1.6 优化后增加了偏向锁、轻量级锁机制)
- Lock:性能较差
synchronized 和 Lock 有什么区别?
- synchronized是Java内置关键字,在JVM层面,Lock是个Java类,是API层面的
- synchronized 可以给类、方法、代码块加锁,而 lock 只能给代码块加锁
- synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
- 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
synchronized和ReentrantLock问这两种锁有哪些区别?
① 底层实现上来说
synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁。
synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向OS申请重量级锁,ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和通过volatile保证数据可见性以实现锁的功能。
② 是否可手动释放
synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。
③ 是否可中断
synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成; ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。
④ 是否公平锁
synchronized为非公平锁
ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁。
⑤ 锁的对象
synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。
synchronized 和 volatile 的区别是什么?
synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。
volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。
synchronized 和 volatile 的区别是什么?
- volatile 是变量修饰符;synchronized 可以修饰类、方法、代码块。
- volatile 仅能实现变量的修改可见性,不能保证原子性;而synchronized 则可以保证可见性和原子性。
- volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
- volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好
补充
synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升, 实际开发中使用 synchronized 关键字的场景还是更多一些。
volatile本质是在告诉JVM当前变量在寄存器中的值是不确定是不是最新值,使用前,需要先从主存中读取,因此可以实现可见性。而对n=n+1,n++等操作时,volatile关键字将失效,不能起到像synchronized一样的线程同步(原子性)的效果。
Java 中能创建 volatile 数组吗?
能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。
volatile 变量和 atomic 变量有什么不同?
- volatile 则是保证了所修饰的变量的可见。因为 volatile 只是在保证了同一个变量在多线程中的可见性,所以它更多是用于修饰作为开关状态的变量,即 Boolean 类型的变量,但它并不能保证原子性,例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的。AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。
- volatile 多用于修饰类似开关类型的变量、Atomic 多用于类似计数器相关的变量、其它多线程并发操作用 synchronized 关键字修饰。
volatile 能使得一个非原子操作变成原子操作吗?为什么?
能
关键字volatile的主要作用是使变量在多个线程间可见,但无法保证原子性,对于多个线程访问同一个实例变量需要加锁进行同步。虽然volatile只能保证可见性不能保证原子性,但用volatile修饰long和double可以保证其操作原子性,一个典型的例子是在类中有一个 long 类型的成员变量。如果你知道该成员变量会被多个线程访问,如计数器、价格等,你最好是将其设置为 volatile,这样这个成员变量就从一个非原子操作变成原子操作(因为对于64位的long和double,如果没有被volatile修饰,那么对其操作可以不是原子的。在操作的时候,可以分成两步,每次对32位操作。如果使用volatile修饰long和double,那么其读写都是原子操作 对于64位的引用地址的读写,都是原子操作)
什么是不可变对象?它对写并发应用有什么帮助?
不可变对象(Immutable Objects)即对象一旦被创建它的状态(属性值)就不能改变,反之即为可变对象(Mutable Objects)。
不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变类,如 String、基本类型的包装类、BigInteger 和 BigDecimal 等。
只有满足所有域都是 final 类型,并且它被正确创建(创建期间没有发生 this 引用的逸出)才是不可变对象
它对写并发应用有什么帮助?
不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。
Java Concurrency API 中的 Lock 接口(Lock interface)是什么?对比同步它有什么优势?
Java Concurrency API 中的 Lock 接口(Lock interface)是什么?
整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁(在大部分情况下,非公平锁是高效的选择),Lock锁使用完后需要手动释放锁,不然可能导致死锁的现象
深入了解:java 锁 Lock接口详解[通俗易懂]-Java架构师必看
对比同步它有什么优势?
- 支持公平锁,可以使锁更公平
- 可以使线程在等待锁的时候响应中断
- 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
- 需要的是显示获取和释放锁,这就为获取和释放锁提供了更多的灵活性
乐观锁和悲观锁的理解及有哪些实现方式?
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
乐观锁的实现方式
1、使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。
2、java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配,那么处理器会自动将该位置值更新为新值 B。否则处理器不做任何操作
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程),Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
什么是CAS?
CAS(Compare And Swap)比较并替换,是线程并发运行时用到的一种技术,其实就是一种有名的无锁算法,即不适用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步。
CAS是原子操作,保证并发安全,而不能保证并发同步,是非阻塞的、轻量级的乐观锁,因为是乐观锁所以CAS不会保证线程同步,乐观的认为在数据更新期间没有其他线程影响,由于CAS是CPU指令,我们只能通过JNI与操作系统交互,关于CAS的方法都在sun.misc包下Unsafe的类里,java.util.concurrent.atomic包下的原子类等通过CAS来实现原子操作。
因为是乐观锁,所以适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的吞吐量。但如果是多写的情况,一般会经常发生冲突,这就会导致CAS算法会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
CAS实现原理
CAS操作有三个操作数—— 内存位置的值(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置更新为新值。否则,处理器不做任何操作。
原子类例如AtomicInteger里的方法都很简单,我们看一下getAndIncrement方法
//该方法功能是Interger类型加1
public final int getAndIncrement() {
//主要看这个getAndAddInt方法
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//var1 是this指针
//var2 是地址偏移量
//var4 是自增的数值,是自增1还是自增N
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//获取内存值,这是内存值已经是旧的,假设我们称作期望值E
var5 = this.getIntVolatile(var1, var2);
//compareAndSwapInt方法是重点,
//var5是期望值,var5 + var4是要更新的值
//这个操作就是调用CAS的JNI,每个线程将自己内存里的内存值M
//与var5期望值E作比较,如果相同将内存值M更新为var5 + var4,否则做自旋操作
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
解释一下getAndAddInt方法的流程:
假设有一下情景:
- A、B两个线程
- jvm主内存的值1,A、B工作内存的值为1(工作内存会拷贝一份主内存的值)
- 当前期望值为1,做加1操作
- 此时var5 = 1,var4 = 1;
-
- A线程将var5与工作内存值M比较,比较var5是否等于1
- 如果相同则将工作内存值修改为var5 + var4 即修改为2并同步到内存,此时this + valueOffset指针里,示例变量value的值就是2,结束循环
- 如果不相同,则是B线程修改了主内存的值,说明B线程已经先于A线程做了加1操作,A线程没有更新成功需要继续循环,注意此时var5更新为新的内存值,假设当前的内存值是2,那么此时var5 = 2,var + var4 = 3,重复上述步骤直到成功(自旋),成功之后,内存地址中的值就改变为3
CAS优缺点
优点
非阻塞的轻量级的乐观锁,通过CPU指令实现,在资源竞争不激烈的情况下性能高,相比synchronized重量锁,synchronized会进行比较复杂的加锁、解锁和唤醒操作。
缺点
- ABA问题:现有线程C、D,线程D将A修改为B后又修改为A,此时C线程以为A没有改变过,java的原子类AtomicStampedReference,通过控制变量值的版本号来保证CAS的正确性。具体解决思路就是在变量前追加上版本号,每次变量更新的时候把版本号加一,那么A - B - A就会变成1A - 2B - 3A。
- 自旋时间过长,消耗CPU资源,如果资源竞争激烈,多线程自旋长时间消耗资源
CAS 的会产生什么问题?
ABA 问题
现有线程C和线程D,线程D将A修改为B后又修改为A,此时C线程以为A没有改变过,java的原子类AtomicStampedReference,通过控制变量值的版本号来保证CAS的正确性。
具体解决思路就是在变量前追加上版本号,每次变量更新的时候把版本号加一,那么A - B - A就会变成1A - 2B - 3A。
循环时间长开销大
对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。
什么是死锁?
当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b, 并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
产生死锁的条件是什么?
- 互斥条件:所谓互斥就是进程在某一时间内独占资源。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之 一不满足,就不会发生死锁。
有什么具体的办法可以避免死锁
死锁是不应该在程序中出现的,在编写程序时应该尽量避免出现死锁。下面有几种常见的方式用来解决死锁问题:
- 避免多次锁定。尽量避免同一个线程对多个 Lock 进行锁定。例如上面的死锁程序,主线程要对 A、B 两个对象的 Lock 进行锁定,副线程也要对 A、B 两个对象的 Lock 进行锁定,这就埋下了导致死锁的隐患。
- 具有相同的加锁顺序。如果多个线程需要对多个 Lock 进行锁定,则应该保证它们以相同的顺序请求加锁。比如上面的死锁程序,主线程先对 A 对象的 Lock 加锁,再对 B 对象的 Lock 加锁;而副线程则先对 B 对象的 Lock 加锁,再对 A 对象的 Lock 加锁。这种加锁顺序很容易形成嵌套锁定,进而导致死锁。如果让主线程、副线程按照相同的顺序加锁,就可以避免这个问题。
- 尝试使用定时锁,使用 lock.tryLock(timeout)来替代使用内部锁机制,该参数指定超过 timeout 秒后会自动释放对 Lock 的锁定,这样就可以解开死锁了。
- 银行家算法。银行家算法的核心思想是在进程提出资源申请时,先预判此次分配是否会导致系统进入不安全状态。如果会进入不安全状态,就暂时不答应这次请求,让该进程先阻塞等待。
死锁与活锁的区别?死锁与饥饿的区别?
死锁
是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
活锁
任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。
活锁和死锁的区别
- 处于活锁的实体是在不断的改变状态,这就是所的“活”, 而处于死锁的实体表现为等待;
- 活锁有可能自行解开,死锁则不能。
饥饿
线程处于饥饿是指不断有优先级高的线程占用资源,导致优先级低的线程一直无法获得所需要的资源,一直无法执行的状态。
死锁与饥饿的区别
线程处于饥饿是因为不断有优先级高的线程占用资源,当不再有高优先级的线程争抢资源时,饥饿状态将会自动解除。死锁没有外力的作用下是不会解除的
Java 中导致饥饿的原因
1、高优先级线程吞噬所有的低优先级线程的 CPU 时间。
2、线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
3、线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。
什么是AQS
AbstractQueuedSynchronizer(AQS)是JDK 1.5提供的一套用于构造同步器的框架,通过一个FIFO(先进先出)队列维护线程同步状态,实现类只需要继承AbstractQueuedSynchronizer该类,并重写指定方法(这些重写方法很简单,无非是对于共享资源state的获取和释放)即可实现一套线程同步机制,这个类在java.util.concurrent.locks包,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。
AQS根据资源互斥级别提供了独占和共享两种资源访问模式(资源共享方式);同时其定义Condition结构提供了wait/signal等待唤醒机制。在JUC中,诸如ReentrantLock、CountDownLatch等都基于AQS实现。
AQS 原理分析
AQS维护了一个volatile int state变量和一个CLH(三个人名缩写)双向队列,当线程获取锁时,即试图对state变量做修改,如修改成功则获取锁;如修改失败则包装为节点挂载到CLH队列中,等待持有锁的线程释放锁并唤醒队列中的节点。
AQS核心思想是如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配
AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
状态信息通过protected类型的getState,setState,compareAndSetState进行操作
深入了解:Java并发之AQS详解 - 掘金
深入了解:AQS详解(面试)_木霖森77的博客-CSDN博客
深入了解:AQS简单介绍 - 简书
AQS 对资源的共享方式
AQS定义两种资源共享方式Exclusive(独占)和Share(共享)
Exclusive(独占)
只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
1、公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
2、非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
Share(共享)
多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、CyclicBarrier、ReadWriteLock 等等
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在底层实现好了。
AQS底层使用了什么设计模式
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样
(模板方法模式很经典的一个应用):
-
- 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
- 将AQS组合在自定义同步组件中实现,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
自定义同步器在实现的时候只需要实现共享资源state的获取和释放方式即可,至于具体线程等待队列的维护,AQS已经在底层实现好了。
自定义同步器时需要重写下面几个AQS提供的模板方法:
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
默认情况下,每个方法都抛出 UnsupportedOperationException。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用(因为继承不了),只有这几个方法可以被其他类使用
以ReentrantLock为例分析
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加), 这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
以CountDownLatch以例分析
以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从 await()函数返回,继续后余动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire/tryRelease、tryAcquireShared/tryReleaseShared中的一种即可。但AQS 也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。
AQS的简单应用
Mutex:不可重入互斥锁,锁资源(state)只有两种状态:0:未被锁定;1:锁定
class Mutex implements Lock, java.io.Serializable {
// 自定义同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 判断是否锁定状态
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 尝试获取资源,立即返回。成功则返回true,否则false。
public boolean tryAcquire(int acquires) {
assert acquires == 1; // 这里限定只能为1个量
if (compareAndSetState(0, 1)) {//state为0才设置为1,不可重入!
setExclusiveOwnerThread(Thread.currentThread());//设置为当前线程独占资源
return true;
}
return false;
}
// 尝试释放资源,立即返回。成功则为true,否则false。
protected boolean tryRelease(int releases) {
assert releases == 1; // 限定为1个量
if (getState() == 0)//既然来释放,那肯定就是已占有状态了。只是为了保险,多层判断!
throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);//释放资源,放弃占有状态
return true;
}
}
// 真正同步类的实现都依赖继承于AQS的自定义同步器!
private final Sync sync = new Sync();
//lock<-->acquire。两者语义一样:获取资源,即便等待,直到成功才返回。
public void lock() {
sync.acquire(1);
}
//tryLock<-->tryAcquire。两者语义一样:尝试获取资源,要求立即返回。成功则为true,失败则为false。
public boolean tryLock() {
return sync.tryAcquire(1);
}
//unlock<-->release。两者语文一样:释放资源。
public void unlock() {
sync.release(1);
}
//锁是否占有状态
public boolean isLocked() {
return sync.isHeldExclusively();
}
}
公平锁非公平锁区别?各自的优缺点?
公平锁非公平锁区别?
区别就是公平锁是每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的,最前面的线程总是最先获取到锁。 非公平锁是多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。简单来说就是公平锁就是先到先得,而非公平锁就是谁先抢到归谁,一个是排队,然后一个个来,一个是不排队,谁能挤到最前面谁就获取锁
各自的优缺点?
公平锁
优点:所有的线程都能得到资源,不会饿死在队列中。
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
缺点:可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
深入了解:阿里面试官:说一下公平锁和非公平锁的区别?_敖 丙的博客-CSDN博客_公平锁和非公平锁区别
ReadWriteLock 是什么
ReadWriteLock 是一个读写锁接口,读写锁是用来提升并发程序性能的锁分离技术,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
而读写锁有以下三个重要的特性:
- 公平选择性:支持非公平(默认)和公平的锁获取方式,非公平的方式性能。
- 可重入:读锁和写锁都支持线程重进入。
- 锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
为什么会有读写锁?
首先明确一下,不是说 ReentrantLock 不好,只是 ReentrantLock 某些时候有局限。如果使用 ReentrantLock,可能本身是为了防止线程 A 在写数据、线程 B 在读数据造成的数据不一致,但这样,如果线程 C 在读数据、线程 D 也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。因为这个,才诞生了读写锁 ReadWriteLock。
并发工具/容器/原子类
什么是同步容器?什么是并发容器?并发容器的实现?
Java的集合容器框架中,主要有四大类别:List、Set、Queue、Map,大家熟知的这些集合类ArrayList、LinkedList、HashMap这些容器都是非线程安全的。
如果有多个线程并发地访问这些容器时,就会出现问题。因此,在编写程序时,在多线程环境下必须要求程序员手动地在任何访问到这些容器的地方进行同步处理,这样导致在使用这些容器的时候非常地不方便。
所以,Java先提供了同步容器供用户使用。
什么是同步容器?
可以简单地理解为通过 synchronized 来实现线程安全的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如 Vector、Stack、Hashtable、Collections.synchronizedSet、Collections.synchronizedList 等方法返回的容器。
可以通过查看Vector,Hashtable等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字synchronized。这样做的代价是削弱了并发性,当多个线程共同竞争容器级的锁时,吞吐量就会降低。
例如: HashTable只要有一条线程获取了容器的锁之后,其他所有的线程访问同步函数都会被阻塞,因此同一时刻只能有一条线程访问同步函数。
因此为了解决同步容器的性能问题,所以才有了并发容器。
什么是并发容器?
java.util.concurrent包中提供了多种并发类容器。
并发类容器(俗称JUC容器)是专门针对多线程、高并发专门设计的一些类,用来替代性能较低的同步容器。最经典的并发容器是ConcurrentHashMap
在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发的访问 map,同时允许一定数量的写操作线程并发地修改 map,所以它可以在并发环境下实现更高的吞吐量。JDK7 中,ConcurrentHashMap 采用了分段锁机制。JDK8 中,摒弃了锁分段机制,改为利用 CAS 算法+synchronize
常见的并发容器
- ConcurrentHashMap
对应的非并发容器:HashMap
目标:代替Hashtable、synchronizedMap,支持复合操作
原理:JDK6中采用一种更加细粒度的加锁机制Segment“分段锁”,JDK8中采用CAS无锁算法。
- CopyOnWriteArrayList
对应的非并发容器:ArrayList
目标:代替Vector、synchronizedList
原理:利用高并发往往是读多写少的特性,对读操作不加锁,对写操作,先复制一份新的集合,在新的集合上面修改,然后将新集合赋值给旧的引用,并通过volatile 保证其可见性,当然写操作的锁是必不可少的了。
- CopyOnWriteArraySet
对应的非并发容器:HashSet
目标:代替synchronizedSet
原理:基于CopyOnWriteArrayList实现,其唯一的不同是在add时调用的是CopyOnWriteArrayList的addIfAbsent方法,其遍历当前Object数组,如Object数组中已有了当前元素,则直接返回,如果没有则放入Object数组的尾部,并返回。
并发容器的实现
- CopyOnWriteArrayList - 线程安全的 ArrayList
- CopyOnWriteArraySet - 线程安全的 Set,因此本质上是由 CopyOnWriteArrayList 实现的。
- ConcurrentSkipListSet - 相当于线程安全的 TreeSet。它是有序的 Set。它由 ConcurrentSkipListMap 实现。
- ConcurrentHashMap - 线程安全的 HashMap。采用分段锁实现高效并发。
- ConcurrentSkipListMap - 线程安全的有序 Map。使用跳表实现高效并发。
- ConcurrentLinkedQueue - 线程安全的无界队列。底层采用单链表。支持 FIFO。
- ConcurrentLinkedDeque - 线程安全的无界双端队列。底层采用双向链表。支持 FIFO 和 FILO。
- ArrayBlockingQueue - 数组实现的阻塞队列。
- LinkedBlockingQueue - 链表实现的阻塞队列。
- LinkedBlockingDeque - 双向链表实现的双端阻塞队列。
Java 中的同步容器与并发容器有什么区别?
不管是同步容器还是并发容器他们都支持线程安全,他们之间主要的区别体现在性能和可扩展性,还有他们如何实现的线程安全上。
同步容器比并发容器在性能上差很多。并发容器的出现就是为了解决同步容器性能问题,造成如此慢的主要原因是锁, 同步容器会把整个容器都锁起来,例如HashTable只要有一条线程获取了容器的锁之后,其他所有的线程访问同步函数都会被阻塞,因此同一时刻只能有一条线程访问同步函数。而并发容器不会。并发集合实现线程安全是通过使用像分段锁、CAS算法等先进的和成熟的技术。例如ConcurrentHashMap 在JDK7 中,ConcurrentHashMap 采用了分段锁机制。JDK8 中,摒弃了锁分段机制,改为利用 CAS 算法
Condition源码分析与等待通知机制
java中Condition类的详细介绍(详解)_普通网友的博客-CSDN博客_condition类
Condition源码分析与等待通知机制_ThinkWon的博客-CSDN博客
BlockingQueue是什么?
并发容器之BlockingQueue详解_ThinkWon的博客-CSDN博客_blockingqueue 并发
BlockingQueue是什么?
BlockingQueue其实就是阻塞队列,是基于阻塞机制实现的线程安全的队列。而阻塞机制的实现是通过在入队和出队时加锁的方式避免并发操作。
BlockingQueue不同于普通的Queue的区别
- 通过在入队和出队时进行加锁,保证了队列线程安全
- 支持阻塞的入队和出队方法:当队列满时,会阻塞入队的线程,直到队列不满;当队列为空时,会阻塞出队的线程,直到队列中有元素。
BlockingQueue常用于生产者-消费者模型中,往队列里添加元素的是生产者,从队列中获取元素的是消费者;通常情况下生产者和消费者都是由多个线程组成
BlockingQueue继承了Queue接口,在Queue接口基础上,又提供了若干其他方法,其定义源码如下:
public interface BlockingQueue<E> extends Queue<E> {
/**
* 入队一个元素,如果有空间则直接插入,并返回true;
* 如果没有空间则抛出IllegalStateException
*/
boolean add(E e);
/**
* 入队一个元素,如果有空间则直接插入,并返回true;
* 如果没有空间返回false
*/
boolean offer(E e);
/**
* 入队一个元素,如果有空间则直接插入,如果没有空间则一直阻塞等待
*/
void put(E e) throws InterruptedException;
/**
* 入队一个元素,如果有空间则直接插入,并返回true;
* 如果没有空间则等待timeout时间,插入失败则返回false
*/
boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
/**
* 出队一个元素,如果存在则直接出队,如果没有空间则一直阻塞等待
*/
E take() throws InterruptedException;
/**
* 出队一个元素,如果存在则直接出队,如果没有空间则等待timeout时间,无元素则返回null
*/
E poll(long timeout, TimeUnit unit) throws InterruptedException;
/**
* 返回该队列剩余的容量(如果没有限制则返回Integer.MAX_VALUE)
*/
int remainingCapacity();
/**
* 如果元素o在队列中存在,则从队列中删除
*/
boolean remove(Object o);
/**
* 判断队列中是否存在元素o
*/
public boolean contains(Object o);
/**
* 将队列中的所有元素出队,并添加到给定的集合c中,返回出队的元素数量
*/
int drainTo(Collection<? super E> c);
/**
* 将队列中的元素出队,限制数量maxElements个,并添加到给定的集合c中,返回出队的元素数量
*/
int drainTo(Collection<? super E> c, int maxElements);
}
BlockingQueue主要提供了四类方法,如下表所示:
方法 | 抛出异常 | 返回特定值 | 阻塞 | 阻塞特定时间 |
入队 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
出队 | remove() | poll() | take() | poll(time, unit) |
获取队首元素 | element() | peek() | 不支持 | 不支持 |
除了抛出异常和返回特定值方法与Queue接口定义相同外,BlockingQueue还提供了两类阻塞方法:一种是当队列没有空间/元素时一直阻塞,直到有空间/有元素;另一种是在特定的时间尝试入队/出队,等待时间可以自定义。
BlockingQueue是线程安全的队列,所以提供的方法也都是线程安全的;那么下面我们就继续看下BlockingQueue的实现类,以及如何实现线程安全和阻塞。
主要实现类
BlockingQueue接口主要由5个实现类,分别如下表所示。
实现类 | 功能 |
ArrayBlockingQueue | 基于数组的阻塞队列,使用数组存储数据,并需要指定其长度,所以是一个有界队列 |
LinkedBlockingQueue | 基于链表的阻塞队列,使用链表存储数据,默认是一个无界队列;也可以通过构造方法中的capacity设置最大元素数量,所以也可以作为有界队列 |
SynchronousQueue | 一种没有缓冲的队列,生产者产生的数据直接会被消费者获取并且立刻消费 |
PriorityBlockingQueue | 基于优先级别的阻塞队列,底层基于数组实现,是一个无界队列 |
DelayQueue | 延迟队列,其中的元素只有到了其指定的延迟时间,才能够从队列中出队 |
其中在日常开发中用的比较多的是ArrayBlockingQueue和LinkedBlockingQueue
好文参考:深入理解Java系列 | BlockingQueue用法详解 - 掘金
在 Queue 中 poll()和 remove()有什么区别?
相同点:
都是返回第一个元素,并在队列中删除返回的对象。
不同点:
如果没有元素 poll()会返回 null,而 remove()会直接抛出NoSuchElementException 异常。
并发容器之ConcurrentLinkedQueue详解与源码分析
并发容器-ConcurrentLinkedQueue详解 - 简书
并发容器之ArrayBlockingQueue与LinkedBlockingQueue详解
ArrayBlockingQueue
什么是ArrayBlockingQueue队列
用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量(类比公平锁和非公平锁)
ArrayBlockingQueue的主要属性
/** The queued items */
final Object[] items;
/** items index for next take, poll, peek or remove */
int takeIndex;
/** items index for next put, offer, or add */
int putIndex;
/** Number of elements in the queue */
int count;
/*
* Concurrency control uses the classic two-condition algorithm
* found in any textbook.
*/
/** Main lock guarding all access */
final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;
从源码中可以看出ArrayBlockingQueue内部是采用数组进行数据存储的(属性items),为了保证线程安全,采用的是ReentrantLock lock,为了保证可阻塞式的插入删除数据利用的是Condition,当获取数据的消费者线程被阻塞时会将该线程放置到notEmpty等待队列中,当插入数据的生产者线程被阻塞时,会将该线程放置到notFull等待队列中。而notEmpty和notFull等中要属性在构造方法中进行创建:
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
put方法详解
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//如果当前队列已满,将线程移入到notFull等待队列中
while (count == items.length)
notFull.await();
//满足插入数据的要求,直接进行入队操作
enqueue(e);
} finally {
lock.unlock();
}
}
该方法的逻辑很简单,当队列已满时(count == items.length)将线程移入到notFull等待队列中,如果当前满足插入数据的条件,就可以直接调用enqueue(e)插入数据元素。
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
//插入数据
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
//通知消费者线程,当前队列中有数据可供消费
notEmpty.signal();
}
enqueue方法的逻辑同样也很简单,先完成插入数据,即往数组中添加数据(items[putIndex] = x),然后通知被阻塞的消费者线程,当前队列中有数据可供消费(notEmpty.signal())。
take方法详解
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//如果队列为空,没有数据,将消费者线程移入等待队列中
while (count == 0)
notEmpty.await();
//获取数据
return dequeue();
} finally {
lock.unlock();
}
}
take方法也主要做了两步:
- 如果当前队列为空的话,则将获取数据的消费者线程移入到等待队列中;
- 若队列不为空则获取数据,即完成出队操作dequeue。
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
//获取数据
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
//通知被阻塞的生产者线程
notFull.signal();
return x;
}
dequeue方法也主要做了两件事情:
- 获取队列中的数据,即获取数组中的数据元素((E) items[takeIndex]);
- 通知notFull等待队列中的线程,使其由等待队列移入到同步队列中,使其能够有机会获得lock,并执行完成功退出。
从以上分析,可以看出put和take方法主要是通过condition的通知机制来完成可阻塞式的插入数据和获取数据。
可阻塞式的插入和删除队列元素是啥意思?
阻塞队列最核心的功能是能够可阻塞式的插入和删除队列元素。也就是当前队列为空时,会阻塞消费数据的线程,直至队列非空时,通知被阻塞的线程;当队列满时,会阻塞插入数据的线程,直至队列未满时,通知插入数据的线程(生产者线程),多线程中消息通知机制最常用的是lock的condition机制
LinkedBlockingQueue
基于链表的阻塞队列,同ArrayListBlockingQueue类似,此队列按照先进先出(FIFO)的原则对元素进行排序,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。
LinkedBlockingQueue是用链表实现的有界阻塞队列,当构造对象时为指定队列大小时,队列默认大小为Integer.MAX_VALUE。从它的构造方法可以看出:
public LinkedBlockingQueue() { this(Integer.MAX_VALUE); }
LinkedBlockingQueue的主要属性
/** Current number of elements */
private final AtomicInteger count = new AtomicInteger();
/**
* Head of linked list.
* Invariant: head.item == null
*/
transient Node<E> head;
/**
* Tail of linked list.
* Invariant: last.next == null
*/
private transient Node<E> last;
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
可以看出与ArrayBlockingQueue主要的区别是LinkedBlockingQueue在插入数据和删除数据时分别是由两个不同的lock(takeLock和putLock)来控制线程安全的,因此,也由这两个lock生成了两个对应的condition(notEmpty和notFull)来实现可阻塞的插入和删除数据。并且,采用了链表的数据结构来实现队列,Node结点的定义为:
static class Node<E> {
E item;
/**
* One of:
* - the real successor Node
* - this Node, meaning the successor is head.next
* - null, meaning there is no successor (this is the last node)
*/
Node<E> next;
Node(E x) { item = x; }
}
put方法详解
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
//如果队列已满,则阻塞当前线程,将其移入等待队列
while (count.get() == capacity) {
notFull.await();
}
//入队操作,插入数据
enqueue(node);
c = count.getAndIncrement();
//若队列满足插入数据的条件,则通知被阻塞的生产者线程
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
put方法的逻辑也同样很容易理解,可见注释。基本上和ArrayBlockingQueue的put方法一样。
take方法详解
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
//当前队列为空,则阻塞当前线程,将其移入到等待队列中,直至满足条件
while (count.get() == 0) {
notEmpty.await();
}
//移除队头元素,获取数据
x = dequeue();
c = count.getAndDecrement();
//如果当前满足移除元素的条件,则通知被阻塞的消费者线程
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
take方法的主要逻辑请见于注释,也很容易理解。
ArrayBlockingQueue与LinkedBlockingQueue的比较
相同点
- LinkedBlockingQueue和ArrayBlockingQueue都是可阻塞的队列,当队列为空,消费者线程被阻塞;当队列装满,生产者线程被阻塞;
- 内部都是使用ReentrantLock和Condition来保证生产和消费的同步,也就是通过condition通知机制来实现可阻塞式插入和删除元素(都使用Condition的方法来同步和通信:await()和signal()),通过ReentrantLock满足线程安全的特性;
不同点
锁机制不同
LinkedBlockingQueue中的锁是分离的,生产者的锁PutLock,消费者的锁takeLock,这样可以降低线程由于线程无法获取到lock而进入WAITING状态的可能性,从而提高了线程并发执行的效率。而ArrayBlockingQueue生产者和消费者使用的是同一把锁;
底层实现机制也不同
LinkedBlockingQueue内部维护的是一个链表结构。在生产和消费的时候,需要创建Node对象进行插入或移除,大批量数据的系统中,其对于GC的压力会比较大。而ArrayBlockingQueue内部维护了一个数组,在生产和消费的时候,是直接将枚举对象插入或移除的,不会产生或销毁任何额外的对象实例。
构造时候的区别
LinkedBlockingQueue有默认的容量大小为:Integer.MAX_VALUE,当然也可以传入指定的容量大小,ArrayBlockingQueue在初始化的时候,必须传入一个容量大小的值
统计元素的个数
LinkedBlockingQueue中使用了一个AtomicInteger对象来统计元素的个数,ArrayBlockingQueue则使用int类型来统计元素。
什么是原子操作?在 Java Concurrency API 中有哪些原子类(atomic classes)?
什么是原子操作?
原子操作(atomic operation)意为”不可被中断的一个或一系列操作” ,原子操作是一个不受其他操作影响的操作任务单元,原子操作是在多线程环境下避免数据不一致必须的手段。
在 Java Concurrency API 中有哪些原子类(atomic classes)?
原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
原子数组 :AtomicIntegerArray,AtomicLongArray, AtomicReferenceArray
原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater, AtomicReferenceFieldUpdater
解决 ABA 问题的原子类:AtomicMarkableReference(通过引入一个boolean来反映中间有没有变过),AtomicStampedReference(通过引入一个int 来累加来反映中间有没有变过)
扩展
处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。在 Java 中可以通过锁和循环 CAS 的方式来实现原子操作。
int++并不是一个原子操作,所以当一个线程读取它的值并加 1 时,另外一个线程有可能会读到之前的值,这就会引发错误。
为了解决这个问题,必须保证增加操作是原子的,在 JDK1.5 之前我们可以使用同步技术来做到这一点。到 JDK1.5,java.util.concurrent.atomic 包提供了 int 和long 类型的原子包装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。
java.util.concurrent 这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择另一个线程进入,这只是一种逻辑上的理解。
说一下 atomic 的原理?
说一下 atomic ?
Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以像自旋锁一样,继续尝试,一直等到执行成功。
说一下 atomic 的原理?
atomic通过CAS(Compare And Wwap)来保证原子性(它的实现很简单,就是用一个预期的值和内存值进行比较,如果两个值相等,就用预期的值替换内存值,并返回true。否则,返回false),CAS是乐观锁和自旋锁的实现核心,JDK1.8通过降低锁粒度(多段锁))增加并发性能从而避免synchronized 的高开销,执行效率大为提升。
在 Java 中 CycliBarriar 和 CountdownLatch 有什么区别?
CountDownLatch与CyclicBarrier都是用于控制并发的工具类,都可以理解成维护的就是一个计数器,但是这两者还是各有不同侧重点的:
- CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
- CountDownLatch强调一个线程等多个线程完成某件事情。CyclicBarrier是多个线程互等,等大家都完成,再携手共进。
- 调用CountDownLatch的countDown方法后,当前线程并不会阻塞, 会继续往下执行;而调用CyclicBarrier的await方法,会阻塞当前线程,直到CyclicBarrier指定的线程全部都到达了指定点的时候,才能继续往下执行;
- CountDownLatch方法比较少,操作比较简单,而CyclicBarrier提供的方法更多,比如能够通过getNumberWaiting(),isBroken()这些方法获取当前多个线程的状态,并且CyclicBarrier的构造方法可以传入barrierAction,指定当所有线程都到达时执行的业务功能;
- CountDownLatch是不能复用的,而CyclicLatch是可以复用的。
并发工具之Semaphore与Exchanger
Semaphore 有什么作用?
Semaphore 就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个 int 型整数 n,表示某段代码最多只有 n 个线程可以访问,如果超出了 n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果 Semaphore 构造函数中传入的int 型整数 n=1,相当于变成了一个 synchronized 了,但是synchronized 和ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
什么是线程间交换数据的工具Exchanger?
Exchanger是一个用于线程间协作的工具类,用于两个线程间交换数据。它提供了一个交换的同步点,在这个同步点两个线程能够交换数据。交换数据是通过exchange方法来实现的,如果一个线程先执行exchange方法,那么它会同步等待另一个线程也执行exchange方法,这个时候两个线程就都达到了同步点,两个线程就可以交换数据。
常用的并发工具类有哪些?
- Semaphore(信号量)-允许多个线程同时访问: synchronized 和ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
- CountDownLatch(倒计时器): CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程 等待直到倒计时结束,再开始执行。
- CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可 以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障 拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。