java并发编程:深入了解synchronized


简介

synchronized是Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。同时它还保证了共享变量的内存可见性。

synchronized使用

synchronized可以修饰普通方法,静态方法和代码块。

  1. 普通同步方法,锁是当前实例对象(不同实例对象之间的锁互不影响)。
  2. 静态同步方法,锁是当前类的class对象
  3. 同步方法块,锁是括号里面的对象

synchronized实现的原理

synchronized的功能是基于monitorentermonitorexit指令实现的。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

什么是monitor

  1. Java Monitor 相当于监视器,一把打开大门的钥匙,也可认为是一个许可证。只有拿到许可证,才可以操作。
  2. 同时也相当于一个同步工具,操作系统中的互斥量(mutex),值为1的信号量。

synchronized对锁的优化

由于JDK之前的版本 synchronized性能比较差,JDK 1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

锁粗化

也就是减少不必要的紧连在一起的unlock,lock操作,将多个连续的锁扩展成一个范围更大的锁。

锁消除

由来
为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制。但是,在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。

原理
锁消除的依据是逃逸分析的数据支持。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于我们程序员来说这还不清楚么?我们会在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?我们虽然没有显示使用锁,但是我们在使用一些 JDK 的内置 API 时,如 StringBuffer、Vector、HashTable 等,这个时候会存在隐性的加锁操作。

逃逸分析是java虚拟机比较前言的优化技术。它并不是直接的优化技术的手段,而是为其他优化技术手段提供依据。

逃逸分析,主要是分析对象的动态作用范围,比如在一个方法里一个对象创建后,在调用外部方法时,该对象作为参数传递到其他方法中,成为方法逃逸;当被其他线程访问,如赋值给其他线程中的实例变量,则成为线程逃逸。

如果可以证明一个对象不会出现方法或者线程逃逸,也就是说别的方法或者线程无法访问到这个对象,则可以对这个对象做优化处理。

逃逸分析的 JVM 参数如下:

开启逃逸分析:-XX:+DoEscapeAnalysis
关闭逃逸分析:-XX:-DoEscapeAnalysis
显示分析结果:-XX:+PrintEscapeAnalysis
逃逸分析技术在 Java SE 6u23+ 开始支持,并默认设置为启用状态,可以不用额外加这个参数。

锁粗化 & 锁消除 栗子

    public synchronized StringBuffer append(int i) {
        toStringCache = null;
        super.append(i);
        return this;
    }

   /**
     * 锁粗化
     */
    public static String synchronizedCoarseningDemo(StringBuffer sb){
        //JVM 检测到对同一个对象(StringBuffer)连续加锁、解锁操作,
        // 会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到 for 循环之外。
        for (int i = 0; i < 1000000; i++) {
            sb.append(i);
            sb.append("&");
        }
        return sb.toString();
    }

    /**
     * 锁消除
     */
    public static String synchronizedEliminationDemo(){
        //jvm通过逃逸分析判断 对象StringBuffer,不存在锁竞争,
        // 所以会把append方法加锁(synchronized)操作去掉
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < 1000000; i++) {
            sb.append(i);
            sb.append("&");
        }
        return sb.toString();
    }

自旋锁

由来

线程的阻塞和唤醒,需要 CPU 从用户态转为核心态。频繁的阻塞和唤醒对 CPU 来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时,我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间。为了这一段很短的时间,频繁地阻塞和唤醒线程是非常不值得的

定义

所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。

自旋等待不能替代阻塞,先不说对处理器数量的要求(多核,貌似现在没有单核的处理器了),虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,典型的占着茅坑不拉屎,这样反而会带来性能上的浪费。

所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。

自旋锁在 JDK 1.4.2 中引入,默认关闭,但是可以使用 -XX:+UseSpinning 开开启。
在 JDK1.6 中默认开启。同时自旋的默认次数为 10 次,可以通过参数 -XX:PreBlockSpin 来调整。

适应自旋锁

JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

轻量级锁

引入轻量级锁的主要目的是:在多线程竞争不激烈的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。需要注意的是轻量级锁并不是取代重量级锁,而是在大多数情况下同步块并不会出现严重的竞争情况,所以引入轻量级锁可以减少重量级锁对线程的阻塞带来的开销。

偏向锁

引入偏向锁的主要目的是:为了在无多线程竞争的情况下尽量减少不必须要的轻量级锁执行路径。其实在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获取,所以引入偏向锁就可以减少很多不必要的性能开销和上下文切换。

锁的升级/锁膨胀

synchronized 同步锁一共具有四种状态:无锁、偏向锁、轻量级锁、重量级锁,他们会随着竞争情况逐渐升级,此过程为不可逆。所以 synchronized 锁膨胀过程其实就是无锁 → 偏向锁 → 轻量级锁 → 重量级锁的一个过程。
注意,锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
锁膨胀原理地址

synchronized 共享变量的内存可见性

java内存模型对加锁&解锁的两个规定

1、线程解锁前,必须把共享变量的最新值刷新到主内存中;
2、线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量需要从主内存中重新读取最新的值(加锁与解锁需要统一把锁)

线程执行互斥锁代码的过程:

1.获得互斥锁
2.清空工作内存
3.从主内存拷贝最新变量副本到工作内存
4.执行代码块
5.将更改后的共享变量的值刷新到主内存中
6.释放互斥锁

参考链接:
芋道源码
synchronized:【
http://cmsblogs.com/?p=5812


http://cmsblogs.com/?p=2071

逃逸分析: https://blog.csdn.net/u010503427/article/details/89188013
内存可见性 :https://blog.csdn.net/ArtisticProgramming/article/details/107103934