java两种同步机制的实现 synchronized和reentrantlock
java两种同步机制的实现 synchronized和reentrantlock
我们知道,java是一种高级语言,java运行在jvm中,java编译器会把我们程序猿写的java代码编译成.class文件,这个.class对于jvm就是相当于汇编对于操作系统(jvm也有类似操作系统一样的指令集),当jvm运行的时候,它会把.class翻译成操作系统认识的指令集然后运行在操作系统(这里对java是解释型还是编译型语言不做深究),当然除此之外,java还可以通过jni(java native interface)调用c++,c的代码(c++,c还可以内嵌汇编代码,所以java是可以间接调用汇编的),jvm内存模型中有一个是Native Method Stacks,这里就是我们调用本地方法的栈,我们今天讲的java同步,就同时和java指令集和jni有关,下面进入正题。
当在java中使用多线程的时候,我们肯定要考虑到线程安全,线程安全简单说就是多个线程操作同一个变量不会出现结果不确定性,那为什么会出现线程不安全?那是因为java的内存模型决定,jvm有一个主内存,还有线程独有的线程上的工作内存,我打个比方,一个线程就是一个cpu,cpu有自己的多级缓存,还有一个内存条的内存。下面就盗个图简单说明一下:
因为这个原因,所以当多个线程修改同一个变量的时候,会出现不确定的结果,这就导致了线程不安全,那怎么样我们才能确保线程安全呢?说的通俗一点,就是排队,没错,当多个线程需要操作同一个变量的时候,排队一个一个来,那要怎样才能实现排队?机器不像人这么聪明,看到前面有“人”就站着等,那最简单粗暴的方法就是加锁,可以想象一下,一个线程要去修改一个变量的时候,前面有一道门,当门被锁住的时候,线程就只能等待或者干其他事。到了java的世界,在语言层面,我们有两种方法去实现我们锁的功能,下面就是我要重点讲的东西,第一个是jdk自带synchronized(关键字),这个就是前面说的利用java自己的指令集实现的锁,第二个是我们Doug Lea 大神主导的并发包中的 reentrantlock(类),这个底层就是通过jni是调用了虚拟机中的C++代码(parker类),下面我们就依次对这两个"东西"进行详细的展开。
synchronized,顾名思义,提供同步的语义,在java语言层面中,可以通过synchronized来控制不同的作用域,可以是类,可以是对象,可以是方法,可以是代码块,我们来看它是怎么来实现锁的功能的,我们直接实战,我先写一个测试类:
public class SyncTest {
Object lock = new Object();
public void sync(){
synchronized (lock) {
System.out.println("get lock");
}
}
}
很简单,我们先new了一个锁,当多个线程需要去调用sync方法然后输出信息的之前,需要这个锁打开才能输出,这是一段典型的同步代码块代码,前面说了,synchronized是利用jvm自带的指令集来实现锁的功能的,那我们现在就利用java自带的反编译工具(javap
-v SyncTest.class),把指令集输出来,我们重点分析这个sync方法:
这个就是反编译class文件的sync方法的结果,下面我们一行一行来分析:
descriptor:()V,这行是方法的说明,表示这个方法没有入参,返回类型是void
flags:ACC_PUBLIC,说明是public方法
code:说明下面是方法代码区域
stack=2,locals=3,args_size=1,表示这个执行这个方法虚拟机栈深度只需要2,本地变 量有3个,有1个参数,1个参数就是this(java方法的第一个参数都是this,只不过隐藏掉了)
aload_0:装载第一个局部变量到操作栈,这里就是this
getfield #3 :#3指向常量池第三个位置,我没有贴出常量池的反编译视图,在这里就是代 码中lock对象,整行指令意思就是访问这个lock对象的引用
dup:复制上面getfield获取的引用压入栈,这里就是lock的引用
astore_1:弹出栈顶的引用,然后放入局部变量1的位置中
monitorenter:得到lock对象的monitor,monitor的进入计数count+1,这个指令就是我们
Synchronized在jvm的底层实现,线程在打印之前需要得到lock的monitor,如果获取不 到,则被挂起,获取到了就继续执行,在monitorenter下面,还有操作系统级别的 mutex重量级锁以及jvm利用cas优化的jvm级的轻量锁,这个不在我们这次讨论范围。
getstatic #4:访问静态变量System.out:PrintStream
ldc #5:将常量池第5个常量“get lock”压入栈
invokevirtual #6:执行数据输出函数
aload_1:装载第二个局部变量,这里就是astore_1从栈上弹出的lock对象的引用
monitorexit:monitor的计数-1,因为是可重入的,就是一个线程可以多次拿到monitor, 所以当monitor计数为0的时候释放lock锁。
goto 25:看一下25行是return,就是返回,如果代码正常执行,那么久流程就结束了,如 果代码中间出现了异常,则继续走
astore_2:弹出栈顶引用,放到局部变量2
aload_1:装载第二个局部变量,这里就是astore_1从栈上弹出的lock对象的引用
monitorexit:monitor的计数-1,出现异常也释放掉锁。
aload_2:装载第三个局部变量,这里就是astore_2从栈顶弹出来的引用。
athrow:将栈顶的数据作为异常抛出,如果为null,则抛空指针异常
return:结束本方法调用的栈帧
下面的异常表,调试行信息和栈图就不解释了,我也不是很懂,以免误导,想深入研究可以找资料学习
到这里,这个方法在jvm指令层面的执行顺序就结束了,java 关键字Synchronized就是通 过monitorenter,monitorexit两个指令来实现的,在monitor下面还有jvm通过cas优化过2的轻量锁以及操作系统级别的重量互斥锁mutex,这个本期不做讨论。
reentrantlock:这个锁在java层面就是一个类,不像上面的Synchronized是一个关键字他是doug lea大神在 java1.5中主导的因为解决那个时候Synchronized效率低下的出现的,这个类可以有很多东西讲,在这里我抽重点讲,首先在java层面,reentrantlock以及其他同步工具类,比如ConcurrentHashMap(java 1.8改动很大,没仔细看源码,好像和以前不太一致了,1.8以前是),CountDownLatch,CyclicBarrier,以及信号量 semaphore,在底层都是通过实现抽象类AbstractQueuedSynchronizer(AQS)来实现各种锁功能,比如乐观锁,悲观锁,互斥锁,共享锁,读写锁等。先来说一下reentrantlock的lock实现原理。
reentrantlock.lock:reentrantlock类自己实现了AQS,内部有两种实现方式,有公平实现 类FairSync和不公平实现类NonfairSync,这里我们说默认的使用不公平sync:
lock方法首先会用CAS采用乐观锁方式获取一次锁,(CAS操作线程安全,因为通过jni直接调用的操作系统cmpxchg指令,(如果是多核CPU,则需要调用lock cmpxchg利用内存屏障来保障指令的原子性)如果成功了,则设置当前线程占有这个锁,如果失败了,则去acquire(1),意思是提交一个获取锁的申请:
首先tryAcquire(1),调用到NonfairSync的nonfairTryAcquire方法:
首先获取一下state,如果是0则说明还没占有锁,则再去CAS获取一次,意图是代码从外面的CAS走到这的时间里,可能别的线程已经释放了锁,所以在进行一次CAS,如果获取到了则返回成功。如果state不是0,且占有锁的线程是当前线程,因为是可重入的锁,则在计数上加1,代表重入一次,等到时候释放锁的时候,要全部释放完等计数为0才表示释放完全。
如果都不是以上情况,则返回false,进行后面的操作:
new一个当前线程的node,然后通过CAS操作,把这个node排在node双向链表的尾部,如果CAS失败,则进行enq:
enq很简单,一个死循环既自旋,如果链表是空的,则初始化,否则一直CAS到node排到链表尾部为止。添加等待node成功之后,执行以下方法:
进行循环,Node p就是当前线程node前面的一个node,如果前一个node就是链表头结点以及马上进行一次CAS,如果成功获取到锁了,那么把当前线程的node设置成head,且断开当前node和前node的链,返回fase,表示获取到锁。如果不是,则进行下一步:
这个方法的目的是返回当前线程是否应该被阻塞,pred node就是当前线程node的前面一个等待node,注释说的很清楚,如果前面的node状态是signal,表示当前线程可以安全的park住,等待被唤醒,如果WS>0就是取消状态,如果前置node是取消状态,则双向链表往前回溯,直到找到当前node的前置node的状态不是取消状态,然后把当前阶段的前置node指向这个节点,如果是其他状态,则要把前置阶段设置成signal状态,等下次循环到这个方法的时候,就可以判断出这个节点是需要park的,当判断需要阻塞线程之后,执行:
这个方法就是调用了unsafe类的park方法,unsafe通过JNI调用了本地的C++实现的mutex锁,这个也不做展开。当这个线程被unpark唤醒的时候,返回的线程的中断状态。到这里,reentrantlock的非公平锁的原理就说完了,公平锁的原理比这个还要简单一些,想知道原理的可以自行查阅资料研究。说到底,reentrantlock以及其他的concurrent包下的同步类,在java层面都是利用集成了AQS的子类,去实现锁,怎样锁则是通过unsafe类调用了C++代码实现的mutex互斥锁。释放锁unlock操作,则是通过释放当前线程的充入数量,然后通过之前的双向链表,通过unpark方法去唤醒next node,当next node被唤醒之后,怎会继续执行
这个代码块,如果这个时候没有其他的线程去抢占这个锁,那么tryAcquire操作就会成功占有锁,如果这个时候有另外一个线程抢先CAS成功了,则这线程继续阻塞,因为这个是非公平锁。
到这里,锁就java的两个锁就说完了,总结一下,synchronized关键字是通过对象的monitor,然后通过monitorenter和monitorexit jvm指令来完成的。而Doug lea大神编写的concurrent包的同步类,在java层面通过继承AQS,然后配合unsafe提供的CAS操作以及链表这个数据结构,去实现乐观锁,互斥,共享,自旋等锁,当需要阻塞线程的时候,通过unsafe类调用操作系统的mutex互斥锁去实现阻塞。
总体已经说完了,java多线程并发涉及的知识点如果要深挖其实还有非常多,如果要一篇说全,包含了太多东西,java内存结构,jvm虚拟机,指令集,第一没这么多时间,很多大神用一本书都说不完,第二,本人知识储备还不够,没掌握的东西就不说了,以免误导,这边文章就介绍java的两种同步机制,synchronized以及reentrantlock。
如有不对的地方,希望可以及时指正,后面我会抽时间再来讲一下java的内存模型,内存分区,垃圾回收机制,以及多线程,线程池等相关的知识点,用来记忆和巩固,28岁了,有些东西不常用的话会忘。。。
java两种同步机制的实现 synchronized和reentrantlock的更多相关文章
- Java两种核心机制
1.Java虚拟机 2.垃圾回收
- ReentrantLock和synchronized两种锁定机制
ReentrantLock和synchronized两种锁定机制 >>应用synchronized同步锁 把代码块声明为 synchronized,使得该代码具有 原子性(atomicit ...
- Java中的ReentrantLock和synchronized两种锁定机制的对比
问题:多个访问线程将需要写入到文件中的数据先保存到一个队列里面,然后由专门的 写出线程负责从队列中取出数据并写入到文件中. http://blog.csdn.net/top_code/article/ ...
- java的两种同步方式, Synchronized与ReentrantLock的区别
java在编写多线程程序时,为了保证线程安全,需要对数据同步,经常用到两种同步方式就是Synchronized和重入锁ReentrantLock. 相似点: 这两种同步方式有很多相似之处,它们都是加锁 ...
- JAVA基础之两种核心机制
突然之间需要学习Java,学校里学的东西早就忘记了,得用最短的时间把Java知识理顺,重点还是J2EE,毕竟所有的ava项目中95%都是J2EE,还是先从基础的J2SE学起吧....... 首先是了解 ...
- java两种反射的区别 - Class.forName()和ClassLoader.loadClass()
在理解这两种反射机制之前,需要弄清楚java类的加载机制. 装载:通过类的全限定名获取二进制字节流(二进制的class文件),将二进制字节流转换成方法区中的运行时数据结构,在内存中生成Java.lan ...
- Redis系列之----Redis的两种持久化机制(RDB和AOF)
Redis的两种持久化机制(RDB和AOF) 什么是持久化 Redis的数据是存储在内存中的,内存中的数据随着服务器的重启或者宕机便会不复存在,在生产环境,服务器宕机更是屡见不鲜,所以,我们希望 ...
- Redis学习一:Redis两种持久化机制
申明 本文章首发自本人公众号:壹枝花算不算浪漫,如若转载请标明来源! 感兴趣的小伙伴可关注个人公众号:壹枝花算不算浪漫 22.jpg 前言 Redis是基于内存来实现的NO SQL数据库,但是我么你都 ...
- 详解Redis中两种持久化机制RDB和AOF(面试常问,工作常用)
redis是一个内存数据库,数据保存在内存中,但是我们都知道内存的数据变化是很快的,也容易发生丢失.幸好Redis还为我们提供了持久化的机制,分别是RDB(Redis DataBase)和AOF(Ap ...
随机推荐
- IntelliJ IDEA部署tomcat时Edit Configuration无artifact选项
IntelliJ使用 ##使用IntelliJ IDEA配置web项目时,选择Edit Configration部署Tomcat的Deployment可能会出现以下情况: 导致新手部署过程中摸不着头脑 ...
- [c#]WebClient异步下载文件并显示进度
摘要 在项目开发中经常会用到下载文件,这里使用winform实现了一个带进度条的例子. 一个例子 using System; using System.Collections.Generic; usi ...
- 如何在IE11中开启WebGL暨微软和WebGL的恩怨情仇录
正如我们上周报道的,国外开发者Francois Remy在泄露版Windows Blue附带的Internet Explorer 11中发现,WebGL接口已经封装完成,但功能上还未能开放支持.在这之 ...
- 【docker-compose】使用docker-compose部署运行spring boot+mysql 【处理容器的时区问题】【详解】【福利:使用docker-compose构建 wordpress+mysql】
==================================================================================================== ...
- 《STL源代码剖析》---stl_set.h阅读笔记
SET是STL中的标准容器,SET里面的元素会依据键值自己主动排序,它不像map那样拥有实值value和键值key的相应,set仅仅有实值.SET的底层实现时RB-tree,当插入到RB-tree中后 ...
- UITableView与UIScrollView的一些问题(持续更新)
UITableView的一些常用操作 --------------------------------------------------------------------------------- ...
- 如何中断正在执行IO的 Quartz 作业
Interrupt a Quartz job that doing IO 如果你想中断正在执行IO的 Quartz 作业,在你使用 InterruptibleChannel 时这是可行的.引用一下Or ...
- Latex:表格制作全攻略
给出一个制作复杂表格的例子,制作表格主要用到multicolumn,multirow和cline,其中,要使用multirow,必须usepackage{multirow} 如果要制作出如下图所示的表 ...
- FLume监控文件夹,将数据发送给Kafka以及HDFS的配置文件详解
详细配置文件flume-conf.properties如下: ############################################ # producer config ###### ...
- js中replace的用法
replace方法的语法是:stringObj.replace(rgExp, replaceText) 其中stringObj是字符串(string),reExp可以是正则表达式对象(RegExp)也 ...