Java锁优化思路及JVM实现
1. 锁优化的思路和方法
这里提到的锁优化,是指在阻塞式的情况下,如何让性能不要变得太差。但是再怎么优化,一般来说性能都会比无锁的情况差一点。
这里要注意的是,在ReentrantLock中的tryLock,偏向于一种无锁的方式,因为在tryLock判断时,并不会把自己挂起。
锁优化的思路和方法总结一下,有以下几种。
- 减少锁持有时间
- 减小锁粒度
- 锁分离
- 锁粗化
- 锁消除\
1.1 减少锁持有时间
public synchronized void syncMethod(){othercode1();mutextMethod();othercode2();
}
像上述代码这样,在进入方法前就要得到锁,其他线程就要在外面等待。
这里优化的一点在于,要减少其他线程等待的时间,所以,只用在有线程安全要求的程序上加锁
public void syncMethod(){othercode1();synchronized(this){mutextMethod();}othercode2();
}
1.2 减小锁粒度
将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。
最最典型的减小锁粒度的案例就是ConcurrentHashMap(当然也是1.8之前了)。
1.3 锁分离
最常见的锁分离就是读写锁ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能。
读写分离思想可以延伸,只要操作互不影响,锁就可以分离。
比如LinkedBlockingQueue:
从头部取出,从尾部放数据。
1.4 锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早的获得资源执行任务。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化。
举个例子:
public void demoMethod(){synchronized(lock){//do sth. }//做其他不需要的同步的工作,但能很快执行完毕 synchronized(lock){//do sth. }
}
这种情况,根据锁粗化的思想,应该合并:
public void demoMethod(){//整合成一次锁请求synchronized(lock){//do sth. //做其他不需要的同步的工作,但能很快执行完毕 }
}
当然这是有前提的,前提就是中间的那些不需要同步的工作是很快执行完成的。
再举一个极端的例子:
for(int i=0;i<CIRCLE;i++){synchronized(lock){}
}
在一个循环内不同得获得锁。虽然JDK内部会对这个代码做些优化,但是还不如直接写成:
synchronized(lock){for(int i=0;i<CIRCLE;i++){}
}
当然如果有需求说,这样的循环太久,需要给其他线程不要等待太久,那只能写成上面那种。如果没有这样类似的需求,还是直接写成下面那种比较好。
1.5 锁消除
锁消除是在编译器级别的事情。
在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作。
也许你会觉得奇怪,既然有些对象不可能被多线程访问,那为什么要加锁呢?写代码时直接不加锁不就好了。
但是有时,这些锁并不是程序员所写的,有的是JDK实现中就有锁的,比如Vector和StringBuffer这样的类,它们中的很多方法都是有锁的。当我们在一些不会有线程安全的情况下使用这些类的方法时,达到某些条件时,编译器会将锁消除来提高性能。
比如:
public static void main(String args[]) throws InterruptedException {long start = System.currentTimeMillis();for (int i = 0; i < 2000000; i++) {createStringBuffer("JVM", "Diagnosis");}long bufferCost = System.currentTimeMillis() - start;System.out.println("craeteStringBuffer: " + bufferCost + " ms");
}public static String createStringBuffer(String s1, String s2) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb.toString();
}
上述代码中的StringBuffer.append是一个同步操作,但是StringBuffer却是一个局部变量,并且方法也并没有把StringBuffer返回,所以不可能会有多线程去访问它。
那么此时StringBuffer中的同步操作就是没有意义的。
开启锁消除是在JVM参数上设置的,当然需要在server模式下:
-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
并且要开启逃逸分析。 逃逸分析的作用呢,就是看看变量是否有可能逃出作用域的范围。
比如上述的StringBuffer,上述代码中craeteStringBuffer的返回是一个String,所以这个局部变量StringBuffer在其他地方都不会被使用。如果将craeteStringBuffer改成:
public static StringBuffer craeteStringBuffer(String s1, String s2) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb;
}
那么这个 StringBuffer被返回后,是有可能被任何其他地方所使用的(譬如被主函数将返回结果put进map啊等等)。那么JVM的逃逸分析可以分析出,这个局部变量 StringBuffer逃出了它的作用域。
所以基于逃逸分析,JVM可以判断,如果这个局部变量StringBuffer并没有逃出它的作用域,那么可以确定这个StringBuffer并不会被多线程所访问,那么就可以把这些多余的锁给去掉来提高性能。
2. 虚拟机内的锁优化
先要介绍下对象头,在JVM中,每个对象都有一个对象头。Mark Word,对象头的标记,32位(32位系统)。描述对象的hash、锁信息,垃圾回收标记,年龄。会保存指向锁记录的指针,指向monitor的指针,偏向锁线程ID等。简单来说,对象头就是要保存一些系统性的信息。
2.1 偏向锁
所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程。
大部分情况是没有竞争的(某个同步块大多数情况都不会出现多线程同时竞争锁),所以可以通过偏向来提高性能。即在无竞争时,之前获得锁的线程再次获得锁时,会判断是否偏向锁指向我,那么该线程将不用再次获得锁,直接就可以进入同步块。
偏向锁的实施就是将对象头Mark的标记设置为偏向,并将线程ID写入对象头Mark。
当其他线程请求相同的锁时,偏向模式结束。
JVM默认启用偏向锁 -XX:+UseBiasedLocking
在竞争激烈的场合,偏向锁会增加系统负担(每次都要加一次是否偏向的判断)
偏向锁的例子:
public static List<Integer> numberList = new Vector<Integer>();public static void main(String[] args) throws InterruptedException {long begin = System.currentTimeMillis();int count = 0;int startnum = 0;while (count < 10000000) {numberList.add(startnum);startnum += 2;count++;}long end = System.currentTimeMillis();System.out.println(end - begin);
}
Vector是一个线程安全的类,内部使用了锁机制。每次add都会进行锁请求。上述代码只有main一个线程再反复add请求锁。
使用如下的JVM参数来设置偏向锁:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
BiasedLockingStartupDelay表示系统启动几秒钟后启用偏向锁。默认为4秒,原因在于,系统刚启动时,一般数据竞争是比较激烈的,此时启用偏向锁会降低性能。
下面关闭偏向锁:
-XX:-UseBiasedLocking
2.2 轻量级锁
Java的多线程安全是基于Lock机制实现的,而Lock的性能往往不如人意。
原因是,monitorenter与monitorexit这两个控制多线程同步的bytecode原语,是JVM依赖操作系统互斥(mutex)来实现的。
互斥是一种会导致线程挂起,并在较短的时间内又需要重新调度回原线程的,较为消耗资源的操作。
为了优化Java的Lock机制,从Java6开始引入了轻量级锁的概念。
轻量级锁(Lightweight Locking)本意是为了减少多线程进入互斥的几率,并不是要替代互斥。
它利用了CPU原语Compare-And-Swap(CAS,汇编指令CMPXCHG),尝试在进入互斥前,进行补救。
如果偏向锁失败,那么系统会进行轻量级锁的操作。它存在的目的是尽可能不用动用操作系统层面的互斥,因为那个性能会比较差。因为JVM本身就是一个应用,所以希望在应用层面上就解决线程同步问题。总结一下就是轻量级锁是一种快速的锁定方法,在进入互斥之前,使用CAS操作来尝试加锁,尽量不要用操作系统层面的互斥,提高了性能。
如果轻量级锁失败,表示存在竞争,升级为重量级锁(常规锁),就是操作系统层面的同步方法。在没有锁竞争的情况,轻量级锁减少传统锁使用OS互斥量产生的性能损耗。在竞争非常激烈时(轻量级锁总是失败),轻量级锁会多做很多额外操作,导致性能下降。
2.3 自旋锁
当竞争存在时,因为轻量级锁尝试失败,之后有可能会直接升级成重量级锁动用操作系统层面的互斥。也有可能再尝试一下自旋锁。
如果线程可以很快获得锁,那么可以不在OS层挂起线程,让线程做几个空操作(自旋),并且不停地尝试拿到这个锁(类似tryLock),当然循环的次数是有限制的,当循环次数达到以后,仍然升级成重量级锁。所以在每个线程对于锁的持有时间很少时,自旋锁能够尽量避免线程在OS层被挂起。
JDK1.6中-XX:+UseSpinning开启
JDK1.7中,去掉此参数,改为内置实现
如果同步块很长,自旋失败,会降低系统性能。如果同步块很短,自旋成功,节省线程挂起切换时间,提升系统性能。
2.4 偏向锁,轻量级锁,自旋锁总结
上述的锁不是Java语言层面的锁优化方法,是内置在JVM当中的。
首先偏向锁是为了避免某个线程反复获得/释放同一把锁时的性能消耗,如果仍然是同个线程去获得这个锁,尝试偏向锁时会直接进入同步块,不需要再次获得锁。
而轻量级锁和自旋锁都是为了避免直接调用操作系统层面的互斥操作,因为挂起线程是一个很耗资源的操作。
为了尽量避免使用重量级锁(操作系统层面的互斥),首先会尝试轻量级锁,轻量级锁会尝试使用CAS操作来获得锁,如果轻量级锁获得失败,说明存在竞争。但是也许很快就能获得锁,就会尝试自旋锁,将线程做几个空循环,每次循环时都不断尝试获得锁。如果自旋锁也失败,那么只能升级成重量级锁。
可见偏向锁,轻量级锁,自旋锁都是乐观锁。
3. 一个错误使用锁的案例
public class IntegerLock {static Integer i = 0;public static class AddThread extends Thread {public void run() {for (int k = 0; k < 100000; k++) {synchronized (i) {i++;}}}}public static void main(String[] args) throws InterruptedException {AddThread t1 = new AddThread();AddThread t2 = new AddThread();t1.start();t2.start();t1.join();t2.join();System.out.println(i);}
}
一个很初级的错误在于,Interger是final不变的,每次++后,会产生一个新的 Interger再赋给i,所以两个线程争夺的锁是不同的。所以并不是线程安全的。
4. ThreadLocal及其源码分析
这里来提ThreadLocal可能有点不合适,但是ThreadLocal是可以把锁代替的方式。所以还是有必要提一下。
基本的思想就是,在一个多线程当中需要把有数据冲突的数据加锁,使用ThreadLocal的话,为每一个线程都提供一个对象实例。不同的线程只访问自己的对象,而不访问其他的对象。这样锁就没有必要存在了。
由于SimpleDateFormat并不线程安全的,直接在多线程下使用是错误的。最简单的方式就是,自己定义一个类去用synchronized包装(类似于Collections.synchronizedMap)。这样做在高并发时会有问题,对 synchronized的争用导致每一次只能进去一个线程,并发量很低。
这里使用ThreadLocal去封装SimpleDateFormat就解决了这个问题:
public class Test {static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>();public static class ParseDate implements Runnable {int i = 0;public ParseDate(int i) {this.i = i;}public void run() {try {if (tl.get() == null) {tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));}Date t = tl.get().parse("2016-02-16 17:00:" + i % 60);System.out.println(i + ":" + t);} catch (ParseException e) {e.printStackTrace();}}}public static void main(String[] args) {ExecutorService es = Executors.newFixedThreadPool(10);for (int i = 0; i < 1000; i++) {es.execute(new ParseDate(i));}}}
每个线程在运行时,会判断是否当前线程有SimpleDateFormat对象:
if (tl.get() == null)
如果没有的话,就new个 SimpleDateFormat与当前线程绑定:
tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
然后用当前线程的 SimpleDateFormat去解析:
tl.get().parse("2016-02-16 17:00:" + i % 60);
一开始的代码中,只有一个 SimpleDateFormat,使用了 ThreadLocal,为每一个线程都new了一个 SimpleDateFormat。
需要注意的是,这里不要把公共的一个SimpleDateFormat设置给每一个ThreadLocal,这样是没用的。 需要给每一个都new一个SimpleDateFormat。
在hibernate中,对ThreadLocal有典型的应用。
下面来看一下ThreadLocal的源码实现
首先Thread类中有一个成员变量:
ThreadLocal.ThreadLocalMap threadLocals = null;
而这个Map就是ThreadLocal的实现关键:
public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);
}
根据 ThreadLocal可以set和get相对应的value。
这里的ThreadLocalMap实现和HashMap差不多,但是在hash冲突的处理上有区别。
ThreadLocalMap中发生hash冲突时,不是像HashMap这样用链表来解决冲突,而是是将索引++,放到下一个索引处来解决冲突。
转载于:https://www.cnblogs.com/john8169/p/9780492.html
Java锁优化思路及JVM实现相关推荐
- sql并发 锁 优化思路_并发优化–减少锁粒度
sql并发 锁 优化思路 在高负载多线程应用程序中,性能非常重要. 开发人员必须意识到并发问题才能获得更好的性能. 当我们需要并发时,我们通常拥有必须由两个或更多线程共享的资源. 在这种情况下,我们处 ...
- JAVA性能优化思路探究
1.背景介绍 一个系统的上线除了常规的功能性测试外,还需要经过严格的性能测试,满足预期的性能指标(常见的有响应时间,tps等),才允许上生产环境.广义的性能测试一般还包含负载测试(用于测试系统的容量: ...
- Java并发优化思路
一.并发优化 1.1.Java高并发包所采用的几个机制 (1).CAS(乐观操作) jdk5以前采用synchronized,对共享区域进行同步操作,synchronized是重的操作, ...
- JAVA性能优化思路探究,让程序超顺畅
1.背景介绍 一个系统的上线除了常规的功能性测试外,还需要经过严格的性能测试,满足预期的性能指标(常见的有响应时间,tps等),才允许上生产环境. 广义的性能测试一般还包含负载测试(用于测试系统的容量 ...
- 抖音 Android 性能优化系列:Java 锁优化
动手点关注干货不迷路
- 赠书:《Java性能优化实践》,众多业内大佬推荐阅读
没有捷径可走的 Java 性能优化 多年来,用 Google 搜索 Java performance tuning,出现的三篇最热门文章之一是于 1997 年到 1998 年左右发表的文章,这篇文章在 ...
- 新书上市 | 《Java性能优化实践》,众多业内大佬推荐阅读
没有捷径可走的 Java 性能优化 多年来,用 Google 搜索 Java performance tuning,出现的三篇最热门文章之一是于 1997 年到 1998 年左右发表的文章,这篇文章在 ...
- 由JVM深入了解Java的线程安全与锁优化
目录 •写在前面 •线程安全 Java语言中的线程安全 线程安全的实现方法 •锁优化 自旋锁与自适应自旋 锁消除 锁粗化 轻量级锁 偏向锁 •写在前面 讲道理,在谈及线程安全以及锁优化之前,需要先搞清 ...
- Java多线程编程 — 锁优化
2019独角兽企业重金招聘Python工程师标准>>> 阅读目录 一.尽量不要锁住方法 二.缩小同步代码块,只锁数据 三.锁中尽量不要再包含锁 四.将锁私有化,在内部管理锁 五.进行 ...
最新文章
- 计算机知识指的是什么意思,计算机上面的m+和m-是什么意思
- 二十八、深入浅出Python中的 logging模块
- PHP----------安装包lnmp1.3-full安装的lnmp环境,如何安装PHP扩展
- primefaces_懒惰的JSF Primefaces数据表分页–第1部分
- memcpy函数_[PART][BUG][MSVCRT][C][CCF NOI1097] 关于memcpy的坑
- word、excel、ppt 办公文件 在线预览
- SEO之Google--PageRank优化剖析(二)
- 西门子smartclient怎么用_西门子200SMART PLC软件各功能怎么用?编程必备!
- 运维:使用awk命令获取文本的某一行,某一列
- Git 操作总结整合篇
- 智慧树工业机器人测试答案_智慧树_工业机器人技术基础_答案章节单元测试答案...
- 用c语言写双人贪吃蛇,试图写了一个双人贪吃蛇,结果蛇竖着跑正常,横着跑就只有头了,求解~...
- C++:实现量化期权交易CDS加密货币衍生品测试实例
- 【行业】2022年ERP的开展趋势
- HTML 表格与表单 个人简历
- 《Android 开源库》 FlycoTabLayout 从头到脚
- MySQL5.7用group by分组根据组中某个字段的最大值求取那条记录(注意是整条记录)
- windows中的一些小技巧
- 2021最全大数据学习路线(建议收藏)
- 鼠标经过显示隐藏盒子