时间:2025-09-14 19:22
人气:
作者:admin
synchronized是常用来保证代码的原子性的。
//1.修饰实例方法 // 有两个对象obj1和obj2,线程A调用Object.test(),线程B调用obj2.test(),不会互斥 // 但A和B如果都调用obj1.test(),会互斥
//场景:一个银行账户对象,不同线程操作同一个账户时要排队 public synchronized void test(){ } //2.修饰静态方法 //线程A调用obj1.test(),线程B调用obj2.test(),仍然会互斥,因为锁是类锁,不是实例锁
//场景:当”类的所有实例共享资源“需要保护时,如修改全局配置、写日志文件、统计类的静态计数器 public static synchronized void test(){ } //3.修饰代码块
//场景:只要锁住”关键区域代码“而不是整个方法。如转账时,只锁定两个账户,避免锁范围太大影响性能 public void test() { synchronized(this){ //不同实例对象可以同时 //synchronized(Example.class),不管有多少个对象实例,同时只能有一个线程进入 //临界区代码 } }
实例方法锁:给一间“卧室”上锁(对象自己的房门),别人想进要拿这间房的钥匙。
静态方法锁:给“整栋楼的大门”上锁(类锁),不管哪间房都进不去,大家都排队。
代码块锁:只给“卧室里的保险柜”上锁(对象里的某个资源),你可以自由走动,但柜子只能一个人开。


2. synchronized锁住的是什么?
每个对象都有一个隐含的锁机制(Monitor),java虚拟机里用ObjectMonitor实现。当线程执行synchronized时,本质就是在尝试获取对象对应的ObjectMonitor。
在ObjectMonitor里,有几个核心变量:
工作机制:
1. 可见性(加锁更新完后其他线程可见)
2. 有序性(最终结果一致)
3. 可重入性
在HotSpot里,每个对象都有一个对象头,其中有一块区域叫Mark Word,里面会存储对象的一些运行时数据(如对象的哈希码、GC分代年龄、锁标志位(2bit)、是否偏向锁标志(1bit)、锁的指针)。JMV就靠着64bit来标记对象当前的锁状态。
① 无锁:默认情况下,对象是无锁状态。
② 偏向锁:若一个对象总是被同一个线程获取锁,JVM就会偏向它,把Mark Word里直接记录这个线程ID。下次这个线程再来,就不用做CAS了,直接认定它已经持有锁-->提高性能
偏向锁获取:
1)检查对象是否可偏向(看偏向锁标志位是否为01、偏向标志)
2)检查是不是自己(看线程ID是否等于自己的-->等于则直接执行代码,不用CAS)
3)不是自己,就尝试CAS:若成功则拿到锁,把线程ID改为自己;若失败,则说明有竞争。
4)竞争失败:JVM在安全点停下来,把偏向锁升级为轻量级锁,然后按照轻量级锁的逻辑来竞争
5)执行同步代码。
偏向锁的释放:(与普通锁不同,不会主动释放,只有其他线程来竞争的时候,才会撤销)
1)有人来抢-->检查原来的持有线程是否还在用
- 不用了:撤销偏向锁(回到无锁)
- 还在用:升级为轻量级锁
2)太频繁就批量优化(批量重偏向/批量撤销)
③ 轻量级锁:当另一个线程也来竞争时,偏向锁就失效了,JVM会升级为轻量级锁。JVM在当前线程的栈帧里创建一个叫Lock Record的结果,会保存【该对象当前的Mark Word副本,线程自己“占有锁”标记】。然后JVM会尝试用CAS把对象头里的Mark Word改为指向这个Lock Record指针。若成功了,说明这个线程抢到了锁。(不再存放哈希码;只涉及用户态,性能高)
④ 重量级锁:多个线程同时竞争,CAS不断失败,会把对象头里的Mark Word改成指向一个Monitor对象的指针,里面有Owner、EntryList和Waitset,此时抢不到锁的线程会先进入“自旋”(默认10次),若达到等待次数后还未获取到锁会被挂起(阻塞),等锁释放后再唤醒。
synchronized做了哪些优化?
(1)AQS(AbstractQueueSynchronizer),是JDK并发包(java.util.concurrent)里的一个抽象类。是一个通用的同步器框架,帮你处理”线程竞争资源-->排队等待-->成功后唤醒的逻辑。
(2)核心组件
(3)工作流程
lock() → 调用 AQS 的 acquire()。
如果 state==0,用 CAS 设置 state=1,成功 → 当前线程获得锁。
如果失败,进入 AQS 队列,挂起等待。
unlock() → 调用 AQS 的 release(),把 state 设为 0,并唤醒下一个线程。
ReentrantLock是可重入的独占锁:只有一个线程可以获取该锁,该线程可以多次获取锁。
// 默认创建⾮公平锁
ReentrantLock lock = new ReentrantLock ();
// 获取锁操作
lock . lock ();
try {
// 执⾏代码逻辑
} catch ( Exception e x ) {
// ...
} finally {
// 解锁操作
lock . unlock ();
}
在构造函数可以传入参数true。非公平锁在调用lock后,会先调用CAS进行一次抢锁,没抢到就排到后面去。
在CAS失败后,和公平锁一样会进入到tryAcquire中,若发现state==0,非公平锁会直接抢锁,但公平锁会判断等待队列是否有线程处于等待,若有则不去抢。
CAS叫Compare And Swap,通过处理器的指令来保证操作的原子性。包含三个参数:共享变量的内存地址A、预期的值B、共享变量的新值C。
只有当A的值 = B时,才能将A的值变为C。作为一条CPU指令,CAS指令本身是能保证原子性的。
//线程A尝试执行: CAS(V, A=100, B=120) //看地址V里是不是100,若是就更新为120 //线程B尝试执行 CAS(V, A=100,B=150) //B以为余额还是100,但此时内存里已经是120了,所以不匹配,CAS失效
import java.util.concurrent.atomic.AtomicInteger;
public class CasDemo {
public static void main(String[] args) {
AtomicInteger balance = new AtomicInteger(100);
// 线程 A:尝试从 100 改成 120
boolean aSuccess = balance.compareAndSet(100, 120);
System.out.println("线程A成功? " + aSuccess + ",余额=" + balance.get());
// 线程 B:尝试从 100 改成 150
boolean bSuccess = balance.compareAndSet(100, 150);
System.out.println("线程B成功? " + bSuccess + ",余额=" + balance.get());
}
}
//1.使用原子类:基于CAS+volatile
AtomicInteger i = new AtomicIntger(0);
i.incrementAndGet();
//性能最好,推荐用于计数器,并发统计
//2.使用JUC包中的锁(ReentrantLock)
ReentrantLock lock = new ReentrantLock();
int i = 0;
lock.lock();
try {
i++;
} finally {
lock.unlock();
}
//可实现公平锁、可中断锁
//3.使用synchronized
//JVM层面提供的内置锁
int i = 0;
synchronized (this) {
i++;
}
//自动释放锁,但功能比ReentrantLock少
public final int getAndIncrement() {
return unsafe.getAndInt(this, valueOffset, 1);
}
//unsafe:这是JVM内部的Unsafe类,提供了底层操作内存的能力
//getAndAddInt:内部基于 CAS 实现,即比较并交换。
//valueOffset:内存偏移量,指向 AtomicInteger 里真正存值的 value 字段。
//1:表示要加的值。
//getAndInt逻辑
do {
int oldValue = getIntVolatile(obj, offset); // 读当前值(保证可见性)
int newValue = oldValue + 1; // 计算新值
} while (!compareAndSwapInt(obj, offset, oldValue, newValue)); // CAS
这里的 compareAndSwapInt 是一个 native 方法,调用 CPU 的 CAS 指令(通常是 cmpxchg)。
如果 内存值 == oldValue,就更新为 newValue,返回 true。
否则说明有竞争,更新失败,循环重试。
AtomicInteger.getAndIncrement() 就是:
读当前值;
用 CAS 尝试把它加 1;
如果失败就重试,直到成功。
靠 CAS 保证了原子性,靠 volatile 保证了可见性。
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成互相等待的现象,在无外力作用下,这些线程会一直相互等待。
造成死锁的四个必要条件:
避免死锁:破坏至少一个条件。
可以使用JDK自带的命令行工具排查:
还可以用图形化的工具。
[1] 沉默王二公众号
Semaphore:state 表示剩余的许可数量。
CountDownLatch:state 表示还需要等待多少个事件。