synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
synchronized关键字最主要的三种使用方式:
synchronized 关键字底层原理属于 JVM 层面。
① synchronized 同步语句块的情况
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
② synchronized 修饰方法的的情况
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率
①偏向锁
引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉。
偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!关于偏向锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。
但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
② 轻量级锁
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。关于轻量级锁的加锁和解锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。
轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!
③ 自旋锁和自适应自旋
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。
一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋。
可重入性:
从名字上理解,ReenTrantLock的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也是可重入的,两者关于这个的区别不大。两者都是同一个线程没进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
锁的实现:
Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现的,有什么区别,说白了就类似于操作系统来控制实现和用户自己敲代码实现的区别。前者的实现是比较难见到的,后者有直接的源码可供阅读。
性能的区别:
在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。
功能区别:
便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。ReenTrantLock 比 synchronized 增加了一些高级功能
锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized
ReenTrantLock独有的能力:
相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)
1. ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
2. ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
3. ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。
1.线程池的最最基本的原理
线程池,说白了就是为了避免频繁创建和销毁线程带来的巨大开销,就维护一定数量的线程池,创建以后别销毁,用完了扔回去再个下一个任务来使用,这样就可以提高线程的使用效率。一般常见于后台处理类的系统,或者是复杂接口中的多数据并发获取。
Java线程池包含4个部分
(1)线程池管理器(ThreadPool):就是负责创建和销毁线程池的
(2)工作线程(PoolWorker):就是线程池中的一个线程
(3)工作任务(Task):这个就是线程池里的某个线程需要执行的业务代码,这个是你自己编写的业务逻辑
(4)任务队列(TaskQueue):这个是扔到线程池里的任务需要进行排队,要进任务队列
2.常用的几种线程池和API
(1)SingleThreadExecutor(单线程线程池,很少用 -> 自己做一个内存队列 -> 启动后台线程去消费)
(2)FixedThreadExecutor(固定数量线程池):比如说,线程池里面固定就100个线程,超过这个线程数就到队列里面去排队等待
(3)CachedThreadExecutor(自动回收空闲线程,根据需要自动新增线程,传说中的无界线程池):无论有多少任务,根据你的需要,无限制的创建任意多的线程,在最短的时间内来满足你,但是高峰过去之后,如果有大量的线程处于空闲状态,没有活儿可以干,等待60s之后空闲的线程就被销毁了
(4)ScheduledThreadExecutor(线程数量无限制,支持定时调度执行某个线程):提交一个任务,对于这个任务不是立马执行的,是可以设定一个定时调度的逻辑,比如说每隔60s执行一次,这个一般不用,一般来说就用spring schedule的支持
一般其实最常用的是FixedThreadExecutor和CachedThreadExecutor
ScheduleThreadExecutor,也可能会使用,但是就是除非你的那个线程任务要定时调度,才会用这个线程池,不过说实话,简单的定时调度一般就是走spring的schedule支持就行了,当然如果你要用这个也行
Java的线程池比较重要的几个API
(1)Executor:代表线程池的接口,有个execute()方法,扔进去一个Runnable类型对象,就可以分配一个线程给你执行
(2)ExecutorService:这是Executor的子接口,相当于是一个线程池的接口,有销毁线程池等方法 -> ExecutorService就代表了一个线程池管理器,会负责管理线程池 -> 线程的创建和销毁 -> 队列排队
(3)Executors:线程池的辅助工具类,辅助入口类,可以通过Executors来快捷的创建你需要的线程池。创建线程池的入口类,包含newSingleThreadExecutor()、newCachedThreadPool()、newScheduleThreadPool()、newFixedThreadPool(),这些方法,就是可以让你创建不同的线程池出来
(4)ThreadPoolExecutor:这是ExecutorService的实现类,这才是正儿八经代表一个线程池的类,一般在Executors里创建线程池的时候,内部都是直接创建一个ThreadPoolExecutor的实例对象返回的,然后同时给设置了各种默认参数。
如果我们要创建一个线程池,两种方式,要么就是Executors.newXX()方法,快捷的创建一个线程池出来,线程池的所有参数设置都采取默认的方式;要么是自己手动构建一个THreadPoolExecutor的一个对象,所有的线程池的参数,都可以自己手动来调整和设置
public class Executors {
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue
}
}
如果我们要用线程池,大部分同学,我估计如果之前使用过线程池的话,一遍都是怎么用的呢?
public class MyTask implements Runnable {
public void run() {
// 实现你的业务逻辑
// 这段业务逻辑,就会交给线程池里的某个线程去执行
}
}
// 创建一个固定线程数量为100的FixedThreadPool线程池
ExecutorService myThreadPool = Executors.newFixedThreadPool(100);
// 要往线程池里提交一个任务
Runnable myTask = new MyTask();
myThreadPool.execute(myTask);
// 上面的代码就是平时大家最最常用的线程池的使用代码
// 执行了线程池的execute()方法,就相当等于是给这个线程提交了一个任务
// 线程池会优先用有已有的线程来处理这个任务
// 但是如果所有的线程池里的线程都处于一个繁忙的状态,此时就会将这个任务扔到队列里去排队,等待某个线程空闲之后来处理这个任务
到此为止,都是讲的很easy的,但是你会发现只是知道这么点是完全不够用的
3.3 线程池的构造参数和真正的工作原理
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue
ThreadPoolExecutor就是线程池,那么这个类的构造函数的所有入参,就是你可以设置的参数,我们来解释一下这些参数吧
corePoolSize:线程池里的核心线程数量
maximumPoolSize:线程池里允许有的最大线程数量
keepAliveTime:如果线程数量大于corePoolSize的时候,多出来的线程会等待指定的时间之后就被释放掉,这个就是用来设置空闲线程等待时间的
unit:这个是上面那个keepAliveTime的单位
workQueue:这个是说,通过ThreadPoolExecutor.execute()方法扔进来的Runnable工作任务,会进入一个队列里面去排队,这就是那个队列
threadFactory:如果需要创建新的线程放入线程池的时候,就是通过这个线程工厂来创建的
handler:假如说上面那个workQueue是有固定大小的,如果往队列里扔的任务数量超过了队列大小,咋办?就用这个handler来处理,AbortPolicy、DiscardPolicy、DiscardOldestPolicy,如果说线程都繁忙,队列还满了,此时就会报错,RejectException
这些参数的含义先解释一下:
假设我们自己手动创建一个ThreadPoolExecutor线程池,设置了以下的一些参数
corePoolSize:2个
mamximumPoolSize:4个
keepAliveTime:60s
workQueue:ArrayBlockingQueue,有界阻塞队列,队列大小是4
handler:默认的策略,抛出来一个ThreadPoolRejectException
(1)一开始线程池里的线程是空的,一个都没有。有一个变量维护的是当前线程数量,这个变量是poolSize,poolSize = 0,如果当前线程的数量小于corePoolSize(2),poolSize < corePoolSize,那么来了一个任务优先创建线程,直到线程池里的线程数量跟corePoolSize一样;poolSize = 1,poolSize < corePoolSize(2),又创建一个线程来处理这个任务;poolSize = 2
(2)如果当前线程池的线程数量(poolSize = 2)大于等于corePoolSize(2)的时候,而且任务队列没满(最大大小是4,但是当前元素数量是0),那么就扔到任务队列里去
(3)如果当前线程池的线程数量大于等于corePoolSize的时候,而且任务队列满了(最大大小是4,当前已经放了4个元素了,已经满了),那么如果当前线程数量小于最大线程数(poolSize = 2,maimumPoolSize = 4,poolSize < maximumPoolSize),就继续创建线程;poolSize = 3,提交了一个任务,poolSize >= corePoolSize,任务队列满,poolSize < maximumPoolSize,再次创建一个任务
(4)如果此时poolSize >= corePoolSize,任务队列满,poolSize == maximumPoolSize,此时再次提交一个任务,当前线程数已经达到了最大线程数了,那么就使用handler来处理,默认是抛出异常,ThreadPoolRejectExeception
(5)此时线程池里有4个线程,都处于空闲状态,corePoolSize指定的是就2个线程就可以了,但是此时超过了corePoolSize 2个线程,所以如果那超出的2个线程空闲时间超过了60s,然后线程池就会将超出的2个线程给回收掉
如何设置池的这些参数?先来看看创建线程池的默认代码
其实上面都说过了,啥时候会创建新线程?其实就是线程数没到corePoolSize的时候,会创建线程;接着就是任务队列满了,但是线程数小于maximumPoolSize的时候,也会创建线程;创建的时候通过threadFactory来创建即可
3.4 常用线程池的工作原理
FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue
}
(1)corePoolSize = 100,maximumPoolSize = 100,keepAliveTime = 0,workQueue = 无界队列
(2)刚开始的时候,比如说假设一开始线程池里没有线程,你就不断的提交任务,瞬间提交了100个任务,一下子创建100个线程出来,其实poolSize == corePoolSize,再提交任务,直接就会发现LinkedBlockQueue根本就没有大小的限制,所以说根本就不会满,所以此时后续的所有任务直接扔到LinkedBlockingQueue里面去排队
(3)100个线程,只要出现了空闲,就会从队列里面去获取任务来处理,以此类推,就100个线程,不停的处理任务
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue
}
总结一下:
(1)FixedThreadPool:线程数量是固定的,如果所有线程都繁忙,后续任务全部在一个无界队列里面排队,无限任务来排队,直到内存溢出
(2)CachcedThreadPool:线程数量是不固定的,如果一下子涌入大量任务,没有空闲线程,那么就创建新的线程来处理;如果有空闲线程,就是一个任务配对一个空闲线程来处理;如果线程空闲时间超过60s,就给回收掉空闲线程
3.5 各种线程池在什么样的场景下使用
FixedThreadPool:比较适用于什么场景呢?负载比较重,而且负载比较稳定的这么一个场景,我给大家来举个例子,我们之前线上有一套系统,负载比较重,后台系统,每分钟要执行几百个复杂的大SQL,就是用FixedThreadPool是最合适的。
因为负载稳定,所以一般来说,不会出现说突然瞬间涌入大量的请求,100个线程处理不过来,然后就直接无限制的排队,然后oom内存溢出,死了
CachedThreadPool:负载很稳定的场景,用CachedThreadPool就浪费了;每天大部分时候可能就是负载很低的,CachedThreadPool,用少量的线程就可以满足低负载,不会给系统引入太大的压力;但是每天如果有少量的高峰期,比如说中午或者是晚上,高峰期可能需要一下子搞几百个线程出来,那么CachedThreadPool就可以满足这个场景;高峰期应付过去之后,线程如果处于空闲状态超过60s,自动回收空闲线程,避免给系统带来过大的负载
3.6 如何设置线程池的参数
这是Executors自己创建的,其实无非就是实例化一个ThreadPoolExecutor对象,你要是不想用Executors创建,就自己构造一个ThreadPoolExecutor对象也行,那么参数你就自己设置不就得了
通常来说,建议大家就用Executors提供的默认的线程池就可以了,我觉得还是挺合适的,因为他们的默认的参数设置可以满足大部分的场景了,但是如果你在学习了这一课之后,确实需要自己去动手定制一下线程池的策略,那么就自己动手构建ThreadPoolExecutor实例就可以了,所有的参数自己设置
先来看看默认的参数设置,其实固定大小的线程池,说白了,就是corePoolSize和maximumPooSize是一样的,那么只要达到了你传入的那个线程数量,而且任务队列满了,就报错;cached线程池,说白了,就是可以无限的创建线程,因为maximumPooSize是无限大的,但是超过60秒空闲就给你回收
如果你要自己创建个线程池,一般自己设置参数好了:
corePoolSize:这个其实就是说你算一下每个任务要耗费多少时间,比如一个任务大概100ms,那么每个线程每秒可以处理10个任务,然后你算算你每秒总共要处理多少个任务啊,比如说200个任务,那么你就需要20个线程,每个线程每秒处理10个任务,不就可以处理200个任务。但是一般都会多设置一些,比如你可以设置个30个线程。
坦白来讲,如果你面试被问到这个问题,体现你水平的地方,其实在于不同场景的理解:
你希望用类似于FixedThreadPool的这个线程池,corePoolSize和maximumPoolSize按照上面说的策略设置成一样的就可以了
如果用的是FixedPool的话,一般在于workQueue和handler的理解,因为你看下默认的实现,其实线程数量达到corePoolSize的时候,就会放入workQueue排队,但是默认使用的是无界队列,LinkedBlockingQueue,所以会无限制往里面排队,然后就是你corePooSize指定数量的线程不断的处理,队列里的任务可能会无限制的增加
这个其实就是适合处理那种长期不断有大量任务进来,长期负载都很重,所以你不能用CachedPool,否则长期让机器运行大量线程,可能导致机器死掉,cpu耗尽。所以你就只能控制线程的数量,用有限的线程不断的处理源源不断进入的任务,有时高峰时任务较多,就做一下排队即可。
所以FixedPool的参数里,对于workQueue,你要考虑一点,默认的是无界队列,可能会有问题,就是要是无限排队,别把机器给搞死了,那么这个时候你可以换成ArrayBlockingQueue,就是有界队列,自己设置一个最大长度,一旦超出了最大长度,就通过handler去处理,你可以自己对handler接口实现自己的逻辑,我给你举个例子,此时你可以把数据放到比如数据库里去,做离线存储或者是什么的
需要去实现CachedThreadPool的这么一个策略
corePoolSize可以设置为0,但是maximumPoolSize考虑不用设置成无限大,有一个风险,假设突然进来的流量高峰,导致你的线程池一下子出来了几万个线程,瞬间会打满cpu负载,直接机器会死掉
maximumPoolSize可以设置成一个,你的机器cpu能负载的最大的线程数,一个经验值,4核8G的虚拟机,你线程池启动的线程数量达到100个就差不多了,如果同时有100个线程,而且做很频繁的操作,cpu可能就快到70%,80%,90%
corePoolSize = 0,maximumPoolSize = 150 -> handler报错 -> 实现一个handler,将多余的线程给离线存储起来,后续高峰过了,再重新扫描出来重新提交
你看下CachedPool,他那里用的是SynchronousQueue,这个queue的意思是如果要插入一个任务,必须有一个任务已经被消费掉了,所以很可能出现说,线程数量达到corePoolSize之后,大量的任务进来,此时SynchronousQueue里的任务如果还没被拿走,那么就会认为队列满了,此时就会创建新的线程,但是maximumPoolSize是无限大的,所以会无限制的创建新的线程。但是如果后续有线程空闲了,那么就会被回收掉。
所以如果你用CachedPool,相当于是在高峰期,无限制的创建线程来拼命耗尽你的机器资源来处理并发涌入的大量的任务
所以CachedPool,可以用在那种瞬时并发任务较高,但是每个任务耗时都较短的场景,就是短时间内突然来个小高峰,那么就快速启动大量线程来处理,但是每个线程处理都很快,而且高峰很快就过去了
3.7 线程池关闭原理
shutdown():调用之后不允许提交新的任务了,所有调用之前提交的任务都会执行,等所有任务执行完了,才会真正关闭线程池,这就是优雅的关闭方式
shutdownNow():返回还没执行的task列表,然后不让等待的task执行,尝试停止正在执行的task,非优雅关闭,强制关闭