java中的各种锁和CAS经典面试题

本文探讨了Java中的锁和CAS概念,包括悲观锁与乐观锁、公平锁与非公平锁、独占锁与共享锁的区别,以及可重入锁、自旋锁的应用。还介绍了ABA问题及其解决方案,并通过例子解析了Java并发面试中常见的问题。
摘要由CSDN通过智能技术生成

java中的各种锁和CAS经典面试题

为什么需要了解锁知识呢?因为在高并发下带来的线程安全性问题,需要以锁的方式解决,保障数据的安全准确,理解各种锁知识,为我们开发多线程程序提供理论支持。同时也是面试中常问的知识点。

乐观锁和悲观锁
乐观锁和悲观锁并不是某个具体的锁,而是一种并发编程的概念。乐观锁最早出现在数据库的设计当中,后来逐渐被java并发包所引入。

悲观锁:悲观锁认为同一个数据的并发操作,一定会发生修改,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,悲观的认为,不加锁的并发操作一定会出问题。

乐观锁:乐观锁正好相反,他获取数据的时候,并不担心数据被修改,每次获取数据的时候也不会加锁,只是在更新数据的时候,通过判断现有的数据是否和原数据一致来判断数据是否被其他线程操作,如果没被其他线程修改则进行数据更新,如果被其他数据修改则不进行数据更新。

公平锁和非公平锁:根据线程获取锁的抢占机制,锁又可以分为公平锁和非公平锁。

公平锁:公平锁是指多个线程按照申请锁的顺序来获取锁。

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

  • 公平锁:new ReentrantLock(true)
  • 非公平锁:new ReentrantLock(false)

独占锁和共享锁:根据锁能否被多个线程持有,可以把锁分为独占锁和共享锁。

独占锁:独占锁是指任何时候都只有一个线程能执行资源操作。

共享锁:共享锁指定是可以同时被多个线程读取,但只能被一个线程修改。比如java中的ReentrantReadWriteLock 就是共享锁的实现方式,它允许一个线程进行写操作,允许多个线程读操作。
ReentrantReadWriteLock 共享锁演示代码如下:

package com.example.dubbo.reentrant.lock.test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReentrantLockDemo {
    public static void main(String[] args) {
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2);
        MyReadWriteLock myReadWriteLock = new MyReadWriteLock();
        for (int i = 0; i < 2; i++) {
            fixedThreadPool.execute(()->{
                myReadWriteLock.read();
            });
        }
        for (int i = 0; i < 2; i++) {
            fixedThreadPool.execute(()->{
                myReadWriteLock.write();
            });
        }
        fixedThreadPool.shutdown();
    }
}
class  MyReadWriteLock{
    private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

    public void read() {
        try {
            reentrantReadWriteLock.readLock().lock();
            System.out.println("读数据,进入 | 线程:"+ Thread.currentThread().getName());
            Thread.sleep(30);
            System.out.println("“读数据,退出 | 线程:"+ Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            reentrantReadWriteLock.readLock().unlock();
        }
    }

    public void write() {
        try {
            reentrantReadWriteLock.writeLock().lock();
            System.out.println("写数据,进入 | 线程:"+ Thread.currentThread().getName());
            Thread.sleep(30);
            System.out.println("写数据,退出 | 线程:"+ Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            reentrantReadWriteLock.writeLock().unlock();
        }
    }
}

结果如下:
在这里插入图片描述
可重入锁:可重入锁指的是该线程获取了该锁之后,可以无限次的进入该锁锁住的代码。

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

CAS与ABA:
CAS(Compare and Swap) 比较并交换,是一种乐观锁的实现,是用非阻塞算法来代替锁定,其中java.util.concurrent包下的AtomicInteger就是借助CAS来实现的。

但CAS也不是没有任何副作用,比如著名的ABA问题就是CAS引起的。

ABA问题描述
老王去银行取钱,余额200,元,老板取100元,但由于程序有问题启动了两个线程,线程1和线程2进行对比扣款,线程1获取原本有200元,扣除100元,余额等于100元,此时朋友给转账了100元,于是启动了线程3在线程2之前执行了转账操作,把100元,又变成了200元,而此时线程2对比之前拿到的200元,和此时经过修改后的200元进行对比,又扣除了100元,余额变为100元。这时老王实际只取到了100元,可是账户里却只留下100元。所以账户余额出错了。虽不可思议,这就是著名的ABA问题。

执行流程如下
线程一:取款,获取原值200元,与200元对比成功,减去100元,修改结果为100元。
线程二:取款,获取原值200元,阻塞等待修改。
线程三:转账,获取原值100元,与100元比对成功,加上100元,修改结果为200元。
线程二:取款,回复执行,原值为200元,与修改后的200元比对成功,减去100元,修改结果为100元。
最终结果账面上还有100元。这显示不是我们期望的结果。

ABA问题解决思路:
常见解决ABA问题的方案是加版本号,来区分值是否发生过变动。以老王取钱的例子为例,如果加上版本号,执行流程如下。
线程一:取款,获取原值200_v1,与200_v1比对成功,减去100元,修改结果为100_v2.
线程二:取款,获取原值200_v1阻塞等待修改。
线程三:转账,获取原值100_v2,与100_v2对比成功,加100元,修改结果为200_v3;
线程二:取款,回复执行,原值200_v1与现值200_v3对比不等,退出修改。

最终结果是200元,这显然是我们需要的结果。

程序中中怎么解决ABA问题呢???

在jdk1.5时候,提供了AtomicStampedReference 原子引用变量,通过添加版本号来解决ABA的问题,具体使用示例如下:

		String name = "老王";
        String newName = "Java";
        AtomicStampedReference<String> asr = new AtomicStampedReference<>(name,1);
        System.out.println("值:"+asr.getReference()+"| Stamp:"+asr.getStamp());
        boolean result = asr.compareAndSet(name, newName, asr.getStamp(), asr.getStamp() + 1);
        System.out.println("值:"+asr.getReference()+"|Stamp:"+asr.getStamp()+"result:"+result);

相关面试题:

1;synchronized是那种锁的实现?为什么?
答:synchronized是悲观锁的实现,因为synchronized修饰的代码,每次执行时会进行加锁操作,同时只允许一个线程进行操作,所以他是悲观锁的实现。

2.new ReentrantLock()创建的是公平锁还是非公平锁?
答:非公平锁,查看ReentrantLock的实现源码可知。

/**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

3.synchronized使用的是公平锁还是非公平锁?
答:synchronized使用的是非公平锁,并且是不可设置的。这是因为非公平锁的吞吐量大于公平锁,并且是主流操作系统线程调度的基本选择,所以这也是synchronized 使用非公平锁的原因。

4:为什么非公平锁的吞吐量大于公平锁?
答:比如A占用资源的时候,B请求获取锁,发现A被占用后,堵塞等待被唤醒,这个时候C同时来获取A占用的资源,如果是公平锁C后来者发现不可用之后一定排在B后面等待被唤醒,而非公平锁则可以让C先用,在B被唤醒之前,C已经使用完成,从而节省了C等待和被唤醒之间的性能消耗,这就是非公平锁比公平锁吞吐量大的原因。

5.volatile作用是什么?
答:volatile是java虚拟机提供的最轻量的同步机制。当变量被定义成volatile之后,具备两种特性:

  • 保证此变量对所有线程的可见性,当一条线程修改了这个变量的值,修改的新值对其他的线程是可见的(可以立即得知)。
  • 禁止指令重排优化,普通变量仅仅能保证在该方法执行过程中,得到正确结果,但是不保证程序代码的执行顺序。

6.volatile与synchronized有什么区别?
答:1、作用范围小:volatile只能作用于变量,而synchronized可以用在方法、类、同步代码块等,使用范围比较广。 (

要说明的是,java里不能直接使用synchronized声明一个变量,而是使用synchronized去修饰一个代码块或一个方法或类。


2、volatile只能保证可见性和有序性,不能保证原子性。而可见性、有序性、原子性synchronized都可以保证。
3、volatile不会造成线程阻塞;synchronized可能会造成线程阻塞。

7.CAS是如何实现的?
答:CAS(Compare and Swap) 比较交换,CAS是通过调用JNI(Java Native Interface)的代码实现的,比如,在windows系统CAS就是借助C语言来调用CPU底层指令完成的。

8.CAS会产生什么问题?应该如何解决?
答:CAS是标准的乐观锁的实现,会产生ABA的问题。ABA的通常解决办法是添加版本号,每次修改操作时版本号加一,这样数据对比的时候就不会出现ABA问题了。

9.以下说法错误的是:

A:独占锁是指任何时候都只有一个线程执行资源操作;

B:共享锁指定是可以同时被多个线程读取和修改的。

C:公平锁是指按照申请锁的顺序来获取锁

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

答:B

题目解析:

共享锁指定是可以同时被多个线程读取,但只能被一个线程修改。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值