1. synchronized 关键字 互斥锁
使用关键字synchronized
修饰的方法或代码块,可以保证其修饰内容在任何时刻,最多只有一个线程可以访问,可用于控制多个线程在访问共享资源时的并发问题。
每个对象都有一把锁,每个synchronized
修饰的方法都必须获得该对象的锁才能够执行,否则线程阻塞;一旦获得锁之后,就会独占该锁,直到该方法返回才能释放锁;
同步方法对应的锁是this
或其它对象;同步代码块对应的锁可以是this
也可以是其它对象或变量;而静态的同步方法的锁为当前类本身,因为静态方法是属于类的而不属于对象
使用synchronized关键字的多种场景:
public class TestSynchronized {private Test test;public TestSynchronized (Test test) {this.test = test;}// 1. 同步代码块 锁对象本质就是需要增删改的对象public void TestBlock {// 锁对象为 thissynchronized (this) {// 同步代码块}// 锁对象为 Object lockObject lock = new Object();synchronized (lock) {// 同步代码块}// 锁对象为 其它类synchronized (test) {// 同步代码块}}// 2. 同步方法public synchronized void TestMethods() {// 默认锁对象为:this// 同步方法体}// 3. 同步静态方法public synchronized static void TestStaticMethods() {// 默认锁对象为:当前类.class// 同步方法体} // 4. 静态方法内的同步代码块public static void TestStaticBlock {// 在静态方法内,同步代码块时就无法用某个对象来上锁了,只用能整个类上锁synchronized (TestSynchronized.class) {// 同步代码块} }
}public void Test {private int temp;
}
特性:
- 可重入锁:
synchronized
关键字实现的是可重入锁。这意味着,在同步方法或同步代码块内可以在不释放这个锁的情况下,再次进入同一个锁保护的区域。这通常用于处理递归方法调用的情况。
Object lock = new Object();
synchronized (lock) {System.out.println("First time acquiring it");synchronized (lock) {System.out.println("Entering again");synchronized (lock) {System.out.println("And again");}}
}
- 不公平性
synchronized
关键字默认情况下不保证公平性,即没有确定的顺序来分配锁。而ReentrantLock
类提供了一个公平性参数,可以创建一个公平的锁,确保等待时间最长的线程最先获取锁。
2. ReentrantLock 互斥锁
相比synchronized
这种隐式定义同步锁,JDK5.0提供了显示定义同步锁ReentrantLock
,可以进行显式上锁、解锁,且使用更加灵活(显式:需要手动开启和关闭锁,需要记得及时关闭锁;隐式:出了作用域范围自动释放);
Lock
只能对代码块上锁,而synchronized
有代码块锁和方法锁;- 使用
ReentrantLock
可以精确控制锁的获取和释放,并且支持更多的高级特性,比如可中断锁、超时获取锁、公平性等。 ReentrantLock
和synchronized
都是可重入锁;ReentrantLock
类实现了Lock
接口以及Serializable
接口,其中Lock
接口源码如下,ReentrantLock
对其中抽象方法进行了实现:
package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;public interface Lock {void lock();void lockInterruptibly() throws InterruptedException;boolean tryLock();boolean tryLock(long time, TimeUnit unit) throws InterruptedException;void unlock();Condition newCondition();
}
2.1. ReentrantLock
构造器
- 当设置true时,锁有利于授予访问最长等待的线程。否则,该锁不保证任何特定的访问顺序。
ReentrantLock构造器
public ReentrantLock() // 创建一个ReentrantLock的实例,这相当于使用ReentrantLock(false)
public ReentrantLock(boolean fair) // 根据给定的公平政策创建一个 ReentrantLock的实例
ReentrantLock 有两个构造函数。第一个是无参构造函数,它创建一个非公平锁。第二个构造函数接受一个布尔值,如果为 true,则创建一个公平锁,否则创建一个非公平锁。公平锁会按照线程请求锁的顺序来获取锁。构造器源码如下:
/*** Creates an instance of {@code ReentrantLock}.* This is equivalent to using {@code ReentrantLock(false)}.*/
public ReentrantLock() {sync = new NonfairSync();
}/*** Creates an instance of {@code ReentrantLock} with the* given fairness policy.** @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();
}
其中Sync
是ReentrantLock
定义的抽象内部类,它继承自 AbstractQueuedSynchronizer(AQS
)。AQS
是一个用于构建锁和同步器的框架,它使用一个整数状态来表示同步状态,并提供了用于阻塞和唤醒线程的机制。AQS 维护了一个同步状态(通过getState
和setState
方法访问),以及一个等待队列(用于阻塞和唤醒线程)。
Sync
是ReentrantLock
的核心组件,负责实现锁的获取和释放逻辑。Sync
类的子类NonfairSync
和FairSync
通过实现tryAcquire
和lock
方法来定义锁的具体行为。在ReentrantLock
的情况下,Sync
类还提供了lock
和nonfairTryAcquire
方法的实现,具体源码如下:
abstract static class Sync extends AbstractQueuedSynchronizer {// 序列化ID,用于确保序列化和反序列化时的兼容性private static final long serialVersionUID = -5179523762034025860L;/*** Performs {@link Lock#lock}. The main reason for subclassing* is to allow fast path for nonfair version.*/// 在 ReentrantLock 中,有两个子类 FairSync(公平锁)和 NonfairSync(非公平锁),它们提供了 lock 方法的具体实现abstract void lock();/*** Performs non-fair tryLock. tryAcquire is implemented in* subclasses, but both need nonfair try for trylock method.*/// 提供了非公平尝试获取锁的逻辑// 是 tryAcquire 方法的一个辅助方法,用于在非公平锁和公平锁的实现中尝试获取锁final boolean nonfairTryAcquire(int acquires) {// 获取当前线程final Thread current = Thread.currentThread();// 获取当前锁的状态int c = getState();// 如果状态为 0(即锁未被任何线程持有),则尝试使用 compareAndSetState 原子地将状态从 0 变更为 acquires(表示获取锁的线程数)。// 如果成功,设置当前线程为锁的独占拥有者,并返回 true。if (c == 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}// 如果状态不为 0,并且当前线程已经是锁的独占拥有者,则增加锁的持有计数。else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;// 如果新的计数超过最大值,则抛出相应错误信息if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");// 否则,更新状态并返回 true。setState(nextc);return true;}// 以上判断条件都不满足,返回 false 表示获取锁失败。return false;}// 尝试释放锁protected final boolean tryRelease(int releases) {// 计算新的锁状态:当前状态减去释放次数int c = getState() - releases;// 如果当前线程不是锁的拥有者,则抛出异常(只有锁的拥有者才能释放锁)if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();// 表示锁是否已经完全释放boolean free = false;// 如果新的锁状态为0,表示锁完全释放,将 free 标志设置为 trueif (c == 0) {free = true;// 锁被完全释放,将锁的独占拥有者更新为 null setExclusiveOwnerThread(null);}// 更新锁状态setState(c);return free;}// 检查当前线程是否独占持有锁protected final boolean isHeldExclusively() {// While we must in general read state before owner,// we don't need to do so to check if current thread is ownerreturn getExclusiveOwnerThread() == Thread.currentThread();}// 用于创建一个新的 ConditionObject 实例,是 Condition 接口的实现,用于管理等待和通知机制,// 允许线程在条件不满足时挂起,并在条件变为真时被唤醒。final ConditionObject newCondition() {return new ConditionObject();}// Methods relayed from outer class// 以下方法从 ReentrantLock 外部类转发调用到 Sync 内部类// 返回锁的当前拥有者final Thread getOwner() {return getState() == 0 ? null : getExclusiveOwnerThread();}// 返回当前线程持有锁的次数final int getHoldCount() {return isHeldExclusively() ? getState() : 0;}// 检查锁是否被持有final boolean isLocked() {return getState() != 0;}/*** Reconstitutes the instance from a stream (that is, deserializes it).*/// Sync 类的序列化构造函数// 它在反序列化时被调用,以确保锁的状态被正确重置为未锁定状态private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException {// 恢复对象的字段状态s.defaultReadObject();// 将锁状态设置为0,表示锁未被持有setState(0); // reset to unlocked state}}
接着往下看Sync
的实现类NonfairSync
和FairSync
的源码:
/*** Sync object for non-fair locks*/static final class NonfairSync extends Sync {// Sync 类的序列化ID,用于确保序列化和反序列化时的兼容性。private static final long serialVersionUID = 7316153563782823691L;/*** Performs lock. Try immediate barge, backing up to normal* acquire on failure.*/final void lock() {// 尝试通过原子操作 compareAndSetState,将锁的状态从 0 更改为 1(将未锁定的锁改为已锁定的锁)if (compareAndSetState(0, 1))// 将当前线程设置为锁的独占拥有者setExclusiveOwnerThread(Thread.currentThread());// 若这个原子操作失败(即锁已经被其他线程持有),则调用 acquire 方法来尝试正常获取锁elseacquire(1);}// 直接调用 Sync 类中定义的非公平尝试获取锁的 nonfairTryAcquire 方法protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}}
/*** Sync object for fair locks*/static final class FairSync extends Sync {// 序列化ID,用于确保序列化兼容性private static final long serialVersionUID = -3000897897090466540L;// 获取锁。这里的参数 1 表示获取一个锁许可// 如果锁没有被另一个线程占用并且立即返回,则将锁定计数设置为1。// 如果当前线程已经保持锁定,则保持计数增加1,该方法立即返回(可重入锁特性)。final void lock() {acquire(1);}/*** Fair version of tryAcquire. Don't grant access unless* recursive call or no waiters or is first.*/protected final boolean tryAcquire(int acquires) {// 获取当前线程(current)final Thread current = Thread.currentThread();// 获取当前锁的状态int c = getState();// 若锁未被任何线程持有if (c == 0) {// 检查是否有线程在等待队列中// 若没有等待的线程,则尝试通过 compareAndSetState 原子地将状态从 0 变更为 acquiresif (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {// 设置当前线程为锁的独占拥有者,并返回 truesetExclusiveOwnerThread(current);return true;}}// 如果状态不为 0,并且当前线程已经是锁的独占拥有者else if (current == getExclusiveOwnerThread()) {// 增加锁的持有计数int nextc = c + acquires;// 若新的计数超过最大值,则抛出错误if (nextc < 0)throw new Error("Maximum lock count exceeded");// 更新锁的新状态并返回setState(nextc);return true;}return false;}}
2.2. ReentrantLock
常用方法
void lock() // 获得锁
void unlock() // 尝试释放此锁// 只有在调用时它不被另一个线程占用才能获取锁
boolean tryLock()
// 如果在给定的等待时间内没有被另一个线程占用,并且当前线程尚未被保留,则获取该锁。
boolean tryLock(long timeout, TimeUnit unit)boolean isFair() // 如果此锁的公平设置为true,则返回 true 。
boolean isLocked() // 查询此锁是否由任何线程持有。
ReentrantLock常用方法简单实例
import java.util.concurrent.locks.ReentrantLock;// 1. 创建锁
ReentrantLock lock = new ReentrantLock();// 2. 上锁解锁
lock.lock();
// 临界区代码
lock.unlock();// 3. 尝试非阻塞地加锁:
boolean isLocked = lock.tryLock();
if (isLocked) {// 临界区代码lock.unlock();
}// 4. 带超时的尝试加锁:
long timeout = 1000; // 1秒
if (lock.tryLock(timeout, TimeUnit.MILLISECONDS)) {try {// 临界区代码} finally {lock.unlock();}
}// 5. 可重入加锁:
lock.lock();
try {// 可以再次获取同一把锁,而不会被阻塞lock.lock();// 临界区代码
} finally {lock.unlock();
}
2.3. lock()方法
具体上锁逻辑,需要看最开始选择了哪一个构造器来实例化ReentrantLock
,是公平锁还是非公平锁;
void lock()
// 获得锁
// 如果锁没有被另一个线程占用并且立即返回,则将锁定计数设置为1。
// 如果当前线程已经保持锁定,则保持计数增加1,该方法立即返回(可重入锁特性)。
// 如果锁被另一个线程保持,则当前线程将被禁用以进行线程调度,并且在锁已被获取之前处于休眠状态,此时锁定保持计数被设置为1。
2.4. unlock()方法
void unlock()
// 尝试释放此锁
// 如果当前线程是该锁的持有者,则保持计数递减(可重入机制)
// 如果保持计数现在为零,则锁被释放。
// 如果当前线程不是该锁的持有者,则抛出IllegalMonitorStateException(表示当前线程不持有此锁)。
2.5. trylock()方法
boolean tryLock()
// 只有在调用时它不被另一个线程占用才能获取锁
// 如果没有被另一个线程保持,则获取锁定,并立即返回值为true ,将锁定保持计数设置为1。
// 如果当前线程已经保存该锁,则保持计数增加1,该方法返回true 。
// 如果锁由另一个线程持有,则该方法将立即返回值为false 。
3. Semaphore 记录型信号量
semaphore
是一种计数器,用来管理一定数量的许可(permit)。它允许多个线程同时访问某些资源,如连接池、对象池等;
相比synchronized
和Lock
主要解决多个线程访问同一资源造成的数据不一致,semaphore
实现了资源的多副本的并发访问控制,可以限制同时访问特定资源的线程数量。此外semaphore
允许任何线程释放许可,并不是只有拥有线程才能释放资源;
Semaphore
适用于需要管理资源访问数量的场景,特别是在限制同时访问资源的线程数量时非常有用。Lock
提供了更多的灵活性和控制能力,适用于需要更复杂同步逻辑的场景。synchronized
则因其简单性,适用于基本的同步需求。
3.1. semaphore
构造器
Semaphore 可以是公平的或非公平的。公平的 Semaphore 会考虑线程的等待时间来分配许可,而非公平的 Semaphore 不考虑这一点。
// 创建一个 Semaphore 与给定数量的许可证和非公平公平设置。
Semaphore(int permits)
// 创建一个 Semaphore 与给定数量的许可证和给定的公平设置。
Semaphore(int permits, boolean fair) // 实例
Semaphore semaphore = new Semaphore(10, true);// 信号量构造器源码
// 非公平信号量
public Semaphore(int permits) {sync = new NonfairSync(permits);
}// 公平信号量
public Semaphore(int permits, boolean fair) {sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
公平信号量FairSync
和非公平信号量NofairSync
都是对Semaphore
内部抽象类Sync
的实现(与ReentrantLock
源码类似)
abstract static class Sync extends AbstractQueuedSynchronizer {// Sync 类的序列化ID,用于确保序列化和反序列化时的兼容性。private static final long serialVersionUID = 1192457210091910933L;// 构造函数// setState 继承自AbstractQueuedSynchronizer(AQS),其中的 State 是 AQS的// private volatile int state; 用于表示同步状态,在不同的同步机制中对应的意义不用// 在 Semaphore 中 State 表示可用的许可Sync(int permits) {setState(permits);}// 返回当前信号量可用的许可数量final int getPermits() {return getState();}// 非公平信号量尝试获取 acquires 个许可final int nonfairTryAcquireShared(int acquires) {// 表示无限循环,这个方法会一直尝试直到成功修改状态或者确定无法获取更多的许可。for (;;) {// 获取当前可用的许可数量int available = getState();// 当前可用许可减去申请许可后的剩余许可数量int remaining = available - acquires;// 若当前许可数量不足以满足请求,就直接返回 remaining,不允许这次请求// 若当前许可数量满意请求(remaining >= 0),就进行状态修改,允许该请求,然后再返回 remaining// 修改状态使用的是原子操作 compareAndSetState 将当前状态 available 修改为 remainingif (remaining < 0 ||compareAndSetState(available, remaining))return remaining;}}// 用于尝试释放 releases 个许可protected final boolean tryReleaseShared(int releases) {// 不断循环,直到操作成功或者出现异常才停止for (;;) {// 当前状态int current = getState();// 释放后的新状态值int next = current + releases;// 若新状态值导致许可数量上溢出,抛出对应错误if (next < current) // overflowthrow new Error("Maximum permit count exceeded");// 同样通过原子操作将当前状态 current 修改为 nextif (compareAndSetState(current, next))return true;}}// 用于减少 reductions 个许可final void reducePermits(int reductions) {for (;;) {// 当前可用许可int current = getState();// 减少后的许可数量int next = current - reductions;// 若减少后的新状态发生了下溢出,抛出对应错误if (next > current) // underflowthrow new Error("Permit count underflow");// 将当前状态 current 修改为 nextif (compareAndSetState(current, next))return;}}// 用于释放所有许可// 通常用于在信号量被销毁或者不再需要时,确保所有的许可都被释放,避免资源泄露。final int drainPermits() {for (;;) {int current = getState();// 如果当前可用许可已经为0,说明没有更多的许可需要释放,就直接返回当前状态// 否则就通过原子操作将当前状态更改为0,然后再返回当前的状态值if (current == 0 || compareAndSetState(current, 0))return current;}}}
接下来看看Sync
抽象类的具体实现,非公平信号量NofairSync
和公平信号量FairSync
的源码,类内部没有声明的方法都是直接继承Sync
中声明;
/*** NonFair version*/static final class NonfairSync extends Sync {private static final long serialVersionUID = -2694183684443567898L;// 调用父类的构造函数NonfairSync(int permits) {super(permits);}// 尝试非公平地获取 acquires 个许可,直接使用父类的方法// 在获取许可时不考虑线程等待队列,这可能会允许后来的线程先获取许可。protected int tryAcquireShared(int acquires) {return nonfairTryAcquireShared(acquires);}}/*** Fair version*/static final class FairSync extends Sync {private static final long serialVersionUID = 2014338818796000944L;// 调用父类的构造函数FairSync(int permits) {super(permits);}// 尝试公平地获取 acquires 个许可// 只有当该线程在资源等待队列中没有前驱线程,才能够进行许可授予protected int tryAcquireShared(int acquires) {for (;;) {// 检查当前线程是否有排队的前驱线程。如果有,返回 -1 表示不授予许可。if (hasQueuedPredecessors())return -1;// 当前可用的许可int available = getState();// 计算尝试获取许可后剩余的许可数量int remaining = available - acquires;// 若当前许可数量不足以满足请求,就直接返回 remaining,不允许这次请求// 若当前许可数量满意请求(remaining >= 0),就进行状态修改,允许该请求,然后再返回 remainingif (remaining < 0 ||compareAndSetState(available, remaining))return remaining;}}}
3.2. semaphore
常用方法
// 从该信号量获取许可证,阻塞直到许可可用,或线程为 interrupted 。
void acquire()
// 从该信号量获取给定数量的许可证,阻塞直到所有许可可用,否则线程为 interrupted 。
void acquire(int permits)// 返回此信号量中当前可用的许可数。
int availablePermits()
// 缩小可用许可证的数量。
protected void reducePermits(int reduction)// 释放许可证,将其返回到信号量。
void release()
// 释放给定数量的许可证,将其返回到信号量。
void release(int permits)// 获取并返回所有可立即获得的许可。
int drainPermits()
4. volatile 关键字
volatile
是 Java 中的一个关键字,用于声明一个变量在多线程环境中的可见性。当一个变量被声明为 volatile
时,它保证每次访问变量时都会从主内存中读取,而不是从线程的工作内存中读取;同样,当变量被修改时,新值也会立即写入到主内存中,以确保其他线程能够看到最新的值。
4.1. volatile 特征
- 内存可见性:
保证此变量对所有的线程的可见性:当一个线程修改了某一个volatile
变量的值,其他线程能够立即看到该变化;
- 有序性:
Java 内存模型(JMM)会禁止volatile
变量前后的读写操作进行指令重排序。这确保了volatile
变量的写操作在读操作之前完成,从而保证了程序的执行顺序。具体实现是通过对volatile
变量的读写操作前后加上各种特定的内存屏障来禁止指令重排序来保障有序性;
- 重排序是指编译器和处理器为了优化程序性能面对指令序列进行重新排序的一种手段,有时候会改变程序予以的先后顺序。
- 内存屏障就是:在内存屏障之前的指令全部执行完成之后,才允许执行内存屏障之后的指令,从而保证代码的顺序性。具体原理如下:
内存屏障可以分为两种:Load Barrier 读屏障和 Store Barrier 写屏障。
- 在读指令之前可以插入读屏障,让工作内存或CPU高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据。
- 在写指令之后可以插入写屏障,强制把写缓冲区的数据刷回到主内存中,让最新数据写入主存,使得其他线程可见。
- 非原子性
volatile
变量的复合操作(如volatile
++)是不具有原子性的
Reference
- Java 并发基础 volatile 关键字
- 必须了解的内存屏障
- Java中的volatile