大厂面试题详解:synchronized的偏向锁和自旋锁怎么实现的

简介: 字节跳动大厂面试题详解:synchronized的偏向锁和自旋锁怎么实现的

大厂面试题详解:synchronized的偏向锁和自旋锁怎么实现的


理解 synchronized 关键字


在 Java 中,synchronized 关键字是实现并发控制的重要工具之一。它用于实现对共享资源的互斥访问,确保在同一时刻只有一个线程可以进入同步代码块或方法。


synchronized 的基本用法

public class SynchronizedExample {
    private int count = 0;

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

在上述示例中,increment() 方法被 synchronized 修饰,保证了对 count 的操作是原子的,即线程安全的。


synchronized 的性能问题


传统的 synchronized 在竞争激烈的情况下可能会导致性能问题,因为它会涉及到线程的上下文切换和用户态与内核态的切换。


偏向锁的引入


为了解决 synchronized 在单线程情况下的性能问题,JDK 1.6 引入了偏向锁的概念。偏向锁是一种针对单线程场景进行优化的锁机制,它可以减少无竞争情况下的同步操作开销。


偏向锁的实现原理

public class SynchronizedExample {
    private int count = 0;

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


在单线程访问的情况下,synchronized 会尝试偏向于这个线程。当有其他线程尝试访问这个锁时,偏向锁就会升级为常规锁,保证多线程下的正确性。


自旋锁的引入


自旋锁是一种基于循环等待的锁,线程在获取锁时不会被挂起,而是不断地尝试获取锁。


自旋锁的实现原理

import java.util.concurrent.atomic.AtomicBoolean;

public class SpinLockExample {
    private AtomicBoolean locked = new AtomicBoolean(false);

    public void lock() {
        while (!locked.compareAndSet(false, true)) {
            // 自旋等待锁释放
        }
    }

    public void unlock() {
        locked.set(false);
    }
}


在自旋等待期间,线程会一直占用 CPU 资源,如果等待时间过长,会导致性能问题。因此,自旋锁适用于短时间内持有锁的情况。


应用场景和最佳实践


synchronized 的适用场景


  • 单线程访问或轻量级的并发情况下,可以使用 synchronized,它简单易用,且性能较好。
  • 对于竞争激烈的场景或高并发环境,考虑使用 ReentrantLock 或者 ConcurrentHashMap 等并发工具类。


偏向锁和自旋锁的应用场景


  • 偏向锁适用于单线程访问的场景,可以提升同步操作的性能。
  • 自旋锁适用于短时间内持有锁的情况,可以减少线程上下文切换的开销。


高级应用和性能优化


偏向锁的优化


偏向锁在单线程场景下提供了很好的性能优化,但在多线程竞争激烈的情况下,偏向锁的性能会下降。为了解决这个问题,JVM 会根据一定的规则自动取消偏向锁。


偏向锁的实现机制涉及到锁标识位的设置与撤销,其主要流程包括偏向锁的获取、撤销和升级。


示例代码:

public class BiasedLockExample {
    private int value = 0;

    public synchronized void increment() {
        value++;
    }
}

上述代码中的 increment() 方法使用了 synchronized 关键字,这意味着该方法是一个同步方法,会使用偏向锁进行优化。


自旋锁的优化


自旋锁的性能受到 CPU 核心数和线程竞争情况的影响。当线程竞争不激烈或锁持有时间较短时,自旋锁可以提升性能。但如果自旋等待时间过长,会造成资源浪费。


自旋锁通过循环重试的方式尝试获取锁,避免了线程阻塞和切换的开销。


示例代码:

import java.util.concurrent.atomic.AtomicBoolean;

public class SpinLock {
    private AtomicBoolean locked = new AtomicBoolean(false);

    public void lock() {
        while (!locked.compareAndSet(false, true)) {
            // 自旋等待锁释放
        }
    }

    public void unlock() {
        locked.set(false);
    }
}

上述代码中,lock() 方法通过 AtomicBoolean 类型的状态标识来实现自旋锁的获取,unlock() 方法用于释放锁。


使用 synchronized 关键字


尽管 ReentrantLock 提供了更多的灵活性和功能,但在绝大多数情况下,synchronized 关键字已经能够满足需求,并且使用更为简单和方便。


synchronized 关键字的使用简单直接,JVM 对其进行了高度优化,性能较为优秀。在绝大多数情况下,synchronized 能够满足锁的需求,并且使用更为简单和方便。


示例代码:

public class SynchronizedExample {
    private int count = 0;

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

上述代码中的 increment() 方法使用了 synchronized 关键字,保证了方法的原子性,避免了多线程并发访问导致的数据不一致问题。


应用场景


偏向锁的应用场景: 偏向锁适用于存在大量读线程的场景,通过偏向锁可以减少无谓的竞争,提高性能。


假设有一个数据结构,被多个线程读取,但只有少数线程会修改数据。在这种场景下,偏向锁可以提供显著的性能优势,因为大多数情况下读线程无需争夺锁,可以直接访问数据,而修改数据的线程会在获得锁后进行修改。

/**
 * 偏向锁的应用场景:
 * 偏向锁适用于存在大量读线程的场景,通过偏向锁可以减少无谓的竞争,提高性能。
 * 
 * 假设有一个数据结构,被多个线程读取,但只有少数线程会修改数据。在这种场景下,偏向锁可以提供显著的性能优势,
 * 因为大多数情况下读线程无需争夺锁,可以直接访问数据,而修改数据的线程会在获得锁后进行修改。
 */

public class BiasedLockExample {
    // 定义共享的数据
    private int data = 0;

    /**
     * 读取数据的方法
     */
    public synchronized void readData() {
        System.out.println("Reading data: " + data);
    }

    /**
     * 修改数据的方法
     * @param newData 新数据
     */
    public synchronized void modifyData(int newData) {
        // 修改数据
        data = newData;
        System.out.println("Data modified to: " + data);
    }
}

在上述代码中,我展示了一个简单的应用场景,适合使用偏向锁进行性能优化。在这个示例中,多个线程可以同时调用 readData() 方法读取数据,而只有在调用 modifyData(int newData) 方法时才需要竞争偏向锁进行数据修改。


通过使用偏向锁,大多数情况下读取数据的线程无需竞争锁资源,可以直接访问数据,而修改数据的线程则会在获得锁之后进行数据修改操作,有效减少了竞争和锁的开销,提高了系统的性能和吞吐量。


自旋锁的应用场景: 自旋锁适用于锁的持有时间短、线程并发数不高的场景,可以有效减少线程阻塞和切换带来的开销。


自旋锁适用于并发竞争不激烈、锁竞争时间较短的场景,例如某个资源被多个线程轮流访问的情况,可以有效减少线程切换的开销。

/**
 * 自旋锁的实现:
 * 自旋锁是一种基于循环等待的锁,线程在获取锁时不会被挂起,而是不断地尝试获取锁。
 * 
 * 自旋锁适用于锁的持有时间较短、线程竞争不激烈的情况下,可以有效减少线程阻塞和切换带来的开销。
 */

import java.util.concurrent.atomic.AtomicBoolean;

public class SpinLock {
    // 使用AtomicBoolean作为状态标志,初始化为false表示未锁定状态
    private AtomicBoolean locked = new AtomicBoolean(false);

    /**
     * 加锁方法,使用自旋等待获取锁
     */
    public void lock() {
        // 使用CAS操作尝试获取锁,直到成功为止
        while (!locked.compareAndSet(false, true)) {
            // 自旋等待锁释放
            // 在自旋过程中,当前线程不会被挂起,而是一直循环尝试获取锁
        }
    }

    /**
     * 解锁方法,将锁标志位设置为false
     */
    public void unlock() {
        // 将锁标志位设置为false,表示释放锁
        locked.set(false);
    }
}

在上述代码中,我实现了一个简单的自旋锁,采用了基于CAS(Compare And Set)操作的方式来实现加锁和解锁的过程。在 lock() 方法中,线程会不断地循环尝试获取锁,直到成功获取为止。而在 unlock() 方法中,线程会将锁的状态标志位设置为false,表示释放锁。


自旋锁适用于锁的持有时间较短、线程竞争不激烈的情况下,可以有效减少线程阻塞和切换带来的开销,提高系统的性能和吞吐量。


synchronized 关键字的应用场景: synchronized 关键字适用于绝大多数的同步场景,尤其是对于简单的同步操作,synchronized 提供了更为简单和便捷的使用方式。


synchronized 关键字是 Java 中最常用的同步机制,它可以用于任何对象和方法,提供了一种简单而强大的同步方式。


示例代码:

/**
 * synchronized 关键字的应用场景:
 * synchronized 关键字适用于绝大多数的同步场景,尤其是对于简单的同步操作,synchronized 提供了更为简单和便捷的使用方式。
 * synchronized 关键字是 Java 中最常用的同步机制,它可以用于任何对象和方法,提供了一种简单而强大的同步方式。
 */

public class SynchronizedCounter {
    // 定义一个计数器变量
    private int count = 0;

    /**
     * synchronized 关键字修饰的方法,用于增加计数器的值
     */
    public synchronized void increment() {
        // 在方法内部使用 synchronized 关键字,确保了对 count 变量的操作是原子的,即线程安全的
        count++;
    }

    /**
     * synchronized 关键字修饰的方法,用于获取计数器的值
     */
    public synchronized int getCount() {
        // 在方法内部使用 synchronized 关键字,确保了对 count 变量的读取操作是线程安全的
        return count;
    }
}

在上述代码中,我定义了一个简单的计数器类 SynchronizedCounter,其中的 increment() 和 getCount() 方法都使用了 synchronized 关键字修饰,这样可以确保对计数器变量 count 的操作是线程安全的。无论是增加计数器的值还是获取计数器的值,都能保证在多线程环境下的正确性和一致性。


synchronized 关键字适用于大多数的同步场景,特别是对于简单的同步操作,它提供了一种简单而强大的同步方式。因此,在绝大多数情况下,我可以使用 synchronized 关键字来确保线程安全,而无需引入更复杂的同步机制。


相关文章
|
1月前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
1月前
|
存储 安全 Java
面试高频:Synchronized 原理,建议收藏备用 !
本文详解Synchronized原理,包括其作用、使用方式、底层实现及锁升级机制。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
面试高频:Synchronized 原理,建议收藏备用 !
|
2月前
|
NoSQL Java API
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴在面试一线互联网企业时遇到了关于Redis分布式锁过期及自动续期的问题。尼恩对此进行了系统化的梳理,介绍了两种核心解决方案:一是通过增加版本号实现乐观锁,二是利用watch dog自动续期机制。后者通过后台线程定期检查锁的状态并在必要时延长锁的过期时间,确保锁不会因超时而意外释放。尼恩还分享了详细的代码实现和原理分析,帮助读者深入理解并掌握这些技术点,以便在面试中自信应对相关问题。更多技术细节和面试准备资料可在尼恩的技术文章和《尼恩Java面试宝典》中获取。
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
|
3月前
|
消息中间件 安全 前端开发
面试官:单核服务器可以不加锁吗?
面试官:单核服务器可以不加锁吗?
53 4
面试官:单核服务器可以不加锁吗?
|
3月前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
2月前
|
存储 Kubernetes 架构师
阿里面试:JVM 锁内存 是怎么变化的? JVM 锁的膨胀过程 ?
尼恩,一位经验丰富的40岁老架构师,通过其读者交流群分享了一系列关于JVM锁的深度解析,包括偏向锁、轻量级锁、自旋锁和重量级锁的概念、内存结构变化及锁膨胀流程。这些内容不仅帮助群内的小伙伴们顺利通过了多家一线互联网企业的面试,还整理成了《尼恩Java面试宝典》等技术资料,助力更多开发者提升技术水平,实现职业逆袭。尼恩强调,掌握这些核心知识点不仅能提高面试成功率,还能在实际工作中更好地应对高并发场景下的性能优化问题。
|
2月前
|
存储 安全 Java
面试题:再谈Synchronized实现原理!
面试题:再谈Synchronized实现原理!
|
4月前
|
缓存 Java
【多线程面试题二十三】、 说说你对读写锁的了解volatile关键字有什么用?
这篇文章讨论了Java中的`volatile`关键字,解释了它如何保证变量的可见性和禁止指令重排,以及它不能保证复合操作的原子性。
|
4月前
|
Java
【多线程面试题二十二】、 说说你对读写锁的了解
这篇文章讨论了读写锁(ReadWriteLock)的概念和应用场景,强调了读写锁适用于读操作远多于写操作的情况,并介绍了Java中`ReentrantReadWriteLock`实现的读写锁特性,包括公平性选择、可重入和可降级。
|
4月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
下一篇
DataWorks