首页 > 基础资料 博客日记
Java并发编程(4)
2025-09-14 19:30:02基础资料围观27次
锁
1、synchronized用过吗?怎么用?
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的实现原理
- 怎么加锁?
- 修饰代码块:JVM采用monitorenter、monitorexit两个指令,一个开始一个结束
- 修饰方法:JVM在方法表里打上一个标志位ACC_SYNCHRONIZED。在执行时,JVM会检查:如果这个方法有这个标志,调用它时就会自动获取对应对象的Monitor锁-->执行完自动释放锁,不需要显式写lock/unLock。
2. synchronized锁住的是什么?
每个对象都有一个隐含的锁机制(Monitor),java虚拟机里用ObjectMonitor实现。当线程执行synchronized时,本质就是在尝试获取对象对应的ObjectMonitor。
在ObjectMonitor里,有几个核心变量:
- _owner:指向当前持有锁的线程
- _count:重入次数(一个线程重复进入同一锁时+1)
- _EntryList:保存所有正在“争抢锁”的线程
- _WaitSet:保存调用了wait()方法的线程(等待被唤醒)
工作机制:
- 加锁:
- 线程尝试获取Monitor
- 如果没人持有,设置_owner = 当前线程,执行成功。
- 若有人持有,就把自己假如_EntryList,进入阻塞状态
- 解锁:
- 当前线程执行完同步代码块,_count -1
- 若_count == 0,说明完全释放锁,把_owner置空
- 然后从_EntryList里挑一个线程来获取锁
- 等待/唤醒
- wait():当前持锁线程把自己放入_WatiSet,同时释放锁(_Owner置空)
- notify():随机从_WaitSet里唤醒一个线程,他会回到_EntryList,等待重新竞争锁
3、除了原子性,synchronized可见性、有序性、可重入性怎么实现?
1. 可见性(加锁更新完后其他线程可见)
- 线程加锁前,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。
- 加锁后,其他现场无法获取哦主内存中的共享变量
- 线程解锁前,必须把共享变量的最新值刷新到主内存中。
2. 有序性(最终结果一致)
- synchronized同步的代码块,具有排他性,一次只能被一个线程拥有,所以能保证同一时刻,代码时单线程执行的。
- as-if-serial:单线程的程序保证最终结果是有序的,但不保证不会指令重排
- 所以synchronized保证的有序是执行结果的有序,而不是指令重排的有序性。
3. 可重入性
- 意思:允许一个线程二次请求自己持有对象锁的临界资源。
- synchronized锁对象的时候会有个计数器,记录下同一线程获取锁的次数,在执行完对应的代码块之后,计数器会-1,直到计数器清零,就释放锁了,别的线程才有机会获取。
4、锁升级?synchronized优化了解吗
在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做了哪些优化?
- JDK1.6之前的:直接调用ObjectMonitor.enter()和ObjectMonitor.exit(),这种就是重量级锁,一旦线程竞争,就会发生操作系统层面的阻塞/唤醒,性能差
- JDK6后引入优化策略:
- 偏向锁:不做CAS,直接改偏向锁标记
- 轻量级锁:每个线程在自己的栈里创建一个Lock Record,用CAS操作尝试获取锁。
- 自旋锁:轻量级锁升级到重量级时,若发现锁被占用,先别急着阻塞,先在CPU上循环“自旋“一段时间(避免不必要的操作系统上下文切换)
- 锁粗化:连续出现多次synchronized,JVM将连续加锁/解锁合并成一个大范围的锁
- 锁消除:JIT编译器发现加锁没有必要(数据没有共享),直接把锁去掉。
5、synchronized和ReentrantLock的区别
- 锁的实现:
- synchronized是Java语言的关键字,基于JVM实现(JVM编译时会自动管理)。
- ReentrantLock是基于JDK的API层面(要自己写代码)实现的(lock和Unlock方法配合try/finnaly语句块来完成)
- 性能:
- 在JDK1.6锁优化之前,synchronized的性能比ReentrantLock差很多。但JDK6开始优化后,性能就差不多了。
- 功能特点:
- synchronized中若一个线程在等待获取锁,只能一直等下去,不能被中断。ReentrantLock提供lockInterruptibly()方法,若线程在等待锁的过程中被interrupt,它会立刻响应中断,放弃等待。
- synchronized永远是非公平锁(JVM不保证等待时间最长的线程一定先拿到锁)。ReentrantLock可以在构造时指定非公平锁还是公平锁。
- synchronized配合wait()、notify()、notifyAll()来实现线程间通信。这些方法是Object的方法,比较原始。ReentrantLock提供Condition对象。这个对象可以创建多个Condition队列,精确控制哪个线程被唤醒,但synchronized的wait/notify只有一个等待队列,无法细分。
- synchronized的锁的获取和释放是JVM自动管理(进入同步块时加锁,退出时自动释放)。ReentrantLock必须手动lock和unclok,虽然灵活但容易因为忘记unlock出现死锁。
6、AQS了解多少
(1)AQS(AbstractQueueSynchronizer),是JDK并发包(java.util.concurrent)里的一个抽象类。是一个通用的同步器框架,帮你处理”线程竞争资源-->排队等待-->成功后唤醒的逻辑。
(2)核心组件
- 同步状态:private volatile int state,state就是一份资源计数。如ReentrantLock:0表示没被占用,1表示已加锁。修改state用CAS来保证原子性,volatile保证可见性。
- 等待队列:若线程抢锁失败,会被封装成一个Node节点,放进队列。这个队列是FIFO双向链表。队列里的线程会挂起(park()),等前驱节点释放锁时被唤醒(unpark())
- 独占 / 共享模式:独占(一个线程持有锁如ReentrantLock);共享(多个线程能同时获取资源(比如Semaphore允许N个线程同时进入)
(3)工作流程
- 尝试获取锁:调用acquire()-->内部会调用tryAcquire()。若state修改成功,当前线程获取锁。若失败,入队等待。
- 等待/挂起:队列里的线程会挂起,节省CPU
- 释放锁:调用release-->内部调用tryRelease(),成功释放后,会唤醒队列中的下一个线程。
📖 举个例子:ReentrantLock(独占锁)
-
lock()
→ 调用 AQS 的acquire()
。 -
如果 state==0,用 CAS 设置 state=1,成功 → 当前线程获得锁。
-
如果失败,进入 AQS 队列,挂起等待。
-
unlock()
→ 调用 AQS 的release()
,把 state 设为 0,并唤醒下一个线程。
7、ReentrantLock的实现原理
ReentrantLock是可重入的独占锁:只有一个线程可以获取该锁,该线程可以多次获取锁。
// 默认创建⾮公平锁 ReentrantLock lock = new ReentrantLock (); // 获取锁操作 lock . lock (); try { // 执⾏代码逻辑 } catch ( Exception e x ) { // ... } finally { // 解锁操作 lock . unlock (); }
- 公平锁:多个线程按照申请锁的顺序来获取锁,线程直接进入队列排队。
- 优点:等待锁的线程不会饿死。
- 缺点:整体吞吐效率相比非公平锁要低。
- 非公平锁:多个线程加锁时尝试获取锁,获取不到采取排队。
- 有点:减少唤起线程的开销,整体吞吐效率高。
- 缺点:处于等待队列中的线程可能会饿死。
- 默认创建的对象lock()非公平的:
- 若所当前没有被其他线程占用,并且当前线程之前没有获取过该锁,那当前线程会获取到该锁,然后设置当前锁的拥有者为当前线程,并设置state为1,直接返回。若当前线程获取过该锁,则这次只是把state+1
- 若该锁之前被其他线程持有,非公平锁会尝试去获取锁,获取失败后,进入AQS队列阻塞挂起。
8、ReentrantLock怎么实现公平锁的
在构造函数可以传入参数true。非公平锁在调用lock后,会先调用CAS进行一次抢锁,没抢到就排到后面去。
在CAS失败后,和公平锁一样会进入到tryAcquire中,若发现state==0,非公平锁会直接抢锁,但公平锁会判断等待队列是否有线程处于等待,若有则不去抢。
9、CAS是什么
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()); } }
10、CAS有什么问题?怎么解决
- ABA问题:线程A期望把值从100改为200;中途线程B把值改成了200,又改为100。线程A再CAS检查时,发现还是100,于是成功更新为120。但线程A不知道这个值已经被修改过了。
- 解决:用版本号。每次被更新,版本号都会+1
- 循环性能开销:若多个线程一直同时竞争同一个变量,CAS可能一直失败,导致CPU飙升,但操作却无法完成。
- 解决:结合锁 或 退避算法(比如退一会儿再尝试)
- 只能保证一个变量的原子操作:若账户A扣钱,B加钱,需要保证两个操作都是原子性的,但CAS只能保证一个变量。
- 解决:用锁来保证。
11、Java有哪些保证原子性的方法?如果保证多线程下i++结果正确
//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少
12、原子操作类了解多少
13、AtomicInteger的原理
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 保证了可见性。
14、线程死锁了解吗?如何避免
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成互相等待的现象,在无外力作用下,这些线程会一直相互等待。
造成死锁的四个必要条件:
- 互斥条件:同一资源同时只能由一个线程占用
- 请求并保持:一个线程持有了至少一个资源,但又想要其他的资源,而新资源已经被别人占有了,则当前线程会被阻塞,但它不会释放他自己拥有的。
- 不可剥夺:线程获取到的资源在自己用完之前不能被别人抢占。
- 循环等待:发生死锁时,必然存在一个线程——资源的环形链。
避免死锁:破坏至少一个条件。
- 互斥:没法破坏。
- 请求并保持:一次性请求所有的资源
- 不可剥夺:占用该资源的可以自动主动释放
- 循环等待:按顺序申请资源。
15、死锁问题如何排查
可以使用JDK自带的命令行工具排查:
- 使用jps查找运行的Java进程:jps -l
- 使用jstack查看线程堆栈信息: jstack -l 进程id
还可以用图形化的工具。
参考
[1] 沉默王二公众号
-
Semaphore
:state 表示剩余的许可数量。 -
CountDownLatch
:state 表示还需要等待多少个事件。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!
标签:
上一篇:Java并发编程(3)
下一篇:没有了