叶志龙 發表於 2025-9-14 19:22:00

Java并发编程(4)

<hr>
<h1 style="text-align: center">锁</h1>
<h1>1、synchronized用过吗?怎么用?</h1>
<p>&nbsp;  synchronized是常用来保证代码的原子性的。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:java;gutter:true;">//1.修饰实例方法
// 有两个对象obj1和obj2,线程A调用Object.test(),线程B调用obj2.test(),不会互斥
// 但A和B如果都调用obj1.test(),会互斥<br>//场景:一个银行账户对象,不同线程操作同一个账户时要排队
public synchronized void test(){

}

//2.修饰静态方法
//线程A调用obj1.test(),线程B调用obj2.test(),仍然会互斥,因为锁是类锁,不是实例锁<br>//场景:当”类的所有实例共享资源“需要保护时,如修改全局配置、写日志文件、统计类的静态计数器
public static synchronized void test(){
   
}

//3.修饰代码块<br>//场景:只要锁住”关键区域代码“而不是整个方法。如转账时,只锁定两个账户,避免锁范围太大影响性能
public void test() {
       synchronized(this){      //不同实例对象可以同时
       //synchronized(Example.class),不管有多少个对象实例,同时只能有一个线程进入
            //临界区代码
       }
}    </pre>
</div>
<ul>
<li data-start="1391" data-end="1437">
<p data-start="1393" data-end="1437"><strong data-start="1393" data-end="1402">实例方法锁</strong>:给一间“卧室”上锁(对象自己的房门),别人想进要拿这间房的钥匙。</p>
</li>
<li data-start="1438" data-end="1484">
<p data-start="1440" data-end="1484"><strong data-start="1440" data-end="1449">静态方法锁</strong>:给“整栋楼的大门”上锁(类锁),不管哪间房都进不去,大家都排队。</p>
</li>
<li data-start="1485" data-end="1540">
<p data-start="1487" data-end="1540"><strong data-start="1487" data-end="1495">代码块锁</strong>:只给“卧室里的保险柜”上锁(对象里的某个资源),你可以自由走动,但柜子只能一个人开。</p>
</li>
</ul>
<h1>2、synchronized的实现原理</h1>
<ol>
<li>&nbsp;怎么加锁?</li>
</ol>
<ul>
<li><strong>修饰代码块</strong>:JVM采用monitorenter、monitorexit两个指令,一个开始一个结束</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/2297173/202509/2297173-20250914164754081-765469512.png"></p>
<ul>
<li><strong>修饰方法</strong>:JVM在方法表里打上一个标志位ACC_SYNCHRONIZED。在执行时,JVM会检查:如果这个方法有这个标志,调用它时就会自动获取对应对象的Monitor锁--&gt;执行完自动释放锁,不需要显式写lock/unLock。</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/2297173/202509/2297173-20250914165020340-1537912168.png"></p>
<p>   2. <strong>synchronized锁住的是什么?</strong></p>
<p>  &nbsp; 每个对象都有一个隐含的锁机制(Monitor),java虚拟机里用ObjectMonitor实现。当线程执行synchronized时,本质就是在尝试获取对象对应的ObjectMonitor。</p>
<p>   在ObjectMonitor里,有几个核心变量:</p>
<ul>
<li>_owner:指向当前持有锁的线程</li>
<li>_count:重入次数(一个线程重复进入同一锁时+1)</li>
<li>_EntryList:保存所有正在“争抢锁”的线程</li>
<li>_WaitSet:保存调用了wait()方法的线程(等待被唤醒)</li>
</ul>
<p>  工作机制:</p>
<ul>
<li>加锁:
<ul>
<li>线程尝试获取Monitor</li>
<li>如果没人持有,设置_owner = 当前线程,执行成功。</li>
<li>若有人持有,就把自己假如_EntryList,进入阻塞状态</li>
</ul>
</li>
<li>解锁:
<ul>
<li>当前线程执行完同步代码块,_count -1</li>
<li>若_count == 0,说明完全释放锁,把_owner置空</li>
<li>然后从_EntryList里挑一个线程来获取锁</li>
</ul>
</li>
<li>等待/唤醒
<ul>
<li>wait():当前持锁线程把自己放入_WatiSet,同时释放锁(_Owner置空)</li>
<li>notify():随机从_WaitSet里唤醒一个线程,他会回到_EntryList,等待重新竞争锁</li>
</ul>
</li>
</ul>
<h1>3、除了原子性,synchronized可见性、有序性、可重入性怎么实现?</h1>
<p>  <strong>1. 可见性(加锁更新完后其他线程可见)</strong></p>
<ul>
<li>线程加锁前,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。</li>
<li>加锁后,其他现场无法获取哦主内存中的共享变量</li>
<li>线程解锁前,必须把共享变量的最新值刷新到<strong>主内存</strong>中。</li>
</ul>
<p>  <strong>2. 有序性(最终结果一致)</strong></p>
<ul>
<li>synchronized同步的代码块,具有排他性,一次只能被一个线程拥有,所以能保证同一时刻,代码时单线程执行的。</li>
<li>as-if-serial:单线程的程序保证最终结果是有序的,但不保证不会指令重排</li>
<li>所以synchronized保证的有序是<strong>执行结果的有序</strong>,而不是指令重排的有序性。</li>
</ul>
<p> <strong> 3. 可重入性</strong></p>
<ul>
<li>意思:允许一个线程二次请求自己持有对象锁的临界资源。</li>
<li>synchronized锁对象的时候会有个计数器,记录下同一线程获取锁的次数,在执行完对应的代码块之后,计数器会-1,直到计数器清零,就释放锁了,别的线程才有机会获取。</li>
</ul>
<h1>4、锁升级?synchronized优化了解吗</h1>
<p>&nbsp;  在HotSpot里,每个对象都有一个对象头,其中有一块区域叫Mark Word,里面会存储对象的一些运行时数据(如对象的哈希码、GC分代年龄、锁标志位(2bit)、是否偏向锁标志(1bit)、锁的指针)。JMV就靠着64bit来标记对象当前的锁状态。</p>
<p><img src="https://img2024.cnblogs.com/blog/2297173/202509/2297173-20250914172014423-1705455842.png">          &nbsp; &nbsp; &nbsp; &nbsp;①&nbsp;<strong>无锁</strong>:默认情况下,对象是无锁状态。</p>
<p><strong>&nbsp;  &nbsp; ② 偏向锁</strong>:若一个对象总是被同一个线程获取锁,JVM就会偏向它,把Mark Word里直接记录这个线程ID。下次这个线程再来,就不用做CAS了,直接认定它已经持有锁--&gt;提高性能</p>
<blockquote>
<p>偏向锁获取:</p>
<p>1)检查对象是否可偏向(看偏向锁标志位是否为01、偏向标志)</p>
<p>2)检查是不是自己(看线程ID是否等于自己的--&gt;等于则直接执行代码,不用CAS)</p>
<p>3)不是自己,就尝试CAS:若成功则拿到锁,把线程ID改为自己;若失败,则说明有竞争。</p>
<p>4)竞争失败:JVM在安全点停下来,把偏向锁升级为轻量级锁,然后按照轻量级锁的逻辑来竞争</p>
<p>5)执行同步代码。</p>
<p>偏向锁的释放:(与普通锁不同,不会主动释放,只有其他线程来竞争的时候,才会撤销)</p>
<p>1)有人来抢--&gt;检查原来的持有线程是否还在用</p>
<ul>
<li>不用了:撤销偏向锁(回到无锁)</li>
<li>还在用:升级为轻量级锁</li>
</ul>
<p>2)太频繁就批量优化(批量重偏向/批量撤销)</p>
</blockquote>
<p>  ③&nbsp;<strong>轻量级锁</strong>:当另一个线程也来竞争时,偏向锁就失效了,JVM会升级为轻量级锁。JVM在当前线程的栈帧里创建一个叫Lock Record的结果,会保存【该对象当前的Mark Word副本,线程自己“占有锁”标记】。然后JVM会尝试用CAS把对象头里的Mark Word改为指向这个Lock Record指针。若成功了,说明这个线程抢到了锁。(不再存放哈希码;只涉及用户态,性能高)</p>
<p>  <strong>④ 重量级锁</strong>:多个线程同时竞争,CAS不断失败,会把对象头里的Mark Word改成指向一个Monitor对象的指针,里面有Owner、EntryList和Waitset,此时抢不到锁的线程会先进入“自旋”(默认10次),若达到等待次数后还未获取到锁会被挂起(阻塞),等锁释放后再唤醒。</p>
<ol>
<li>【注意】这个过程时不可逆的。</li>
</ol>
<p>  <strong>synchronized做了哪些优化?</strong></p>
<ul>
<li>JDK1.6之前的:直接调用ObjectMonitor.enter()和ObjectMonitor.exit(),这种就是重量级锁,一旦线程竞争,就会发生操作系统层面的阻塞/唤醒,性能差</li>
<li>JDK6后引入优化策略:
<ul>
<li>偏向锁:不做CAS,直接改偏向锁标记</li>
<li>轻量级锁:每个线程在自己的栈里创建一个Lock Record,用CAS操作尝试获取锁。</li>
<li>自旋锁:轻量级锁升级到重量级时,若发现锁被占用,先别急着阻塞,先在CPU上循环“自旋“一段时间(避免不必要的操作系统上下文切换)</li>
<li>锁粗化:连续出现多次synchronized,JVM将连续加锁/解锁合并成一个大范围的锁</li>
<li>锁消除:JIT编译器发现加锁没有必要(数据没有共享),直接把锁去掉。</li>
</ul>
</li>
</ul>
<h1>5、synchronized和ReentrantLock的区别</h1>
<ul>
<li>&nbsp;锁的实现:
<ul>
<li>synchronized是Java语言的关键字,基于JVM实现(JVM编译时会自动管理)。</li>
<li>ReentrantLock是基于JDK的API层面(要自己写代码)实现的(lock和Unlock方法配合try/finnaly语句块来完成)</li>
</ul>
</li>
<li>性能:
<ul>
<li>在JDK1.6锁优化之前,synchronized的性能比ReentrantLock差很多。但JDK6开始优化后,性能就差不多了。</li>
</ul>
</li>
<li>功能特点:
<ul>
<li>synchronized中若一个线程在等待获取锁,只能一直等下去,不能被中断。ReentrantLock提供lockInterruptibly()方法,若线程在等待锁的过程中被interrupt,它会立刻响应中断,放弃等待。</li>
<li>synchronized永远是非公平锁(JVM不保证等待时间最长的线程一定先拿到锁)。ReentrantLock可以在构造时指定非公平锁还是公平锁。</li>
<li>synchronized配合wait()、notify()、notifyAll()来实现线程间通信。这些方法是Object的方法,比较原始。ReentrantLock提供Condition对象。这个对象可以创建多个Condition队列,精确控制哪个线程被唤醒,但synchronized的wait/notify只有一个等待队列,无法细分。</li>
<li>synchronized的锁的获取和释放是JVM自动管理(进入同步块时加锁,退出时自动释放)。ReentrantLock必须手动lock和unclok,虽然灵活但容易因为忘记unlock出现死锁。</li>
</ul>
</li>
</ul>
<h1>6、AQS了解多少</h1>
<p>&nbsp;  (1)AQS(AbstractQueueSynchronizer),是JDK并发包(java.util.concurrent)里的一个抽象类。是一个通用的同步器框架,帮你处理”线程竞争资源--&gt;排队等待--&gt;成功后唤醒的逻辑。</p>
<p>  (2)核心组件</p>
<ul>
<li>同步状态:private volatile int state,state就是一份资源计数。如ReentrantLock:0表示没被占用,1表示已加锁。修改state用CAS来保证原子性,volatile保证可见性。</li>
<li>等待队列:若线程抢锁失败,会被封装成一个Node节点,放进队列。这个队列是FIFO双向链表。队列里的线程会挂起(park()),等前驱节点释放锁时被唤醒(unpark())</li>
<li>独占 / 共享模式:独占(一个线程持有锁如ReentrantLock);共享(多个线程能同时获取资源(比如Semaphore允许N个线程同时进入)</li>
</ul>
<p>  (3)工作流程</p>
<ul>
<li>尝试获取锁:调用acquire()--&gt;内部会调用tryAcquire()。若state修改成功,当前线程获取锁。若失败,入队等待。</li>
<li>等待/挂起:队列里的线程会挂起,节省CPU</li>
<li>释放锁:调用release--&gt;内部调用tryRelease(),成功释放后,会唤醒队列中的下一个线程。</li>
</ul>
<h2 data-start="1171" data-end="1200">📖 举个例子:ReentrantLock(独占锁)</h2>
<p>&nbsp;</p>
<ol data-start="1201" data-end="1372">
<li data-start="1201" data-end="1238">
<p data-start="1204" data-end="1238"><code data-start="1204" data-end="1212">lock()</code> → 调用 AQS 的 <code data-start="1224" data-end="1235">acquire()</code>。</p>

</li>
<li data-start="1239" data-end="1286">
<p data-start="1242" data-end="1286">如果 state==0,用 CAS 设置 state=1,成功 → 当前线程获得锁。</p>

</li>
<li data-start="1287" data-end="1312">
<p data-start="1290" data-end="1312">如果失败,进入 AQS 队列,挂起等待。</p>

</li>
<li data-start="1313" data-end="1372">
<p data-start="1316" data-end="1372"><code data-start="1316" data-end="1326">unlock()</code> → 调用 AQS 的 <code data-start="1338" data-end="1349">release()</code>,把 state 设为 0,并唤醒下一个线程。</p>
</li>
</ol>
<h1>7、ReentrantLock的实现原理</h1>
<p>&nbsp;  ReentrantLock是可重入的独占锁:只有一个线程可以获取该锁,该线程可以多次获取锁。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:java;gutter:true;">// 默认创建⾮公平锁
ReentrantLock lock = new ReentrantLock ();
// 获取锁操作
lock . lock ();
try {
// 执⾏代码逻辑
} catch ( Exception e x ) {
// ...
} finally {
// 解锁操作
lock . unlock ();
}
</pre>
</div>
<ul>
<li>公平锁:多个线程按照申请锁的顺序来获取锁,线程直接进入队列排队。
<ul>
<li>优点:等待锁的线程不会饿死。</li>
<li>缺点:整体吞吐效率相比非公平锁要低。</li>
</ul>
</li>
<li>非公平锁:多个线程加锁时尝试获取锁,获取不到采取排队。
<ul>
<li>有点:减少唤起线程的开销,整体吞吐效率高。</li>
<li>缺点:处于等待队列中的线程可能会饿死。</li>
</ul>
</li>
<li>默认创建的对象lock()非公平的:
<ul>
<li>若所当前没有被其他线程占用,并且当前线程之前没有获取过该锁,那当前线程会获取到该锁,然后设置当前锁的拥有者为当前线程,并设置state为1,直接返回。若当前线程获取过该锁,则这次只是把state+1</li>
<li>若该锁之前被其他线程持有,非公平锁会尝试去获取锁,获取失败后,进入AQS队列阻塞挂起。</li>
</ul>
</li>
</ul>
<h1>8、ReentrantLock怎么实现公平锁的</h1>
<p>&nbsp;  在构造函数可以传入参数true。非公平锁在调用lock后,会先调用CAS进行一次抢锁,没抢到就排到后面去。</p>
<p>  在CAS失败后,和公平锁一样会进入到tryAcquire中,若发现state==0,非公平锁会直接抢锁,但公平锁会判断等待队列是否有线程处于等待,若有则不去抢。</p>
<h1>9、CAS是什么</h1>
<p>&nbsp;  CAS叫Compare And Swap,通过处理器的指令来保证操作的原子性。包含三个参数:共享变量的内存地址A、预期的值B、共享变量的新值C。</p>
<p>  只有当A的值 = B时,才能将A的值变为C。作为一条CPU指令,CAS指令本身是能保证原子性的。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:java;gutter:true;">//线程A尝试执行:
CAS(V, A=100, B=120)
//看地址V里是不是100,若是就更新为120

//线程B尝试执行
CAS(V, A=100,B=150)
//B以为余额还是100,但此时内存里已经是120了,所以不匹配,CAS失效
</pre>
</div>
<p>  </p>
<div class="cnblogs_Highlighter">
<pre class="brush:java;gutter:true;">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());
    }
}
</pre>
</div>
<p>  </p>
<h1>10、CAS有什么问题?怎么解决</h1>
<ul>
<li>&nbsp;ABA问题:线程A期望把值从100改为200;中途线程B把值改成了200,又改为100。线程A再CAS检查时,发现还是100,于是成功更新为120。但线程A不知道这个值已经被修改过了。
<ul>
<li>解决:用版本号。每次被更新,版本号都会+1</li>
</ul>
</li>
<li>循环性能开销:若多个线程一直同时竞争同一个变量,CAS可能一直失败,导致CPU飙升,但操作却无法完成。
<ul>
<li>解决:结合锁 或 退避算法(比如退一会儿再尝试)</li>
</ul>
</li>
<li>只能保证一个变量的原子操作:若账户A扣钱,B加钱,需要保证两个操作都是原子性的,但CAS只能保证一个变量。
<ul>
<li>解决:用锁来保证。</li>
</ul>
</li>
</ul>
<h1>11、Java有哪些保证原子性的方法?如果保证多线程下i++结果正确</h1>
<div class="cnblogs_Highlighter">
<pre class="brush:java;gutter:true;">//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少</pre>
</div>
<h1>12、原子操作类了解多少</h1>
<p>&nbsp;</p>
<h1>13、AtomicInteger的原理</h1>
<div class="cnblogs_Highlighter">
<pre class="brush:java;gutter:true;">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。
否则说明有竞争,更新失败,循环重试。
</pre>
</div>
<p>  <code data-start="1015" data-end="1048">AtomicInteger.getAndIncrement()</code> 就是:</p>
<ul data-start="1055" data-end="1101">
<li data-start="1055" data-end="1064">
<p data-start="1057" data-end="1064">读当前值;</p>
</li>
<li data-start="1065" data-end="1083">
<p data-start="1067" data-end="1083">用 CAS 尝试把它加 1;</p>
</li>
<li data-start="1084" data-end="1101">
<p data-start="1086" data-end="1101">如果失败就重试,直到成功。</p>
</li>
</ul>
<p data-start="1103" data-end="1136">靠 CAS 保证了原子性,靠 volatile 保证了可见性。</p>
<h1>14、线程死锁了解吗?如何避免</h1>
<p>&nbsp;  死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成互相等待的现象,在无外力作用下,这些线程会一直相互等待。</p>
<p>  造成死锁的四个必要条件:</p>
<ul>
<li>互斥条件:同一资源同时只能由一个线程占用</li>
<li>请求并保持:一个线程持有了至少一个资源,但又想要其他的资源,而新资源已经被别人占有了,则当前线程会被阻塞,但它不会释放他自己拥有的。</li>
<li>不可剥夺:线程获取到的资源在自己用完之前不能被别人抢占。</li>
<li>循环等待:发生死锁时,必然存在一个线程——资源的环形链。</li>
</ul>
<p>  避免死锁:破坏至少一个条件。</p>
<ul>
<li>互斥:没法破坏。</li>
<li>请求并保持:一次性请求所有的资源</li>
<li>不可剥夺:占用该资源的可以自动主动释放</li>
<li>循环等待:按顺序申请资源。</li>
</ul>
<h1>15、死锁问题如何排查</h1>
<p>&nbsp;  可以使用JDK自带的命令行工具排查:</p>
<ul>
<li>使用jps查找运行的Java进程:jps -l</li>
<li>使用jstack查看线程堆栈信息: jstack -l 进程id</li>
</ul>
<p>  还可以用图形化的工具。</p>
<p>&nbsp;</p>
<h1>参考</h1>
<p> 沉默王二公众号</p>
<ul>
<li data-start="459" data-end="491">
<p data-start="461" data-end="491"><code data-start="461" data-end="472">Semaphore</code>:state 表示剩余的许可数量。</p>

</li>
<li data-start="494" data-end="534">
<p data-start="496" data-end="534"><code data-start="496" data-end="512">CountDownLatch</code>:state 表示还需要等待多少个事件。</p>
</li>
</ul><br><br>
来源:https://www.cnblogs.com/xiaoqian01/p/19091423
頁: [1]
查看完整版本: Java并发编程(4)