首页 > 基础资料 博客日记

Java并发编程(4)

2025-09-14 19:30:02基础资料围观27

这篇文章介绍了Java并发编程(4),分享给大家做个参考,收藏Java资料网收获更多编程知识


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的实现原理

  1.  怎么加锁?
  • 修饰代码块:JVM采用monitorenter、monitorexit两个指令,一个开始一个结束

image

  • 修饰方法:JVM在方法表里打上一个标志位ACC_SYNCHRONIZED。在执行时,JVM会检查:如果这个方法有这个标志,调用它时就会自动获取对应对象的Monitor锁-->执行完自动释放锁,不需要显式写lock/unLock。

image

   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来标记对象当前的锁状态。

image                 ① 无锁:默认情况下,对象是无锁状态。

     ② 偏向锁:若一个对象总是被同一个线程获取锁,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次),若达到等待次数后还未获取到锁会被挂起(阻塞),等锁释放后再唤醒。

  1. 【注意】这个过程时不可逆的。

  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(独占锁)

 

  1. lock() → 调用 AQS 的 acquire()

  2. 如果 state==0,用 CAS 设置 state=1,成功 → 当前线程获得锁。

  3. 如果失败,进入 AQS 队列,挂起等待。

  4. 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 表示还需要等待多少个事件。


文章来源:https://www.cnblogs.com/xiaoqian01/p/19091423
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!

标签:

上一篇:Java并发编程(3)
下一篇:没有了

相关文章

本站推荐

标签云