前面我们已经知道了 Synchronized 在使用的时候有两种方式,一种修饰方法,一种是同步块的方式。
可以看出示例已经显示了这两种方式,我们可以通过这段代码的字节码来具体分析下它的底层工作原理。
可以运行两个命令:
我们可以看到如下的字节码文件:
从字节码可以看出,同步代码块和同步方法稍微有些不同。
Java 对象头和 monitor 是实现 Synchronized 的基础! 下面就这两个概念来做详细介绍。
Java 对象头:Hotspot 虚拟机的对象头主要包括两部分数据:** Mark Word(标记字段)、Klass Pointer(类型指针)**。
Monitor:我们可以把它理解为一个同步工具,也可以描述为一种同步机制, 它通常被描述为对象监视器。
当多个线程同时请求某个对象监视器时,对象监视器会设置几种状态用来区分请求的线程:
ContentionList 是一个先进先出(FIFO)的队列,每次新加入 Node 时都会在队头进行,通过 CAS 改变第一个节点的指针为新增节点。
当多个线程同时访问一段同步代码时,多个线程会先被存放在 EntryList 集合中,处于 Block 状态的线程,都会被加入到该列表。接下来当线程获取到对象的 Monitor 时,Monitor 是依靠底层操作系统的 Mutex Lock 来实现互斥的,线程申请 Mutex 成功,则持有该 Mutex,其它线程将无法获取到该 Mutex。
如果线程调用 wait() 方法,就会释放当前持有的 Mutex,并且该线程会进入 WaitSet 集合中,等待下一次被唤醒。如果当前线程顺利执行完方法,也将释放 Mutex。
Synchronized 内置锁在后续的 JDK 版本更新中做了许多的优化,在并发量不高,锁竞争不激烈的情况下,和 Lock 锁的性能基本没有太大差别,但在高并发下还是和 Lock 锁有一点差距,Lock 锁性能会更稳定些。
Lock 锁是基于 AQS(AbstractQueuedSynchronizer)实现的,底层会维护一个链表实现的 CLH 队列,用于存储阻塞的线程,而 state 用于描述加锁状态。
下图是我画的 ReentrantLock 获取锁的过程:
上图是 Lock 锁的基本流程图,大家熟记这个图,基本就能回答出 Lock 锁的实现过程了。
但实际工作中可能我们大多数的场景都是读操作要多于写操作,但在多线程的情况下,读操作本身不存在线程安全问题,并不会去修改共享变量。所以 java 针对这种情况。有专门的读写锁来处理这类问题。
ReentrantReadWriteLock 本身也是实现了 Lock 接口的,ReentrantLock 是一个独占锁,同一时间只允许单个线程访问,但是 ReentrantReadWriteLock 是允许多个读线程同时访问的,但是在写线程访问时,所有读线程和写线程都会被阻塞。
ReentrantReadWriteLock 特性:
公平性:支持公平锁和非公平锁,默认是非公平锁。非公平锁比公平锁有更高的吞吐量。
可重入:允许读锁可写锁可重入。写锁可以获得读锁,读锁不能获得写锁。读写锁最多支持 65535 个递归写入锁和 65535 个递归读取锁(这里有个需要注意的地方,ReentrantLock 的可重入次数为 2^32-1,而 ReentrantReadWriteLock 可重入次数为 2^16-1=65535,为啥呢?这是因为 ReentrantReadWriteLock 将 AQS 中的 state 域分成了两部分,读锁和写锁各占 16 位,具体可以往下看)。
(1 << 16) - 1 = 2^16-1
锁降级:在获得写锁的情况下再获得读锁(这和写锁是独占的并不冲突,这里是指一个线程可以活得写锁再获得读锁,独占是指多个线程之间),然后释放写锁称为锁降级,反之称为锁升级。允许写锁降低为读锁,反之不允许。
那读写锁又是如何实现锁分离来保证共享资源的原子性呢?
ReentrantReadWriteLock 也是基于 AQS 实现的,它的自定义同步器(继承 AQS)需要在同步状态 state 上维护多个读线程和一个写线程的状态,该状态的设计成为实现读写锁的关键。ReentrantReadWriteLock 很好地使用了高低位,来实现一个整型控制两种状态的功能,读写锁将变量切分成了两个部分,高 16 位表示读,低 16 位表示写。
线程获取读锁时:会先判断同步状态 state 是否为 0。如果 state 等于 0,说明暂时没有其它线程获取锁,此时判断是否需要阻塞,如果需要阻塞,则进入 CLH 队列进行阻塞等待;如果不需要阻塞,则 CAS 更新同步状态为读状态。
如果 state 不等于 0,会判断同步状态低 16 位,如果存在写锁,则获取读锁失败,进入 CLH 阻塞队列;反之,判断当前线程是否应该被阻塞,如果不应该阻塞则尝试 CAS 同步状态,获取成功更新同步锁为读状态。
线程获取写锁时:会先判断同步状态 state 是否为 0。如果 state 等于 0,说明暂时没有其它线程获取锁;如果 state 不等于 0,则说明有其它线程获取了锁。
此时再判断同步状态 state 的低 16 位(w)是否为 0,如果 w 为 0,则说明其它线程获取了读锁,此时进入 CLH 队列进行阻塞等待;如果 w 不为 0,则说明其它线程获取了写锁,此时要判断获取了写锁的是不是当前线程,若不是就进入 CLH 队列进行阻塞等待;若是,就应该判断当前线程获取写锁是否超过了最大次数,若超过,抛异常,反之更新同步状态。
对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!
不用多说,相信大家都有一个共识:无论什么行业,最牛逼的人肯定是站在金字塔端的人。所以,想做一个牛逼的程序员,那么就要让自己站的更高,成为技术大牛并不是一朝一夕的事情,需要时间的沉淀和技术的积累。
现在竞争这么激烈,只有通过不断学习,提高自己,才能保持竞争力。
对于一些不知道学习什么,没有一个系统路线的程序员,这里给大家提供一些学习资料
需要的小伙伴,可以一键三连, 点击这里获取免费领取方式!
内容涵盖:Java基础、JVM、高并发、多线程、分布式、设计模式、Spring全家桶、Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、MongoDB、Redis、MySQL、RabbitMQ、Kafka、Linux、Netty、Tomcat、数据库、云计算等
需要的小伙伴,可以一键三连, 点击这里获取免费领取方式!
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70000438/viewspace-2787396/ ,如需转载,请注明出处,否则将追究法律责任。