java性能优化实战优化多线程锁提高代码性能
之前我们提到可以使用ThreadLocal,来避免SimpleDateFormat在并发环境下引起的时间错乱问题。其实还有一种解决方式,就是通过对parse方法进行加锁,也能保证日期处理类的正确运行,代码如下图:
其实锁对性能的影响,是非常大的。因为对资源加锁以后,资源就被加锁的线程独占,其他的线程就只能排队等待这个锁,此时程序由并行执行,变相地成了顺序执行,执行速度自然就降低了。
下面是开启了50个线程,使用ThreadLocal和同步锁方式性能的一个对比。BenchmarkModeCntScoreErrorUnitsSynchronizedNormalBenchmark。syncthrpt102554。6285098。059opsmsSynchronizedNormalBenchmark。threadLocalthrpt103750。902103。528opsms去掉业务影响BenchmarkModeCntScoreErrorUnitsSynchronizedNormalBenchmark。syncthrpt1026905。5141688。600opsmsSynchronizedNormalBenchmark。threadLocalthrpt107041876。244355598。686opsms
可以看到,使用同步锁的方式,性能是比较低的。如果去掉业务本身逻辑的影响(删掉执行逻辑),这个差异会更大。代码执行的次数越多,锁的累加影响越大,对锁本身的速度优化,是非常重要的。
我们都知道,Java中有两种加锁的方式:一种就是常见的synchronized关键字,另外一种,就是使用concurrent包里面的Lock。针对这两种锁,JDK自身做了很多的优化,它们的实现方式也是不同的。本课时将从这两种锁讲起,看一下对锁的一些优化方式。synchronied
synchronized关键字给代码或者方法上锁时,都有显示或者隐藏的上锁对象。当一个线程试图访问同步代码块时,它首先必须得到锁,而退出或抛出异常时必须释放锁。给普通方法加锁时,上锁的对象是this;给静态方法加锁时,锁的是class对象;给代码块加锁,可以指定一个具体的对象作为锁。1。monitor原理
在面试中,面试官很可能会问你:synchronized在字节码中,是怎么体现的呢?参照下面的代码,在命令行执行javac,然后再执行javapvp,就可以看到它具体的字节码。
可以看到,在字节码的体现上,它只给方法加了一个flag:ACCSYNCHRONIZED。synchronizedvoidsyncMethod(){System。out。println(syncMethod);}字节码synchronizedvoidsyncMethod();descriptor:()Vflags:ACCSYNCHRONIZEDCode:stack2,locals1,argssize10:getstatic43:ldc55:invokevirtual68:return
我们再来看下同步代码块的字节码。可以看到,字节码是通过monitorenter和monitorexit两个指令进行控制的。voidsyncBlock(){synchronized(Test。class){}}字节码voidsyncBlock();descriptor:()Vflags:Code:stack2,locals3,argssize10:ldc22:dup3:astore14:monitorenter5:aload16:monitorexit7:goto1510:astore211:aload112:monitorexit13:aload214:athrow15:returnExceptiontable:fromtotargettype5710any101310any
这两者虽然显示效果不同,但他们都是通过monitor来实现同步的。我们可以通过下面这张图,来看一下monitor的原理。
注意了,下面是面试题目高发地。比如,你能描述一下monitor锁的实现原理吗?
如上图所示,我们可以把运行时的对象锁抽象地分成三部分。其中,EntrySet和WaitSet是两个队列,中间虚线部分是当前持有锁的线程,我们可以想象一下线程的执行过程。
当第一个线程到来时,发现并没有线程持有对象锁,它会直接成为活动线程,进入RUNNING状态。
接着又来了三个线程,要争抢对象锁。此时,这三个线程发现锁已经被占用了,就先进入EntrySet缓存起来,进入BLOCKED状态。此时,从jstack命令,可以看到他们展示的信息都是waitingformonitorentry。httpnio8084exec120143daemonprio5osprio31cpu122。86mselapsed317。88stid0x00007fedd8381000nid0x1af03waitingformonitorentry〔0x00007000150e1000〕java。lang。Thread。State:BLOCKED(onobjectmonitor)atjava。io。BufferedInputStream。read(java。base13。0。1BufferedInputStream。java:263)waitingtolock0x0000000782e1b590(ajava。io。BufferedInputStream)atorg。apache。commons。httpclient。HttpParser。readRawLine(HttpParser。java:78)atorg。apache。commons。httpclient。HttpParser。readLine(HttpParser。java:106)atorg。apache。commons。httpclient。HttpConnection。readLine(HttpConnection。java:1116)atorg。apache。commons。httpclient。HttpMethodBase。readStatusLine(HttpMethodBase。java:1973)atorg。apache。commons。httpclient。HttpMethodBase。readResponse(HttpMethodBase。java:1735)
处于活动状态的线程,执行完毕退出了;或者由于某种原因执行了wait方法,释放了对象锁,进入了WaitSet队列,这就是在调用wait之前,需要先获得对象锁的原因。
就像下面的代码:synchronized(lock){try{lock。wait();}catch(InterruptedExceptione){e。printStackTrace();}}
此时,jstack显示的线程状态是WAITING状态,而原因是inObject。wait()。waitdemo12prio5osprio31cpu0。14mselapsed12。58stid0x00007fb66609e000nid0x6103inObject。wait()〔0x000070000f2bd000〕java。lang。Thread。State:WAITING(onobjectmonitor)atjava。lang。Object。wait(java。base13。0。1NativeMethod)waitingon0x0000000787b48300(ajava。lang。Object)atjava。lang。Object。wait(java。base13。0。1Object。java:326)atWaitDemo。lambdamain0(WaitDemo。java:7)locked0x0000000787b48300(ajava。lang。Object)atWaitDemoLambda140x0000000800b44840。run(UnknownSource)atjava。lang。Thread。run(java。base13。0。1Thread。java:830)
发生了这两种情况,都会造成对象锁的释放,进而导致EntrySet里的线程重新争抢对象锁,成功抢到锁的线程成为活动线程,这是一个循环的过程。
那WaitSet中的线程是如何再次被激活的呢?接下来,在某个地方,执行了锁的notify或者notifyAll命令,会造成WaitSet中的线程,转移到EntrySet中,重新进行锁的争夺。
如此周而复始,线程就可按顺序排队执行。2。分级锁
在JDK1。8中,synchronized的速度已经有了显著的提升,它都做了哪些优化呢?答案就是分级锁。JVM会根据使用情况,对synchronized的锁,进行升级,它大体可以按照下面的路径进行升级:偏向锁轻量级锁重量级锁。
锁只能升级,不能降级,所以一旦升级为重量级锁,就只能依靠操作系统进行调度。
要想了解锁升级的过程,需要先看一下对象在内存里的结构。
如上图所示,对象分为MarkWord、ClassPointer、InstanceData、Padding四个部分。
和锁升级关系最大的就是MarkWord,它的长度是24位,我们着重介绍一下。它包含ThreadID(23bit)、Age(6bit)、Biased(1bit)、Tag(2bit)四个部分,锁升级就是靠判断ThreadId、Biased、Tag等三个变量值来进行的。偏向锁
在只有一个线程使用了锁的情况下,偏向锁能够保证更高的效率。
具体过程是这样的:当第一个线程第一次访问同步块时,会先检测对象头MarkWord中的标志位Tag是否为01,以此判断此时对象锁是否处于无锁状态或者偏向锁状态(匿名偏向锁)。
01也是锁默认的状态,线程一旦获取了这把锁,就会把自己的线程ID写到MarkWord中,在其他线程来获取这把锁之前,锁都处于偏向锁状态。
当下一个线程参与到偏向锁竞争时,会先判断MarkWord中保存的线程ID是否与这个线程ID相等,如果不相等,会立即撤销偏向锁,升级为轻量级锁。轻量级锁
轻量级锁的获取是怎么进行的呢?它们使用的是自旋方式。
参与竞争的每个线程,会在自己的线程栈中生成一个LockRecord(LR),然后每个线程通过CAS(自旋)的方式,将锁对象头中的MarkWord设置为指向自己的LR的指针,哪个线程设置成功,就意味着哪个线程获得锁。
当锁处于轻量级锁的状态时,就不能够再通过简单地对比Tag的值进行判断,每次对锁的获取,都需要通过自旋。
当然,自旋也是面向不存在锁竞争的场景,比如一个线程运行完了,另外一个线程去获取这把锁;但如果自旋失败达到一定的次数,锁就会膨胀为重量级锁。重量级锁
重量级锁,即我们对synchronized的直观认识,这种情况下,线程会挂起,进入到操作系统内核态,等待操作系统的调度,然后再映射回用户态。系统调用是昂贵的,所以重量级锁的名称由此而来。
如果系统的共享变量竞争非常激烈,锁会迅速膨胀到重量级锁,这些优化就名存实亡。如果并发非常严重,可以通过参数XX:UseBiasedLocking禁用偏向锁,理论上会有一些性能提升,但实际上并不确定。Lock
在concurrent包里,我们能够发现ReentrantLock和ReentrantReadWriteLock两个类。Reentrant就是可重入的意思,它们和synchronized关键字一样,都是可重入锁。
这里有必要解释一下可重入这个概念,这是一个面试高频考点。它的意思是,一个线程运行时,可以多次获取同一个对象锁,这是因为Java的锁是基于线程的,而不是基于调用的。
比如下面这段代码,由于方法a、b、c锁的都是当前的this,线程在调用a方法的时候,就不需要多次获取对象锁。publicsynchronizedvoida(){b();}publicsynchronizedvoidb(){c();}publicsynchronizedvoidc(){}1。主要方法
Lock是基于AQS(AbstractQueuedSynchronizer)实现的,而AQS是基于volitale和CAS实现的(关于CAS,我们将在下一课时讲解)。
Lock与synchronized的使用方法不同,它需要手动加锁,然后在finally中解锁。Lock接口比synchronized灵活性要高,我们来看一下几个关键方法。Lock:Lock方法和synchronized没什么区别,如果获取不到锁,都会被阻塞;tryLock:此方法会尝试获取锁,不管能不能获取到锁,都会立即返回,不会阻塞,它是有返回值的,获取到锁就会返回true;tryLock(longtime,TimeUnitunit):与tryLock类似,但它在拿不到锁的情况下,会等待一段时间,直到超时;LockInterruptibly:与Lock类似,但是可以锁等待,可以被中断,中断后返回InterruptedException;
一般情况下,使用Lock方法就可以;但如果业务请求要求响应及时,那使用带超时时间的tryLock是更好的选择:我们的业务可以直接返回失败,而不用进行阻塞等待。tryLock这种优化手段,采用降低请求成功率的方式,来保证服务的可用性,在高并发场景下常被高频采用。2。读写锁
但对于有些业务来说,使用Lock这种粗粒度的锁还是太慢了。比如,对于一个HashMap来说,某个业务是读多写少的场景,这个时候,如果给读操作,也加上和写操作一样的锁的话,效率就会很慢。
ReentrantReadWriteLock是一种读写分离的锁,它允许多个读线程同时进行,但读和写、写和写是互斥的。
使用方法如下所示,分别获取读写锁,对写操作加写锁,对读操作加读锁,并在finally里释放锁即可。ReentrantReadWriteLocklocknewReentrantReadWriteLock();LockreadLocklock。readLock();LockwriteLocklock。writeLock();publicvoidput(Kk,Vv){writeLock。lock();try{map。put(k,v);}finally{writeLock。unlock();}}。。。3。公平锁与非公平锁非公平锁
我们平常用到的锁,都是非公平锁,可以回过头来看一下monitor的原理。当持有锁的线程释放锁的时候,EntrySet里的线程就会争抢这把锁,这个争抢过程,是随机的,也就是说你并不知道哪个线程会获取对象锁,谁抢到了就算谁的。
这就有一定的概率会发生,某个线程总是抢不到锁的情况。比如,某个线程通过setPriority设置得比较低的优先级,这个抢不到锁的线程,就一直处于饥饿状态,这就是线程饥饿的概念。公平锁
而公平锁通过把随机变成有序,可以解决这个问题,synchronized没有这个功能,在Lock中可以通过构造参数设置成公平锁,代码如下:publicReentrantReadWriteLock(booleanfair){syncfair?newFairSync():newNonfairSync();readerLocknewReadLock(this);writerLocknewWriteLock(this);}
由于所有的线程都需要排队,需要在多核的场景下维护一个同步队列,在多个线程争抢锁的时候,吞吐量就很低。
下面是20个并发之下,锁的JMH测试结果,可以看到,非公平锁比公平锁的性能高出两个数量级。BenchmarkModeCntScoreErrorUnitsFairVSNoFairBenchmark。fairthrpt10186。14427。462opsmsFairVSNoFairBenchmark。nofairthrpt1035195。6496503。375opsms锁的优化技巧1。死锁
我们可以先看一下锁冲突最严重的一种情况:死锁。下面这段示例代码,两个线程分别持有对方所需要的锁,并进入了相互等待的状态,那么它们就进入了死锁。
在面试中,经常会要求被面试者手写下面这段代码:publicclassDeadLockDemo{publicstaticvoidmain(String〔〕args){Objectobject1newObject();Objectobject2newObject();Threadt1newThread((){synchronized(object1){try{Thread。sleep(200);}catch(InterruptedExceptione){e。printStackTrace();}synchronized(object2){}}},deadlockdemo1);t1。start();Threadt2newThread((){synchronized(object2){try{Thread。sleep(200);}catch(InterruptedExceptione){e。printStackTrace();}synchronized(object1){}}},deadlockdemo2);t2。start();}}
代码创建了两把对象锁,线程1首先拿到了object1的对象锁,200ms后尝试获取object2的对象锁。但这个时候,object2的对象锁已经被线程2获取了。这两个线程进入了相互等待的状态,产生了死锁。
使用我们上面提到的,带超时时间的tryLock方法,有一方超时让步,可以一定程度上避免死锁。2。优化技巧
锁的优化理论其实很简单,那就是减少锁的冲突。无论是锁的读写分离,还是分段锁,本质上都是为了避免多个线程同时获取同一把锁。
所以我们可以总结一下优化的一般思路:减少锁的粒度、减少锁持有的时间、锁分级、锁分离、锁消除、乐观锁、无锁等。
减少锁粒度
通过减小锁的粒度,可以将冲突分散,减少冲突的可能,从而提高并发量。简单来说,就是把资源进行抽象,针对每类资源使用单独的锁进行保护。
比如下面的代码,由于list1和list2属于两类资源,就没必要使用同一个对象锁进行处理。publicclassLockLessDemo{ListStringlist1newArrayList();ListStringlist2newArrayList();publicsynchronizedvoidaddList1(Stringv){this。list1。add(v);}publicsynchronizedvoidaddList2(Stringv){this。list2。add(v);}}
可以创建两个不同的锁,改善情况如下:publicclassLockLessDemo{ListStringlist1newArrayList();ListStringlist2newArrayList();finalObjectlock1newObject();finalObjectlock2newObject();publicvoidaddList1(Stringv){synchronized(lock1){this。list1。add(v);}}publicvoidaddList2(Stringv){synchronized(lock2){this。list2。add(v);}}}减少锁持有时间
通过让锁资源尽快地释放,减少锁持有的时间,其他线程可更迅速地获取锁资源,进行其他业务的处理。
考虑到下面的代码,由于slowMethod不在锁的范围内,占用的时间又比较长,可以把它移动到Synchronized代码块外面,加速锁的释放。publicclassLockTimeDemo{ListStringlistnewArrayList();finalObjectlocknewObject();publicvoidaddList(Stringv){synchronized(lock){slowMethod();this。list。add(v);}}publicvoidslowMethod(){}}锁分级
锁分级,指的是我们文章开始讲解的Synchronied锁的锁升级,属于JVM的内部优化,它从偏向锁开始,逐渐升级为轻量级锁、重量级锁,这个过程是不可逆的。锁分离
我们在上面提到的读写锁,就是锁分离技术。这是因为,读操作一般是不会对资源产生影响的,可以并发执行;写操作和其他操作是互斥的,只能排队执行。所以读写锁适合读多写少的场景。锁消除
通过JIT编译器,JVM可以消除某些对象的加锁操作。举个例子,大家都知道StringBuffer和StringBuilder都是做字符串拼接的,而且前者是线程安全的。
但其实,如果这两个字符串拼接对象用在函数内,JVM通过逃逸分析这个对象的作用范围就是在本函数中,就会把锁的影响给消除掉。
比如下面这段代码,它和StringBuilder的效果是一样的。Stringm1(){StringBuffersbnewStringBuffer();sb。append();returnsb。toString();}
当然,对于读多写少的互联网场景,最有效的做法,是使用乐观锁,甚至无锁。小结
Java中有两种加锁方式:一种是使用Synchronized关键字,另外一种是concurrent包下面的Lock。
我们详细地了解了它们的一些特性,包括实现原理,其对比如下:
类别
Synchronized
Lock
实现方式
monitor
AQS
底层细节
JVM优化
JavaAPI
分级锁
是
否
功能特性
单一
丰富
锁分离
无
读写锁
锁超时
无
带超时时间的tryLock
可中断
否
lockInterruptibly
Lock的功能是比Synchronized多的,能够对线程行为进行更细粒度的控制。
但如果只是用最简单的锁互斥功能,建议直接使用Synchronized,有两个原因:Synchronized的编程模型更加简单,更易于使用Synchronized引入了偏向锁,轻量级锁等功能,能够从JVM层进行优化,同时JIT编译器也会对它执行一些锁消除动作。
我们还了解了公平锁与非公平锁,以及可重入锁的概念,以及一些通用的优化技巧。有冲突,才会有优化空间,那么无锁队列是怎么回事呢?它又是怎么实现的呢?后续我们会继续讲到。
房贷转经营贷陷阱多把个人住房贷款转换成个人经营性贷款,这一操作已引起监管部门的高度重视。中国银保监会消费者权益保护局日前发布2022年第8期风险提示,提醒广大消费者认清转贷操作的不良后果和风险隐患,
美国激进加息伤及世界经济顾客在美国纽约一家商店购物。(新华社发)今年以来,生活成本危机多数地区金融环境不断收紧乌克兰危机以及新冠肺炎疫情持续等因素严重拖累经济前景,其中美国货币政策所引发的金融动荡和流动性
理性看待广义货币增速创新高近日,中国人民银行公布的数据显示,11月份广义货币(M2)同比增速为12。4,11月狭义货币(M1)增速为4。6,M2同比增速达到近6年峰值,同时与M1的差距再度拉大。这一变化意味
中国经济新亮点黑龙江乘数而上撬动产业转型升级电流数据直线下降,但没触底,初步判断抽油机皮带断裂。日前,在大庆油田有限责任公司采油五厂生产管理中心,操作员唐静发现管控平台数据异常后第一时间汇报,有关方面及时赶到现场抢修。这是大
20222028全球及中国电动弯管机行业研究及十四五规划分析报告辰宇信息咨询市场调研公司最近发布20222028全球与中国电动弯管机市场调研报告内容摘要本文同时着重分析电动弯管机行业竞争格局,包括全球市场主要厂商竞争格局和中国本土市场主要厂商竞
金税四期下,经销商常见税务风险及应对(一)导语虚开发票并不只是买卖发票那么简单,一不小心,可能就会涉入其中。作者丨蔡艳富任文青编者按本月新经销发表文章金税四期来了,经销商严重低估了它的重要性!,在行业内引起广泛关注。过去,
栉风沐雨活力不减,逆风飞翔奔向未来中央经济工作会议强调指出,明年要坚持稳字当头稳中求进。稳的基调更加明确,进的要求更加积极,传递的信息十分丰富。当前我国经济恢复的基础尚不牢固,需求收缩供给冲击预期转弱三重压力仍然较
别怕,预计最早春节后,退烧药就会产能过剩,价格雪崩!抗原也是其实,众多解热镇痛药的核心成分对乙酰氨基酚,本就非常便宜。比如东北2元20片已经卖了很多年。即便在现在缺药的情况下,有网友都买到了45元1000片的对乙酰氨基酚,这还是涨价后的价格
文山州特色高效农业发展势头正劲文山三七西畴猕猴桃丘北辣椒广南高峰牛富宁八角麻栗坡魔芋在今年11月举办的第6届中国南亚博览会上,一个个来自文山壮族苗族自治州的特色农产品惊艳亮相,深受消费者喜爱。会展期间,文山州已
财政金融齐发力扩内需稳增长央视网消息(新闻联播)近期,财政金融政策协同发力,扩大有效投资,提振居民消费,多渠道扩内需稳增长。临近年底,各地投资加码发力,一批重大项目密集开工。在广州,总投资109亿元的新能源
长垣市四企业入选2022年河南省新一代信息技术融合应用新模式示范项目近日,河南省工业和信息化厅公示了2022年河南省新一代信息技术融合应用新模式示范项目拟确定名单,全省共有100家企业入选,其中,新乡市长垣市4家企业成功入选,分别是河南东起机械有限