Synchronized 实现原理

Java的锁

锁的内存语义

  1. 锁可让临界区互斥执行,还可让释放锁的线程向同一个锁的线程发送消息
  2. 锁的释放要遵循Happens-before原则(锁规则:解锁必然发生在随后的加锁以前)
  3. 锁在Java中的具体表现是 Synchronized 和 Lock

锁的释放

线程A释放锁后,会将共享变动操做刷新到主内存中java

锁的获取

线程B获取锁时,JMM会将该线程的本地内存置为无效,被监视器保护的临界区代码必须从主内存中读取共享变量数组

锁的释放与获取

  1. 锁获取与volatile读有相同的内存语义
  2. 线程A释放一个锁,实质是线程A告知下一个获取到该锁的某个线程其已变动该共享变量
  3. 线程B获取一个锁,实质是线程B获得了线程A告知其(在释放锁以前)变动共享变量的消息线程
  4. A释放锁,随后线程B竞争到该锁,实质是线程A经过主内存向线程B发消息告知其变动了共享变量

Synchronized的综述

  1. 同步机制: synchronized是Java同步机制的一种实现,即互斥锁机制,它所得到的锁叫作互斥锁
  2. 互斥锁: 指的是每一个对象的锁一次只能分配给一个线程,同一时间只能由一个线程占用
  3. 做用: synchronized用于保证同一时刻只能由一个线程进入到临界区,同时保证共享变量的可见性、原子性和有序性
  4. 使用: 当一个线程试图访问同步代码方法(块)时,它首先必须获得锁,退出或抛出异常时必须释放锁

Synchronized的使用

Synchronized的三种应用方式

使用同步代码块的好处在于其余线程仍能够访问非synchronized(this)的同步代码块

Synchronized的使用规则

/** * 先定义一个测试模板类 * 这里补充一个知识点:Thread.sleep(long)不会释放锁 * 读者可参见笔者的`并发番@Thread一文通` */ 
public class SynchronizedDemo {
    public static synchronized void staticMethod(){
        System.out.println(Thread.currentThread().getName() + "访问了静态同步方法staticMethod");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "结束访问静态同步方法staticMethod");
    }
    public static void staticMethod2(){
        System.out.println(Thread.currentThread().getName() + "访问了静态同步方法staticMethod2");
        synchronized (SynchronizedDemo.class){
            System.out.println(Thread.currentThread().getName() + "在staticMethod2方法中获取了SynchronizedDemo.class");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public synchronized void synMethod(){
        System.out.println(Thread.currentThread().getName() + "访问了同步方法synMethod");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "结束访问同步方法synMethod");
    }
    public synchronized void synMethod2(){
        System.out.println(Thread.currentThread().getName() + "访问了同步方法synMethod2");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "结束访问同步方法synMethod2");
    }
    public void method(){
        System.out.println(Thread.currentThread().getName() + "访问了普通方法method");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "结束访问普通方法method");
    }
    private Object lock = new Object();
    public void chunkMethod(){
        System.out.println(Thread.currentThread().getName() + "访问了chunkMethod方法");
        synchronized (lock){
            System.out.println(Thread.currentThread().getName() + "在chunkMethod方法中获取了lock");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public void chunkMethod2(){
        System.out.println(Thread.currentThread().getName() + "访问了chunkMethod2方法");
        synchronized (lock){
            System.out.println(Thread.currentThread().getName() + "在chunkMethod2方法中获取了lock");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public void chunkMethod3(){
        System.out.println(Thread.currentThread().getName() + "访问了chunkMethod3方法");
        //同步代码块
        synchronized (this){
            System.out.println(Thread.currentThread().getName() + "在chunkMethod3方法中获取了this");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public void stringMethod(String lock){
        synchronized (lock){
            while (true){
                System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
复制代码

普通方法与同步方法调用互不关联

当一个线程进入同步方法时,其余线程能够正常访问其余非同步方法缓存

public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> {
        //调用普通方法
        synDemo.method();
    });
    Thread thread2 = new Thread(() -> {
        //调用同步方法
        synDemo.synMethod();
    });
    thread1.start();
    thread2.start();
}

复制代码

输出:安全

Thread-1访问了同步方法synMethod
    Thread-0访问了普通方法method
    Thread-0结束访问普通方法method
    Thread-1结束访问同步方法synMethod
复制代码

分析:经过结果可知,普通方法和同步方法是非阻塞执行的数据结构

全部同步方法只能被一个线程访问

当一个线程执行同步方法时,其余线程不能访问任何同步方法多线程

public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> {
        synDemo.synMethod();
        synDemo.synMethod2();
    });
    Thread thread2 = new Thread(() -> {
        synDemo.synMethod2();
        synDemo.synMethod();
    });
    thread1.start();
    thread2.start();
}
复制代码

输出:并发

Thread-0访问了同步方法synMethod
    Thread-0结束访问同步方法synMethod
    Thread-0访问了同步方法synMethod2
    Thread-0结束访问同步方法synMethod2
    Thread-1访问了同步方法synMethod2
    Thread-1结束访问同步方法synMethod2
    Thread-1访问了同步方法synMethod
    Thread-1结束访问同步方法synMethod
复制代码

分析:经过结果可知,任务的执行是阻塞的,显然Thread-1必须等待Thread-0执行完毕以后才能继续执行app

同一个锁的同步代码块同一时刻只能被一个线程访问

当同步代码块都是同一个锁时,方法能够被全部线程访问,但同一个锁的同步代码块同一时刻只能被一个线程访问ide

public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> {
        //调用同步块方法
        synDemo.chunkMethod();
        synDemo.chunkMethod2();
    });
    Thread thread2 = new Thread(() -> {
        //调用同步块方法
        synDemo.chunkMethod();
        synDemo.synMethod2();
    });
    thread1.start();
    thread2.start();
}
复制代码

输出:工具

Thread-0访问了chunkMethod方法
Thread-1访问了chunkMethod方法
Thread-0在chunkMethod方法中获取了lock  
...停顿等待...
Thread-1在chunkMethod方法中获取了lock
...停顿等待...
Thread-0访问了chunkMethod2方法
Thread-0在chunkMethod2方法中获取了lock
...停顿等待...
Thread-1访问了chunkMethod2方法
Thread-1在chunkMethod2方法中获取了lock
复制代码

分析可知:

  1. 即便普通方法有同步代码块,但方法的访问是非阻塞的,任何线程均可以自由进入
  2. 对于同一个锁的同步代码块的访问必定是阻塞的

线程间同时访问同一个锁的多个同步代码的执行顺序不定

线程间同时访问同一个锁多个同步代码的执行顺序不定,即便是使用同一个对象锁

public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> {
        //调用同步块方法
        synDemo.chunkMethod();
        synDemo.chunkMethod2();
    });
    Thread thread2 = new Thread(() -> {
        //调用同步块方法
        synDemo.chunkMethod2();
        synDemo.chunkMethod();
    });
    thread1.start();
    thread2.start();
}
---------------------
//输出:
Thread-0访问了chunkMethod方法
Thread-1访问了chunkMethod2方法
Thread-0在chunkMethod方法中获取了lock
...停顿等待...
Thread-0访问了chunkMethod2方法
Thread-1在chunkMethod2方法中获取了lock
...停顿等待...
Thread-1访问了chunkMethod方法
Thread-0在chunkMethod2方法中获取了lock
...停顿等待...
Thread-1在chunkMethod方法中获取了lock

//分析可知:
//现象:对比20行、22行和24行、25行可知,虽然是同一个lock对象,但其不一样代码块的访问是非阻塞的
//缘由:根源在于锁的释放和从新竞争,当Thread-0访问完chunkMethod方法后会先释放锁,这时Thread-1就有机会能获取到锁从而优先执行,依次类推到24行、25行时,Thread-0又从新获取到锁优先执行了
//注意:但有一点是必须的,对于同一个锁的同步代码块的访问必定是阻塞的
//补充:同步方法之因此会被所有阻塞,是由于synDemo对象一直被线程在内部把持住就没释放过
复制代码

不一样锁之间访问非阻塞

因为三种使用方式的锁对象都不同,所以相互之间不会有任何影响但有两种状况除外:

  1. 当同步代码块使用的Class对象和类对象一致时属于同一个锁
  2. 当同步代码块使用的是this,即与同步方法使用锁属于同一个锁
public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> synDemo.chunkMethod() );
    Thread thread2 = new Thread(() -> synDemo.chunkMethod3());
    Thread thread3 = new Thread(() -> staticMethod());
    Thread thread4 = new Thread(() -> staticMethod2());
    thread1.start();
    thread2.start();
    thread3.start();
    thread4.start();
}
---------------------
//输出:
Thread-1访问了chunkMethod3方法
Thread-1在chunkMethod3方法中获取了this
Thread-2访问了静态同步方法staticMethod
Thread-0访问了chunkMethod方法
Thread-0在chunkMethod方法中获取了lock
Thread-3访问了静态同步方法staticMethod2
...停顿等待...
Thread-2结束访问静态同步方法staticMethod
Thread-3在staticMethod2方法中获取了SynchronizedDemo.class
//分析可知:
//现象:虽然是同一个lock对象,但其不一样代码块的访问是非阻塞的
//缘由:根源在于锁的释放和从新竞争,当Thread-0访问完chunkMethod方法后会先释放锁,这时Thread-1就有机会能获取到锁从而优先执行,,Thread-0又从新获取到锁优先执行了
复制代码

Synchronized的可重入性

重入锁:当一个线程再次请求本身持有对象锁的临界资源时,这种状况属于重入锁,请求将会成功实现:一个线程获得一个对象锁后再次请求该对象锁,是容许的,每重入一次,monitor进入次数+1

public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> {
        synDemo.synMethod();
        synDemo.synMethod2();
    });
    Thread thread2 = new Thread(() -> {
        synDemo.synMethod2();
        synDemo.synMethod();
    });
    thread1.start();
    thread2.start();
}
---------------------
//输出:
Thread-0访问了同步方法synMethod
Thread-0结束访问同步方法synMethod
Thread-0访问了同步方法synMethod2
Thread-0结束访问同步方法synMethod2
Thread-1访问了同步方法synMethod2
Thread-1结束访问同步方法synMethod2
Thread-1访问了同步方法synMethod
Thread-1结束访问同步方法synMethod
//分析:在代码块中继续调用了当前实例对象的另一个同步方法,再次请求当前实例锁时,将被容许,进而执行方法体代码,这就是重入锁最直接的体现
复制代码

Synchronized与String锁

隐患:因为在JVM中具备String常量池缓存的功能,所以相同字面量是同一个锁!!!注意:严重不推荐将String做为锁对象,而应该改用其余非缓存对象提示:对字面量有疑问的话请先回顾一下String的基础

public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> synDemo.stringMethod("sally"));
    Thread thread2 = new Thread(() -> synDemo.stringMethod("sally"));
    thread1.start();
    thread2.start();
}
---------------------
//输出:
Thread-0
Thread-0
Thread-0
Thread-0
...死循环...
//分析:输出结果永远都是Thread-0的死循环,也就是说另外一个线程,即Thread-1线程根本不会运行
//缘由:同步块中的锁是同一个字面量
复制代码

Synchronized与不可变锁

隐患:当使用不可变类对象(finalClass)做为对象锁时,使用synchronized一样会有并发问题缘由:因为不可变特性,看成为锁但同步块内部仍然有计算操做,会生成一个新的锁对象注意:严重不推荐将final Class做为锁对象时仍对其有计算操做补充:虽然String也是final Class,但它的缘由倒是字面量常量池

public class SynchronizedDemo {
    static Integer i = 0;   //Integer是final Class
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int j = 0;j<10000;j++){
                    synchronized (i){
                        i++;
                    }
                }
            }
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(i);
    }
}
---------------------
//输出:
14134
//分析:跟预想中的20000不一致,当使用Integer做为对象锁时但还有计算操做就会出现并发问题
复制代码咱们经过反编译发现执行i++操做至关于执行了i = Integer.valueOf(i.intValue()+1)经过查看Integer的valueOf方法实现可知,其每次都new了一个新的Integer对象,锁变了有木有!!!
复制代码
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);  //每次都new一个新的锁有木有!!!
}
复制代码

Synchronized与死锁

死锁:当线程间须要相互等待对方已持有的锁时,就造成死锁,进而产生死循环

public static void main(String[] args) {
    Object lock = new Object();
    Object lock2 = new Object();
    Thread thread1 = new Thread(() -> {
        synchronized (lock){
            System.out.println(Thread.currentThread().getName() + "获取到lock锁");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock2){
                System.out.println(Thread.currentThread().getName() + "获取到lock2锁");
            }
        }
    });
    Thread thread2 = new Thread(() -> {
        synchronized (lock2){
            System.out.println(Thread.currentThread().getName() + "获取到lock2锁");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock){
                System.out.println(Thread.currentThread().getName() + "获取到lock锁");
            }
        }
    });
    thread1.start();
    thread2.start();
}
---------------------
//输出:
Thread-1获取到lock2锁
Thread-0获取到lock锁
.....
//分析:线程0得到lock锁,线程1得到lock2锁,但以后因为两个线程还要获取对方已持有的锁,但已持有的锁都不会被双方释放,线程"假死",没法往下执行,从而造成死循环,即死锁,以后一直在作无用的死循环,严重浪费系统资源
复制代码

咱们用 jstack 查看一下这个任务的各个线程运行状况,能够发现两个线程都被阻塞 BLOCKED

咱们很明显的发现,Java-level=deadlock,即死锁,两个线程相互等待对方的锁

Synchronized实现原理

Synchronization

  1. 在JVM中,同步的实现是经过监视器锁的进入和退出实现的,要么显示得经过monitorenter 和 monitorexit指令实现,要么隐示地经过方法调用和返回指令实现
  2. 对于Java代码来讲,或许最经常使用的同步实现就是同步方法(代码块)。其中同步代码块是经过使用 monitorenter 和 monitorexit 实现的,而同步方法倒是使用 ACC_SYNCHRONIZED 标记符隐示的实现,原理是经过方法调用指令检查该方法在常量池中是否包含 ACC_SYNCHRONIZED 标记符

反编译

预准备

为了能直观了解Synchronized的工做原理,咱们经过反编译SynchronizedDeme类的class文件的方式看看都发生了什么

public class SynchronizedDemo {
    public static synchronized void staticMethod() throws InterruptedException {
        System.out.println("静态同步方法开始");
        Thread.sleep(1000);
        System.out.println("静态同步方法结束");
    }
    public synchronized void method() throws InterruptedException {
        System.out.println("实例同步方法开始");
        Thread.sleep(1000);
        System.out.println("实例同步方法结束");
    }
    public synchronized void method2() throws InterruptedException {
        System.out.println("实例同步方法2开始");
        Thread.sleep(3000);
        System.out.println("实例同步方法2结束");
    }
    public static void main(String[] args) {
        final SynchronizedDemo synDemo = new SynchronizedDemo();
        Thread thread1 = new Thread(() -> {
            try {
               synDemo.method();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread thread2 = new Thread(() -> {
            try {
                synDemo.method2();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread1.start();
        thread2.start();
    }
}
复制代码

生成.class文件

javac -encoding UTF-8 SynchronizedDemo.java

最终咱们将获得一个 .class 文件,即 SynchronizedDemo.class

javap反编译

javap -v SynchronizedDemo

复制代码经过反编译咱们会获得常量池、同步方法、同步代码块的不一样编译结果

常量池图

常量池除了会包含基本类型和字符串及数组的常量值外,还包含以文本形式出现的符号引用:

类和接口的全限定名

字段的名称和描述符

方法和名称和描述符
复制代码

同步方法图示

同步方法会包含一个ACC_SYNCHCRONIZED标记符

同步代码块图示

同步代码块会在代码中插入 monitorenter 和 monitorexist 指令

同步代码块同步原理

monitor监视器

  1. 每一个对象都有一个监视器,在同步代码块中,JVM经过monitorenter和monitorexist指令实现同步锁的获取和释放功能
  2. 当一个线程获取同步锁时,便是经过获取monitor监视器进而等价为获取到锁
  3. monitor的实现相似于操做系统中的管程

monitorenter指令

每一个对象都有一个监视器。当该监视器被占用时便是锁定状态(或者说获取监视器便是得到同步锁)。线程执行monitorenter指令时会尝试获取监视器的全部权,过程以下:

  1. 若该监视器的进入次数为0,则该线程进入监视器并将进入次数设置为1,此时该线程即为该监视器的全部者
  2. 若线程已经占有该监视器并重入,则进入次数+1
  3. 若其余线程已经占有该监视器,则线程会被阻塞直到监视器的进入次数为0,以后线程间会竞争获取该监视器的全部权只有首先得到锁的线程才能容许继续获取多个锁

monitorexit指令

执行monitorexit指令将遵循如下步骤:

  1. 执行monitorexit指令的线程必须是对象实例所对应的监视器的全部者
  2. 指令执行时,线程会先将进入次数-1,若-1以后进入次数变成0,则线程退出监视器(即释放锁)其余阻塞在该监视器的线程能够从新竞争该监视器的全部权

实现原理

  1. 在同步代码块中,JVM经过monitorenter和monitorexist指令实现同步锁的获取和释放功能
  2. monitorenter指令是在编译后插入到同步代码块的开始位置
  3. monitorexit指令是插入到方法结束处和异常处
  4. JVM要保证每一个monitorenter必须有对应的monitorexit与之配对
  5. 任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态
  6. 线程执行monitorenter指令时,将会尝试获取对象所对应的monitor的全部权,即尝试得到对象的锁
  7. 线程执行monitorexit指令时,将会将进入次数-1直到变成0时释放监视器
  8. 同一时刻只有一个线程可以成功,其它失败的线程会被阻塞,并放入到同步队列中,进入BLOCKED状态

补充

因为 wait/notify 等方法底层实现是基于监视器,所以只有在同步方法(块)中才能调用wait/notify等方法,不然会抛出 java.lang.IllegalMonitorStateException 的异常的缘由

同步方法同步原理

区别于同步代码块的监视器实现,同步方法经过使用 ACC_SYNCHRONIZED 标记符隐示的实现原理是经过方法调用指令检查该方法在常量池中是否包含 ACC_SYNCHRONIZED 标记符,若是有,JVM 要求线程在调用以前请求锁

进阶原理

Monitor Obejct模式

Monitor Obejct模式综述

Monitor实际上是一种同步工具,也能够说是一种同步机制,它一般被描述为一个对象,主要特色是互斥和信号机制

  1. 互斥: 一个Monitor锁在同一时刻只能被一个线程占用,其余线程没法占用
  2. 信号机制(signal): 占用Monitor锁失败的线程会暂时放弃竞争并等待某个谓词成真(条件变量),但该条件成立后,当前线程会经过释放锁通知正在等待这个条件变量的其余线程,让其能够从新竞争锁

Mesa派的signal机制

  1. Mesa派的signal机制又称"Non-Blocking condition variable"
  2. 占有Monitor锁的线程发出释放通知时,不会当即失去锁,而是让其余线程等待在队列中,从新竞争锁
  3. 这种机制里,等待者拿到锁后不能肯定在这个时间差里是否有别的等待者进入过Monitor,所以不能保证谓词必定为真,因此对条件的判断必须使用while
  4. Java中采用就是Mesa派的singal机制,即所谓的notify

Monitor Obejct模式结构

在 Monitor Object 模式中,主要有四种类型的参与者:

Monitor Obejct模式协做过程

  1. 同步方法的调用和串行化:

    • 当客户线程调用监视者对象的同步方法时,必须首先获取它的监视锁
    • 只要该监视者对象有其余同步方法正在被执行,获取操做便不会成功
    • 当监视者对象已被线程占用时(即同步方法正被执行),客户线程将被阻塞直到它获取监视锁
    • 当客户线程成功获取监视锁后,进入临界区,执行方法实现的服务
    • 一旦同步方法完成执行,监视锁会被自动释放,目的是使其余客户线程有机会调用执行该监视者对象的同步方法
  2. 同步方法线程挂起:

    • 若是调用同步方法的客户线程必须被阻塞或是有其余缘由不能马上进行,它可以在一个监视条件(Monitor Condition)上等待,这将致使该客户线程暂时释放监视锁,并被挂起在监视条件上
  3. 监视条件通知:

    • 一个客户线程可以通知一个监视条件,目的是通知阻塞在该监视条件(该监视锁)的线程恢复运行
  4. 同步方法线程恢复:

    • 一旦一个早先被挂起在监视条件上的同步方法线程获取通知,它将继续在最初的等待监视条件的点上执行
    • 在被通知线程被容许恢复执行同步方法以前,监视锁将自动被获取(线程间自动相互竞争锁)

对象头

JVM内存中的对象

在JVM中,对象在内存中的布局分红三块区域:对象头、示例数据和对齐填充

对象头: 对象头主要存储 Mark Word(对象的hashCode、锁信息)、类型指针、数组长度(如果数组的话)等信息

示例数据:存放类的属性数据信息,包括父类的属性信息,若是是数组的实例部分还包括数组长度,这部份内存按4字节对齐

填充数据:因为JVM要求对象起始地址必须是8字节的整数倍,当不知足8字节时会自动填充(所以填充数据并非必须的,仅仅是为了字节对齐)

对象头综述

  1. synchcronized的锁是存放在Java对象头中的

  2. 若是对象是数组类型,JVM用3个字宽(Word)存储对象头,不然是用2个子宽在32位虚拟机中,1字宽等于4个字节,即32bit;64位的话就是8个字节,即64bit

Mark Word的存储结构

32位JVM的Mark Word的默认存储结构(无锁状态)

在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化(32位)

64位JVM的Mark Word的默认存储结构(对于32位无锁状态,有25bit没有使用)

Monitor Record

Monitor Record综述

  1. MonitorRecord(统一简称MR)是Java线程私有的数据结构,每个线程都有一个可用MR列表,同时还有一个全局的可用列表
  2. 一个被锁住的对象都会和一个MR关联(对象头的MarkWord中的LockWord指向MR的起始地址)
  3. MR中有一个Owner字段存放拥有该锁的线程的惟一标识,表示该锁被这个线程占用

Monitor Record结构

Monitor Record工做原理

  1. 线程若是得到监视锁成功,将成为该监视锁对象的拥有者
  2. 在任一时刻,监视器对象只属于一个活动线程(Owner)
  3. 拥有者能够调用wait方法自动释放监视锁,进入等待状态

锁优化

自旋锁

  1. 痛点:因为线程的阻塞/唤醒须要CPU在用户态和内核态间切换,频繁的转换对CPU负担很重,进而对并发性能带来很大的影响
  2. 现象:经过大量分析发现,对象锁的锁状态一般只会持续很短一段时间,不必频繁地阻塞和唤醒线程
  3. 原理:经过执行一段无心义的空循环让线程等待一段时间,不会被当即挂起,看持有锁的线程是否很快释放锁,若是锁很快被释放,那当前线程就有机会不用阻塞就能拿到锁了,从而减小切换,提升性能
  4. 隐患:若锁能很快被释放,那么自旋效率就很好(真正执行的自旋次数越少效率越好,等待时间就少);但如果锁被一直占用,那自旋其实没有作任何有意义的事但又白白占用和浪费了CPU资源,反而形成资源浪费
  5. 注意:自旋次数必须有个限度(或者说自旋时间),若是超过自旋次数(时间)还没得到锁,就要被阻塞挂起
  6. 使用: JDK1.6以上默认开启-XX:+UseSpinning,自旋次数可经过-XX:PreBlockSpin调整,默认10次

自适应自旋锁

  1. 痛点:因为自旋锁只能指定固定的自旋次数,但因为任务的差别,致使每次的最佳自旋次数有差别
  2. 原理:经过引入"智能学习"的概念,由前一次在同一个锁上的自旋时间和锁的持有者的状态来决定自旋的次数,换句话说就是自旋的次数不是固定的,而是能够经过分析上次得出下次,更加智能
  3. 实现:若当前线程针对某锁自旋成功,那下次自旋此时可能增长(由于JVM认为此次成功是下次成功的基础),增长的话成功概率可能更大;反正,若自旋不多成功,那么自旋次数会减小(减小空转浪费)甚至直接省略自旋过程,直接阻塞(由于自旋彻底没有意义,还不如直接阻塞)
  4. 补充:有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,JVM对锁的情况预测会愈来愈准确,JVM会变得愈来愈智能

阻塞锁

阻塞锁

  1. 加锁成功:当出现锁竞争时,只有得到锁的线程可以继续执行
  2. 加锁失败:竞争失败的线程会由running状态进入blocking状态,并被放置到与目标锁相关的一个等待队列中
  3. 解锁:当持有锁的线程退出临界区,释放锁后,会将等待队列中的一个阻塞线程唤醒,令其从新参与到锁竞争中

公平锁

公平锁就是得到锁的顺序按照先到先得的原则,从实现上说,要求当一个线程竞争某个对象锁时,只要这个锁的等待队列非空,就必须把这个线程阻塞并塞入队尾(插入队尾通常经过一个CAS操做保持插入过程当中没有锁释放)

非公平锁

相对的,非公平锁场景下,每一个线程都先要竞争锁,在竞争失败或当前已被加锁的前提下才会被塞入等待队列,在这种实现下,后到的线程有可能无需进入等待队列直接竞争到锁(随机性)

锁粗化

  1. 痛点:屡次链接在一块儿的加锁、解锁操做会形成
  2. 原理:将屡次链接在一块儿的加锁、解锁操做合并为一次,将多个连续的锁扩展成一个范围更大的锁
  3. 使用:将多个彼此靠近的同步块合同在一个同步块 或 把多个同步方法合并为一个方法
  4. 补充:在JDK内置的API中,例如StringBuffer、Vector、HashTable都会存在隐性加锁操做,可合并
/** * StringBuffer是线程安全的字符串处理类 * 每次调用stringBuffer.append方法都须要加锁和解锁,若是虚拟机检测到有一系列连串的对同一个对象加锁和解锁操做,就会将其合并成一次范围更大的加锁和解锁操做,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁 */
StringBuffer stringBuffer = new StringBuffer();
public void append(){
    stringBuffer.append("kira");
    stringBuffer.append("sally");
    stringBuffer.append("mengmeng");
}
复制代码

锁消除

  1. 痛点:根据代码逃逸技术,若是判断到一段代码中,堆上的数据不会逃逸出当前线程,那么能够认为这段代码是线程安全的,没必要要加锁
  2. 原理: JVM在编译时经过对运行上下文的描述,去除不可能存在共享资源竞争的锁,经过这种方式消除无用锁,即删除没必要要的加锁操做,从而节省开销
  3. 使用: 逃逸分析和锁消除分别可使用参数-XX:+DoEscapeAnalysis和-XX:+EliminateLocks(锁消除必须在-server模式下)开启
  4. 补充:在JDK内置的API中,例如StringBuffer、Vector、HashTable都会存在隐性加锁操做,可消除
/** * 好比执行10000次字符串的拼接 */
public static void main(String[] args) {
    SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
    for (int i = 0 ; i < 10000 ; i++){
        synchronizedDemo.append("kira","sally");
    }
}
public void append(String str1,String str2){
    //因为StringBuffer对象被封装在方法内部,不可能存在共享资源竞争的状况
    //所以JVM会认为该加锁是无心义的,会在编译期就删除相关的加锁操做
    //还有一点特别要注明:明知道不会有线程安全问题,代码阶段就应该使用StringBuilder
    //不然在没有开启锁消除的状况下,StringBuffer不会被优化,性能可能只有StringBuilder的1/3
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append(str1).append(str2);
}/** 复制代码

锁的升级

  1. 从JDK1.6开始,锁一共有四种状态:无锁状态、偏向锁状态、轻量锁状态、重量锁状态

  2. 锁的状态会随着竞争状况逐渐升级,锁容许升级但不容许降级

  3. 不容许降级的目的是提升得到锁和释放锁的效率

  4. 后面会经过倒序的方式,即重量级锁->轻量级锁->偏向锁进行讲解,由于一般后者是前者的优化

锁的升级过程

重量级锁

  1. 重量级锁经过对象内部的monitor实现(见上文的Monitor Object模式)
  2. monitor的本质是依赖于底层操做系统的MutexLock实现,操做系统实现线程间的切换是经过用户态与内核态的切换完成的,而切换成本很高
  3. MutexLock最核心的理念就是尝试获取锁.若可获得就占有.若不能,就进入睡眠等待

轻量级锁

轻量级锁综述

  1. 痛点:因为线程的阻塞/唤醒须要CPU在用户态和内核态间切换,频繁的转换对CPU负担很重,进而对并发性能带来很大的影响
  2. 主要目的: 在没有多线程竞争的前提下,减小传统的重量级锁使用操做系统互斥量产生的性能消耗
  3. 升级时机: 当关闭偏向锁功能或多线程竞争偏向锁会致使偏向锁升级为轻量级锁
  4. 原理: 在只有一个线程执行同步块时进一步提升性能
  5. 数据结构: 包括指向栈中锁记录的指针、锁标志位

轻量级锁流程图

线程1和线程2同时争夺锁,并致使锁膨胀成重量级锁

轻量级锁加锁

  1. 线程在执行同步块以前,JVM会先在当前线程的栈帧中建立用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中(Displaced Mark Word-即被取代的Mark Word)作一份拷贝
  2. 拷贝成功后,线程尝试使用CAS将对象头的Mark Word替换为指向锁记录的指针(将对象头的Mark Word更新为指向锁记录的指针,并将锁记录里的Owner指针指向Object Mark Word)
  3. 若是更新成功,当前线程得到锁,继续执行同步方法
  4. 若是更新失败,表示其余线程竞争锁,当前线程便尝试使用自旋来获取锁,若自旋后没有得到锁,此时轻量级锁会升级为重量级锁,当前线程会被阻塞

轻量级锁解锁

  1. 解锁时会使用CAS操做将Displaced Mark Word替换回到对象头,
  2. 若是解锁成功,则表示没有竞争发生
  3. 若是解锁失败,表示当前锁存在竞争,锁会膨胀成重量级锁,须要在释放锁的同时唤醒被阻塞的线程,以后线程间要根据重量级锁规则从新竞争重量级锁

轻量级锁注意事项

隐患:对于轻量级锁有个使用前提是"没有多线程竞争环境",一旦越过这个前提,除了互斥开销外,还会增长额外的CAS操做的开销,在多线程竞争环境下,轻量级锁甚至比重量级锁还要慢

偏向锁

偏向锁综述

  1. 痛点: Hotspot做者发如今大多数状况下不存在多线程竞争的状况,而是同一个线程屡次获取到同一个锁,为了让线程得到锁代价更低,所以设计了偏向锁 (这个跟业务使用有很大关系)
  2. 主要目的: 为了在无多线程竞争的状况下尽可能减小没必要要的轻量级锁执行路径
  3. 原理: 在只有一个线程执行同步块时经过增长标记检查而减小CAS操做进一步提升性能
  4. 数据结构: 包括占有锁的线程id,是不是偏向锁,epoch(偏向锁的时间戳),对象分代年龄、锁标志位

偏向锁流程图

线程1演示了偏向锁的初始化过程,线程2演示了偏向锁的撤销锁过程

偏向锁初始化

  1. 当一个线程访问同步块并获取到锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后该线程在进入和退出同步块时不须要花费CAS操做来加锁和解锁,而是先简单检查对象头的MarkWord中是否存储了当前线程
  2. 若是已存储,说明当前线程已经获取到锁,继续执行任务便可
  3. 若是未存储,则须要再判断当前锁否是偏向锁(即对象头中偏向锁的标识是否设置为1,锁标识位为01)
  4. 若是没有设置,则使用CAS竞争锁(说明此时并非偏向锁,必定是等级高于它的锁)
  5. 若是设置了,则尝试使用CAS将对象头的偏向锁指向当前线程,也就是结构中的线程ID

偏向锁撤销锁

  1. 偏向锁使用一种等到竞争出现才释放锁的机制,只有当其余线程竞争锁时,持有偏向锁的线程才会释放锁

  2. 偏向锁的撤销须要等待全局安全点(该时间点上没有字节码正在执行)

  3. 偏向锁的撤销须要遵循如下步骤: -

    • 首先会暂停拥有偏向锁的线程并检查该线程是否存活:
      • 若是线程非活动状态,则将对象头设置为无锁状态(其余线程会从新获取该偏向锁)
      • 若是线程是活动状态,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,并将对栈中的锁记录和对象头的MarkWord进行重置
  4. 要么从新偏向于其余线程(即将偏向锁交给其余线程,至关于当前线程"被"释放了锁)

  5. 要么恢复到无锁或者标记锁对象不适合做为偏向锁(此时锁会被升级为轻量级锁)

  6. 最后唤醒暂停的线程,被阻塞在安全点的线程继续往下执行同步代码块

偏向锁关闭锁

  1. 偏向锁在JDK1.6以上默认开启,开启后程序启动几秒后才会被激活
  2. 有必要可使用JVM参数来关闭延迟 -XX:BiasedLockingStartupDelay = 0
  3. 若是肯定锁一般处于竞争状态,则可经过JVM参数 -XX:-UseBiasedLocking=false 关闭偏向锁,那么默认会进入轻量级锁

偏向锁注意事项

  1. 优点:偏向锁只须要在置换ThreadID的时候依赖一次CAS原子指令,其他时刻不须要CAS指令(相比其余锁)
  2. 隐患:因为一旦出现多线程竞争的状况就必须撤销偏向锁,因此偏向锁的撤销操做的性能损耗必须小于节省下来的CAS原子指令的性能消耗(这个一般只能经过大量压测才可知)
  3. 对比:轻量级锁是为了在线程交替执行同步块时提升性能,而偏向锁则是在只有一个线程执行同步块时进一步提升性能

偏向锁 vs 轻量级锁 vs 重量级锁

相关文章
相关标签/搜索