前言
又过去了一个周末,最近陆陆续续的看了《并发编程的艺术》一书,对锁有不少感悟,这次就聊聊Java中的锁事。本文纯粹是漫谈,想到哪说到哪,但准确性肯定会保证,倘若有不正确之处,还请交流指正。
正文
作为Java开发, 说到锁第一时间想到的肯定是synchronized和juc包中的lock锁这俩兄弟,但如果眼光放开点,会发现还有很多其他的锁:集群/分布式环境下的分布式锁、mysql中的那一家子锁、操作系统中基于信号量/互斥量等构成的锁。。。上面说的是锁的应用场景,而说起锁本身,定义的类型也不少,什么悲观锁/乐观锁、独占锁/共享锁、读写锁、公平锁/非公平锁、重入锁、轻量级锁/重量级锁、自旋锁、偏向锁...
锁是干什么的?用个人的话来总结一下就是:用于控制不同访问来源对同一数据的访问顺序。也就是说锁的应用涉及到两个关键条件:不同访问来源(可以是不同线程、不同进程、不同APP)和操作同一数据(即共享的数据)。
下面对锁进行一下总结,首先按锁的实现思想分类:
悲观锁和乐观锁属于锁的实现思想,synchronized是悲观锁,而基于乐观锁思想的实现是以CAS为基础构建的lock锁,同样mysql中的锁属于悲观锁,用redis或者zk构建的分布式锁也都是悲观锁,操作系统中基于信号量/互斥量等构建起来的锁也都属于悲观锁范畴。所以用悲观锁/乐观锁的思想标准来看锁,发现各种各样的锁定义其实都在这二者的思想范围内,没有能跑出该范围的。
独占锁/共享锁(读写锁)属于锁的占用类型,有这两种分类的原因就是为了提高系统的并发能力--读不影响读,这里可以再引申一步,mysql为了进一步提高并发能力通过数据多版本使得读跟写也不冲突。其实这里就是提高并发能力的一条线:先是不管读请求还是写请求统统排队进行,后来将读写分开提高读读的效率,再后来通过数据多版本使得读写可以同时进行,后面还能再提高吗?或许参考redis的单线程内存操作是一个方向,但对于复杂数据形式和大数据量却不适合。
公平锁和非公平锁主要针对的是获取锁的方式,公平就是一起排队先排队的先获取到锁,而非公平则表示一起竞争后来的也可能先获取到锁。公平锁的应用场景很少,而且主要是通过lock锁实现的,平时基本都是用非公平锁,无他,非公平锁并发量比公平的强了不止一点。但要额外说一下,lock锁中的非公平模式并不是完全的非公平,如果两次获取不到锁则进入阻塞队列,进入阻塞队列中后,它就只能按队列中的顺序挨个获取锁了,所以lock锁中的非公平模式并不是彻头彻尾的非公平,世间尚存一丝公道。。。
重入锁和非重入锁属于锁的性质,这个很好理解,可以同一获取锁的来源能重复获取的锁就是可重入的,非可重入的场景很少,我们平时接触的基本都是可重入。可重入的实现,基本原理都是在获取到锁之后,将对象记录下来,下次再触发获取锁的操作时先比对一下当前对象与已记录对象是否是同一个,是的话则能获取到锁,锁计数+1。详情可见ReentrantLock的加锁过程。
由于synchronized锁是JVM本身自带的关键字,所以针对synchronized锁做了很多优化,偏向锁/轻量级锁/自旋锁/重量级锁等概念都是来自于此(对synchronized锁等讲解可移步博主之前写过的一/二/三系列【https://i.cnblogs.com/posts?cateId=1466867&page=1】)。其实从原理上来说,自旋锁不是锁,只是在获取不到锁时先自循环一定次数继续尝试获取锁,如果仍然获取不到再阻塞,是针对很快能获取到锁的场景进行的优化处理。
下面再来几张图梳理下思绪: