高并发之Synchronized(超详细笔记)

高并发之Synchronized

原文链接:高并发之Synchronized

Synchronized简介

Synchronized的作用

官方解释

Synchronized methods enable a simple strategy for preventing thread interference and memory consistency errors:if an object is visible to more than one thread,all reads or writes to that object’s variables are done through synchronized methods.

一句话说出Synchronized的作用

能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。

Synchronized的地位

  • Synchronized是Java的一个关键字
  • 是最基本的互斥同步的手段
  • 是并发编程中的元老级角色,是并发编程的必学内容

不用并发手段的后果

代码实战:两个线程同事a++,最后结果会比预期少

/**
 * @program: concurrency_demo
 * @Author: [email protected]
 * @Date: 2019/9/17 15:14
 * @Description: 高并发之消失的请求,两个线程对i++,执行100000次,结果一定小于200000
 */
public class DisappearRequest1 implements Runnable{

    static DisappearRequest1 disappearRequest1 = new DisappearRequest1();
    static int i = 0;
    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(disappearRequest1);
        Thread t2 = new Thread(disappearRequest1);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }

    @Override
    public void run() {

        for (int j = 0; j < 100000; j++) {
            i++;
        }
    }
}

运行结果:

132889

原因:
count++,它看上去只是一个操作,实际上包含了三个动作:

  1. 读取count
  2. 将count加一
  3. 将count的值写道内存中
    这三个动作执行到任何一个动作时,都有可能被打断,如此时count为9,线程a执行到+1操作,count的值变为10.但是还没来得及写入内存,线程b就开始执行,此时读取到的count依然是9,这和我们的预期就不同了,我们称之为线程不安全

Synchronized的两种用法

Synchronized的两种用法介绍

对象锁,包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己指定锁对象)

类锁,指synchronized修饰静态的方法或指定锁为Class对象

第一个用法:对象锁

代码块锁

代码块形式:手动指定锁对象

锁对象的选取:若不特定锁对象,可以用this(当前对象)作为我们的锁。

但有时候情况比较复杂,需要我们自己去选取锁对象,比如我们有多个个synchronized代码块,不是它们其中一个执行其它的就不能执行,而是可以同步执行,如下例,定义了两个Object类型的对象作为我们的锁,此时代码运行就可以是两两配对地运行。

/**
 * @program: concurrency_demo
 * @Author: [email protected]
 * @Date: 2019/9/12 14:08
 * @Description: 对象锁示例一:代码块锁
 */
public class SynchronizedObjectCodeBlock2 implements Runnable {
    static SynchronizedObjectCodeBlock2 instance = new SynchronizedObjectCodeBlock2();

    final Object lock1 = new Object();
    final Object lock2 = new Object();

    @Override
    public void run() {
        //代码块形式
        synchronized (lock1) {
            System.out.println("我是lock1。我叫" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "lock1运行结束");
        }

        synchronized (lock2) {
            System.out.println("我是lock2。我叫" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "lock2运行结束");
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while(t1.isAlive()||t2.isAlive()){

        }
        System.out.println("finished");
    }
}

运行结果:

我是lock1。我叫Thread-0
Thread-0lock1运行结束
我是lock2。我叫Thread-0
我是lock1。我叫Thread-1
Thread-1lock1运行结束
Thread-0lock2运行结束
我是lock2。我叫Thread-1
Thread-1lock2运行结束
finished

除了这种情况,可能还有更复杂的情形,比如三个线程等待一个线程,再比如线程之间还有通信,就很难处理了,可能通过我们自己的努力也是能写出来,但是不建议自己写,因为如果稍有疏忽,就可能出现比较大的错误,那怎么去避免?实际上jdk为我们提交了几个非常完善的同步控制工具类,例如CountDownLatch、CyclicBarrier、Semaphore、Exchanger等等,有兴趣的可以去了解。

方法锁形式

synchronized修饰普通方法(非静态方法),锁对象默认为this

/**
 * @program: concurrency_demo
 * @Author: [email protected]
 * @Date: 2019/9/12 14:08
 * @Description: 对象锁示例二:方法锁
 */
public class SynchronizedObjectMethod3 implements Runnable {
    static SynchronizedObjectMethod3 instance = new SynchronizedObjectMethod3();

    @Override
    public void run() {
        method();
    }

    private synchronized void method() {

        System.out.println("我是对象锁的方法修饰符形式,我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");

    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");
    }
}

运行结果:

我是对象锁的方法修饰符形式,我叫Thread-1
Thread-1运行结束
我是对象锁的方法修饰符形式,我叫Thread-0
Thread-0运行结束
finished

第二个用法:类锁

类锁的概念

概念:Java类可能有很多对象,但只有一个Class对象

本质:所谓的类锁,就是Class对象的锁

用法和效果:类锁在同一时刻只能被一个对象拥有

形式一:synchronized加在static方法上

形式二:synchronized(*.class)代码块

例如有一个student类,它可以创造出很多个实例,例如小明同学、小美同学, 但是在Java中万物皆对象,对于这个类这个class它也是个对象,这个对象只有一个,也就是说无论是小明还是小美,他们是这个对象的实例,但他们都会有一个对应的这个类的对象,而这个对象(类对象)只有一个,不会实例化很多个出来。

本质:不同的实例不同的线程,它们去访问用类锁的方法的时候,所获取到的那把锁其实是class对象,而且class对象只有一个,所以不论是哪个实例过来的线程,他们能获取到这唯一的一个锁,也就是说,类锁是一个概念上的东西,它并不是真实存在的有这个一个类锁,它这个概念是为了帮我们理解实例方法和静态方法的区别的。class对象从本质上来说也是一个Java对象,只不过这个对象很特殊,这时我们就应该明白了,所谓的类锁本质上是将class对象作为我们的锁,而由于它只有一个,所以不同的实例之间会互斥,只有一个线程在同一时刻访问被类锁锁住的方法。

类锁的第一种形式

synchronized加载static方法上

/**
 * @program: concurrency_demo
 * @Author: [email protected]
 * @Date: 2019/9/18 10:37
 * @Description: 类锁的第一种形式:static形式
 */
public class SynchronizedClassStatic4 implements Runnable {


    static SynchronizedClassStatic4 instance1 = new SynchronizedClassStatic4();
    static SynchronizedClassStatic4 instance2 = new SynchronizedClassStatic4();

    @Override
    public void run() {
        method();
    }

    /**
     * 这里的method方法上加了static,此时线程0和线程1会串行,如果将static去掉,线程0和1将会串行
     * 所以如果我们需要在全局的情况下同步这个方法,不仅仅是一个对象等小层面,我们可以用下面这种形式
     */
    private static synchronized void method(){
        System.out.println("我是类锁的第一种形式:static形式,我叫"+Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"执行结束");
    }

    public static void main(String[] args) {
        //两个不同实例的线程
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        t1.start();
        t2.start();

        while(t1.isAlive()||t2.isAlive()){

        }
        System.out.println("finished");
    }
}

运行结果:

我是类锁的第一种形式:static形式,我叫Thread-0
Thread-0执行结束
我是类锁的第一种形式:static形式,我叫Thread-1
Thread-1执行结束
finished

这里我们新建SynchronizedClassStatic4类,并创建出两个实例instance1和instance2,不同实例的线程去调用被synchronized修饰的static方法。不同实例去访问synchronized修饰的方法,如果这个方法不是static的话,线程还是会并行的,加上static,可以做到全局的同步。

类锁的第二种形式

synchronized(*.class)

/**
 * @program: concurrency_demo
 * @Author: [email protected]
 * @Date: 2019/9/18 11:11
 * @Description: 类锁的第二种形式:synchronized(*.class)
 */
public class SynchronizedClassClass5 implements Runnable{

    static SynchronizedClassClass5 instance1 = new SynchronizedClassClass5();
    static SynchronizedClassClass5 instance2 = new SynchronizedClassClass5();

    @Override
    public void run() {

        method();
    }

    private void method(){

        //可以试试将下面的SynchronizedClassClass5.class换成this
        //换成this后,不同实例的线程来访问时,还是会并行
        synchronized (SynchronizedClassClass5.class){
            //在这个synchronized里面包含的代码执行起来,它的锁对象就会是SynchronizedClassClass5.class这个对象
            //所以无论哪个实例运用的都是同一个对象,所以无论是本例中的instance1还是instance2,执行到这里时,都需要拿到锁才能执行
            System.out.println("我是类锁的第二种形式:synchronized(*.class),我叫"
                    +Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"执行完毕");

        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        t1.start();
        t2.start();
        while(t1.isAlive()||t2.isAlive()){

        }
        System.out.println("finished");
    }
}

运行结果:

我是类锁的第二种形式:synchronized(*.class),我叫Thread-1
Thread-1执行完毕
我是类锁的第二种形式:synchronized(*.class),我叫Thread-0
Thread-0执行完毕
finished

我们可以看到,在本例 method 方法中的 synchronized 代码块,它标明的锁是 可以试试将下面的 SynchronizedClassClass5.class 换成 this 或者是 *.class (其他类,并不是直接写 *.class)。换成 this 后,不同实例的线程来访问时,还是会并行;换成 *.class 的话,能到达一样的串行效果。

在这个 synchronized 里面包含的代码执行起来,它的锁对象就会是 SynchronizedClassClass5.class 这个对象,所以无论哪个实例运用的都是同一个对象,所以无论是本例中的 instance1 还是 instance2 ,执行到这里时,都需要拿到锁才能执行。

多线程访问同步方法的7种情况

1. 两个线程同事访问一个对象的同步方法

两个线程依次执行,锁生效

2. 两个线程访问的是两个对象的同步方法

此时synchronized不生效,两个线程并行执行

3. 两个线程访问的是synchronized的静态方法

两个线程依次执行,锁生效

4. 同时访问同步方法与非同步方法

代码示例:

/**
 * @program: concurrency_demo
 * @Author: [email protected]
 * @Date: 2019/9/18 16:08
 * @Description: 同时访问同步方法和非同步方法
 */
public class SynchronizedYesAndNo6 implements Runnable{

    static SynchronizedYesAndNo6 instance = new SynchronizedYesAndNo6();
    @Override
    public void run() {
        if("Thread-0".equals(Thread.currentThread().getName())){
            method1();
        }else{
            method2();
        }
    }

    public synchronized void method1(){
        System.out.println("我是枷锁方法,我叫"+Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"运行结束");
    }

    public  void method2(){
        System.out.println("我是不枷锁方法,我叫"+Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"运行结束");
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");
    }
}

运行结果:

我是不枷锁方法,我叫Thread-1
我是枷锁方法,我叫Thread-0
Thread-0运行结束
Thread-1运行结束
finished

从这里可以看出来synchronized关键字只作用于我们指定的被修饰的方法中,对于其他没加修饰符的方法,不会受到影响。

访问同一个对象的不同的普通同步方法

代码示例:

/**
 * @program: concurrency_demo
 * @Author: [email protected]
 * @Date: 2019/9/18 16:23
 * @Description: 同时访问一个类两个不同的普通同步方法
 */
public class SynchronizedDifferentMethod7 implements Runnable{

    static SynchronizedDifferentMethod7 instance = new SynchronizedDifferentMethod7();
    @Override
    public void run() {
        if("Thread-0".equals(Thread.currentThread().getName())){
            method1();
        }else{
            method2();
        }
    }

    public synchronized void method1(){
        System.out.println("同步方法1,我叫"+Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"运行结束");
    }

    public  synchronized void method2(){
        System.out.println("同步方法2,我叫"+Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"运行结束");
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");
    }
}

运行结果:

同步方法1,我叫Thread-0
Thread-0运行结束
同步方法2,我叫Thread-1
Thread-1运行结束
finished

在这个例子中,虽然没指明锁对象,本质上其实是默认为指定 this 这个对象作为它的这把锁,所以对于同一个实例 instance 来讲,两个方法 method1 和 method2 拿到的锁是一样的,所以这两个方法没办法同时运行。

同时访问静态synchronized和非静态的synchronized方法

代码示例:

/**
 * @program: concurrency_demo
 * @Author: [email protected]
 * @Date: 2019/9/18 16:47
 * @Description: 同时访问静态synchronized和非静态的synchronized方法
 */
public class SynchronizedStaticAndNormal8 implements Runnable{
    static SynchronizedStaticAndNormal8 instance = new SynchronizedStaticAndNormal8();
    @Override
    public void run() {
        if("Thread-0".equals(Thread.currentThread().getName())){
            method1();
        }else{
            method2();
        }
    }

    public synchronized static void method1(){
        System.out.println("静态加锁方法1,我叫"+Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"运行结束");
    }

    public synchronized void method2(){
        System.out.println("非静态加锁方法2,我叫"+Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"运行结束");
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");
    }
}

运行结果:

静态加锁方法1,我叫Thread-0
非静态加锁方法2,我叫Thread-1
Thread-0运行结束
Thread-1运行结束
finished

这里Thread-0和Thread-1几乎是同时运行同时结束的。为什么呢?

在这个例子中的method1方法,就是我们前面提到的类锁的第一种形式,它的锁对象是类的class对象;在method2方法中,是我们前面提到的对象锁的方法锁形式,即synchronized修饰普通方法(不可为静态方法),锁对象默认为 this。锁不同,当然可以同时拿到两把锁,然后同时访问了。

方法抛异常后,会释放锁

区别与lock,synchronized修饰的方法抛异常后,会释放锁,而lock不会自动释放,需要通过unLock()去释放,具体lock和synchronized的区别可以自行去查询一些资料,或者关注我的后续更新,我们先来看看synchronized抛异常释放锁的例子:

/**
 * @program: concurrency_demo
 * @Author: [email protected]
 * @Date: 2019/9/18 17:15
 * @Description: 方法抛异常,自动释放锁,展示不抛出异常前和抛出异常后的对比:一旦抛出异常,第二个线程会立刻进入同步方法,意味着锁已经释放。
 */
public class SynchronizedException9 implements Runnable{
    static SynchronizedException9 instance = new SynchronizedException9();
    @Override
    public void run() {
        if("Thread-0".equals(Thread.currentThread().getName())){
            method1();
        }else{
            method2();
        }
    }

    public synchronized void method1(){
        System.out.println("加锁方法1,我叫"+Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        throw new RuntimeException();
//        System.out.println(Thread.currentThread().getName()+"运行结束");
    }

    public synchronized void method2(){
        System.out.println("加锁方法2,我叫"+Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"运行结束");
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");
    }

运行结果:

加锁方法1,我叫Thread-0
加锁方法2,我叫Thread-1
Exception in thread "Thread-0" java.lang.RuntimeException
	at demo.SynchronizedException9.method1(SynchronizedException9.java:28)
	at demo.SynchronizedException9.run(SynchronizedException9.java:15)
	at java.lang.Thread.run(Thread.java:745)
Thread-1运行结束
finished

在这个例子的method1方法中抛出了一个运行时异常,我们没在这里做任何释放锁的操作,Thread-0抛出异常后,Thread-1就拿到了锁并执行,这时因为由synchronized修饰的方法抛出异常后,jvm会自动地帮我们释放锁,然后下一个线程就可以正常运行了。

总结:3点核心思想

  1. 一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(对应第1、5种情况);
  2. 每个实例都对应有自己的一把锁,不同实例之间互不影响;例外:锁对象是 *.class 以及synchronized 修饰的是 static 方法的时候,所有的对象共用同一把锁(对应2、3、4、6种情况)
  3. 无论是方法正常执行或者方法抛出异常,都会释放锁(对应第7种情况)

思考:加入一个线程进入到一个被synchronized修饰的方法,再这个方法种又调用了一个没有被synchronized修饰的方法,此时还是线程安全的吗?


Synchronized的性质

可重入性质

什么是可重入

指的是统一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁

一个线程拿到一把锁,下次需要用锁的时候,可以直接拿到,就是可重入的;如果拿到锁之后,想再次使用,此时不可直接再次拿到,必须和其他线程一起竞争,这就是不可重入的,可重入锁也称递归锁

好处

避免死锁、提升封装性

那么它又是如何避免死锁的?

假设有两个被 synchronized 修饰的方法A和B,线程0访问了A方法,这个时候,线程0又想要去访问B方法,如果 synchronized 不具备可重入性,那么现在线程0想去访问B方法,它不能直接使用已经拿到的锁,需要拿锁,但它又没释放锁,就陷入了死锁状态了。

粒度

可理解为范围,线程而非调用(用三种情况来说明和pthread的区别)

我们先假设我们不知道synchronized的粒度是线程的,我们一步一步探索:

情况一:证明同一个方法是可重入的

/**
 * @program: concurrency_demo
 * @Author: [email protected]
 * @Date: 2019/9/18 19:31
 * @Description: 可重入粒度测试:递归调用本方法
 */
public class SynchronizedRecursion10 {

    //定义一个a来标识执行次数
    private int a = 0 ;
    public static void main(String[] args) {
        SynchronizedRecursion10 synchronizedRecursion10 = new SynchronizedRecursion10();
        synchronizedRecursion10.method();
    }

    private synchronized void method() {
        //输出a来看看执行次数
        System.out.println("a的值为:"+a);
        //开始时a为0,满足判断,a++后值为1,再次调用就不满足了,所以method方法执行了两次
        if(a==0){
            a++;
            method();
        }
    }
}

运行结果:

a的值为:0
a的值为:1

情况二:证明可重入不要求是同一个方法

/**
 * @program: concurrency_demo
 * @Author: [email protected]
 * @Date: 2019/9/18 19:43
 * @Description: 可重入粒度测试:调用类内另外的方法
 */
public class SynchronizedOtherMethod11 {

    private synchronized void method1(){
        System.out.println("我是method1");
        method2();
    }

    private synchronized void method2() {
        System.out.println("我是method2");
    }

    public static void main(String[] args) {
        SynchronizedOtherMethod11 synchronizedOtherMethod11 = new SynchronizedOtherMethod11();
        synchronizedOtherMethod11.method1();
    }
}

运行结果:

我是method1
我是method2

情况三:证明可重入不要求是同一个类

/**
 * @program: concurrency_demo
 * @Author: [email protected]
 * @Date: 2019/9/18 20:00
 * @Description: 可重入粒度测试:调用父类方法
 */
public class SynchronizedSuperClass12 {

    public synchronized void doSomeThing(){
        System.out.println("我是父类方法");
    }
}

class TestClass extends SynchronizedSuperClass12{
    @Override
    public synchronized void doSomeThing(){
        System.out.println("我是子类方法");
        super.doSomeThing();
    }

    public static void main(String[] args) {
        TestClass testClass = new TestClass();
        testClass.doSomeThing();
    }
}

运行结果:

我是子类方法
我是父类方法

通过三种情况的探索,我们可以证明synchronized的粒度是线程

当一个线程,执行时拿到了一把锁,如果此时它需要去执行另一个方法,且这个方法需要的锁依然是该线程手中的这把锁,那么synchronized可重入的性质就会被激发出来,就不需要显式的去释放锁或者重新获取锁。

不可中断性质

一旦这个锁已经被别人获取,如果我还想获得,我只能选择等待或者阻塞,直到别的线程释放这个锁。乳沟别人永远不释放,那么我只能永远等待下去。


深入原理

  1. 加锁和释放锁的原理:现象、时机、深入JVM看字节码
  2. 可重入原理:加锁次数计数器
  3. 保证可见性的原理:内存模型

加锁和释放锁的原理

现象

每一个类的实例对应着一把锁,而每一个synchronized方法都必须首先获得调用该方法的类的实例的锁,方能执行,否则线程会阻塞,而方法一旦执行了它就独占了这把锁,直到该方法返回或者是抛出异常,才将锁释放,释放之后,之前那些阻塞的线程才能获得这把锁,重新进入到可执行的状态。

也就意味着,当一个类中,有synchronized修饰的方法或代码块时,想要执行这个方法或者代码块,就必须先获得对应的对象锁,如果这个对象锁已经被其他线程占用,就必须等待到它被释放。所用的Java对象都含有一个互斥锁,由jvm自动地获取和释放,我们只需要指定这个对象就可以了,至于锁的获取和释放不需要我们操心。

获取和释放的时机:内置锁

每一个java对象都可以用作一个实现同步的锁,被称为内置锁,或者成为监视器锁(monitor lock)。

线程在进入同步方法或者代码块之前,会自动获得这把锁,并且在推出时会自动释放,无论是正常退出还是抛出异常退出都会释放。获取这个锁的唯一途径就是进入这个锁所保护的同步方法或者同步代码块之中。

等价代码

我们用代码来模拟锁获取和释放的时机:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @program: concurrency_demo
 * @Author: [email protected]
 * @Date: 2019/9/19 9:34
 * @Description:
 */
public class SynchronizedToLock13 {

    private Lock lock = new ReentrantLock();

    public synchronized void method1(){
        System.out.println("我是synchronized形式的锁");
    }

    public void method2(){
        //加锁
        lock.lock();
        try{
            System.out.println("我是lock形式的锁");
        }finally {
            //释放锁
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        SynchronizedToLock13 synchronizedToLock13 = new SynchronizedToLock13();
        synchronizedToLock13.method1();
        synchronizedToLock13.method2();
    }
}

运行结果:

我是synchronized形式的锁
我是lock形式的锁

在本例中,method1和method2其实是等价的,lock形式的锁需要显式的去加锁和释放锁,synchronized形式的锁是隐式的。synchronized形式的锁在执行开始时,jvm会自动地执行monitorenter,这就是加锁的动作,在抛出异常或者正常退出时,jvm会自动地执行monitorexit,monitorexit可能会有多个,因为释放锁的情况也有多个,后面我们会提到。

反编译看monitor指令

如何反编译

我们先编写一个简单的类,里面是同步代码块的形式:

/**
 * @program: concurrency_demo
 * @Author: [email protected]
 * @Date: 2019/9/19 9:50
 * @Description: 反编译字节码
 */
public class Decompilation14 {
    private final Object object = new Object();

    public void inster(Thread thread){
        synchronized(object){

        }
    }
}

打开命令行 win+r ,并利用javac编译

javac Decompilation14.java

此时会生成一个class文件,再利用javap反编译

javap -verbose Decompilation14.class  //-verbose表示将所有的信息都打印

反编译并打印出信息,我们截取我们需要的部分

 public void inster(java.lang.Thread);
    descriptor: (Ljava/lang/Thread;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=2
         0: aload_0
         1: getfield      #3                  // Field object:Ljava/lang/Object;
         4: dup
         5: astore_2
         6: monitorenter
         7: aload_2
         8: monitorexit
         9: goto          17
        12: astore_3
        13: aload_2
        14: monitorexit
        15: aload_3
        16: athrow
        17: return

我们可以看到地6行的monitorenter,以及第8、14的monitorexit,这就是我们前面提到的synchronized的隐式加锁、释放锁。

Monitorenter和Monitorexit指令

Monitorenter和Monitorexit指令在执行的时候,会让锁计数器+1或-1

这个操作和操作系统中的PV操作很像,PV就是来用控制多线程对于临界资源的访问的。

Monitorenter

实际上每一个对象都与一个monitor相关联,而一个monitor的lock锁在同一时间只能被一个线程获得,一个线程在尝试获取与对象关联的monitor的所有权的时候,只有以下三种情况:

  1. 锁计数器当前为0,表示这把锁还没有被占用,此时线程就会立刻拿到锁,并将monitor锁计数器+1,+1之后锁计数器变成1,就说明已经被占用,其他线程想要获取,就必须等待锁释放。
  2. 线程已经获得monitor的所有权,因为synchronized的可重入性,线程可以再次获得锁,此时计数器再次+1,所以计数器的值不仅仅只有0和1,还可能是更多。
  3. 锁计数器当前不为0,是1或者更多,这个时候线程没法获得monitor的所有权,只能等待锁的释放,等计数器变成0,再去竞争锁。

Monitorexit

Monitorexit指令就相当于释放锁,当然这里必须是拥有锁才能释放锁。释放的过程比较简单,就是将monitor的计数器减1,如果减完之后,如果不是0,说明刚刚是可重入进来的,线程继续拥有monitor所有权;如果变成0,就说明当前线程已经释放锁,不再拥有monitor的所有权,这也意味着刚刚被阻塞的线程会再次尝试获取锁。

可重入原理

一个线程拿到一把锁之后,如果还想进入由这把锁控制的其他方法,那么它可以直接进入。

可重入原理是利用加锁计数器来实现的。

JVM跟踪计数

首先每个对象都自动的含有一把锁,JVM负责跟踪对象被加锁的次数。

递增

线程第一次给对象加锁,计数器会从0变成1,每当这个相同的线程在此对象再次获取锁时,计数器会递增,当然当前已经获取了锁的线程,才能继续在这个对象上多次获取这把锁。

递减

当任务离开时,计数递减,当计数为0时,锁被完全释放。

可见性原理

可见性原理:Java内存模型

我们先简要的了解一下java内存模型,下图所表达的是两个线程如果想要通信,它们是如何做到的

[外链图片转存失败(img-RilFZH2r-1568885946536)(https://ronghua-yu-blog.oss-cn-beijing.aliyuncs.com/图床/blog/可见性原理:Java内存模型.png)]

可以看到线程A和线程B都有一个本地内存,它们是将主内存中的数据变量复制一份放到自己的本地内存,那它们是如何通信呢?

  1. 线程A需要将自己本地内存的共享变量副本写入主内存,主内存是线程通信的桥梁
  2. 主内存中数据更新后,线程B再从主内存中读取,此时线程B就能读取到A更新后的变量数据

[外链图片转存失败(img-9R3xyWQI-1568885946537)(https://ronghua-yu-blog.oss-cn-beijing.aliyuncs.com/图床/blog/线程通信.png)]

可见性原理核心:一旦一个方法或者代码块被synchronized修饰,在进入这个方法或代码块时,被锁定的对象的数据是从主内存中读取;那么它在执行完毕之后,被锁住的对象做的任何修改都要在释放锁之前,从线程内存写回到主内存中。

所以每一次synchronized执行的数据都是最新的,每一次执行都是可靠的,它保证了可见性。


Synchronized的缺陷

效率低

  1. 锁的释放情况少

synchronized锁的释放情况只有两种:正常执行完毕以及抛出异常。而如果锁不释放,其他处于阻塞的线程只能干巴巴的等着。当在执行IO或sleep等比较费时间的操作时,其他线程只能等很久,这是非常不好的,不过lock可以避免这种情况。

  1. 试图获得锁时,不能设定超时
  2. 不能中断一个正在试图获取锁的线程

不够灵活(读写锁更灵活)

  1. 加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的
  2. 例如读写锁,读数据不加锁,写数据加锁,因为读的时候不会嗲来风险,这样可以大大提高效率

无法知道是否成功获取到锁

Lock接口

关于synchronized的缺陷,在Lock中得到了很好的解决

我们来看一下Lock接口的源码,了解一下它的方法:

public interface Lock {

    //加锁
    void lock();

    //加锁,能够响应中断
    void lockInterruptibly() throws InterruptedException;

    //尝试获取锁,返回boolean
    boolean tryLock();

    //尝试获取锁,设置超时时间,返回boolean
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    //释放锁
    void unlock();

   //与条件相关的,处理线程的接口,这里不做展开
    Condition newCondition();
}

在本例中,需要特别注意的是 lock()/ tryLock()/ boolean tryLock(long time, TimeUnit unit)/ unlock() ,这四个方法,这也是Lock常用的四个方法。

关于lockInterruptibly(),它比较特殊,当通过这个方法去获取锁时,如果其他线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。


面试常见问题

1. synchronized使用注意点

  • 锁对象不可为空:作为锁的对象必须实例过的,或者通过其他方法创建的,不能是一个空对象。因为锁的信息是保存在对象的对象头中的,如果没有对象就更没有对象头了,那么这把锁是不能工作的。
  • 作用域不宜过大:可以并行的代码,要尽量并行,我们加锁也只是为了保证线程的安全,不过将本可以并行的代码串行,这就大大降低了程序运行效率,也有违高并发编程的初衷。
  • 要注意避免死锁

2. 如何选择Lock和synchronized

  • 如果可以,尽量不要使用这两个,可以使用java.util.concurrent包中各种各样的工具类,例如CountDownLatch等,使用这些类不需要我们自己去做同步工作,更方便快捷
  • 如果synchronized适用我们的程序,可以优先选择它,因为它可以减少我们的代码量,也就减少了出错的机率
  • 如果我们特别需要用到Lock的独有的特性的时候,我们才用它
  • 选择思想策略:避免出错
    • 优先选择现成有的工具类
    • 没有现成的类我们就用synchronized
    • 需要用到Lock特性,我们才去使用Lock

3. 多线程访问同步方法的各种情况

可以看看前面介绍的内容,这是很重要的一部分。


思考题

1. 多个线程等待同一个synchronized锁的时候,JVM如何选择下一个获取锁的线程是哪个?

2.Synchronized使得同时只有一个线程可以运行,性能较差,有什么办法可以提升性能?

  • 优化使用范围
  • 使用其他类的锁

我想更灵活地控制锁的获取和释放(现在释放锁的时机都被规定死了),怎么办?

4. 什么是锁的升级、降级?什么是JVM里的偏斜锁、轻量级锁、重量级锁?

总结

1. 一句话介绍synchronized:
JVM会自动通过使用monitor来加锁和解锁,保证了同时只有一个i小鹌鹑可以执行指定代码,从而保证了线程安全,同时具有可重入和不可中断的性质

2. synchronized的作用、地位、不控制并发的后果

3. 两种用法:对象锁和类锁

4. 多线程访问同步方法的7种情况:是否是static、Synchronized方法等

5. Synchronized的性质:可重入、不可中断

6. 原理:加解锁原理、可重入原理、可见性原理

7. Synchronized的缺陷:效率低、不够灵活、无法预判是否获取到锁

发布了7 篇原创文章 · 获赞 3 · 访问量 917

猜你喜欢

转载自blog.csdn.net/weixin_42334475/article/details/101030028