锁优化的思路及方法html
一旦用到锁,就说明这是阻塞式的,因此在并发度上来讲,通常都会比无锁的状况低一些。java
这里所讲的锁优化,是指在阻塞式的状况下,经过优化让性能不会变得太差。固然,不管怎样优化,理论上来讲性能都会比无锁的状况差一点。git
总结来讲,锁优化的思路和方法有以下几种:程序员
只对须要同步的代码加锁!github
只有在真正须要同步加锁的时候才加锁,以此减小锁的持有时间,有助于减低锁冲突的可能性,进而提高系统的并发能力。正则表达式
public synchronized void syncMethod(){ otherCode1(); mutextMethod(); otherCode2(); } |
像以上这代代码,在进入方法以前就须要获取锁,此时其余线程就要在方法外等待。编程
这里优化的一点在于,要减小其余线程等待的时间,因此,在在有线程安全要求的程序上加锁。优化后的代码以下:api
public void syncMethod(){ otherCode1(); synchronized (this) { mutextMethod(); } otherCode2(); } |
好比JDK中的处理正则表达式的java.util.regex.Pattern类安全
/** * Creates a matcher that will match the given input against this pattern. * * @param input * The character sequence to be matched * * @return A new matcher for this pattern */ public Matcher matcher(CharSequence input) { if (!compiled) { synchronized(this) { if (!compiled) compile(); } } Matcher m = new Matcher(this, input); return m; } |
缩小锁定对象的范围,从而减小冲突的可能性。数据结构
下面以HashTable和ConcurrentHashMap为例进行说明:
HashTable的数据结构看起来以下所示:
HashTable是使用了锁来保证线程安全的,而且全部同步操做使用的都是用一个锁对象。这样如有n个线程同时要执行get,这n个线程要串行等待来获取锁,此时加锁锁住的是整个HashTable对象。
而JDK1.7中,ConcurrentHashMap采用了Segment + HashEntry的方式进行实现,结构以下:
对于ConcurrentHashMap来讲,它减小锁粒度就是将其内部结构再次分红多个Segment,其中Segment
在实现上继承了ReentrantLock
,这样就自带了锁的功能。put时,只须要对key所在的Segment加锁,而其余Segment能够并行读写,所以在保证线程安全的同时,兼顾了效率。只有在须要修改全局信息时,才须要对所有Segment加锁。
根据功能将独占锁分离!
锁分离最多见的例子就是ReadWriteLock。与ReentrantLock独占所相比,它根据功能将独占锁分离成读锁和写锁,这样能够作到读读不互斥,读写互斥,写写互斥,既保证了线程安全,又提升了性能。相似的还有LinkedBlockingQueue,take和put方法就是使用了takeLock、putLock两把锁实现,以此使得take和put操做能够并发执行。
为了提升并发效率,咱们要减少持有锁的时间,而后再释放锁。但凡事都有一个度,反复对锁进行请求也会浪费资源,下降性能。若是遇到一连串连续对同一锁进行请求,那么咱们就须要把全部锁请求整合成对锁的一次请求,这就是锁的粗化。
synchronized (this) { for(int i = 0; i < 10000; i++) { count++; } } for(int i = 0; i < 10000; i++) { synchronized (this) { count++ } } |
Java虚拟机针对锁优化,提供了偏向锁、轻量级锁、自旋锁和锁消除四种机制。
所谓的偏向,就是偏爱,即锁会偏向于当前已经占有锁的线程 。
核心思想是:若是一个线程得到了锁,那么所就进入偏向模式。当这个线程再次请求锁时,无需再作任何同步操做,就能够直接得到锁。这样就节省了有关锁申请的操做,从而提升了程序的性能。所以,对于几乎没有锁竞争的场合,偏向锁有比较好的优化效果,由于连续屡次极有多是同一个线程请求相同的锁。而对于锁竞争比较激烈的场合,其效果不佳,由于在竞争激烈的场合,最有可能的状况是每次都是不一样的线程来请求相同的锁。这种场景下,偏向模式会时效,还不如不启用偏向锁(每次都要加一次是否偏向的判断)。
启用偏向锁:-XX:+UseBiasedLocking
java的多线程安全是基于Lock机制实现的,而Lock的性能每每不如人意。缘由是,monitorenter与monitorexit这两个控制多线程同步的bytecode原语,是JVM依赖操做系统互斥(mutex)来实现的。
互斥是一种会致使线程挂起,并在较短的时间内又须要从新调度回原线程的,较为消耗资源的操做。
为了优化Java的Lock机制,从Java6开始引入了轻量级锁的概念。
轻量级锁(Lightweight Locking)本意是为了减小多线程进入互斥的概率,并非要替代互斥。
若是偏向锁失败,那么系统会利用CPU原语Compare-And-Swap(CAS)来尝试加锁的操做,尝试在进入互斥前,进行补救。它存在的目的是尽量不用动用操做系统层面的互斥,由于那个性能会比较差。
若是轻量级锁失败,表示存在竞争,升级为重量级锁(常规锁),就是操做系统层面的同步方法。在没有锁竞争的状况,轻量级锁减小传统锁使用OS互斥量产生的性能损耗。在竞争很是激烈时(轻量级锁老是失败),轻量级锁会多作不少额外操做,致使性能降低。
锁膨胀以后,虚拟机为了不线程真实的系统层面挂起线程,虚拟机还会作最后的尝试——自旋锁。
因为当前线程暂时没法获取锁,可是何时可以得到锁也是一个未知数,也许在将来的几个CPU周期以后就能够得到锁,这种状况下,简单粗暴的把线程挂起多是一种得不偿失的操做。所以,基于这种假设,虚拟机会让当前线程作几个空循环(这也是自旋的含义),而且不停地尝试拿到这个锁。若是通过若干次循环后,能够得到到,那么就顺利的进入临界区。若是还不能得到锁,此时自旋锁就会膨胀为重量级锁,真实的在OS层面挂起线程。
因此在每一个线程对于锁的持有时间不多时,自旋锁可以尽可能避免线程在OS层被挂起,这也是自旋锁提高系统性能的关键所在。
JDK1.7中,自旋锁为内置实现。
偏向锁、轻量级锁、自旋锁总结
偏向锁、轻量级锁和自旋锁锁不是Java语言层面的锁优化方法,是内置在JVM当中的。
偏向锁是为了不某个线程反复得到/释放同一把锁时的性能消耗,若是仍然是同个线程去得到这个锁,尝试偏向锁时会直接进入同步块,不须要再次得到锁。
轻量级锁和自旋锁都是为了不直接调用操做系统层面的互斥操做,由于挂起线程是一个很耗资源的操做。
为了尽可能避免使用重量级锁(操做系统层面的互斥),首先会尝试轻量级锁,轻量级锁会尝试使用CAS操做来得到锁,若是轻量级锁得到失败,说明存在竞争。可是也许很快就能得到锁,就会尝试自旋锁,将线程作几个空循环,每次循环时都不断尝试得到锁。若是自旋锁也失败,那么只能升级成重量级锁。
可见偏向锁,轻量级锁,自旋锁都是乐观锁。
锁消除是在编译器级别作的事情。在即时编译时,经过对运行上下文进行扫描,去除不可能存在共享资源竞争的锁。经过锁消除,能够节省无心义的请求锁时间。
也许你会以为奇怪,既然有些对象不可能被多线程访问,那为何要加锁呢?写代码时直接不加锁不就行了?
其实有时候,这些锁并非程序员所写的,有的是JDK实现中就有锁的,好比Vector和StringBuffer这样的类,它们中的不少方法都是有锁的。当咱们在一些不会有线程安全的状况下使用这些类的方法时,达到某些条件时,编译器会将锁消除来提升性能。
下面以StringBuffer为例说明:
@Override public synchronized StringBuffer append(CharSequence s) { toStringCache = null; super.append(s); return this; } /** * @throws IndexOutOfBoundsException {@inheritDoc} * @since 1.5 */ @Override public synchronized StringBuffer append(CharSequence s, int start, int end) { toStringCache = null; super.append(s, start, end); return this; } @Override public synchronized StringBuffer append(char[] str) { toStringCache = null; super.append(str); return this; } /** * @throws IndexOutOfBoundsException {@inheritDoc} */ @Override public synchronized StringBuffer append(char[] str, int offset, int len) { toStringCache = null; super.append(str, offset, len); return this; } @Override public synchronized StringBuffer append(boolean b) { toStringCache = null; super.append(b); return this; } @Override public synchronized StringBuffer append(char c) { toStringCache = null; super.append(c); return this; } @Override public synchronized StringBuffer append(int i) { toStringCache = null; super.append(i); return this; } |
可见,StringBuffer的append方法都使用了synchronize关键字修饰,都是同步的,使用时都须要得到锁。
public String createString() { StringBuffer sb = new StringBuffer(); for (int i=0; i<10000; i++) { sb.append("aaa"); sb.append("bbb"); } return sb.toString(); } |
上述StringBuffer对象,只在createString方法中使用,所以它是一个局部变量。局部变量是在线程栈上分配的,属于线程的私有资源,所以不可能被其余线程访问,这种状况下,StringBuffer内部的全部加锁同步都是不必的,若是虚拟机检测到这种状况,就会将这些不用的锁去除。
锁消除涉及到的一箱关键技术叫作逃逸分析。逃逸分析就是观察一个变量是否会逃出某个做用域。
逃逸分析必须在-server模式下运行,使用-XX:+DoEscapeAnalysis打开,使用-XX:+EliminateLocks参数打开锁消除。
ThreadLocal是解决线程安全问题一个很好的思路,它经过为每一个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在不少状况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
在同步机制中,经过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的。
ThreadLocal则从另外一个角度来解决多线程的并发访问。ThreadLocal会为每个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。由于每个线程都拥有本身的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,能够把不安全的变量封装进ThreadLocal。
package com.lixiuyu.demo; import java.text.SimpleDateFormat; /** * Created by lixiuyu on 2017/6/20. */ import java.text.ParseException; import java.util.Date; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class SimpleDateFormatDemo { private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static class ParseDate implements Runnable { int i = 0; public ParseDate(int i) { this.i = i; } public void run() { try { Date t = sdf.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 < 10000; i++) { es.execute(new ParseDate(i)); } } } |
因为SimpleDateFormat是非线程安全的,所以某些状况下可能会出现相似以下的异常:
上述代码的一种可行的优化方案是在sdf.parse()先后加锁。这里咱们使用ThreadLocal来优化上述代码:
public class SimpleDateFormatDemo { private 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("2017-06-20 17:00:" + i % 60); System.out.println(i + ":" + t); } catch (ParseException e) { e.printStackTrace(); } } } } |
从这里也能够看出,为每个线程分配不一样的私有对象的工做并非ThreadLocal来完成的,而是须要再应用层面保证,ThreadLocal只是起到了简单的容器做用。
锁是一种悲观的策略,它老是假设每一次对临界区的操做都会产生冲突,所以必须对操做当心翼翼。若是有多个线程同时须要访问临界区,处于安全考虑,宁肯牺牲性能让线程排队等待,因此说锁会阻塞线程执行。
与锁相比,无锁是一种乐观的策略,他假设会资源的访问是没有冲突的,既然没有冲突,天然无需等待,因此全部的线程均可以在不停顿的状态下执行。
固然,冲突是不可能避免发生的,那么遇到冲突怎么办呢?无锁策略使用了一种叫作比较交换的技术(CAS、Compare And Set)在鉴别线程冲突,一旦检测到冲突,就重试当前操做直至没有冲突为止。
CAS指令是个原子化的操做,它包含三个参数:CAS(param, expectValue, newValue);
param:要更新的变量
expectValue:预期值
newValue:新值
当且仅当变量param的值等于expectValue时,才将param的值改成newValue。若是param的值跟expectValue不一样,表示已经有其余线程作了更新,当前线程什么都不作。
jdk并发包中的atomic包,里面实现了一些直接使用CAS操做的线程安全的类型,如AtomicInteger、AtomicLong等。
private volatile int value;// 初始化值 /** * 建立一个AtomicInteger,初始值value为initialValue */ public AtomicInteger(int initialValue) { value = initialValue; } /** * 建立一个AtomicInteger,初始值value为0 */ public AtomicInteger() { } /** * 返回value */ public final int get() { return value; } /** * 为value设值(基于value),而其余操做是基于旧值<--get() */ public final void set(int newValue) { value = newValue; } public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } /** * 基于CAS为旧值设定新值,采用无限循环,直到设置成功为止 * * @return 返回旧值 */ public final int getAndSet(int newValue) { for (;;) { int current = get();// 获取当前值(旧值) if (compareAndSet(current, newValue))// CAS新值替代旧值 return current;// 返回旧值 } } /** * 当前值+1,采用无限循环,直到+1成功为止 * @return the previous value 返回旧值 */ public final int getAndIncrement() { for (;;) { int current = get();//获取当前值 int next = current + 1;//当前值+1 if (compareAndSet(current, next))//基于CAS赋值 return current; } } /** * 当前值-1,采用无限循环,直到-1成功为止 * @return the previous value 返回旧值 */ public final int getAndDecrement() { for (;;) { int current = get(); int next = current - 1; if (compareAndSet(current, next)) return current; } } /** * 当前值+delta,采用无限循环,直到+delta成功为止 * @return the previous value 返回旧值 */ public final int getAndAdd(int delta) { for (;;) { int current = get(); int next = current + delta; if (compareAndSet(current, next)) return current; } } /** * 当前值+1, 采用无限循环,直到+1成功为止 * @return the updated value 返回新值 */ public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next;//返回新值 } } /** * 当前值-1, 采用无限循环,直到-1成功为止 * @return the updated value 返回新值 */ public final int decrementAndGet() { for (;;) { int current = get(); int next = current - 1; if (compareAndSet(current, next)) return next;//返回新值 } } /** * 当前值+delta,采用无限循环,直到+delta成功为止 * @return the updated value 返回新值 */ public final int addAndGet(int delta) { for (;;) { int current = get(); int next = current + delta; if (compareAndSet(current, next)) return next;//返回新值 } } /** * 获取当前值 */ public int intValue() { return get(); } |
http://www.importnew.com/21353.html
http://www.10tiao.com/html/194/201703/2651478260/1.html
http://www.cnblogs.com/ten951/p/6212285.html
https://sakuraffy.github.io/intercurrent_lock_majorizing/
http://www.cnblogs.com/java-zhao/p/5140158.html
《Java高并发程序设计》
《Java并发编程的艺术》