Java 锁详解

1. 公平锁 vs 非公平锁

公平锁:是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。类似排队打饭,先来后到。

非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。

比较

公平锁,就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照 FIFO 的规则从队列中取到自己。

非公平锁,比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁的方式。

公平锁的优点是等待锁的线程不会饿死。

非公平锁的优点在于吞吐量比公平锁大。但在高并发的情况下,有可能会造成优先级反转饥饿现象

内窥

并发包中 ReentrantLock 的创建可以指定构造函数的 boolean 类型来得到公平锁或非公平锁,默认为非公平锁。

查看 ReentrantLock,可以看到有一个继承自 AbstractQueuedSynchronizer 的内部类 Sync,添加锁和释放锁的大部分操作实际上都是在 Sync 中实现的。它有公平锁 FairSync 和非公平锁 NonfairSync 两个子类。

public class ReentrantLock implements Lock, java.io.Serializable {
   
    private static final long serialVersionUID = 7373984872572414699L;
    private final Sync sync;
    
    public ReentrantLock() {
   
        sync = new NonfairSync();
    }
    
    public ReentrantLock(boolean fair) {
   
        sync = fair ? new FairSync() : new NonfairSync();
    }
    
    abstract static class Sync extends AbstractQueuedSynchronizer {
   
        private static final long serialVersionUID = -5179523762034025860L;

        abstract void lock();
		//......
    }
  
    static final class NonfairSync extends Sync {
   
		//......
    }

    static final class FairSync extends Sync {
   
        //......
    }   
}

两个构造方法对比,可以看出公平锁和非公平锁的区别

  • 非公平锁在调用 lock() 后,首先就会通过 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了,否则按公平锁的方式去排队,进入到阻塞队列等待唤醒
  • 公平锁在获取同步状态(获取锁)时 tryAcquire() 多了一个限制条件:!hasQueuedPredecessors() ,用来判断当前线程是否位于同步队列中的第一个

Synchronized关键字,也是一种非公平锁。


2. 乐观锁 VS 悲观锁

乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。在 Java 和数据库中都有此概念对应的实际应用。

  • 悲观锁是一种悲观思想,它总认为自己在使用数据的时候一定有别的线程来修改,所以悲观锁在持有数据的时候总会把资源或数据锁住,这样其他线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止。传统的关系型数据库里边就用到了很多这种锁机制,**比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。**悲观锁的实现往往依靠数据库本身的锁功能实现。

    Java 中,synchronized 关键字和 Lock 的实现类都是悲观锁。

  • 而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)

    乐观锁的实现方案一般来说有两种: 版本号机制CAS实现

    Java 中 java.util.concurrent.atomic 包下面的原子变量类的递增操作就是通过 CAS 实现了乐观锁。

比较

悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。

乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

悲观锁比较适合强一致性的场景,但效率比较低,特别是读的并发低。乐观锁则适用于读多写少,并发冲突少的场景。

乐观锁常见问题:

  • ABA 问题
  • 循环时间长开销大
  • 只能保证一个共享变量的原子操作

3. 可重入锁(递归锁)

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。

也就是说,线程可以进入任何一个它已经拥有的锁同步着的代码块。

可重入锁的最大作用是可一定程度避免死锁ReentrackLockSynchronized 就是典型的可重入锁。

public class Widget {
   
    public synchronized void doSomething() {
   
        System.out.println("方法1执行...");
        doOthers();
    }

    public synchronized void doOthers() {
   
        System.out.println("方法2执行...");
    }
}

在上面的代码中,类中的两个方法都是被内置锁 synchronized 修饰的,doSomething() 方法中调用 doOthers() 方法。因为内置锁是可重入的,所以同一个线程在调用 doOthers() 时可以直接获得当前对象的锁,进入doOthers() 进行操作。

如果是一个不可重入锁,那么当前线程在调用 doOthers() 之前需要将执行 doSomething() 时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。

自旋锁

自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗 CPU

public class SpinLockDemo {
   

    AtomicReference<Thread> lock = new AtomicReference<>();

    public void myLock(){
   
        Thread thread = Thread.currentThread();
        //如果不为空,自旋
        while (!lock.compareAndSet(null,thread)){
   

        }
    }

    public void myUnlock(){
   
        Thread thread = Thread.currentThread();
        //解锁后,将锁置为 null
        lock.compareAndSet(thread,null);
    }
}

优缺点)优缺点

缺点:

  1. 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU 使用率极高。
  2. 上面 Java 实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

优点:

  1. 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是 active 的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
  2. 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核&#
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中的机制是线程同步的关键,它用于控制多个线程对共享资源的访问,以避免数据竞争和并发问题。Java主要由`synchronized`关键字、`Lock`接口(如`ReentrantLock`)以及`Condition`类来实现。 **底层原理概述:** 1. **监视器(Monitor)和定:**每个对象都关联着一个监视器,当一个线程进入对象的`synchronized`代码块或方法时,它会获取该对象的监视器。如果另一个线程尝试获取同一对象的,那么线程会被阻塞直到第一个线程释放。 2. **互斥(Mutual Exclusion):**同一时间只有一个线程可以持有对象的,这就是互斥原则。其他等待的线程必须等到当前线程执行完毕并释放。 3. **可见性(Visibility):**当一个线程修改了共享状态后,必须调用`notify()`或`notifyAll()`方法通知其他等待的线程,使它们能够重新检查条件并获得。 4. **等待(Waiting)和唤醒(Signal):**`wait()`方法会让当前线程放弃,进入等待状态,直到被其他线程调用`notify()`或`notifyAll()`唤醒。如果等待过程中线程被中断,它会抛出`InterruptedException`。 5. **重入(Recursion):`synchronized`关键字支持一个线程在同一对象上多次获取,但只有在未被其他线程占用时才允许。 **ReentrantLock扩展:**Java的`ReentrantLock`提供了更多的灵活性,比如可选择公平(按等待线程顺序获取)和非公平(最快的线程优先获取),以及显式的获取和释放。此外,它还提供`tryLock()`方法用于无操作,以及条件变量`Condition`用于更复杂的同步场景。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值