锁可以解决什么问题?
锁可以解决并行执行任务执行过程中对,共享数据顺序访问、修改的场景。比如对同一个账户进行并行扣款或者转账。下面我们展开讨论下 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 来看可以说是并发包中非常基础的类,也是我们学习并发的基础,在后续的文章中我会给展开做更加深入的分析。
如何选择锁?
- 对于单机环境我们在 JDK 内进行并发控制我们可以使用 synchronized (内置锁) 和 RentrantLock 。
- 对于自增或者原子数据累计我们可以使用 Unsafe 提供的原子类,比如
AtomicInteger
,AtomicLong
- 对于数据库的话,对于用户金额扣除的场景我们可以使用乐观锁的方式来进行控制,SQL 如下
update table_name set amount = 100, version = version + 1 where id = 1 and version = 1;
- 对于分布式场景下我们需要保证一致性,可以使用 Redis 或者 Zk 实现分布式锁。来进行分布式场景下的并发控制。