【面试】一文搞懂synchronized关键字相关考点

一 初始synchronized

        synchronized 是Java中的一个关键字,用于保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。在JDK1.6版本之前,它常被称为“重量级锁”,这是因为它是基于操作系统提供的互斥量(Mutexes)来实现线程间的同步的,这涉到及了从用户态到内核态的切换以及线程上下文切换等相对昂贵的操作。一旦一个线程获得了锁,其他试图获取相同锁的线程将会被阻塞,这种阻塞操作会导致线程状态的改变和 CPU 资源的消耗。在JDK1.6之后,JVM引入了锁升级机制,也就是无锁--偏向锁--轻量级锁---重量级锁的一个升级过程,从而减轻了锁带来的性能消耗。

二 锁升级的实现原理

        在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,JVM让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

三 偏向锁、轻量级锁、重量级锁

偏向锁

        由于在大多数情况下,不存在多线程竞争同一个锁,且锁总是由一个线程多次获得,因此我们就可以给该线程加上偏向锁,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程ID等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。

轻量级锁

        轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁,注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块。

重量级锁

        重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景。

三种锁的对比

四 synchronized如何使用

  • 修饰普通方法:作用于当前对象实例,进入同步代码前要获得当前对象实例的锁

public class Counter {  
    private int count = 0;  
  
    // synchronized 修饰实例方法  
    public synchronized void increment() {  
        count++;  
    }  
  
    public synchronized int getCount() {  
        return count;  
    }  
  
    public static void main(String[] args) throws InterruptedException {  
        Counter counter = new Counter();  
  
        // 创建两个线程来模拟并发访问  
        Thread t1 = new Thread(() -> {  
            for (int i = 0; i < 1000; i++) {  
                counter.increment();  
            }  
        });  
  
        Thread t2 = new Thread(() -> {  
            for (int i = 0; i < 1000; i++) {  
                counter.increment();  
            }  
        });  
  
        t1.start();  
        t2.start();  
  
        t1.join();  
        t2.join();  
  
        System.out.println(counter.getCount()); // 预期输出 2000  
    }  
}
  • 修饰静态方法:作用于当前类,进入同步代码前要获得当前类对象的锁,synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁

public class StaticCounter {  
    private static int count = 0;  
  
    // synchronized 修饰静态方法  
    public static synchronized void increment() {  
        count++;  
    }  
  
    public static synchronized int getCount() {  
        return count;  
    }  
  
    // ... 与上面类似的主方法,用于测试  
}
  • 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁

public class BlockCounter {  
    private int count = 0;  
    private final Object lock = new Object(); // 锁对象  
  
    // synchronized 修饰代码块  
    public void increment() {  
        synchronized (lock) {  
            count++;  
        }  
    }  
  
    public int getCount() {  
        // 这里不需要同步,因为只是读取操作  
        return count;  
    }  
  
    // ... 与上面类似的主方法,用于测试  
}

五 synchronized 和 volatile 的区别是什么?

1. 作用位置和方式

synchronized:可以修饰方法、代码块或整个类。

volatile:仅能修饰变量,通过内存屏障(Memory Barrier)来确保变量的可见性和禁止指令重排,但不涉及锁机制。

2. 功能和特性

synchronized保证原子性,确保一个线程在执行同步代码块或方法时,其他线程不能同时执行该代码块或方法,从而保证了操作的原子性。保证可见性,在锁释放时,会将数据写入主内存,保证其他线程能看到最新的数据。可能会造成线程的阻塞,当一个线程持有锁时,其他需要该锁的线程将等待锁的释放。

volatile保证可见性,当一个线程修改了volatile变量的值时,这个新值对其他线程立即可见。不保证原子性,对于复合操作(如i++),volatile不能保证操作的原子性。不会造成线程的阻塞,volatile通过内存屏障实现可见性,不涉及锁机制,因此不会造成线程的阻塞。

3. 内存开销和性能

synchronized有一定的内存开销,包括锁的申请、释放、等待等操作。在多线程竞争激烈时,性能可能受到较大影响。

volatile没有锁的开销,通过CPU的缓存一致性协议来实现数据的可见性。在某些情况下,性能优于synchronized,尤其是在读多写少的场景下

4. 使用场景

synchronized:适用于需要在多个线程之间同步共享变量的情况,例如对共享资源的读写操作。需要保证操作的原子性和可见性时,synchronized是较好的选择。

volatile:适用于只需要保证某个变量的可见性,而不需要同步控制的场景,如状态标志位等。在单例模式的双重检查锁定(Double-Check Locking)等场景中也有应用。

六 synchronized 和 Lock 的区别是什么?

1.性质与实现方式

synchronized:是Java中的一个关键字,由JVM(Java虚拟机)内置支持,属于Java语言层面的特性。它是一种隐式的锁机制,当线程进入synchronized修饰的代码块或方法时,会自动加锁,离开时自动解锁。

Lock:是一个接口,位于java.util.concurrent.locks包下,属于JDK层面实现的接口。它提供了比synchronized更灵活的锁操作,如尝试非阻塞地获取锁、可中断地获取锁以及超时获取锁等。

2. 锁的获取与释放

synchronized:锁的获取和释放是自动的,无需手动操作。当线程进入synchronized修饰的代码块或方法时,会自动获取锁;当线程执行完毕或抛出异常时,会自动释放锁。

Lock:需要手动获取和释放锁。通过调用lock()方法来获取锁,调用unlock()方法来释放锁。因此,在使用Lock时,通常需要将unlock()方法放在finally块中,以确保锁能够被正确释放。

3. 锁的灵活性

synchronized:锁的灵活性相对较低,它只能控制代码块或方法的同步访问。无法提供尝试非阻塞地获取锁、可中断地获取锁以及超时获取锁等高级功能。

Lock:提供了更高的灵活性。它支持尝试非阻塞地获取锁(tryLock())、可中断地获取锁(lockInterruptibly())以及超时获取锁(tryLock(long time, TimeUnit unit))等高级功能。

4. 锁的公平性

synchronized:默认的锁是非公平的,即线程获取锁的顺序不是按照请求的顺序来的。

Lock:提供了公平锁和非公平锁的实现。通过实现ReentrantLock类,并指定其构造函数中的fair参数为true或false,可以分别创建公平锁和非公平锁。

5. 锁的等待与唤醒机制

synchronized:使用Object类的wait()、notify()和notifyAll()方法进行线程的等待和唤醒。

Lock:提供了Condition接口,用于实现更灵活的线程等待和唤醒机制。一个Lock对象可以关联多个Condition对象,每个Condition对象都可以独立地管理一组等待线程。

6. 性能与开销

synchronized:由于是JVM内置的关键字,其性能优化通常更好在轻量级锁和偏向锁等优化技术的支持下,synchronized的性能在大多数情况下是可以接受的。

Lock:相对于synchronized来说,Lock的灵活性带来了额外的性能开销。在某些情况下,如需要高度定制的锁行为时,使用Lock可能会带来更好的性能。

七 synchronized有哪些作用

原子性:确保线程互斥的访问同步代码;

可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 “对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;

有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”。

  • 32
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值