大厂面试题详解:java中有哪些类型的锁

简介: 字节跳动大厂面试题详解:java中有哪些类型的锁

大厂面试题详解:java中有哪些类型的锁


Java中的锁类型及详解


在Java中,锁是用来控制对共享资源的访问的机制。它们提供了多线程环境下的同步和互斥,以确保线程安全性。Java中有多种类型的锁,包括对象锁、类锁、读写锁、自旋锁等。


1. 对象锁(Synchronized)


对象锁是Java中最基本的锁类型之一,使用关键字 synchronized 来实现。它可以用于同步对对象实例方法和代码块的访问。


示例代码:
public class SynchronizedExample {
    private int count = 0;

    // 对象实例方法使用对象锁
    public synchronized void increment() {
        count++;
    }

    // 对象实例方法也可以使用代码块来加锁
    public void decrement() {
        synchronized (this) {
            count--;
        }
    }
}


2. 类锁(Synchronized)


类锁与对象锁类似,但是作用于类的所有实例。使用 synchronized 关键字修饰静态方法或者通过 Class 对象实现。


示例代码:
public class ClassLockExample {
    private static int count = 0;

    // 静态方法使用类锁
    public static synchronized void increment() {
        count++;
    }

    // 通过Class对象实现类锁
    public void decrement() {
        synchronized (ClassLockExample.class) {
            count--;
        }
    }
}


3. 读写锁(ReentrantReadWriteLock)


读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。这提高了读操作的并发性能。

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
    private int value = 0;
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    // 读取操作
    public void read() {
        lock.readLock().lock(); // 获取读锁
        try {
            System.out.println("Read value: " + value); // 输出当前值
        } finally {
            lock.readLock().unlock(); // 释放读锁
        }
    }

    // 写入操作
    public void write(int newValue) {
        lock.writeLock().lock(); // 获取写锁
        try {
            value = newValue; // 更新值
            System.out.println("Write value: " + value); // 输出更新后的值
        } finally {
            lock.writeLock().unlock(); // 释放写锁
        }
    }
}


在上面的代码中,我使用ReentrantReadWriteLock实现了一个简单的读写锁示例。这个示例包括一个私有变量value用于存储数据,以及一个ReentrantReadWriteLock对象lock用于管理并发访问。


  • read() 方法用于读取数据。它首先获取读锁,然后输出当前的value值,并最终释放读锁。
  • write(int newValue) 方法用于写入数据。它首先获取写锁,然后更新value的值为newValue,输出更新后的值,并最终释放写锁。


通过使用读写锁,我可以实现对共享资源的并发访问控制,提高了程序的并发性能。


4. 自旋锁(Spin Lock)


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

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * 自旋锁是一种基于循环等待的锁,线程在获取锁时不会被挂起,而是不断地尝试获取锁。
 */
public class SpinLockExample {
    private AtomicBoolean locked = new AtomicBoolean(false);

    /**
     * 获取锁的方法
     */
    public void lock() {
        // 使用自旋方式尝试获取锁
        while (!locked.compareAndSet(false, true)) {
            // 如果获取失败,继续尝试获取
            // 在高并发情况下,可能会导致线程长时间处于自旋状态,消耗CPU资源
        }
    }

    /**
     * 释放锁的方法
     */
    public void unlock() {
        // 释放锁,将锁状态设置为false
        locked.set(false);
    }
}


在上述代码中,我实现了一个简单的自旋锁(Spin Lock)示例。自旋锁使用了AtomicBoolean来表示锁的状态,false表示锁未被持有,true表示锁已被持有。


  • lock() 方法用于获取锁。它使用了自旋的方式来尝试获取锁,不断地循环检查锁的状态,直到成功获取锁。
  • unlock() 方法用于释放锁。它将锁的状态设置为false,表示锁已被释放。


自旋锁的优势在于避免了线程的上下文切换,适用于短时间内持有锁的情况。然而,自旋锁可能会导致线程长时间处于忙等待状态,消耗CPU资源,因此在实际应用中需要谨慎使用。


5. 重入锁(ReentrantLock)


重入锁是一种与synchronized相似的锁,但提供了比synchronized更多的灵活性和功能,例如可中断锁、公平性等。

import java.util.concurrent.locks.ReentrantLock;

/**
 * ReentrantLock是Java并发包提供的可重入锁实现,允许同一个线程多次获取同一把锁。
 */
public class ReentrantLockExample {
    private int count = 0;
    private ReentrantLock lock = new ReentrantLock();

    /**
     * 对计数器进行加一操作
     */
    public void increment() {
        lock.lock(); // 获取锁
        try {
            count++; // 对计数器进行加一操作
        } finally {
            lock.unlock(); // 释放锁
        }
    }
}


在上述代码中,我展示了使用ReentrantLock实现的一个简单示例。这个示例包括一个私有计数器count和一个ReentrantLock对象lock。


  • increment() 方法用于对计数器进行加一操作。在方法执行过程中,首先通过lock()方法获取锁,然后对计数器进行加一操作,最后通过unlock()方法释放锁。


ReentrantLock是Java并发包提供的可重入锁实现,允许同一个线程多次获取同一把锁。相比于synchronized关键字,ReentrantLock提供了更多的锁定操作和更灵活的控制,适用于更复杂的并发场景。


Java中锁的应用场景和详细案例


对象锁的应用场景


对象锁通常用于保护对对象实例的访问,例如多个线程对同一个对象进行操作时,可以使用对象锁确保线程安全。


示例代码:
public class ObjectLockExample {
    private int count = 0;

    // 对象实例方法使用对象锁
    public synchronized void increment() {
        count++;
    }
}


类锁的应用场景


类锁作用于类的所有实例,常用于控制对静态变量的访问,或者对静态方法的调用。


示例代码:
public class ClassLockExample {
    private static int count = 0;

    // 静态方法使用类锁
    public static synchronized void increment() {
        count++;
    }
}


读写锁的应用场景


读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。适用于读操作远远多于写操作的场景。


示例代码:
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
    private int value = 0;
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void read() {
        lock.readLock().lock();
        try {
            System.out.println("Read value: " + value);
        } finally {
            lock.readLock().unlock();
        }
    }

    public void write(int newValue) {
        lock.writeLock().lock();
        try {
            value = newValue;
            System.out.println("Write value: " + value);
        } finally {
            lock.writeLock().unlock();
        }
    }
}


自旋锁的应用场景


自旋锁适用于锁保护时间短、线程竞争不激烈的情况。它避免了线程挂起和恢复的开销,适用于多核CPU并发度高的场景。


示例代码:
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);
    }
}


重入锁的应用场景


重入锁提供了比synchronized更多的灵活性和功能,例如可中断锁、公平性等。适用于复杂的同步需求场景。


示例代码:
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private int count = 0;
    private ReentrantLock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
}


Java中锁的性能比较和最佳实践


锁的性能比较


在选择锁时,除了考虑功能和应用场景外,性能也是一个重要因素。不同类型的锁在性能上有所差异,因此需要根据具体情况进行选择。


  • 对象锁(Synchronized): JVM对Synchronized进行了优化,性能较高。但是,它是一种悲观锁,可能会导致线程阻塞和上下文切换。
  • 重入锁(ReentrantLock): 提供了比Synchronized更多的功能,例如可中断锁、公平性等。但是,它的性能略低于Synchronized。
  • 读写锁(ReentrantReadWriteLock): 适用于读操作远远多于写操作的场景,可以提高读操作的并发性能。
  • 自旋锁(SpinLock): 适用于锁保护时间短、线程竞争不激烈的情况,避免了线程挂起和恢复的开销。但是,如果锁保护时间过长或线程竞争激烈,会导致CPU消耗过多。
  • StampedLock: Java 8引入的新型锁,适用于读多写少的场景,性能优于ReentrantReadWriteLock。


锁的最佳实践


  • 选择合适的锁类型: 根据具体场景选择合适的锁类型,避免过度同步。
  • 精细化锁的粒度: 尽量缩小锁的范围,以减少锁的竞争,提高并发性能。
  • 避免死锁: 设计良好的锁顺序,避免出现死锁情况。
  • 合理使用锁的超时和中断功能: 在获取锁时可以设置超时时间,避免线程长时间等待,提高系统的响应性。
  • 使用局部变量和线程封闭: 尽量使用局部变量和线程封闭的方式,避免共享资源的竞争。
  • 优化并发数据结构: 使用Java并发包提供的并发数据结构,如ConcurrentHashMap、CopyOnWriteArrayList等,避免自己实现复杂的同步逻辑。


示例代码
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
    private int count = 0;
    private Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
}


以上示例代码展示了如何使用重入锁(ReentrantLock),是一种性能较好且灵活的锁实现方式,适用于大多数并发场景。


Java并发编程锁的常见文体


1. 竞态条件(Race Conditions)


竞态条件是多线程环境下常见的问题,当多个线程同时访问共享资源,并且对资源的访问顺序产生依赖时,可能导致不确定的结果。


解决方案:


  • 使用锁来保护共享资源,确保同一时间只有一个线程访问。
  • 使用原子类(Atomic类)来实现原子操作,避免非线程安全操作。


2. 死锁(Deadlocks)


死锁是指两个或多个线程被无限期地阻塞,彼此等待对方释放资源,从而无法继续执行的情况。


解决方案:


  • 设计良好的锁顺序,避免出现循环等待的情况。
  • 使用tryLock()方法来避免死锁,及时释放已经获取的锁。


3. 上下文切换(Context Switching)


多线程之间的切换会带来上下文切换的开销,尤其是在多核CPU上,上下文切换可能成为性能瓶颈。


解决方案:


  • 减少锁的粒度,尽量缩小同步代码块的范围,减少锁竞争。
  • 使用无锁数据结构,减少对共享资源的争用。


4. 内存可见性(Memory Visibility)


在多线程环境下,如果一个线程对共享变量的修改对另一个线程是不可见的,可能导致意想不到的结果。


解决方案:


  • 使用volatile关键字来保证变量的可见性。
  • 使用synchronized关键字或ReentrantLock来保证线程间的内存可见性。


5. 并发集合的安全性


Java提供了许多并发安全的集合类,如ConcurrentHashMap、CopyOnWriteArrayList等,但是在特定场景下仍需注意安全性。


解决方案:


  • 选择合适的并发集合类,并了解其特性和限制。
  • 使用迭代器时,注意遍历过程中集合的修改操作,避免ConcurrentModificationException异常。
相关文章
|
5天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
18 2
|
10天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
12天前
|
缓存 Java
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
本文介绍了几种常见的锁机制,包括公平锁与非公平锁、可重入锁与不可重入锁、自旋锁以及读写锁和互斥锁。公平锁按申请顺序分配锁,而非公平锁允许插队。可重入锁允许线程多次获取同一锁,避免死锁。自旋锁通过循环尝试获取锁,减少上下文切换开销。读写锁区分读锁和写锁,提高并发性能。文章还提供了相关代码示例,帮助理解这些锁的实现和使用场景。
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
|
15天前
|
存储 缓存 Oracle
Java I/O流面试之道
NIO的出现在于提高IO的速度,它相比传统的输入/输出流速度更快。NIO通过管道Channel和缓冲器Buffer来处理数据,可以把管道当成一个矿藏,缓冲器就是矿藏里的卡车。程序通过管道里的缓冲器进行数据交互,而不直接处理数据。程序要么从缓冲器获取数据,要么输入数据到缓冲器。
Java I/O流面试之道
|
13天前
|
存储 Java 开发者
Java 中 Set 类型的使用方法
【10月更文挑战第30天】Java中的`Set`类型提供了丰富的操作方法来处理不重复的元素集合,开发者可以根据具体的需求选择合适的`Set`实现类,并灵活运用各种方法来实现对集合的操作和处理。
|
11天前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
37 4
|
12天前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
53 4
|
13天前
|
Java 编译器 开发者
Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面
本文探讨了Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面,帮助开发者提高代码质量和程序的健壮性。
31 2
|
25天前
|
存储 Java 程序员
Java面试加分点!一文读懂HashMap底层实现与扩容机制
本文详细解析了Java中经典的HashMap数据结构,包括其底层实现、扩容机制、put和查找过程、哈希函数以及JDK 1.7与1.8的差异。通过数组、链表和红黑树的组合,HashMap实现了高效的键值对存储与检索。文章还介绍了HashMap在不同版本中的优化,帮助读者更好地理解和应用这一重要工具。
52 5
|
23天前
|
存储 Java
[Java]面试官:你对异常处理了解多少,例如,finally中可以有return吗?
本文介绍了Java中`try...catch...finally`语句的使用细节及返回值问题,并探讨了JDK1.7引入的`try...with...resources`新特性,强调了异常处理机制及资源自动关闭的优势。
19 1