前言:刚开始我看到这个标题的时候我感觉“很熟悉,但是又很陌生”,因为锁是有效的解决并发情况下保证临界资源操作原子性的有效手段之一。下面我就从我们几个开发使用的角度来说我们常用的锁。
锁可以解决什么问题?
锁可以解决并行执行任务执行过程中对,共享数据顺序访问、修改的场景。比如对同一个账户进行并行扣款或者转账。下面我们展开讨论下 synchronized 、ReetranLock 以及他们的使用。
synchronized
synchronized 是 JDK 提供的内置锁, 由 JVM 虚拟机内部实现,是基于 monitor 机制, 在 JDK 1.6 之后被优化,会有一个锁升级的过程,将锁的状态存储到对象头中。
锁升级过程,默认是无锁状态,首先会进行判断,如果是没有字段竞争的情况下会使用偏向锁,偏向锁的本质就是将当前获得锁的线程 id 设置到共享数据的对象头中。然后升级为轻量级锁,轻量级锁的本质是通过 CAS 来修改 MarkWord 来实现的。最后再升级为重量级锁,我们可以通过操作系统的 monitor 依赖操作系统的 MutexLock(互斥锁)来实现的 。
四种使用方式
- 在静态方法上使用
- 在普通方法上使用
- 锁定 this 状态
- 锁定静态类
加锁状态记录位置
对象加锁,记录在对象头中,对象头如下图所示。
在运行期间,Mark Word里面存储的数据会随着锁标志位的变化而变化。Mark Word可能变为存储以下4种数据,如下图所示
锁的膨胀和升级
锁的升级和膨胀时候不可逆转的。
使用场景
JDK 在并发包中, 使用 synchroinzed 的地方有:
- ConcurrentHashMap (jdk 1.8)
- HashTable
ReetrantLock
ReetrantLock 开发作者是 Doug Lea ,从 JDK1.5 开始过后加入 JDK 的锁,主要是通过 QAS 的方式来实现的, 通过 Unsafe 包提供的 CAS 操作来进行锁状态(state)的竞争。然后通过 LockSupport.park(this). 进行 park 住线程,如果在 AQS 队列头的对象进行唤醒执行 unpack 方法,然后让他去竞争锁。
ReetrantLock 还分为公平锁和非公平锁,默认是非公平锁。因为公平锁,是需要保证竞争者按照获取锁的顺序进行获得,性能略低于非公平锁。
AQS 队列结构如下所示,它的本质是一个 FIFO 的线程安全的同步队列,如下图所示:
ReetrantLock 加锁和解锁的过程如下图所示:
使用方式
ReetrantLock 的使用方式如下,主要是有三个步骤:创建、加锁、解锁。
- class X {
- private final ReentrantLock lock = new ReentrantLock();
- // ...
- public void m() {
- lock.lock(); // block until condition holds
- try {
- // ... method body
- } finally {
- lock.unlock()
- }
- }
- }
使用场景
JDK 在并发包中, 使用 ReetrantLock 的地方有:
- CyclicBarrier
- DelayQueue
- LinkedBlockingDeque
- ThreadPoolExecutor
- ReentrantReadWriteLock
- StampedLock
上面我只是列举了一部分,对于 ReetrantLock 来看可以说是并发包中非常基础的类,也是我们学习并发的基础,在后续的文章中我会给展开做更加深入的分析。
如何选择锁?
1.对于单机环境我们在 JDK 内进行并发控制我们可以使用 synchronized (内置锁) 和 RentrantLock 。
2.对于自增或者原子数据累计我们可以使用 Unsafe 提供的原子类,比如 AtomicInteger , AtomicLong
3.对于数据库的话,对于用户金额扣除的场景我们可以使用乐观锁的方式来进行控制,SQL 如下
- update table_name set amount = 100,
- version = version + 1 where id = 1 and version = 1;
4.对于分布式场景下我们需要保证一致性,可以使用 Redis 或者 Zk 实现分布式锁。来进行分布式场景下的并发控制。
参考信息
《深入理解 Java 虚拟机》周志明
https://blog.csdn.net/wangbo199308/article/details/108688109
【编辑推荐】