对于Java开发者来讲synchronized关键字确定不陌生,对它的用法咱们可能已经能信手扭来了,可是咱们真的对它深刻了解吗?html
虽然网上有不少文章都已经将synchronized关键字的用法和原理讲明白了,可是我仍是想根据我我的的认识,来跟你们伙来聊一聊这个关键字。java
我不想上来就搞什么实现原理,咱们来一块儿看看synchronized的用法,再由浅到深的聊聊synchronized的实现原理,从而完全来完全掌握它。面试
咱们都知道synchronized关键字是Java语言级别提供的锁,它能够为代码提供有序性和可见性的保。spring
synchronized做为一个互斥锁,一次只能有一个线程在访问,咱们也能够把synchronized修饰的区域看做是一个临界区,临界区内只能有一个线程在访问,当访问线程退出临界区,另外一个线程才能访问临界区资源。设计模式
一、怎么用网络
synchronized通常有两种用法:synchronized 修饰方法和 synchronized 代码块。数据结构
咱们就经过下面的例子,一块儿感觉一下synchronized的使用,感觉一下synchronized这个锁到底锁的是什么。多线程
public class TestSynchronized { private final Object object = new Object(); //修饰静态方法 public synchronized static void methodA() { System.out.println("methodA....."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } //代码块synchronized(object) public void methodB() { synchronized (this) { System.out.println("methodB....."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } //代码块synchronized(class) public void methodC() { synchronized (TestSynchronized.class) { System.out.println("methodC....."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } //修饰普通法法 public synchronized void methodD() { System.out.println("methodD....."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } //修饰普通的object public void methodE() { synchronized (object) { System.out.println("methodE....."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
咱们上面的例子基本上包含了synchronized的全部使用方法,咱们经过运行这个例子,看一下方法的打印顺序是怎么样的。并发
首先咱们调用同一个对象的 methodB和methodD 方法,来对比下 synchronized (this)和 synchronized method(){} 这两种方式。oracle
final TestSynchronized obj = new TestSynchronized(); new Thread(() -> { obj.methodB(); }).start(); new Thread(() -> { //obj.methodB(); obj.methodD(); }).start();
不论是两个线程调用同一个方法,仍是不一样的方法,咱们经过运行代码能够看到,控制台都是先打印methodB.....等了一秒钟才打印出另外一个线程调用的方法输出结果。
为何会先打印了methodB过一会才打印methodD呢?先看下图,咱们就以调用不一样方法为例。
咱们文章刚开始也介绍了synchronized的做用其实至关于一把锁,其实咱们也能够看作是一个临界区,经过代码的运行结果,咱们看到这里先打印了methodB....,过了一会才打印了methodD方法。
咱们能够感受到这两个线程访问的好像是同一块临界区,否则的话,控制台应该几乎同时打印出来methodB....和methodD...,这个的话咱们也能够本身运行上面的例子,来看一下打印的前后顺序。
咱们也能够经过代码来分析一下,this指的是什么呢?
this指的是调用这个方法的对象,那调用synchronized method()的又是被实例化出来的对象,因此当在同一个实例对象调用synchronized method()和synchronized(this)的时候,使用的是一个临界区,也就是咱们所说的使用的同一个锁。
这里的话我我的以为临界区的这个概念应该会比较好理解一点。咱们能够把synchronized method()和 synchronized()修饰的代码都当作一个临界区,若是调用synchronized修饰的方法对象和synchronized代码块里面传入的参数是同一个对象(这里咱们说的是同一个对象是指他们的hashCode是相等的),则代表使用的是同一个临界区,不然就不是。
那咱们来继续看看下面的这个
final TestSynchronized obj = new TestSynchronized(); final TestSynchronized obj1= new TestSynchronized(); new Thread(() -> { //obj.methodC(); obj1.methodC(); }).start(); new Thread(() -> { //obj.methodC(); TestSynchronized.methodA(); }).start();
无论咱们使用obj1 仍是 obj 调用methodC方法,或者是obj调用methodC()和obj1调用methodC()方法,打印的顺序都是先打印methodC.....过一秒才打印出来另外的一个输出。
那这里的话其实和上面使用object对象相似,只不过这里换成了Class对象。代码在运行时,只会生成一个Class对象。咱们知道static修饰方法时,那方法就属于类方法,因此这里的话synchronized(Object.Class)和 statci synchronized method()都是使用的Object.class做为锁。
这里咱们就不一一去举例说明了,可能你们伙也能知道我想传达的意思,有兴趣的小伙伴能够本身动手跑一跑代码,看一下结果。在这里的话我就直接给出告终论了。
1)当一个线程访问同一个object对象中的synchronized(this)代码块或synchronized method()方法时,或者一个线程访问object的synchronized(this)同步代码块,另外线程访问synchronized method()时都会被阻塞。
一次只能有一个线程获得执行。
另外一个线程必须等待当前线程执行完这个代码块之后才能执行该代码块。 当一个线程访问一个对象的synchronized(object)代码块或synchronized method()时,其余线程能够同时访问这个对象的非synchronized(obj)或synchronized method()方法。 这里须要注意的是对于同一个对象
2)当一个线程访问static synchronized method()修饰的静态方法或synchronized()代码块,里面的参数是一个Classs时,另一个线程访问 synchronized(Object.class)或 访问 static synchronized method()都会被阻塞。
当一个线程访问synchronized修饰的静态方法是,其余线程能够同时访问其余synchronized 修饰的非静态方法,或者者是非synchronized(Object.class)。注意的是这里的class必须是同一个class
这里要说明一下其实 Object.class == Object.getClass(); .class 表明了一个Class的对象。这里的Class不是Java中的关键字,而是一个类。
二、 能够解决什么问题
咱们上面已经了解synchronized的一些用法,咱们前面其实也介绍过synchronized能够解决多线程并发访问共享变量时带来可见性、原子性问题。除此以外呢,其实还能够利用synchronized和wait()/notify()来实现线程的交替顺序执行。咱们就经过下面的例子或者图片看一下。
下面一个例子是经典的经典的打印ABC的问题
public class TestABC implements Runnable{ private String name; private Object pre; private Object self; public TestABC(String name,Object pre,Object self){ this.name = name; this.pre = pre; this.self = self; } @Override public void run(){ int count = 10; while(count>0){ synchronized (pre) { synchronized (self) { System.out.print(name); count --; //释放锁,开启下一次的条件 self.notify(); } try { //给以前的数据加锁 pre.wait(); } catch (Exception e) { // TODO: handle exception } } } } public static void main(String[] args) throws InterruptedException { Object a = new Object(); Object b = new Object(); Object c = new Object(); Thread pa = new Thread(new TestABC("A",c,a)); Thread pb = new Thread(new TestABC("B",a,b)); Thread pc = new Thread(new TestABC("C",b,c)); pa.start(); TimeUnit.MILLISECONDS.sleep(100); pb.start(); TimeUnit.MILLISECONDS.sleep(100); pc.start(); } }
可能有人一开始理解不了,这是什么鬼代码,其实我刚开始学习这个synchronized关键字时,也没有很好地能理解这个快代码,那就来一块儿分析看一下。
上图是我利用一个桌面程序跑出来的效果,可是它这边只能是在同一个锁上进行的,没有办法模拟多个锁,可是咱们能够看到wait()/notify()带来的效果。
当正在执行的线程调用wait()的时候,线程会主动让出来锁的归属权,咱们也能够理解为离开了临界区,那其余线程就能够进入到这个临界区。
调用wait()的线程,只能经过被调用notify()才能唤醒,唤醒以后又能够从新去或者取临界区的执行权。
那经过上图就更好解释示例代码是如何运行的了。
咱们启动线程的时候是按照A、B、C这样的前后顺序来启动的。当A线程执行完之后,这里会在c临界区等待被唤醒,也就是左上角的步骤3,一样线程B执行完之后会在a临界区等待被唤醒,一样线程C会在b临界区等待被唤醒。
当线程按照这个顺序启动完成之后,以后的线程调度就交由CPU去进行执行顺序是不肯定的,可是当线程C执行完之后,会唤醒在c临界区等待的线程A,而线程B会一直被阻塞,直到在a临界区上等待的线程被唤醒(也就是执行a.notify()),才能从新执行。同理,其余两个线程也是如此,这样就完成了线程的顺序执行。
经过上述所说,咱们可能大概也许对synchronized有那么一点感受了。其实synchronized就是一个锁(也能够理解为临界区),那synchronized究竟是如何实现的呢?synchronized到底锁住的是什么呢?
这是咱们接下来要说的主要内容。
咱们在说内存模型的时候,提到了Java内存模型中提供了8个原子操做,其中有两个操做是lock和unlock,这两个原子操做在Java中使用了两个更高级的指令moniterenter和moniterexit来实现的,synchronized实现线程的互斥就是经过这两个指令实现的,可是synchronized 修饰方法 以及synchronized代码块实现还有稍微的有一些区别,那我就来看看这两个实现的区别。
一、同步方法
咱们经过javap命令对咱们的Java代码进行反编译一下,咱们能够看到以下图的字节码
咱们经过反编译之后的字节码没有发现任何和锁有关的线索。不要着急,咱们经过javap -v 命令来反编译看一下
咱们发现methodD()方法中有一个flags属性,里面有一个ACC_SYNCHRONIZED,这个看起来好像和synchronized有些关系。
经过查资料发现JVM规范对于synchronized同步方法的一些说明:
资料1:https://docs.oracle.com/javas...
资料2:https://docs.oracle.com/javas...
其大体意思能够归纳为如下几点
这里给出了method_info的一些详细说明,能够参官方文档。
https://docs.oracle.com/javas...
二、同步代码块
咱们经过反编译咱们上面的代码,获得methodC的字节码以下。
这里咱们能够看到有两个指令moniterenter和moniterexit,JVM规范对于这两个指令的给出了说明
Monitorenter
Each object has a monitor associated with it. The thread that executes monitorenter gains ownership of the monitor associated with objectref. If another thread already owns the monitor associated with objectref, the current thread waits until the object is unlocked,
每一个对象都有一个监视器(Monitor)与它相关联,执行moniterenter指令的线程将得到与objectref关联的监视器的全部权,若是另外一个线程已经拥有与objectref关联的监视器,则当前线程将等待直到对象被解锁为止。
Monitorexit
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
当执行monitorexit的时候,和该线程关联的监视器的计数就减1,若是计数为0则退出监视器,该线程则再也不是监视器的全部者。
三、synchronized的实现
JVM规范中也说到每个对象都有一个与之关联的Monitor,接下来咱们来看看到底他们之间有什么关联。
对象内存结构
HotSpot虚拟机中,对象在内存中存储的布局能够分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
HotSpot虚拟机对象的对象头部分包括两类信息。第一类是用于存储对象自身的运行时数据,如哈 希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
另一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机经过这个指针来肯定该对象是哪一个类的实例。
咱们所说的锁标识就存储在Mark Word中,其结构以下。
其中标志位10对应的指针,就是指向Monitor对象的,monitor是由ObjectMonitor实现的,其主要数据结构以下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)
ObjectMonitor() { _header = NULL; _count = 0; //记录个数 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
ObjectMonitor中有两个队列,_WaitSet
和 _EntryList
,用来保存ObjectWaiter对象列表( 每一个等待锁的线程都会被封装成ObjectWaiter对象),_owner
指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList
集合,当线程获取到对象的monitor 后进入 _Owner
区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1。
若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其余线程进入获取monitor(锁)。
以下图所示
由此看来,monitor对象存在于每一个Java对象的对象头中(存储的指针的指向),synchronized锁即是经过这种方式互斥的。
原文连接:
https://juejin.im/post/5e8abd...
文源网络,仅供学习之用,若有侵权,联系删除。我将面试题和答案都整理成了PDF文档,还有一套学习资料,涵盖Java虚拟机、spring框架、Java线程、数据结构、设计模式等等,但不只限于此。
关注公众号【java圈子】获取资料,还有优质文章每日送达。