Android Synchronized 关键字学习

面试官:能说说 Synchronized 吗?

答:Synchronized 是Java的一个关键字,使用于多线程并发环境下,可以用来修饰实例对象和类对象,确保在同一时刻只有一个线程可以访问被Synchronized修饰的对象,并且能确保线程间的共享变量及时可见性,还可以避免重排序,从而保证线程安全。

面试官:你背书呢?可以再具体的深入一点吗?

答:行!

1. 前言

相信很多 Android程序员跟我一样,最开始接触到 Synchronized 这个关键字是在创建单例的时候,如:

public class SingleTon {
     

    private static volatile SingleTon instance;

    public static SingleTon getInstance() {
     
        if (instance == null) {
     
        	//同步锁,保证同一时刻只有一个线程进入该代码块。
            synchronized (SingleTon.class) {
     
                if (instance == null) {
     
                    instance = new SingleTon();
                }
            }
        }
        return instance;
    }

}

这里前辈们告诉我们,这叫同步锁,保证同一时刻只有一个线程进入同步锁修饰的代码块,从而保证在多线程的环境下也只会创建一个 SingleTon 实例,达到单例效果。

那除了单例,Synchronized 的其他使用方法及其原理,你有额外了解过吗?今天就让我们来重头学习一遍吧!

2. Synchronized 的使用方法

从Java语法上来说,它有三种使用方法,分别是:

  1. 修饰普通方法
  2. 修饰静态方法
  3. 修饰代码块

再来看一段代码:

public class SynchronizedTestRunnable implements Runnable {
     

    @Override
    public void run() {
     
        a();
        b();
    }

    public void a() {
     
        System.out.println(Thread.currentThread().getName() + " a start on " + getCurrentTime());
        try {
     
            Thread.sleep(3000);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " a end " + getCurrentTime());
    }

    public void b() {
     
        System.out.println(Thread.currentThread().getName() + " b start " + getCurrentTime());
        try {
     
            Thread.sleep(1000);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " b end " + getCurrentTime());
    }

    public static String getCurrentTime() {
     
        String dateFormat = "yyyy-MM-dd hh:mm:ss";
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(System.currentTimeMillis());
        return simpleDateFormat.format(calendar.getTime());
    }

    public static void main(String[] args) {
     
        SynchronizedTestRunnable synchronizedTestRunnable = new SynchronizedTestRunnable();
        new Thread(synchronizedTestRunnable).start();
        new Thread(synchronizedTestRunnable).start();
    }

}


其执行结果为:

Thread-1 a start on 2020-11-24 11:49:04
Thread-0 a start on 2020-11-24 11:49:04
Thread-0 a end 2020-11-24 11:49:07
Thread-1 a end 2020-11-24 11:49:07
Thread-0 b start 2020-11-24 11:49:07
Thread-1 b start 2020-11-24 11:49:07
Thread-0 b end 2020-11-24 11:49:08
Thread-1 b end 2020-11-24 11:49:08

根据执行结果可以看到两个线程会同时执行同一个 runnable 中的方法 a() 与 b(),并且不存在顺序关系,接下来我们试试加上 Synchronized 关键字。

2.1 修饰普通方法

给普通方法 a() 与 b() 分别都加上 Synchronized 关键字修饰,如下:

public class SynchronizedTestRunnable implements Runnable {
     

    @Override
    public void run() {
     
        a();
        b();
    }

    public synchronized void a() {
     
        System.out.println(Thread.currentThread().getName() + " a start on " + getCurrentTime());
        try {
     
            Thread.sleep(3000);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " a end " + getCurrentTime());
    }

    public synchronized void b() {
     
        System.out.println(Thread.currentThread().getName() + " b start " + getCurrentTime());
        try {
     
            Thread.sleep(1000);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " b end " + getCurrentTime());
    }

    public static String getCurrentTime() {
     
        String dateFormat = "yyyy-MM-dd hh:mm:ss";
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(System.currentTimeMillis());
        return simpleDateFormat.format(calendar.getTime());
    }

    public static void main(String[] args) {
     
        SynchronizedTestRunnable synchronizedTestRunnable = new SynchronizedTestRunnable();
        new Thread(synchronizedTestRunnable).start();
        new Thread(synchronizedTestRunnable).start();
    }

}


其执行结果为:

Thread-0 a start on 2020-11-24 11:50:10
Thread-0 a end 2020-11-24 11:50:13
Thread-0 b start 2020-11-24 11:50:13
Thread-0 b end 2020-11-24 11:50:14
Thread-1 a start on 2020-11-24 11:50:14
Thread-1 a end 2020-11-24 11:50:17
Thread-1 b start 2020-11-24 11:50:17
Thread-1 b end 2020-11-24 11:50:18

两个线程按顺序同步执行同一个 runnable 中的方法 a() 与 b(),达到同步锁效果。

思考一下:那要是两个线程分别执行两个 runnable 呢?如:

public static void main(String[] args) {
     
    SynchronizedTestRunnable synchronizedTestRunnable = new SynchronizedTestRunnable();
    SynchronizedTestRunnable synchronizedTestRunnable2 = new SynchronizedTestRunnable();

    new Thread(synchronizedTestRunnable).start();
    new Thread(synchronizedTestRunnable2).start();
}

其执行结果为:

Thread-1 a start on 2020-11-24 11:51:02
Thread-0 a start on 2020-11-24 11:51:02
Thread-1 a end 2020-11-24 11:51:05
Thread-0 a end 2020-11-24 11:51:05
Thread-1 b start 2020-11-24 11:51:05
Thread-0 b start 2020-11-24 11:51:05
Thread-0 b end 2020-11-24 11:51:06
Thread-1 b end 2020-11-24 11:51:06

根据结果来看,虽然我们为方法 a() 与 方法 b() 都加上了 Synchronized 修饰,但是由于 Thread-0 与 Thread-1 执行的是两个runnable,所以两个线程还是同时并发执行了方法a() 与 方法 b(),这是为什么呢?思考一下,后面解释。

2.2 修饰静态方法

在 2.1 修饰普通方法 中我们用 Synchronized 修饰普通方法,但是我们发现,当我们用两个线程分别执行两个 runnable时,同步锁失效了,两个线程还是同时并发执行。
现在,我们稍微修改一下上述代码,将方法a() 与方法 b() 修改成静态方法,如下:

public class SynchronizedTestRunnable implements Runnable {
     

    @Override
    public void run() {
     
        a();
        b();
    }

    public static synchronized void a() {
     
        System.out.println(Thread.currentThread().getName() + " a start on " + getCurrentTime());
        try {
     
            Thread.sleep(3000);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " a end " + getCurrentTime());
    }

    public static synchronized void b() {
     
        System.out.println(Thread.currentThread().getName() + " b start " + getCurrentTime());
        try {
     
            Thread.sleep(1000);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " b end " + getCurrentTime());
    }

    public static String getCurrentTime() {
     
        String dateFormat = "yyyy-MM-dd hh:mm:ss";
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(System.currentTimeMillis());
        return simpleDateFormat.format(calendar.getTime());
    }


    public static void main(String[] args) {
     
        SynchronizedTestRunnable synchronizedTestRunnable = new SynchronizedTestRunnable();
        SynchronizedTestRunnable synchronizedTestRunnable2 = new SynchronizedTestRunnable();

        new Thread(synchronizedTestRunnable).start();
        new Thread(synchronizedTestRunnable2).start();
    }

}

其执行结果为:

Thread-0 a start on 2020-11-24 11:51:46
Thread-0 a end 2020-11-24 11:51:49
Thread-0 b start 2020-11-24 11:51:49
Thread-0 b end 2020-11-24 11:51:50
Thread-1 a start on 2020-11-24 11:51:50
Thread-1 a end 2020-11-24 11:51:53
Thread-1 b start 2020-11-24 11:51:53
Thread-1 b end 2020-11-24 11:51:54

同样是两个线程执行两个runnble,但是与 2.1 修饰普通方法 的结果不同的是,两个线程变成了同步顺序执行,这是为什么呢?就加了两个 static 关键字,思考一下,往后看,后面解释。

2.3 修饰代码块

前面介绍的都是用 Synchronized 关键字来修饰方法,但是很多时候我们只需要同步一小块代码,而不需要同步整个方法,从而减小系统开销。现在,我们接着再修改一下代码,就将方法 a() 的 Thread.sleep(3000) 锁住,方法 b() 还是普通方法,如下:

public class SynchronizedTestRunnable implements Runnable {
     

    @Override
    public void run() {
     
        a();
        b();
    }

    public void a() {
     
        System.out.println(Thread.currentThread().getName() + " a start on " + getCurrentTime());
        synchronized (this) {
     
            try {
     
                Thread.sleep(3000);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " a end " + getCurrentTime());
    }

    public void b() {
     
        System.out.println(Thread.currentThread().getName() + " b start " + getCurrentTime());
        try {
     
            Thread.sleep(1000);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " b end " + getCurrentTime());
    }

    public static String getCurrentTime() {
     
        String dateFormat = "yyyy-MM-dd hh:mm:ss";
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(System.currentTimeMillis());
        return simpleDateFormat.format(calendar.getTime());
    }

    public static void main(String[] args) {
     
        SynchronizedTestRunnable synchronizedTestRunnable = new SynchronizedTestRunnable();

        new Thread(synchronizedTestRunnable).start();
        new Thread(synchronizedTestRunnable).start();
    }

}

其执行结果为:

Thread-1 a start on 2020-11-24 11:52:35
Thread-0 a start on 2020-11-24 11:52:35
Thread-1 a end 2020-11-24 11:52:38
Thread-1 b start 2020-11-24 11:52:38
Thread-1 b end 2020-11-24 11:52:39
Thread-0 a end 2020-11-24 11:52:41
Thread-0 b start 2020-11-24 11:52:41
Thread-0 b end 2020-11-24 11:52:42

两个线程执行同一个runnable,Thread-0 与 Thread-1 同时进入方法 a(),但是由于锁的存在,Thread-0 与 Thread-1 存在竞争关系,这里 Thread-1 先获取到锁,所以会往下执行,而 Thread-0 则阻塞,直到 Thread-1 执行完方法 a(),Thread-0 才开始执行方法 a()。

2.4 总结

我们想要搞清楚这三个方法的区别,就需要知道它们本质上锁的到底是什么对象。比如 2.2 Synchronized 修饰静态方法 中,锁住的是类对象,所以在多线程中,尽管new了多个实例对象,但是本质上是属于同一个类对象,所以还是存在同步关系。而 2.1 Synchronized 修饰普通方法 中,锁住的是类的实例对象,所以在多线程中,如果多个线程执行同一个runnable,就存在同步关系,而如果new了多个实例对象,且线程间各自执行不同的runnable,线程之间就不存在同步关系了。

修饰方法:

a() 修饰普通方法,锁住类的实例对象 b() 修饰静态方法,锁住类对象
Android Synchronized 关键字学习_第1张图片 Android Synchronized 关键字学习_第2张图片

修饰代码块:

锁住当前类的实例对象 锁住当前类对象 锁住任意实例对象
Android Synchronized 关键字学习_第3张图片 Android Synchronized 关键字学习_第4张图片 Android Synchronized 关键字学习_第5张图片

3. Synchronized 原理分析

上面我们介绍了 Synchronized 的三种使用方法,分别是1. 修饰普通方法;2. 修饰静态方法;3. 修饰代码块。接下来为了更好的理解Synchronized 的工作原理,我们反编译一下这三种方法。

//1.修饰普通方法
public synchronized void a() {
     
    System.out.println(Thread.currentThread().getName() + " a start on " + getCurrentTime());
    try {
     
        Thread.sleep(3000);
    } catch (InterruptedException e) {
     
        e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + " a end " + getCurrentTime());
}

//2.修饰静态方法
public static synchronized void b() {
     
    System.out.println(Thread.currentThread().getName() + " b start " + getCurrentTime());
    try {
     
        Thread.sleep(1000);
    } catch (InterruptedException e) {
     
        e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + " b end " + getCurrentTime());
}

//3.修饰代码块
public void c() {
     
    System.out.println(Thread.currentThread().getName() + " c start " + getCurrentTime());
    try {
     
        synchronized (this) {
     
            Thread.sleep(1000);
        }
    } catch (InterruptedException e) {
     
        e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + " c end " + getCurrentTime());
}

方法 a()、 b()、 c() 反编译的结果如下所示:

a() 修饰普通方法 b() 修饰静态方法 c() 修饰代码块
Android Synchronized 关键字学习_第6张图片 Android Synchronized 关键字学习_第7张图片 Android Synchronized 关键字学习_第8张图片

结果分析:(注意看上图我标红的地方)

  • 方法 c() : monitorenter 与 monitorexit 指令,monitorenter指令的作用是获取 monitor所有权,monitorexit指令的作用是释放 monitor所有权。monitorenter 与 monitorexit 指令都是成对出现的,但是注意仔细看上图 c() 的话,你会发现出现了一个 monitorenter 对应 2个 monitorexit,看到这里是不是内心有疑惑,不是刚说成对出现吗?打脸来的这么快?正常执行的情况下,23行 monitorenter 对应31行的 monitorexit,然后直接 goto 到40行,略过了37行的 monitorexit。注意:37行的 monitorexit 是在抛出异常的情况下执行的,正常情况下不执行,这样也就保证了始终都有一个 monitorexit 来对应 monitorenter,所以也就证实了他们都是成对出现的
  • 方法 a() 与 方法 b() :没有发现 monitorenter 与 monitorexit 指令,而是多出了 ACC_SYNCHRONIZED 标识符,JVM就是根据该标识符来判断是否需要实现方法同步。当方法调用时,调用指令会检查方法中是否含有 ACC_SYNCHRONIZED 标识符,如果有,则执行线程将获取 monitor所有权,成功获取到 monitor所有权之后才能往下执行方法体,方法体执行完后再释放 monitor所有权。并且在方法体执行期间,其他任何线程都无法再获得到同一个 monitor对象的所有权。 其实本质上与 monitorenter | monitorexit指令没有区别,只是是以一种隐式的方式来实现同步,无需通过字节码来完成,所以也被称为隐式同步

说了这么多获取 monitor所有权、释放 monitor所有权,那 monitor 又是什么呢
他被称为内置锁(intrinsic lock)或监视器锁(monitor lock),它其实是一种互斥锁,也就是同一时刻最多只有一个线程可以持有这种锁,当线程A想去获取一个由线程B持有的锁时,线程A必须等待或阻塞,直到线程B释放该锁,如果线程B永远不释放持有的该锁,那么线程A也将永远的等待或阻塞着。monitor lock 存在于 Java对象头中,获得 monitor lock 的唯一途径就是进入由这个锁(Synchronized)保护的同步代码块或方法中。

《Java并发编程实战》是这么介绍的:

每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁和监视器锁。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,而无论是通过正常的控制路径退出,还是通过从代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。

总结一下:
我们用 Synchronized 修饰的同步方法中,是通过监视器锁来实现同步效果的,而这个监视器锁存在于 Java对象头中。

3.1 Java对象头

想要知道 Java对象头,我们就得先了解 Java对象是什么。

关于Java对象,在《深入理解Java虚拟机》中是这么介绍的:在 HotSpot 虚拟机里,对象在堆内存中的存储布局可以分划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

Android Synchronized 关键字学习_第9张图片

这里我们主要关注对象头的 Mark Word,用于存储对象自身的运行时数据,如哈希码HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分的数据长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特,官方称为 Mark Word。
引用《深入理解Java虚拟机》中的插图,帮助理解,如下所示:
Android Synchronized 关键字学习_第10张图片
Android Synchronized 关键字学习_第11张图片

对象头的最后两位存储了锁的标志位,如00是轻量级锁,10位重量级锁。随着锁级别的不同,对象头里存储的内容不同,具体如上图所示。所以这里也验证了每个Java对象都可以做一个实现同步的锁

3.2 虚拟机底层原理

通过上面的学习,我们知道了用 Synchronized 修饰的同步方法,本质上是通过获取Java对象头中的监视器锁来实行同步的,接下来我们来看看底层虚拟机是通过怎样的方式来实现同步的(再来回忆一下,同一时刻只有一个线程可以进入到同步方法中)。

来看看底层虚拟机源码

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //用来记录当前线程获取该锁的次数
    _waiters      = 0, //等待线程数
    _recursions   = 0; //锁的重入次数
    _object       = NULL;
    _owner        = NULL; //表示持有ObjectMonitor对象的线程
    _WaitSet      = NULL; //线程队列:存放处于wait状态的线程
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //线程队列:存放正在等待锁释放而处于block状态的线程
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0; //前一个持有者的线程ID
  }

引用一张 monitor 工作原理经典图
Android Synchronized 关键字学习_第12张图片

当多个线程同时访问同一同步代码时,先获取到 monitor 的线程会先成为 _owner,_count加一,而其他线程则进入 _EntryList 队列中,处于阻塞状态,直到当前线程 _owner 释放了 monitor(此时_count为0),这些处于 _EntryList 中阻塞的线程才会被唤醒然后去竞争 monitor,新竞争到 monitor 的线程就会成为新的 _owner。
获取到 monitor 的线程在调用 wait() 方法后,_owner 会释放 monitor,_count减一,该线程会加入到 _WaitSet 队列中,直到调用 notify()/notifyAll() 方法出队列,再次获取到 monitor。

_EntryList 与 _WaitList 的区别
注意:处于 _EntryList 队列中的线程是还没有进入到同步方法中的,而处于 _WaitList 队列的线程是已经进入到了同步方法中,但是由于某些条件(调用了wait()方法)暂时释放了 monitor,等待某些条件(调用notify()/notifyAll()方法)再次获取到 monitor。
这个问题也称之为锁阻塞与等待阻塞的区别,锁阻塞是被动地,还没进入到同步方法中,而等待阻塞是主动的,已经进入到了同步方法中,只是等待另一个获取到monitor锁的线程调用 notify() 唤醒。

上述回答中的 wait()/notify()/notifyAll() 方法也被称之为监控条件(Monitor Condition) ,它与 monitor 是相互关联的,所以你想使用监控条件必须先获取到 monitor,所以 wait()/notify()/notifyAll() 方法必须用在同步方法中(因为Synchronized修饰的同步方法中可以获取到 monitor),否则会抛出 IllegalMonitorStateException 异常。

进一步思考:那 _EntryList 与 _WaitList 里的线程会一起竞争monitor吗?还是说 _WaitList 里的线程会比 _EntryList 里的线程优先获取到 monitor 呢?
会公平竞争monitor
参考Does Java monitor’s wait set has a priority over entry set?

4. Q & A

由小伙伴提出的问题延伸,欢迎大家提出有疑问的地方,让我们共同进步呀 ~

4.1 类锁与对象锁有什么区别

想知道类锁与对象锁有什么区别,我们就要先知道类对象与类的实例对象有什么区别,因为他们四个的对应关系为:

类对象            --> 类锁
类的实例对象 --> 对象锁

我们在编写完 .java 文件后,JVM 是不能直接运行该文件的,需要先将 .java 文件编译成 .class 文件后,JVM 再把 class 文件加载到内存中,创建一个 Class对象,并且存在 JVM数据区中的方法区中,被所有线程所共享,这时候才可以使用这个 .java 文件,也就是这个类
这个Class对象,就是我们说的类对象,而我们刚刚说的类锁,就是这个类对象实现的。

总结一下:
一个Java类可以有很多的实例对象,但只有一个类对象,不管是类对象还是类的实例对象,都是 Java对象,所以类锁与对象锁其实都是内置锁,都是通过 Java对象头中的 Mark Word 中的标志位来实现的,其实现原理也是一样的。


OK,到这里是不是对 Synchronized 有了进一步的理解,其实还可以进阶 Synchronized 的锁优化,篇幅及能力关系,这里就不展开了。

参考文献:
《Java并发编程实战》
《深入理解Java虚拟机》
synchronized实现原理

其实分享文章的最大目的正是等待着有人指出我的错误,如果你发现哪里有错误,请毫无保留的指出即可,虚心请教。
另外,如果你觉得文章不错,对你有所帮助,请给我点个赞,就当鼓励,谢谢~Peace~!

你可能感兴趣的:(Android,synchronized,android,并发编程,同步)