关于锁的那点事儿

一 synchronized底层原理

咱们先经过反编译下面的代码来讲明问题。html

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("Method 1 start");
        }
    }
}

反编译结果:java

关于这两条指令的做用,咱们直接参考JVM规范中描述:安全

monitorenter :多线程

每一个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的全部权,过程以下:并发

一、若是monitor的进入数为0,则该线程进入monitor,而后将进入数设置为1,该线程即为monitor的全部者。app

二、若是线程已经占有该monitor,只是从新进入,则进入monitor的进入数加1.jvm

3.若是其余线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再从新尝试获取monitor的全部权。函数

 

monitorexit:工具

执行monitorexit的线程必须是objectref所对应的monitor的全部者。性能

指令执行时,monitor的进入数减1,若是减1后进入数为0,那线程退出monitor,再也不是这个monitor的全部者。其余被这个monitor阻塞的线程能够尝试去获取这个 monitor 的全部权。 

  经过这两段描述,咱们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是经过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为何只有在同步的块或者方法中才能调用wait/notify等方法,不然会抛出java.lang.IllegalMonitorStateException的异常的缘由。

 

 

咱们再看一下同步方法的反编译结果

public class SynchronizedMethod {
    public synchronized void method() {
        System.out.println("Hello World!");
    }
}

反编译结果:

 

从反编译的结果来看,方法的同步并无经过指令monitorenter和monitorexit来完成(理论上其实也能够经过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,若是设置了,执行线程将先获取monitor,获取成功以后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其余任何线程都没法再得到同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需经过字节码来完成。

 

 

 二 锁的状态

上面介绍了synchronized用法和实现原理。咱们已经知道,synchronized是经过对象内部的一个叫作监视器锁(monitor)来实现的。可是监视器锁本质又是依赖于底层的操做系统的mutex lock来实现的。而操做系统实现线程之间的切换就须要从用户态转换到核心态,这个成本很是高,状态之间的转换须要相对比较长的时间,这就是synchronized效率低的缘由。所以,这种依赖于操做系统mutex lock所实现的锁咱们称为"重量级锁"。jdk对于synchronized作的种种优化,其核心都是为了减小这种重量级锁的使用。jdk1.6之后,为了减小得到锁和释放锁带来的性能消耗,引入了"轻量级锁"和"偏向锁"

 

锁的状态总共有四种:无锁状态偏向锁轻量级锁重量级锁。随着锁的竞争,锁能够从偏向锁升级到轻量级锁,再升级的重量级锁(可是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。JDK 1.6中默认是开启偏向锁和轻量级锁的,咱们也能够经过-XX:-UseBiasedLocking来禁用偏向锁。

(1)偏向锁(自旋)

引入偏向锁是为了在无多线程竞争的状况下尽可能减小没必要要的轻量级锁执行路径,由于轻量级锁的获取及释放依赖屡次CAS原子指令,而偏向锁只须要在置换ThreadID的时候依赖一次CAS原子指令(因为一旦出现多线程竞争的状况就必须撤销偏向锁,因此偏向锁的撤销操做的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。咱们知道,轻量级锁是为了在线程交替执行同步块时提升性能,而偏向锁则是在只有一个线程执行同步块时进一步提升性能

(2)轻量级锁

“轻量级”是相对于使用操做系统互斥量来实现的传统锁而言的。可是,首先须要强调一点的是,轻量级锁并非用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减小传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程以前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的状况,若是存在同一时间访问同一锁的状况,就会致使轻量级锁膨胀为重量级锁。

 

轻量级锁也是一种多线程优化,它与偏向锁的区别在于,轻量级锁是经过CAS来避免进入开销较大的互斥操做,而偏向锁是在无竞争场景下彻底消除同步,连CAS也不执行

 

 

 

三 其余优化

一、适应性自旋(Adaptive Spinning):从轻量级锁获取的流程中咱们知道当线程在获取轻量级锁的过程当中执行CAS操做失败时,是要经过自旋来获取重量级锁的。问题在于,自旋是须要消耗CPU的,若是一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,若是还没获取到锁就进入阻塞状态。可是JDK采用了更聪明的方式——适应性自旋,简单来讲就是线程若是自旋成功了,则下次自旋的次数会更多,若是自旋失败了,则自旋的次数就会减小。

二、锁粗化(Lock Coarsening):锁粗化的概念应该比较好理解,就是将屡次链接在一块儿的加锁、解锁操做合并为一次,将多个连续的锁扩展成一个范围更大的锁。举个例子:

public class StringBufferTest {
    StringBuffer stringBuffer = new StringBuffer();

    public void append(){
        stringBuffer.append("a");
        stringBuffer.append("b");
        stringBuffer.append("c");
    }
}

  这里每次调用stringBuffer.append方法都须要加锁和解锁,若是虚拟机检测到有一系列连串的对同一个对象加锁和解锁操做,就会将其合并成一次范围更大的加锁和解锁操做,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

 三、锁消除(Lock Elimination):锁消除即删除没必要要的加锁操做。根据代码逃逸技术,若是判断到一段代码中,堆上的数据不会逃逸出当前线程,那么能够认为这段代码是线程安全的,没必要要加锁。看下面这段程序:

public class SynchronizedTest02 {

    public static void main(String[] args) {
        SynchronizedTest02 test02 = new SynchronizedTest02();
        //启动预热
        for (int i = 0; i < 10000; i++) {
            i++;
        }
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            test02.append("abc", "def");
        }
        System.out.println("Time=" + (System.currentTimeMillis() - start));
    }

    public void append(String str1, String str2) {
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }
}

虽然StringBuffer的append是一个同步方法,可是这段程序中的StringBuffer属于一个局部变量,而且不会从该方法中逃逸出去,因此其实这过程是线程安全的,能够将锁消除。

为了尽可能减小其余因素的影响,这里禁用了偏向锁(-XX:-UseBiasedLocking)。经过上面程序,能够看出消除锁之后性能仍是有比较大提高的。

 

 

四 总结

本文重点介绍了JDk中采用轻量级锁和偏向锁等对Synchronized的优化,可是这两种锁也不是彻底没缺点的,好比竞争比较激烈的时候,不但没法提高效率,反而会下降效率,由于多了一个锁升级的过程,这个时候就须要经过-XX:-UseBiasedLocking来禁用偏向锁。下面是这几种锁的对比:

 

参考: http://www.cnblogs.com/paddix/p/5405678.html

 

 

五 其余问题

1 synchronized 和ReentrantLock区别

实现上,synchronized是jvm层面实现的,能够经过一些监控工具监控synchronized的锁定,并且在代码执行出现异常时jvm会自动释放锁定。可是ReentrantLock则不行,彻底是经过jdk实现的,须要程序保证锁必定会释放,必须讲unLock放在finally{}中

功能上,ReentrantLock有锁投票,定时锁,中断锁,公平非公平锁等额外功能

(1) 定时锁

a) lock(), 若是获取了锁当即返回,若是别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁

b) tryLock(), 若是获取了锁当即返回true,若是别的线程正持有锁,当即返回false

c) tryLock(long timeout,TimeUnit unit), 若是获取了锁定当即返回true,若是别的线程正持有锁,等待给定的时间,在等待的过程当中,获取锁定返回true,若是等待超时,返回false

(2) 中断锁

lockInterruptibly若是获取了锁定当即返回,若是没有获取锁定,当前线程处于休眠状态,直到得到锁定,或者当前线程被别的线程中断

(3) 非公平锁

 reenTrantLock能够指定是公平锁仍是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先得到锁。 

性能上,在synchronized优化之前,synchronized的性能是比ReenTrantLock差不少的,( 由于采用的是cpu悲观锁,即线程得到是独占锁,独占锁意味着其余线程只能依靠阻塞来等待线程释放锁,而在cpu转换线程阻塞时会引发线程上下文切换,当有不少线程竞争锁的时候,会引发cpu频繁的上下文切换致使效率很低。)可是自从java1.6之后,synchronized引入了偏向锁,轻量级锁, 锁消除,锁粗化,适应性自旋等,二者的性能就差很少了,在两种方法均可用的状况下,官方甚至建议使用synchronized,其实synchronized的优化我感受就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

另外,ReentrantLock提供了一个Condition类,用来实现分组唤醒一批线程,而不是像synchronized要么随机唤醒一个线程要么唤醒所有线程。

 

2 synchronized修饰方法和修饰代码块时有何不一样

持有锁的对象不一样:

  1. 修饰方法时:this引用的当前实例持有锁
  2. 修饰代码块时:要指定一个对象,该对象持有锁

 有一个类这样定义:

public class SynchronizedTest
{
    public synchronized void method1(){}
    public synchronized void method2(){}
    public static synchronized void method3(){}
    public static synchronized void method4(){}
}

那么,有SynchronizedTest的两个实例a和b,对于一下的几个选项有哪些能被一个以上的线程同时访问呢?

A. a.method1() vs. a.method2()   instance_a instance_a
B. a.method1() vs. b.method1()    instance_a instance_b
C. a.method3() vs. b.method4()  Synchronized.class Synchronized.class
D. a.method3() vs. b.method3()  Synchronized.class Synchronized.class
E. a.method1() vs. a.method3()   instance_a Synchronized.class

答案是什么呢?BE

 

3 悲观锁和乐观锁

悲观锁: 悲观锁假设在最坏的状况下,而且确保其余线程不会干扰(获取正确的锁)的状况下才能执行下去。常见实现如独占锁等。安全性更高,但在中低并发程度下效率低

乐观锁: 乐观锁接触冲突检查机制来判断更新过程当中是否存在其余线程的干扰,若是存在,这个操做将失败,而且能够重试(也能够不重试)。常见实现如CAS等。乐观锁削弱了一致性,但中低并发程序下的效率大大提升。

 

4 异常时是否释放锁?同步是否具有继承性?

当一个线程执行的代码出现异常时,其所持有的锁会自动释放。同步不具备继承性(声明为synchronized的父类方法A,在子类中重写以后并不具有synchronized的特性)

 

5 是否了解自旋锁

自旋锁也常常用于线程(进程)之间的同步。通常来讲,咱们知道在线程A得到普通锁以后,若是再有线程B试图得到锁,那么这个线程将会挂起(阻塞)。那么咱们来考虑这样一种case: 若是两个线程资源竞争不是特别激烈,而处理器阻塞一个线程引发的线程上下文切换的代价高于等待资源的代价的时候(锁的已保持者保持锁时间比较短),那么线程b能够不放弃cpu时间片,而是在"原地"盲等,直到锁的持有者释放了该锁,这就是自选锁的原理,能够自旋锁是一种非阻塞锁

自旋锁可能的问题:

1 过多占据cpu的时间: 若是锁的当前持有者长时间不释放该锁,那么等待过程将长时间地占据cpu时间片,致使cpu资源浪费。所以能够设定一个超时时间,过时等待着放弃cpu时间片

2 误用有死锁风险: 当一个线程连续两次得到自旋锁(如递归),那么第一次这个线程得到了该锁,当第二次试图加锁的时候,检测到锁已被占用(被本身),那么这时线程会一直等待本身释放该锁,而不能继续执行形成死锁。所以递归程序使用自旋锁应该遵循如下原则: 递归决不能在持有自旋锁时调用它本身,也决不能在递归调用时试图得到相同的自旋锁。

 

6 volatile变量和atomic变量有什么不一样

volatile变量能够确保先行关系,如写操做会发生在后续的读操做以前,但它并不能保证原子性。例如volatile修饰count变量那么count++操做就不是原子性的。而AtomicInteger类提供的atomic方法可让这种操做具备原子性,如getAndIncrement()方法会原子性地进行增量操做把当前值+1。

 

7 公平锁和非公平锁

 在Java的ReentrantLock构造函数中提供了两种锁:建立公平锁和非公平锁(默认)。代码以下:

public ReentrantLock() {
       sync = new NonfairSync();
}

 public ReentrantLock(boolean fair) {
       sync = fair ? new FairSync() : new NonfairSync();
}

在公平的锁上, 线程按照他们发出请求的顺序获取锁. 但在非公平锁上,则容许'插队': 当一个线程请求非公平锁时,若是在发出请求的同时该锁变成可用状态,那么这个线程会跳过队列中全部的等待线程而得到锁。非公平锁提倡插队行为,可是没法防止某个线程在合适的时候进行插队。

在公平的锁中,若是有另外一个线程持有锁或者有其余线程在等待队列中等待这个锁,那么新发出的请求的线程将被放入到队列中。而非公平锁上,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中。

非公平锁性能高于公平锁性能的缘由:

在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。

假设线程A持有一个锁,而且线程B请求这个锁。因为锁被A持有,所以B将被挂起。当A释放锁时,B将被唤醒,所以B会再次尝试获取这个锁。与此同时,若是线程C也请求这个锁,那么C极可能会在B被彻底唤醒以前得到、使用以及释放这个锁。这样就是一种共赢的局面:B得到锁的时刻并无推迟,C更早的得到了锁,而且吞吐量也提升了。

当持有锁的时间相对较长或者请求锁的平均时间间隔较长,应该使用公平锁。在这些状况下,插队带来的吞吐量提高(当锁处于可用状态时,线程却还处于被唤醒的过程当中)可能不会出现。

相关文章
相关标签/搜索