如何应对Android面试官->synchronized详解,什么是STW?

synchronized 原理

synchronized 作用在代码块

public class VolatileTest {    
    public volatile int count;    
    public void add() {        
        synchronized (this) {            
            count ++;        
        }    
    }    
    
    private static class Count extends Thread {        
        VolatileTest volatileTest;        
        public Count(VolatileTest volatileTest) {            
            this.volatileTest = volatileTest;        
        }        
        
        @Override        
        public void run() {            
            super.run();            
            for (int i = 0; i < 10000; i++) {                
                volatileTest.add();            
            }        
        }    
    }    
    
    public static void main(String[] args) throws InterruptedException {        
        VolatileTest volatileTest = new VolatileTest();        
        Count count1 = new Count(volatileTest);        
        Count count2 = new Count(volatileTest);        
        count2.start();        
        count1.start();        
        Thread.sleep(50);        
        System.out.println(volatileTest.count);    
    }
}

执行这段代码之后,我们反编译 生成的 VolatileTest.class 文件,使用 javap -v VolatileTest.class

我们发现,这段代码被一个 monitorenter 和 monitorexit 所包裹,这两个字节码指令是由 JVM 在 .java 编译成 .class 的时候 帮我们插入的;

当执行到 monitorenter 这个指令的时候,就会尝试获取锁,每一个 monitorenter 在 JVM 中都是有一个 Monitor 对象与之相对应,我们所说的拿锁,本质上就是拿到这个对象的所有权,谁拿到这个对象的所有权,就表示谁进入了一个锁的状态;

所以说 synchronized 本质原理:就是通过 monitorenter 和 monitorexit 字节码指令实现的;

monitorenter:插入到同步代码块开始的位置;

monitorexit:插入到同步代码块结束的位置;

synchronized 作用在方法

我们把 synchronized 放在方法上

public synchronized void add() {        
     count ++;    
} 

然后查看编译后的字节码

flags 多了一个 ACC_SYNCHRONIZED 标识符,代表着这是一个同步方法,底层还是使用的 monitor,与同步代码块的不同的是我们看不到 monitor 的存在,它是在运行时被添加的;

锁的存放位置

我们使用 synchronized 来加锁,那么锁的位置存放在哪里呢?对于 synchronized 关键字而言,它存放在 Java 的对象头里;

当我们 new 一个对象的时候,它在内存中除了对象体之外,还有对象头,对象头中就包含了一些我们当前对象的关键信息,比如:GC年龄、对象hasCode、属性指针、类型指针(KlassPoint)等等,这个对象头在虚拟机中有个专门的称呼:MarkWord;

GC年龄

  • 对象被回收的次数,当一个对象在堆中被创建之后,经过多次的垃圾回收而没有被回收到(JVM 缺省是15次),就会被放入老年代,记录这个次数的就是GC年龄;

类型指针(KlassPoint)

  • new 的这个对象属于哪个类,通过对象头中的这个类型指针来标记;

synchronized 就存放在这个对象头(MarkWord)区域;

可以看到这是在无锁状态下 hashCode、分代年龄 就已经把这个 MarkWord 区域占满了,当我们进行加锁的时候,这个锁的信息位置放在了哪里呢?这个对象头的内容随着对象的运行是会发生改变的,而且根据 synchronized 关键字它在实现上引入了一个偏向锁、轻量级锁、以及GC的情况下,这个MarkWord内的值是会不断的发生变化的;

偏向锁的时候,对象头中存放了线程ID、时间戳(Epoch)、对象分代年龄、是否是偏向锁;

轻量级锁的时候,对象头中存放的内容就会变成指向栈中锁记录的指针;

重量级锁的时候,对象头中存放的内容就会变成指向互斥量的指针;

发生GC的时候,对象头中就是空的;

也就是说:对象头中的内容并不是一成不变的,而是随着对象的运行在不断的发生着变化;

从多个线程竞争同步资源的流程细节有没有区别 来看锁的类型有哪些?

因为 synchronized 加锁之后,其他线程是被阻塞在那里的,那么每个线程都会发生至少两次上下文切换(一次上下文切换耗费大概 3 - 5us,CAS基本原理中有介绍),Java 为了优化这块的耗时,就引入了 轻量级锁、重量级锁、偏向锁;

轻量级锁

通过CAS操作来加锁和解锁;也就是说:没有拿到锁的线程不挂起,而是在那里进行自旋操作,这样就不用发生上下文切换了;轻量级锁里面使用的就是自旋锁来实现;

假设加锁的代码块要执行很长时间,如果阻塞的线程一直在那里自旋操作的话,也是很大的消耗;于是虚拟机在自旋锁之上又引入了一个自适应自旋锁的概念,用来控制自旋的次数,早期的JDK版本是10次,在1.6的时候这个值不再是固定的10次,而是由虚拟机自行进行判定(一般来说就是一个线程上下文切换的时间);一旦超过这个时间就不再进行自旋,膨胀为重量级锁;

偏向锁

一般情况下,一个锁总是由同一个线程获取,如果连这个 CAS 操作都不想执行了,那就提前测试一下,当我拿锁的时候,测试下是不是当前线程自己,如果是当前线程自己,那就直接来用,这就是偏向锁;在线程拿锁的时候,总是偏向于拿到这个锁的第一个线程;相比于轻量级锁来说,它连CAS操作都不执行了;通过线程 ID 来测试是不是自己;

偏向锁一般用于没有竞争的时候,一旦发生了竞争,就需要升级为轻量级锁;

升级为轻量级锁,首先就要撤销偏向锁,那么对象头中的内容也要跟着发生变化,把偏向锁的数据撤销,替换成轻量级锁的数据;这种撤销替换的过程引入了一个 STW (Stop The World)

Stop The World

工作线程会不断的往堆里面产生对象,当堆中满的时候,需要把我们不用的一些对象进行回收,如果垃圾回收器在回收的过程中,还有工作线程不停的往堆中生产对象,这对于垃圾回收器来说是不友好的,这个时候就产生了 Stop The World

当垃圾回收器需要进行垃圾回收的时候,它就会划一条节点,当所有的工作线程运行到这个节点的时候,全部停止工作,当所有的线程停止工作后,垃圾回收器将堆中无用的对象清理掉,清理掉之后,再告诉所有的线程 继续执行;

所有线程停止的现象,这就是 stop the world;

在撤销偏向锁的过程中,也存在这个 stop the world,当线程 B 去撤销线程 A 的偏向锁的时候,是会修改线程 A 的堆栈上的内容的,每个线程都有自己的工作内存,这个工作内存如果说硬要和虚拟机进行连接的话,这个工作内存就可以理解为虚拟机中的线程栈内存,每个线程的工作内存是不能相互访问的,但是对于虚拟机来讲,它是没有这个限制的,它就会让线程 B 去修改线程 A 中的相关数据,在这种情况下,如果线程 A 在不停的修改对象头内容,那么线程 B 是无法修改线程 A 的内容的,所以这里也引入 stop the world 概念,用来执行撤销操作;

所以在 synchronized 的优化过程中,jdk 给我们引入了偏向锁 -> 轻量级锁 -> 重量级锁;

Lock 和 synchronized 对比来说,如果你的需求没有尝试拿锁的话,尽可能的还是使用 synchronized,尽量少用显示锁;

简历润色

简历上可写:深度理解 synchronized 实现原理及优化过程,可基于不同的并发场景来高效使用锁机制;

下一章预告

文件IO,手写APK加固框架;

欢迎三连

来都来了,点个关注、点个赞吧~~你的支持是我最大的动力

<think>好的,我现在要解释用户提供的这几行Java代码的意思和作用。首先,我需要仔细分析每一行代码的功能,然后结合它们的作用来整体理解。 用户给出的代码是: Object userLock = orderMap.computeIfAbsent(userId, k -> new Object()); synchronized (userLock) { } 首先,我需要理解`computeIfAbsent`方法。这个方法属于Java的Map接口,具体来说,是ConcurrentHashMap中的一个方法。它的作用是:当给定的键(在这里是userId)不存在于Map中时,使用提供的函数(这里是k -> new Object())来计算值,并将其放入Map中。如果键已经存在,就直接返回对应的值。这个方法可以确保操作的原子性,避免了并发情况下的竞态条件。 所以第一行代码的作用是,从orderMap中获取与userId对应的值。如果userId不存在,就创建一个新的Object对象作为该键的值,并存入Map中。最终,userLock变量会被赋值为这个键对应的值,也就是新创建的对象或者已经存在的对象。 接下来是synchronized块。synchronized关键字在Java中用于实现线程同步,确保同一时间只有一个线程可以执行某个代码块。这里的synchronized块使用了userLock作为锁对象。也就是说,当一个线程进入这个同步块时,它必须先获取userLock对象的锁,其他线程想要进入就必须等待,直到锁被释放。 结合这两部分的作用,整个代码的作用可能是:为每个userId创建一个唯一的锁对象,确保同一用户的操作在并发环境下被串行化处理。这样做的目的是防止多个线程同时处理同一个用户的操作,从而避免数据竞争或不一致的问题。 举个例子,假设有一个订单处理的系统,多个线程可能同时处理不同用户的订单。如果不加控制,同一用户的订单可能被多个线程同时处理,导致订单状态错误。通过为每个userId分配一个唯一的锁,可以确保同一时间只有一个线程在处理该用户的订单,其他线程必须等待,从而保证线程安全。 需要确认的是,orderMap是否是一个线程安全的Map,比如ConcurrentHashMap。因为computeIfAbsent在普通的HashMap中并不是线程安全的,多个线程同时调用可能会导致重复创建对象或者数据不一致。而ConcurrentHashMap的computeIfAbsent是原子操作,确保了线程安全。 总结起来,这段代码的作用是为每个用户ID提供一个专用的锁对象,通过synchronized块确保同一用户的操作在并发时按顺序执行,避免并发问题。这样既提高了并发性能(不同用户之间可以并行处理),又保证了同一用户操作的线程安全。</think>这段代码主要用于在多线程环境下,**为每个用户(userId)分配独立的锁对象**,从而实现**细粒度的并发控制**。核心作用是通过对同一用户的操作加锁,保证线程安全,同时允许不同用户的操作并行处理,提升效率。具体分析如下: --- ### 逐行解析: #### 1. `Object userLock = orderMap.computeIfAbsent(userId, k -> new Object());` - **`orderMap`**:通常是一个线程安全的 `ConcurrentHashMap`,键为 `userId`,值为专属锁对象。 - **`computeIfAbsent`**:原子性操作。检查 `userId` 是否存在: - **存在**:直接返回对应的锁对象。 - **不存在**:通过 `k -> new Object()` 创建新锁对象,存入 Map 后返回。 - **作用**:确保每个 `userId` 有且只有一个全局唯一的锁对象,避免并发问题。 #### 2. `synchronized (userLock) { ... }` - **同步块**:以 `userLock` 为锁对象,确保同一时间**只有一个线程能进入该代码块处理该用户的操作**。 - **效果**:同一用户的请求串行执行,不同用户的请求可并行处理,兼顾安全与效率。 --- ### 适用场景示例 假设一个订单系统处理用户下单: ```java ConcurrentHashMap<Long, Object> orderMap = new ConcurrentHashMap<>(); void processOrder(Long userId) { // 获取或创建用户专属锁 Object lock = orderMap.computeIfAbsent(userId, k -> new Object()); synchronized (lock) { // 同一用户的订单处理串行化 // 修改用户订单状态、扣减库存等操作 } } ``` - **线程A处理userId=1**:获取锁,开始操作。 - **线程B处理userId=1**:等待锁释放后才能执行。 - **线程C处理userId=2**:使用另一把锁,无需等待,直接并行执行。 --- ### 关键优势 1. **细粒度锁**:按用户维度加锁,比全局锁性能更高。 2. **线程安全**:确保同一用户资源操作的原子性。 3. **动态管理锁**:自动为新用户创建锁,避免内存泄漏(需适时清理不用的锁)。 --- ### 注意事项 - **Map的选择**:必须使用线程安全的Map(如 `ConcurrentHashMap`),否则 `computeIfAbsent` 可能引发竞态条件。 - **锁对象管理**:长期运行的系统需定期清理不再使用的 `userId` 对应的锁,防止内存占用过多。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值