【JUC面试题】锁


1、乐观锁与悲观锁

悲观锁和乐观锁是在并发编程中使用的两种不同的锁机制。悲观锁认为在并发环境下,数据很可能会被其他线程修改,因此在访问数据时会先上锁,阻止其他线程对该数据进行修改。悲观锁的实现方式包括数据库中的行锁和表锁、Java中的synchronized关键字等。乐观锁则认为在并发环境下,数据很少被其他线程修改,因此不会上锁,而是在更新数据时检查数据版本号等标识,如果发现数据已经被其他线程修改,则放弃更新并通知用户。乐观锁的实现方式包括数据库中的乐观锁和Java中的CAS(Compare and Swap)等。总的来说,悲观锁适用于并发修改频繁的情况,而乐观锁适用于并发修改较少的情况。

  • 悲观锁的实现

    1. Mysql:在Mysql Innodb 引擎中,自带行锁机制。当多个线程对同一行数据进行修改的时候,悲观锁认为数据很可能会被其他线程修改,因此在访问数据时会先上锁,阻止其他线程对该数据进行修改。(其他的线程此时处于阻塞的状态)
    2. JAVA: 站在JAVA锁层面,没有获取到锁的线程会进入阻塞等待,后期需要重新被我们的cpu从阻塞等待 -》 就绪调度 -》 运行状态,唤醒的锁的成本就会非常高, 如Java中的synchronized关键字;
      因此,一般情况下,不推荐使用悲观锁;
      在这里插入图片描述
  • 乐观锁的实现

  1. Mysql : 据库中的乐观锁, 在mysql表结构中,会新增一个版本字段 version varchar(255) DEFAULT NULL, 多个线程对同一行数据进行修改操作时,会提前查询当前最新的 version 版本号,作为 update 条件查询,如果当前 version 版本号码发生了变化,则查询不到该数据,则表示修改数据失败,会进行不断重试 ,重新查询最新的版本实现 update条件。(需要注意控制乐观锁循环的次数,避免 cpu 飙高的问题。)
    在这里插入图片描述

2. JAVA:

在 Java 中,使用乐观锁可以通过 CAS(Compare and Swap)实现。CAS 是一种基于硬件的原子性操作,它可以在并发环境下实现无锁的同步操作。

CAS 操作包括三个参数:需要修改的内存值 V、预期的值 A 和新值 B。当且仅当 V 的值等于 A 时,CAS 将 V 的值修改为 B,否则不做任何操作。

在使用乐观锁时,需要在更新数据时先读取数据的版本号或者其他标识,然后使用 CAS 操作更新数据。如果 CAS 操作成功,说明数据没有被其他线程修改,更新成功;如果 CAS 操作失败,则说明数据已经被其他线程修改,需要重新读取数据的版本号或者其他标识,再次尝试更新。

以下是一个简单的使用 CAS 实现乐观锁的示例代码:

public class OptimisticLock {
    private AtomicInteger version=new AtomicInteger(2);

    private int data=0;

    public void update(int newData){
        /**
         * compareAndSet(int expect, int update)
         * 第一个参数是预期值 判断的内存值和预期值是否相等 不相等返回false 反之true
         * 第二个参数是修改值 如果内存值和预期值相等 则将新值赋值给内存值;
         */
        int oldVersion = version.get();
        int newVersion=oldVersion+1;
        while (!version.compareAndSet(oldVersion,newVersion)){
            oldVersion=version.get();
        }

        data=newData;

    }
    

}

在这个示例中,version 用于保存数据的版本号,data 用于保存数据。在 update 方法中,先读取当前的版本号 oldVersion,然后使用 CAS 操作将 version 的值从 oldVersion 修改为 oldVersion + 1。如果 CAS 操作成功,则说明数据没有被其他线程修改,可以更新数据;如果 CAS 操作失败,则说明数据已经被其他线程修改,需要重新读取数据的版本号,再次尝试更新。

利用原子类手写CAS无锁:

public class AtomicTryLock {

    AtomicInteger atomic=new AtomicInteger(0);
    private Thread localCurrentThread;

    /**
     * 获取锁
     * 0 表示锁没有获取 1 表示锁已经被获取
     *
     */
    public boolean lock(){
        while (true){
            boolean result = atomic.compareAndSet(0, 1);
            if (result){
                return true;
            }
        }
    }


    /**
     * 释放锁 必须获取到锁的线程才能释放锁
     */
    public boolean unlock(){
        if (localCurrentThread!=Thread.currentThread()) {
            return false;
        }
        return atomic.compareAndSet(1,0);
    }


    public static void main(String[] args) {
        AtomicTryLock atomicTryLock = new AtomicTryLock();
        IntStream.range(1, 10).forEach((i) -> new Thread(() -> {
            try {
                boolean result = atomicTryLock.lock();
                if (result) {
                    atomicTryLock.localCurrentThread=Thread.currentThread();
                    System.out.println(Thread.currentThread().getName() + ",获取锁成功~");
                } else {
                    System.out.println(Thread.currentThread().getName() + ",获取锁失败~");
                }
            } catch (Exception e) {
                System.err.println(e.getMessage());
            } finally {
                boolean unlock = atomicTryLock.unlock();
                if (unlock) {
                    System.out.println(Thread.currentThread().getName() + ",释放锁失败~");
                }
            }
        }).start()); }
}

在这里插入图片描述
CAS锁ABA
当两个线程对同一个数据进行操作时,其中一个线程先将这个数据从 A 改为 B,再由 B 改回 A,而另一个线程在这之间读取了这个数据,这时候就容易出现 ABA 问题。因为第二个线程可能会误认为这个数据没有发生过变化,而实际上它的值已经发生了变化。

如果使用同步锁机制,可以通过锁的互斥性来避免 ABA 问题的出现。但是使用 CAS 进行无锁操作时,需要引入版本号等机制来解决 ABA 问题。在JAVA并发包中,为我们提供的 AtomicStampedReference类来专门解决ABA的问题;

AtomicStampedReference 常用的API如下:

方法名作用
public AtomicStampedReference(V initialRef, int initialStamp)创建一个包含给定初始值和初始版本号的新 AtomicStampedReference 对象。
public V getReference()返回当前对象的引用值
public int getStamp()返回当前对象的版本号
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)比较并设置更新对象引用和版本号。如果当前对象的引用值等于 expectedReference ,并且版本号等于 expectedStamp ,则将其更新为 newReference 和 newStamp ,并返回 true;否则,返回 false。

代码实例:

public class AtomicStampedReferenceDemo {
    private  static final AtomicStampedReference<Integer> atomic=new AtomicStampedReference<>(1,1);


    public static void main(String[] args) throws InterruptedException {


        Thread t1 = new Thread(() -> {
            int stamp = atomic.getStamp(); // 获取预期值
            System.out.println("任务1: 获取初始版本号 " + stamp);


            atomic.compareAndSet(1, 2, stamp, stamp + 1);
            System.out.println("任务1: 将1 修改为 2 ");


            atomic.compareAndSet(2, 1, stamp + 1, stamp + 2);
            System.out.println("任务1: 将2 修改为 1 ");


        });



        Thread t2=new Thread(()->{

            int stamp = atomic.getStamp();
            System.out.println("任务 2:获取初始版本号:" + stamp);

            try {
                Thread.sleep(1000);  // 等待任务1执行结束
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            boolean result = atomic.compareAndSet(1, 3, stamp + 2, stamp + 3);

            System.out.println("任务2: 将1 修改为3 , 结果为result: "+ result);

        });


        t1.start();
        t2.start();

        t1.join(); // 主线程 等待两个线程执行完毕后再输出最终的结果。
        t2.join();


        System.out.println("最终值: "+atomic.getReference() +"最终版本号:"+atomic.getStamp());



    }
}

在这里插入图片描述

CAS锁的优缺点:

  • 优点:没有获取到锁的线程,会一直在用户态,不会阻塞,没有锁的线程会一直通过循环控制重试。
  • 缺点:通过死循环控制,消耗 cpu 资源比较高,需要控制循次数,避免 cpu 飙高问题;

2、公平锁与非公平锁

公平锁和非公平锁是指锁在竞争时是否遵循“先来先得”的公平原则。

公平锁:在多线程竞争下,每个线程在等待锁时会先进入一个等待队列,当锁释放后,等待时间最久的线程会获得锁。公平锁能够保证多个线程按照申请锁的顺序来进行竞争,避免了线程饥饿情况的发生。

非公平锁:在多线程竞争下,每个线程会直接尝试获取锁,如果获取不到就不断尝试,直到获取到锁为止。非公平锁虽然不能保证等待时间最长的线程一定先获得锁,但它的开销通常比公平锁要小,因为它省略了排队的过程。

对于公平锁和非公平锁,实际应用中需要根据具体情况进行选择。如果对线程的公平性要求比较高,应该使用公平锁,如果追求高并发性能,则可以使用非公平锁。

在Java中,ReentrantLock既可以是公平锁,也可以是非公平锁。判断锁是否公平,可以通过构造函数中传入boolean值来控制,如下所示:

ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock nonFairLock = new ReentrantLock(false); // 非公平锁

ReentrantLock是Java中的一个锁实现,它允许线程在获取锁时可以重入[允许同一个线程在持有锁的情况下,再次获取该锁,而不会被自己所持有的锁所阻塞],也就是说,一个线程可以多次获取同一个锁。这个锁实现提供了比synchronized更多的灵活性和控制力,因为它允许线程在获取锁时可以设置超时时间、中断等待线程、以及在等待锁时可以响应中断等。
ReentrantLock还提供了公平锁和非公平锁两种模式,公平锁会按照线程请求锁的顺序来获取锁,而非公平锁则允许线程在等待锁时插队获取锁,这样可以提高并发性能,但可能会导致某些线程长时间等待锁。

  • 生产者, 消费者模型:
    多个线程共享一个队列,生产者线程往队列中添加元素,消费者线程从队列中取出元素。如果队列满了,生产者需要等待,直到有空间可以添加元素;如果队列为空,消费者需要等待,直到队列中有元素可以取出。

假设我们有一个生产者消费者模型,多个线程共享一个队列,生产者线程往队列中添加元素,消费者线程从队列中取出元素。如果队列满了,生产者需要等待,直到有空间可以添加元素;如果队列为空,消费者需要等待,直到队列中有元素可以取出。

public class MyQueue<T> {
    /**
     * 使用ReentrantLock和Condition实现线程同步。
     * 当队列满时,生产者线程会等待队列不满时再进行放入;
     * 当队列为空时,消费者线程会等待队列不空时再进行取出。
     * 这样就能够保证数据的正确性,避免生产者和消费者同时访问队列导致的竞争问题。
     */
    private Object[] elements;
    private int index;

    private final ReentrantLock lock=new ReentrantLock(); // 非公平
    /**
   Condition 是在 Java 5 中引入的一个新特性,它是与 Lock 相关联的一个
   对象,可以用来实现等待/通知机制。在多线程编程中,有时候我们需要等待某个
   条件的发生,如果条件未发生,线程需要暂停等待。而 Condition 就是用来实
   现这一需求的,它提供了 await() 和 signal()/signalAll() 方法来实现线
   程的等待和通知。
    */
    // 此处为了便于理解用takeCondition , putCondition 实则上是一样的;
    private final Condition takeCondition= lock.newCondition();
    private final Condition putCondition= lock.newCondition();


    public MyQueue(int size){
        elements=new Object[size];
    }
    
    public void put(T t) throws InterruptedException {
        lock.lock();
        try {
            while (index == elements.length && elements.length != 0){
                putCondition.await(); // put 线程等待
            }
            elements[index++]=t;
            takeCondition.signal();  // 唤醒take 线程
        }finally {
            lock.unlock();
        }
    }
    
    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (index == 0){
                takeCondition.await(); // take线程等待
            }
            T t = (T) elements[--index];
            putCondition.signal(); // put线程换新
            return t;
        }finally {
            lock.unlock();
        }

    }
}

3、 独享锁与共享锁

  • 独享锁:也叫排它锁,是指同一时间只有一个线程可以获得该锁,其他线程必须等待该线程释放锁之后才可以获取该锁。在Java中,使用synchronized关键字可以实现独享锁,即一个线程进入synchronized代码块后,其他线程无法进入该代码块。

  • 共享锁:是指同一时间可以有多个线程同时获得该锁,但是这些线程只能读取该共享资源,不能修改。只有在所有线程都释放共享锁之后,才能有线程获取排它锁进行修改。Java中的ReadWriteLock就是一种支持读写分离的共享锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值