1 锁机制概述:为何需要锁
在如今这种多线程编程的环境里面, 当有好几个线程都需要在同一时间, 去访问同一个共享资源, 并且还要进行修改的时候, 如果缺少了能够进行有效协调的手段, 那么就很有可能会出现数据不一致之类的问题, 从而引发线程安全方面的风险。
而锁这个工具, 正是用来控制多个线程怎么去访问那些共享资源的, 它能够协助我们保证线程是安全的, 并且可以避免出现数据竞争的情况, 也能防止死锁问题发生。
java这门编程语言里, 它给出了特别丰富的锁机制办法, 这些办法主要用来解决并发访问时候产生的各种问题和麻烦之处。
关于Java语言里锁的分类体系, 我们来简单地说一说。
在Java这个编程语言里面, 锁这个东西可以根据各种不同的特点来进行多个角度的分类, 下面所展示的这个表格内容, 对核心层面的那些锁的类型以及它们各自具备的特点进行了汇总和概括。
关于锁的分类维度, 其具体表现是锁的类型以及对应的核心思想和特点, 并且通常还包括典型的实现方式和具体应用场景。
并发策略
悲观锁
假设并发冲突的情况是比较严重的, 那么在访问资源之前, 需要先加上锁。
,
乐观锁
先进行操作, 假设并发冲突比较低的情况存在, 然后在提交的那一刻再去检测是否存在并发冲突问题。
(基于CAS)
调度公平性
公平锁
它是按照线程申请锁的顺序来进行分配的, 这种顺序符合先入先出的原则, 这样一来就能够防止出现饥饿的情况, 尽管它的吞吐量可能比较低。
(true)
非公平锁
这种机制是允许插队的, 所以吞吐量往往是比较高的, 不过这同时也是可能会造成线程饥饿的。
, ()(这是默认的选项)
重入性
可重入锁
就是同一个线程可以去多次获取同一把锁, 这样做的话就可以避免发生死锁了。
,
资源共享
独占锁(写锁)
它具有排他这一种特性, 具体来说就是同一个时间点当中, 只能给那个唯一的线程带来访问的机会。
, , ck.
所谓共享锁, 这种被大家称之为读锁的东西。
允许多个线程在同一个时间里头, 去同时对那些共享了的资源进行读取操作。
ck.
我们要深入剖析核心锁机制的情况, 同时把悲观锁以及乐观锁这两部分内容进行对比。
这两种锁是两种最基本的并发控制哲学所代表的对象。
在性能开支的这个维度上, 由于悲观锁需要去执行加锁的动作, 还要去执行释放锁的动作, 并且还要涉及到线程挂起以及唤醒这样一些操作, 因此它所带来的性能开销是比较显著的。
而乐观锁这种机制呢, 如果在没有产生冲突的情况下, 或者是在冲突发生比较少的场景里, 它的性能表现是非常高的, 原因是因为它的这些操作通常根本不需要去阻断线程的执行。
可是如果遇到那些冲突非常激烈的场景, 那么就会出现问题了, 因为那样会导致更新的失败次数变得特别多, 还需要进行很多次的重试操作, 就好像是CAS的那种循环一样的操作在不断的发生, 这种做法最终会极大地消耗掉系统中的CPU资源, 从而让整体的运行效率大打折扣。
典型的应用场景。
️ 公平锁 vs 非公平锁
这组概念关注的是, 等待锁的线程, 它们的调度策略是什么样的。
性能与特点:
实现与默认策略: 在Java中, 关键字是非公平的。类则提供了灵活性, 通过构造函数参数 new (true)可以创建公平锁, 默认(无参或 false)则是非公平锁。由于性能优势, 在大多数情况下, 非公平锁是默认且更优的选择。
锁的优化与升级
为了能在保证线程安全的情况下, 去提升性能的这一效果, JVM, 特别是它, 还进行了一连串的锁优化方面的操作, 这些操作有一个名字, 就是锁升级, 这个过程的方向是固定的, 是单向性的, 它的目的, 主要是为了减少在获得锁和释放锁时, 所带来的那种性能上的消耗, 它的路径表现为无锁到偏向锁, 再到轻量级锁, 最后到达重量级锁。
在项目实战的过程当中,关于锁的选择问题。
在现实的具体项目开展过程里, 去选择那个最合适的锁这个动作, 是非常非常重要的。下面这些内容是一些用来给大家提供指导的原则, 还有在面对各种常见场景时候所给出的解决方案。
2.第1点内容关于悲观锁与乐观锁之间的对比, 这二者分别代表了两种不同的并发处理理念。
悲观锁 的基本思路是"先取锁,再访问"。在Java中,最典型的实现是 关键字和 。它们通过在访问共享资源前加锁,确保操作的原子性。
// 悲观锁示例 - synchronizedpublic synchronized void deductStock(int quantity) {
if (stock >= quantity) {
stock -= quantity;
return true;
}
return false;
}
// 悲观锁示例 - ReentrantLock
private final ReentrantLock lock = new ReentrantLock();
public boolean transfer(int amount) {
lock.lock(); // 获取锁
try {
if (balance >= amount) {
balance -= amount;
return true;
}
return false;
} finally {
lock.unlock(); // 释放锁
}
}
大家所说的乐观锁, 并不会真地去锁住数据内容, 它采用的是另一套法子来处理冲突检测的问题, 这套法子通常是基于所谓的 CAS 机制或者版本号机制来实现的。
// 乐观锁示例 - CAS(AtomicInteger)private AtomicInteger count = new AtomicInteger(0);
public boolean increment() {
int oldValue, newValue;
do {
oldValue = count.get();
newValue = oldValue + 1;
} while (!
count.compareAndSet(oldValue, newValue)); // CAS操作
return true;
}
// 乐观锁示例 - 版本号机制
public class OptimisticProduct {
private int stock = 100;
private int version = 0; // 版本号
public boolean deductStock(int quantity) {
synchronized(this) {
if (this.version == version) { // 检查版本
if (stock >= quantity) {
stock -= quantity;
version++; // 更新版本号
return true;
}
}
return false;
}
}
}
应用场景对比:
2.在第二点内容里面, 大家要对比一下公平锁和非公平锁这两个东西, 这里面的核心重点就是去选择那个调度的策略。
公平锁的内部会维护出来一个等待的队列, 当一把锁被释放的时候, 这把锁会把权限优先分配给这个队列里面等待时间最长的哪一个线程, 也就是说就是队列开头的那个线程, 这种操作是完全按照先进先出这样的一种顺序来进行的。
非公平锁是允许插队的操作的, 当一个新的线程去尝试获取锁的时候, 哪怕在它的等待队列里面已经存在其他线程在排队等候了, 这个新来的线程还是会先试图直接进行获取动作的, 如果在这个时候锁的状态恰好是处于可用的情况的, 它就能立刻把这个锁给拿到手, 从而就不需要去参与后面的排队过程了。
// 创建公平锁ReentrantLock fairLock = new ReentrantLock(true);
// 创建非公平锁(默认)
ReentrantLock nonFairLock = new ReentrantLock();
性能与特点:
在Java语言里面, 那个叫关键字的东西, 它代表的锁是非公平的。另外, 类这个地方是提供了灵活性的, 它是通过构造函数的参数, 从而创建公平锁的。因为在性能方面是有优势的, 所以在大多数的情况之下, 非公平锁是默认的选择, 并且被认为是最优的选项。
关于三, 也就是Java锁的实现原理与特性, 这一章里面的第一个小标题, 是三, 一节的实现的具体的情况以及后续的优化措施。
是Java内置的同步机制, 依赖于Java虚拟机(JVM)实现。它可以用于同步方法或同步代码块。
// 同步实例方法:锁住当前实例(this)public synchronized void synchronizedMethod() {
// 临界区代码
}
// 同步静态方法:锁住类对象(Class)
public static synchronized void staticSynchronizedMethod() {
// 临界区代码
}
// 同步代码块:可以锁定特定对象
public void method() {
synchronized (lockObject) {
// 临界区代码
}
}
它具有可重入这一个属性, 具体来说就是当一个线程获取到了锁之后, 它可以多次进入同步代码中去, 这样做的好处是可以避免线程因为自己操作的原因而造成死锁这种情况的发生。
锁升级这个过程是现在这个JVM的一个重要优化措施, 它的进化路径是这样的: 第一步是从无锁状态开始, 然后走向偏向锁的状态, 接着再变成轻量级锁的状态, 最后会到达重量级锁的这个阶段。
偏向锁这一机制, 是特别适用于那种实际上只有一个线程去访问同步块的场景当中的, 它能够通过记录那个线程的id, 来避免掉重复给加锁所带来的开销问题的;至于轻量级锁, 则是在当有轻微竞争发生时, 会采取通过自旋这种循环尝试的方式去尝试获取锁的;而重量级锁呢, 是在当竞争激烈的时候, 锁会进行升级为重量级锁的操作的, 此时那些未获取到锁的线程会被挂起起来的, 在3.2章节里的这个高级特性部分。
是 java.util..locks包中的高级锁,它提供了比更丰富的功能 :
public class ReentrantLockExample {private final ReentrantLock lock = new ReentrantLock();
public void execute() {
lock.lock(); // 获取锁
try {
// 临界区代码
} finally {
lock.unlock(); // 务必在finally中释放锁
}
}
// 尝试获取锁(可超时、可中断)
public boolean tryExecute(long timeout, TimeUnit unit) throws InterruptedException {
if (lock.tryLock(timeout, unit)) {
try {
// 临界区代码
return true;
} finally {
lock.unlock();
}
}
return false;
}
}
主要优势特性:
3.第3点, 读写锁以及那个括号。
这是一个更细粒度的锁。它允许多个读线程同时访问共享资源。但是, 在有写操作的时候, 写线程会独占资源。
public class ReadWriteLockExample {private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private int data = 0;
public int read() {
rwLock.readLock().lock(); // 获取读锁(共享)
try {
return data;
} finally {
rwLock.readLock().unlock();
}
}
public void write(int value) {
rwLock.writeLock().lock(); // 获取写锁(独占)
try {
data = value;
} finally {
rwLock.writeLock().unlock();
}
}
}
这是Java 8引入的一种改进型的读写锁, 这种锁提供了乐观读模式, 其性能比更高。
public class StampedLockExample {private final StampedLock lock = new StampedLock();
private int data = 0;
public int read() {
// 尝试乐观读
long stamp = lock.tryOptimisticRead();
int currentData = data;
// 验证是否有写操作发生
if (!
lock.validate(stamp)) {
// 升级为悲观读锁
stamp = lock.readLock();
try {
currentData = data;
} finally {
lock.unlockRead(stamp);
}
}
return currentData;
}
}
对于第4部分锁的性能优化与最佳实践中的第4.1小节提到的锁性能优化的10个核心原则来说, 读者需要尽量使用无锁方案。这是因为对于简单的计数器或者累加操作这样的场景, 大家应该优先使用特定的工具类。同时, 大家需要减小锁粒度。这意味着只锁定必要的代码块, 而不要去锁定整个方法。
此外, 大家还需要避免锁嵌套。因为锁嵌套容易导致死锁现象出现, 并且还会增加锁竞争。最后, 大家可以使用读写锁。这主要用在读多写少的场景中, 为了进一步优化效果大家可以参考相关内容。当存在不同的业务逻辑时, 应当采用不同的锁进行隔离, 从而避免发生竞争。
需要严格避免在获取锁的状态之内执行耗时较长的操作, 例如发起网络请求或进行IO操作等。在使用集合数据结构的时候, 应该调用线程安全级别更高的相关实现类包, 这样能够获得更好的运行性能。如果非必要的话, 请尽量避免使用公平锁机制, 因为从性能角度来看, 公平锁的表现通常不如非公平锁优秀。
在处理高并发场景时, 可以考虑采用分段锁这种设计方式以提供参考借鉴实例。对于系统中锁的竞争状况, 建议及时利用诸如JDK工具集中自带的监控组件等来进行跟踪观察与分析。4.在项目实战当中, 我们会选择什么样的锁策略。
在实实在在的项目执行过程里面, 挑选一个对路的锁是非常关键的一件事情。下面这里有一些可以拿来参考的指导性原则, 还有一些针对常见应用场景拿出来的解决办法。
我们需要进行第一步, 也就是对冲突发生的可能性做出判断。
针对具体场景的锁选择, 以下是给出的相关建议内容:
要避开那些平常会犯错的坑穴。