面试:Java中有哪些锁?

112 阅读5分钟

Java中有哪些锁?

:synchronized同步锁、reentrantlock锁,atomic原子锁,悲观锁乐观锁,公平锁非公平锁,自旋锁,segment分段锁,readwritelock读写锁。

1)synchronized同步锁

Java内置同步锁,用于方法或代码块,确保同一时间只有一个线程可以执行被锁的代码。这个锁是可重入的,即一个线程可以多次获取同一个锁。这个锁的粒度较粗,可能导致性能问题,尤其是在高并发场景下。

//同步方法,锁住的是当前实例对象(this)。如果是静态同步方法,则锁住的是类的Class对象。
public synchronized void synchronizedMethod() {
    // 需要同步的代码
}

//同步代码块
synchronized (this) { // 锁定当前对象
    // 需要同步的代码
}

// 锁定整个类
synchronized (MyClass.class) {
    // 需要同步的代码
}

2)reentrantlock锁

是java.util.concurrent.locks包中提供的显式锁,是可重入的,它提供了比内置锁更灵活丰富的锁操作,例如尝试锁定(tryLock)、可中断的锁定(lockInterruptibly),也支持公平锁和非公平锁(默认是非公平锁)。

ReentrantLock lock = new ReentrantLock();
lock.lock(); // 获取锁
try {
    // 需要同步的代码finally {
    lock.unlock(); // 释放锁
}

3)atomic原子锁

通过CAS无锁操作实现并发控制,AtomicInteger 提供了对整数的原子操作,适用于需要在多线程环境中安全地进行递增、递减或其他操作的场景。AtomicBoolean 提供了对布尔值的原子操作,适用于需要在多线程环境中安全地设置或检查布尔值的场景。

import java.util.concurrent.atomic.AtomicBoolean;

public class SpinLock {
    private final AtomicBoolean locked = new AtomicBoolean(false);
    public void lock() {
        while (!locked.compareAndSet(falsetrue)) {
            // 等待锁释放
        }
    }
    public void unlock() {
        locked.set(false);
    }
}

4)悲观锁和乐观锁

悲观锁:

    对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。(Java中synchronized关键字,进入就锁或Lock接口)

乐观锁:

    对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-设置这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。(Java中Atomic类,AtomicInteger,使用了一种叫Compare-And-Swap的机制来实现线程安全)

两者对比:

特性悲观锁乐观锁
锁定机制使用互斥机制,锁定资源不锁定资源,只在更新时检查冲突
优点数据一致性高,适合高并发写操作性能好,对读操作友好 
缺点可能导致死锁或性能瓶颈如果冲突频繁,可能导致性能下降
适用场景金融交易、库存管理查询为主、写入较少的场景

  

5)公平锁和非公平锁

主要区别在于线程获取锁的顺序和调度方式。在Java中,ReentrantLock类提供了公平锁和非公平锁的实现。通过构造函数的参数可以指定锁的类型:公平锁new ReentrantLock(true),非公平锁new ReentrantLock(false),默认是非公平锁。

公平锁:

    当一个线程请求锁时,如果锁已经被其他线程持有,请求线程会被放入一个等待队列中。当锁被释放时,等待队列中的第一个线程将优先获得锁。通常通过维护一个FIFO队列来记录线程的请求顺序。

非公平锁:

    当一个线程请求锁时,它会首先尝试直接获取锁。如果获取成功,则无需进入等待队列,如果失败,线程才会被放入等待队列。某些线程可能会因为频繁的插队而长时间无法获取锁,从而导致线程饥饿。

两者对比:

特性公平锁非公平锁 
获取顺序按请求顺序获取锁不保证顺序,允许插队
性能开销较高,需要维护队列较低,减少了队列操作 
线程饥饿不会发生可能会发生
适用场景 对公平性要求高的场景,银行交易对吞吐量要求高的场景,缓存日志

6)自旋锁

线程在尝试获取锁时不会直接进入阻塞状态,而是通过循环等待(自旋-原地徘徊等公共厕所坑位)来尝试获取锁。自旋锁适用于锁持有时间非常短的场景,避免线程上下文切换的开销。自旋锁减少了线程阻塞和上下文切换的开销,但如果锁持有时间过长,就可能导致CPU资源浪费。

7)segment分段锁

是一种将锁分段的机制,用于减少锁竞争。ConcurrentHashMap在JDK 1.7及之前版本中使用了分段锁。它将数据分成多个段(Segment),每一段都是一个哈希表,每个段都独立加锁,从而减少了锁竞争,提高并发性能。 在JDK 1.8及之后版本中,ConcurrentHashMap不再使用分段锁,而是通过CAS操作和细粒度锁实现线程安全。

8)readwritelock读写锁

允许多个线程同时读取共享资源,但写操作需要独占锁。这适用于读多写少的场景,可以提高读操作的并发性能,控制写操作的独占,确保数据一致性。 

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

ReadWriteLock lock = new ReentrantReadWriteLock();
// 读操作
lock.readLock().lock();
try {
    // 读取共享资源finally {
    lock.readLock().unlock();
}

// 写操作
lock.writeLock().lock();
try {
    // 修改共享资源finally {
    lock.writeLock().unlock();
}

关注公众号:咖啡Beans

在这里,我们专注于软件技术的交流与成长,分享开发心得与笔记,涵盖编程、AI、资讯、面试等多个领域。无论是前沿科技的探索,还是实用技巧的总结,我们都致力于为大家呈现有价值的内容。期待与你共同进步,开启技术之旅。