(二)【Java精选面试题】JUC锁的架构原理(含答案)

目录

  • 1. 什么是悲观锁?什么是乐观锁?
  • 2. Mysql 层面如何实现乐观锁呢
  • 3. 乐观锁实现方式
  • 4. Java 有哪些锁的分类呢
  • 5. 公平锁与非公平锁之间的区别
  • 6. 公平锁底层是如何实现的
  • 7. 独占锁与共享锁之间的区别
  • 8. 什么是锁的可重入性
  • 9. 什么是 CAS(自旋锁),它的优缺点
  • 10. CAS 如何解决 ABA 的问题
  • 11. 利用原子类手写 CAS 无锁


1. 什么是悲观锁?什么是乐观锁?

悲观锁:
1.站在 mysql 的角度分析:悲观锁就是比较悲观,当多个线程对同一行数据实现修改的时候,最后只有一个线程能修改成功,只要谁能够获取到行锁则其他线程是不能够对该数据做任何修改操作,且是阻塞状态。
2.站在 java 锁层面,如果没有获取到锁,则会阻塞等待,后期唤醒的锁的成本就会非常高,从新被我们 cpu 从就绪调度为运行状态。
Lock syn 锁 悲观锁没有获取到锁的线程会阻塞等待;

乐观锁:
乐观锁比较乐观,通过预值或者版本号比较,如果不一致性的情况则通过循环控制修改,当前线程不会被阻塞,是乐观,效率比较高,但是乐观锁比较消耗 cpu 的资源

乐观锁:获取锁----如果没有获取到锁,当前线程是不会阻塞等待,通过死循环控制。乐观锁属于无锁机制,没有竞争锁流程。

2. Mysql 层面如何实现乐观锁呢

在我们表结构中,会新增一个字段就是版本字段
version varchar(255) DEFAULT NULL, 多个线程对同一行数据实现修改操作,提前查询当前最新的 version 版本号码,作为 update 条件查询,如果当前 version 版本号码发生了变化,则查询不到该数据。表示如果修改数据失败,则不断重试 ,有从新查询最新的版本实现update。需要注意控制乐观锁循环的次数,避免 cpu 飙高的问题。
注意:mysql 的 innodb 引擎中存在行锁的概念

3. 乐观锁实现方式

1.版本号机制
一般是说在数据表中加上一个数据库版本号version字段,在表述数据被修改的次数当数据被修改时,它的version 值会加1。
如:
当前线程A需要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

2.CAS 算法
CAS(compare and swap) 比较并交换,有三个操作数,内存地址V ,预期值B,要替换得到的目标子A;
CAS指令执行时,比较内存地址V与预期值B是否相等,若相等则将A赋给B,(不相等则会循环比较直到相等)整个比较赋值操作是一个原子操作;

4. Java 有哪些锁的分类呢

  1. 悲观与乐观锁
  2. 公平锁与非公平锁
  3. 自旋锁/重入锁
  4. 重量级锁与轻量级锁
  5. 独占锁与共享锁

5. 公平锁与非公平锁之间的区别

公平锁:就是比较公平,根据请求锁的顺序排列,先来请求的就先获取锁,后来获取锁就最后获取到, 采用队列存放 类似于吃饭排队。
非公平锁:不是据请求的顺序排列, 通过争抢的方式获取锁。
非公平锁效率是公平锁效率要高,Synchronized 是非公平锁

New ReentramtLock()(true)—公平锁
New ReentramtLock()(false)—非公平锁
底层基于 aqs 实现

6. 公平锁底层是如何实现的

公平锁:就是比较公平,根据请求锁的顺序排列,先来请求的就先获取锁,后来获取锁就最后获取到, 采用队列存放 类似于吃饭排队。

7. 独占锁与共享锁之间的区别

独占锁:在多线程中,只允许有一个线程获取到锁,其他线程都会等待。
共享锁:多个线程可以同时持有锁,例如 ReentrantLock 读写锁。读读可以共享、写写互斥、读写互斥、写读互斥。

8. 什么是锁的可重入性

在同一个线程中锁可以不断传递的,可以直接获取。

9. 什么是 CAS(自旋锁),它的优缺点

CAS
没有获取到锁的线程是不会阻塞的,通过循环控制一直不断的获取锁。

CAS: Compare and Swap,翻译成比较并交换。 执行函数 CAS(V,E,N)CAS 有 3 个操作数,内存值 V,旧的预期值 E,要修改的新值 N。当且仅当预期值E和内存值 V 相同时,将内存值 V 修改为 N,否则什么都不做。
(二)【Java精选面试题】JUC锁的架构原理(含答案)_第1张图片

  1. Cas 是通过硬件指令,保证原子性
  2. Java 是通过 unsafe jni 技术

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

基于 cas 实现锁机制原理
Cas 无锁机制原理:

  1. 定义一个锁的状态;
  2. 状态状态值=0 则表示没有线程获取到该锁;
  3. 状态状态值=1 则表示有线程已经持有该锁;

实现细节:
CAS 获取锁:
将该锁的状态从 0 改为 1-----能够修改成功 cas 成功则表示获取锁成功如果获取锁失败–修改失败,则不会阻塞而是通过循环(自旋来控制重试)
CAS 释放锁:
将该锁的状态从 1 改为 0 如果能够改成功 cas 成功则表示释放锁成功。

10. CAS 如何解决 ABA 的问题

Cas 主要检查 内存值 V 与旧的预值值=E 是否一致,如果一致的情况下,则修改。这时候会存在 ABA 的问题:
如果将原来的值 A,改为了 B,B 又改为了 A 发现没有发生变化,实际上已经发生了变化,所以存在 ABA 问题。
解决办法:通过版本号码,对每个变量更新的版本号码做+1

/**
* 演示 aba 的问题
* (1)第一个参数 expectedReference:表示预期值。
* (2)第二个参数 newReference:表示要更新的值。
* (3)第三个参数 expectedStamp:表示预期的时间戳。
* (4)第四个参数 newStamp:表示要更新的时间戳。
*/
public class AtomicMarkableReferenceTest {
	// 注意:如果引用类型是 Long、Integer、Short、Byte、Character 一定一定要注意值的缓存区间!
	// 比如 Long、Integer、Short、Byte 缓存区间是在-128~127,会直接存在常量池中,而不在这个区间内对象的值则会每次都 new 一个对象,那么即使两个对象的值相同,CAS 方法都会返回 false // 先声明初始值,修改后的值和临时的值是为了保证使用 CAS 方法不会因为对象不一样而返回 false private static final Integer INIT_NUM = 1000;
	private static final Integer UPDATE_NUM = 100;
	private static final Integer TEM_NUM = 200;
	private static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(INIT_NUM, 1);
	public static void main(String[] args) { 
		new Thread(() -> {
			Integer value = (Integer) atomicStampedReference.getReference();
			int stamp = atomicStampedReference.getStamp();
			System.out.println(Thread.currentThread().getName() + " : 当前值为:" + value + " 版本号为:" + stamp);
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) { 
				e.printStackTrace();
			}
			// value 旧值 内存中的值 UPDATE_NUM 修改的值
			if (atomicStampedReference.compareAndSet(value, UPDATE_NUM, 0, stamp + 1)) {
				System.out.println(Thread.currentThread().getName() + " : 当前值为:" + atomicStampedReference.getReference() + " 版本号为:" + atomicStampedReference.getStamp());
			} else {
				System.out.println("版本号不同,更新失败!");
			}
		}, "线程 A").start();
	}
}

11. 利用原子类手写 CAS 无锁

/**
* 利用 cas 手写 锁
*/
public class AtomicTryLock {
	private AtomicLong atomicLong=new AtomicLong(0); 
	private Thread lockCurrentThread; 
	/**
	* 1 表示锁已经被获取 0 表示锁没有获取 利用 cas 将 0 改为 1 成功则表示获取锁
	* @return
	*/
	public boolean lock(){ 
		return atomicLong.compareAndSet(0, 1); 
	}
	
	public boolean unlock(){
		if(lockCurrentThread!=Thread.currentThread()){
			return false; 
		}
		return atomicLong.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.lockCurrentThread=Thread.currentThread(); 
					System.out.println(Thread.currentThread().getName() + ",获取锁成功~"); 
				} else {
					System.out.println(Thread.currentThread().getName() + ",获取锁失败~"); 
				}
			} catch (Exception e) {
			} finally {
				if(atomicTryLock!=null){ 
					atomicTryLock.unlock();
				}
			}
		}).start()); 
	}
}

你可能感兴趣的:(Java面试宝典,java,面试题,精选)