Java开发常见面试题详解(LockSupport,AQS,Spring循环依赖,Redis)

本文详细探讨了Java开发面试中常见的几个重要概念,包括LockSupport的工作原理及其与wait/notify、await/signal的区别,AQS(AbstractQueuedSynchronizer)的理论基础和应用,Spring框架中的循环依赖问题,以及Redis的使用场景和分布式锁的实现。通过对锁机制、线程阻塞工具和数据存储的深入理解,揭示了Java并发编程和分布式系统的底层机制。
摘要由CSDN通过智能技术生成

总览

问题 详解
String.intern()的作用 link
LeetCode的Two Sum题 link
什么是可重入锁? link
谈谈LockSupport link
谈谈AQS link
Spring的AOP顺序 link
Spring的循环依赖 link
Redis各基本类型应用场景 link
Redis分布式锁 link
Redis内存配置及内存淘汰策略 link
实现LRU算法 link
- - -
总览 00_前言闲聊和课程说明 01_字符串常量Java内部加载-上
02_字符串常量Java内部加载-下 03_闲聊力扣算法第一题 04_TwoSum暴力解法
05_TwoSum优化解法 06_闲聊AQS面试 07_可重入锁理论
08_可重入锁的代码验证-上 09_可重入锁的代码验证-下 10_LockSupport是什么
11_waitNotify限制 12_awaitSignal限制 13_LockSupport方法介绍
14_LockSupport案例解析 15_AQS理论初步 16_AQS能干嘛
17_AQS源码体系-上 18_AQS源码体系-下 19_AQS源码深度解读01
20_AQS源码深度解读02 21_AQS源码深度解读03 22_AQS源码深度解读04
23_AQS源码深度解读05 24_AQS源码深度解读06 25_AQS源码深度解读07
26_AQS小总结 27_Aop的题目说明要求 28_spring4下的aop测试案例
29_spring4下的aop测试结果 30_spring5下的aop测试 31_spring循环依赖题目说明
32_spring循环依赖纯java代码验证案例 33_spring循环依赖bug演示 34_spring循环依赖debug前置知识
35_spring循环依赖debug源码01 36_spring循环依赖debug源码02 37_spring循环依赖debug源码03
38_spring循环依赖debug源码04 39_spring循环依赖小总结 40_redis版本升级说明
41_redis两个小细节说明 42_string类型使用场景 43_hash类型使用场景
44_list类型使用场景 45_set类型使用场景 46_zset类型使用场景
47_redis分布式锁前情说明 48_boot整合redis搭建超卖程序-上 49_boot整合redis搭建超卖程序-下
50_redis分布式锁01 51_redis分布式锁02 52_redis分布式锁03
53_redis分布式锁04 54_redis分布式锁05 55_redis分布式锁06
56_redis分布式锁07 57_redis分布式锁08 58_redis分布式锁09
59_redis分布式锁10 60_redis分布式锁总结回顾 61_redis内存调整默认查看
62_redis打满内存OOM 63_redis内存淘汰策略 64_lru算法简介
65_lru的思想 66_巧用LinkedHashMap完成lru算法 67_手写LRU-上
68_手写LRU-下 69_总结闲聊 -

00_前言闲聊和课程说明

教学视频

暖身面试题

  • Redis默认端口是多少?- 6379 link
  • Spring官网地址 - https://spring.io
  • 经典计算机图书看过吗?

01_字符串常量Java内部加载-上

Returns a canonical representation for the string object.

A pool of strings, initially empty, is maintained privately by the class String.

When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.

It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true.

All literal strings and string-valued constant expressions are interned. String literals are defined in section 3.10.5 of the The Java™ Language Specification.

public native String intern();

由于运行时常量池是方法区的一部分,所以这两个区域的溢出测试可以放到一起进行。HotSpot从JDK 7开始逐步“去永久代”的计划,并在JDK 8中完全使用元空间来代替永久代的背景故事,在此我们就以测试代码来观察一下,使用"永久代"还是“元空间"来实现方法区,对程序有什么实际的影响。

String:intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。在JDK 6或更早之前的HotSpot虚拟机中,常量池都是分配在永久代中,我们可以通过-XX:PermSize和-XX:MaxPermSize限制永久代的大小,即可间接限制其中常量池的容量。

public class StringInternDemo {
   

	public static void main(String[] args) {
   
		
		String str1 = new StringBuilder("58").append("tongcheng").toString();
		System.out.println(str1);
		System.out.println(str1.intern());
		System.out.println(str1 == str1.intern());

		System.out.println();
		
		String str2 = new StringBuilder("ja").append("va").toString();
		System.out.println(str2);
		System.out.println(str2.intern());
		System.out.println(str2 == str2.intern());
		
	}

}

输出结果:

58tongcheng
58tongcheng
true

java
java
false

02_字符串常量Java内部加载-下

按照代码结果,Java字符串答案为false必然是两个不同的java,那另外一个java字符串如何加载进来的?

有一个初始化的Java字符串(JDK出娘胎自带的),在加载sun.misc.Version这个类的时候进入常量池。

递推步骤

  • System代码解析 System -> initializeSystemClass() -> Version
package java.lang;

public final class System {
   

    /* register the natives via the static initializer.
     *
     * VM will invoke the initializeSystemClass method to complete
     * the initialization for this class separated from clinit.
     * Note that to use properties set by the VM, see the constraints
     * described in the initializeSystemClass method.
     */
    private static native void registerNatives();
    static {
   
        registerNatives();
    }
    
    //本地方法registerNatives()将会调用initializeSystemClass()
    private static void initializeSystemClass() {
   

		...
        
        sun.misc.Version.init();

		...
    }
    ...
}
package sun.misc;

//反编译后的代码
public class Version {
   
	private static final String launcher_name = "java";
	...
}
  • 类加载器和rt.jar - 根加载器提前部署加载rt.jar

  • OpenJDK8源码

    • http://openjdk.java.net/
    • openjdk8\jdk\src\share\classes\sun\misc
  • 考查点 - intern()方法,判断true/false?- 《深入理解java虚拟机》书原题是否读过经典JVM书籍

这段代码在JDK 6中运行,会得到两个false,而在JDK 7中运行,会得到一个true和一个false。产生差异的原因是,在JDK 6中,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,返回的也是永久代里面这个字符串实例的引用,而由StringBuilder创建的字符串对象实例在Java堆上,所以必然不可能是同一个引用,结果将返回false。

而JDK 7(以及部分其他虚拟机,例如JRockit)的intern()方法实现就不需要再拷贝字符串的实例到永久代了,既然字符串常量池已经移到Java堆中,那只需要在常量池里记录一下首次出现的实例引用即可,因此intern()返回的引用和由StringBuilder创建的那个字符串实例就是同一个。而对str2比较返回false,这是因为“java”这个字符串在执行StringBuilder.toString()之前就已经出现过了,字符串常量池中已经有它的引用,不符合intern()方法要求“首次遇到"”的原则,“计算机软件"这个字符串则是首次出现的,因此结果返回true。

sun.misc.Version类会在JDK类库的初始化过程中被加载并初始化,而在初始化时它需要对静态常量字段根据指定的常量值(ConstantValue〉做默认初始化,此时被sun.misc.Version.launcher静态常量字段所引用的"java"字符串字面量就被intern到HotSpot VM的字符串常量池——StringTable里了。

03_闲聊力扣算法第一题

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。

你可以按任意顺序返回答案。

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

示例 2:

输入:nums = [3,2,4], target = 6
输出:[1,2]

示例 3:

输入:nums = [3,3], target = 6
输出:[0,1]

提示:

  • 2 <= nums.length <= 103
  • -109 <= nums[i] <= 109
  • -109 <= target <= 109
  • 只会存在一个有效答案

link

04_TwoSum暴力解法

public static int[] twoSum1(int[] nums,int target){
   
	for (int i = 0; i < nums.length; i++) {
   
		for (int j = i + 1; j < nums.length; j++) {
   
			if(target - nums[i] == nums[j])
				return new int[]{
   i,j};
	return null;
}

通过双重循环遍历数组中所有元素的两两组合。

05_TwoSum优化解法

我的Blog

public int[] twoSum(int[] nums, int target) {
   
    Map<Integer, Integer> map = new HashMap<>();

    for (int i = 0; i < nums.length; i++) {
   
        int diff = target - nums[i];
        if (map.containsKey(diff)) {
   
            return new int[] {
    map.get(diff), i };
        }
        map.put(nums[i], i);
    }
    return new int[] {
    -1, -1 };
}

哈希(更优解法)

考点热衷考算法

06_闲聊AQS面试

07_可重入锁理论

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

Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

将字分开解释:

  • 可:可以

  • 重:再次

  • 入:进入

  • 锁:同步锁

  • 进入什么? - 进入同步域(即同步代码块/方法或显示锁锁定的代码)

一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。

自己可以获取自己的内部锁。

可重入锁的种类:

  • 隐式锁(即synchronized关键字使用的锁)默认是可重入锁。

    • 同步块
    • 同步方法
  • Synchronized的重入的实现机理。

  • 显式锁(即Lock)也有ReentrantLock这样的可重入锁。

08_可重入锁的代码验证-上

可重入锁的种类:

  • 隐式锁(即synchronized关键字使用的锁)默认是可重入锁。
    • 同步块
    • 同步方法
public class ReentrantLockDemo2 {
   
    Object object = new Object();

    public void sychronizedMethod(){
   
       new Thread(()->{
   
           synchronized (object){
   
               System.out.println(Thread.currentThread().getName()+"\t"+"外层....");
               synchronized (object){
   
                   System.out.println(Thread.currentThread().getName()+"\t"+"中层....");
                   synchronized (object){
   
                       System.out.println(Thread.currentThread().getName()+"\t"+"内层....");
                   }
               }
           }
       },"Thread A").start();
    }

    public static void main(String[] args) {
   
        new ReentrantLockDemo2().sychronizedMethod();
    }
    
}

输出结果:

Thread A	外层....
Thread A	中层....
Thread A	内层....

public class ReentrantLockDemo2 {
   

    public static void main(String[] args) {
   
        new ReentrantLockDemo2().m1();
        
    }
    
    public synchronized void m1() {
   
    	System.out.println("===外");
    	m2();
    }
    
    public synchronized void m2() {
   
    	System.out.println("===中");
    	m3();
    }
    
    public synchronized void m3() {
   
    	System.out.println("===内");
    	
    }
}

输出结果:

===外
===中
===内

09_可重入锁的代码验证-下

  • Synchronized的重入的实现机理

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。

当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。

在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。

当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

  • 显式锁(即Lock)也有ReentrantLock这样的可重入锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Phone2 implements Runnable{
   

    Lock lock = new ReentrantLock();

    /**
     * set进去的时候,就加锁,调用set方法的时候,能否访问另外一个加锁的set方法
     */
    public void getLock() {
   
        lock.lock();
        try {
   
            System.out.println(Thread.currentThread().getName() + "\t get Lock");
            setLock();
        } finally {
   
            lock.unlock();
        }
    }

    public void setLock() {
   
        lock.lock();
        try {
   
            System.out.println(Thread.currentThread().getName() + "\t set Lock");
        } finally {
   
            lock.unlock();
        }
    }

    @Override
    public void run() {
   
        getLock();
    }
}

public class ReentrantLockDemo {
   


    public static void main(String[] args) {
   
        Phone2 phone = new Phone2();

        /**
         * 因为Phone实现了Runnable接口
         */
        Thread t3 = new Thread(phone, "t3");
        Thread t4 = new Thread(phone, "t4");
        t3.start();
        t4.start();
    }
}

输出结果:

t3	 get Lock
t3	 set Lock
t4	 get Lock
t4	 set Lock

10_LockSupport是什么

LockSupport Java doc

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。

LockSupport中的park()和 unpark()的作用分别是阻塞线程和解除阻塞线程。

总之,比wait/notify,await/signal更强。

3种让线程等待和唤醒的方法

  • 方式1:使用Object中的wait()方法让线程等待,使用object中的notify()方法唤醒线程
  • 方式2:使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
  • 方式3:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

11_waitNotify限制

Object类中的wait和notify方法实现线程等待和唤醒

public class WaitNotifyDemo {
   

	static Object lock = new Object();
	
	public static void main(String[] args) {
   
		new Thread(()->{
   
			synchronized (lock) {
   
				System.out.println(Thread.currentThread().getName()+" come in.");
				try {
   
					lock.wait();
				} catch (Exception e) {
   
					e.printStackTrace();
				}
			}
			System.out.println(Thread.currentThread().getName()+" 换醒.");
		}, "Thread A").start();
		
		new Thread(()->{
   
			synchronized (lock) {
   
				lock.notify();
				System.out.println(Thread.currentThread().getName()+" 通知.");
			}
		}, "Thread B").start();
	}
}

wait和notify方法必须要在同步块或者方法里面且成对出现使用,否则会抛出java.lang.IllegalMonitorStateException。

调用顺序要先wait后notify才OK。

12_awaitSignal限制

Condition接口中的await后signal方法实现线程的等待和唤醒,与Object类中的wait和notify方法实现线程等待和唤醒类似。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionAwaitSignalDemo {
   
		
	public static void main(String[] args) {
   
		
		ReentrantLock lock = new ReentrantLock();
		Condition condition = lock.newCondition();
		
		new Thread(()->{
   
			
			try {
   
				System.out.println(Thread.currentThread().getName()+" come in.");
				lock.lock();
				condition.await();				
			} catch (InterruptedException e) {
   
				e.printStackTrace();
			} finally {
   
				lock.unlock();
			}
			
			System.out.println(Thread.currentThread().getName()+" 换醒.");
		},"Thread A").start();
		
		new Thread(()->{
   
			try {
   
				lock.lock();
				condition.signal();
				System.out.println(Thread.currentThread().getName()+" 通知.");
			}finally {
   
				lock.unlock();
			}
		},"Thread B").start();
	}
	
}

输出结果:

Thread A come in.
Thread B 通知.
Thread A 换醒.

await和signal方法必须要在同步块或者方法里面且成对出现使用,否则会抛出java.lang.IllegalMonitorStateException。

调用顺序要先await后signal才OK。

13_LockSupport方法介绍

传统的synchronized和Lock实现等待唤醒通知的约束

  • 线程先要获得并持有锁,必须在锁块(synchronized或lock)中
  • 必须要先等待后唤醒,线程才能够被唤醒

LockSupport类中的park等待和unpark唤醒

Basic thread blocking primitives for creating locks and other synchronization classes.

This class associates, with each thread that uses it, a permit (in the sense of the Semaphore class). A call to park will return immediately if the permit is available, consuming it in the process; otherwise it may block. A call to unpark makes the permit available, if it was not already available. (Unlike with Semaphores though, permits do not accumulate. There is at most one.) link

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。

LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和零,默认是零。

可以把许可看成是一种(0.1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。

通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作

park()/park(Object blocker) - 阻塞当前线程阻塞传入的具体线程

public class LockSupport {
   

    ...
    
    public static void park() {
   
        UNSAFE.park(false, 0L);
    }

    public static void park(Object blocker) {
   
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }
    
    ...
    
}

permit默认是0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为0并返回。

unpark(Thread thread) - 唤醒处于阻塞状态的指定线程

public class LockSupport {
   
 
    ...
    
    public static void unpark(Thread thread) {
   
        if (thread != null)
            UNSAFE.unpark(thread);
    }
    
    ...

}

调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,pemit值还是1)会自动唤醒thead线程,即之前阻塞中的LockSupport.park()方法会立即返回。

14_LockSupport案例解析

public class LockSupportDemo {
   

	public static void main(String[] args) {
   
		Thread a = new Thread(()->{
   
//			try {
   
//				TimeUnit.SECONDS.sleep(2);
//			} catch (InterruptedException e) {
   
//				e.printStackTrace();
//			}
			System.out.println(Thread.currentThread().getName() + " come in. " + System.currentTimeMillis());
			LockSupport.park();
			System.out.println(Thread.currentThread().getName() + " 换醒. " + System.currentTimeMillis());
		}, "Thread A");
		a.start();
		
		Thread b = new Thread(()->{
   
			try {
   
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
   
				e.printStackTrace();
			}
			LockSupport.unpark(a);
			System.out.println(Thread.currentThread().getName()+" 通知.");
		}, "Thread B");
		b.start();
	}
	
}

输出结果:

Thread A come in.
Thread B 通知.
Thread A 换醒.

正常 + 无锁块要求。

先前错误的先唤醒后等待顺序,LockSupport可无视这顺序。

重点说明

LockSupport是用来创建锁和共他同步类的基本线程阻塞原语。

LockSuport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻寨之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。

LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程

LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,

调用一次unpark就加1变成1,

调用一次park会消费permit,也就是将1变成0,同时park立即返回。

如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累凭证

形象的理解

线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。

当调用park方法时

  • 如果有凭证,则会直接消耗掉这个凭证然后正常退出。

  • 如果无凭证,就必须阻塞等待凭证可用。

而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无放。

面试题

为什么可以先唤醒线程后阻塞线程?

因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。

为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

因为凭证的数量最多为1(不能累加),连续调用两次 unpark和调用一次 unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行。

15_AQS理论初步

是什么?AbstractQueuedSynchronizer 抽象队列同步器。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
   
    
    ...
    
}

是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态。

CLH:Craig、Landin and Hagersten队列,是一个单向链表,AQS中的队列是CLH变体的虚拟双向队列FIFO。

16_AQS能干嘛

AQS为什么是JUC内容中最重要的基石?

和AQS有关的

进一步理解锁和同步器的关系

  • 锁,面向锁的使用者 - 定义了程序员和锁交互的使用层APl,隐藏了实现细节,你调用即可

  • 同步器,面向锁的实现者 - 比如Java并发大神DougLee,提出统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等。

能干嘛?

加锁会导致阻塞 - 有阻塞就需要排队,实现排队必然需要有某种形式的队列来进行管理

解释说明

抢到资源的线程直接使用处理业务逻辑,抢不到资源的必然涉及一种排队等候机制。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。

既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?

如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node),通过CAS、自旋以及LockSupportpark)的方式,维护state变量的状态,使并发达到同步的控制效果。

17_AQS源码体系-上

Provides a framework for implementing blocking locks and related synchronizers (semaphores, events, etc) that rely on first-in-first-out (FIFO) wait queues. This class is designed to be a useful basis for most kinds of synchronizers that rely on a single atomic int value to represent state. Subclasses must define the protected methods that change this state, and which define what that state means in terms of this object being acquired or released. Given these, the other methods in this class carry out all queuing and blocking mechanics. Subclasses can maintain other state fields, but only the atomically updated int value manipulated using methods getState(), setState(int) and compareAndSetState(int, int) is tracked with respect to synchronization.

AbstractQueuedSynchronizer (Java Platform SE 8 )

提供一个框架来实现阻塞锁和依赖先进先出(FIFO)等待队列的相关同步器(信号量、事件等)。此类被设计为大多数类型的同步器的有用基础,这些同步器依赖于单个原子“int”值来表示状态。子类必须定义更改此状态的受保护方法,以及定义此状态在获取或释放此对象方面的含义。给定这些,这个类中的其他方法执行所有排队和阻塞机制。子类可以维护其他状态字段,但是只有使用方法getState()setState(int)compareAndSetState(int,int)操作的原子更新的’int’值在同步方面被跟踪。

有阻塞就需要排队,实现排队必然需要队列

AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFo队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node,节点来实现锁的分配,通过CAS完成对State值的修改。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
   

    private static final long serialVersionUID = 7373984972572414691L;

     * Creates a new {
   @code AbstractQueuedSynchronizer} instance
    protected AbstractQueuedSynchronizer() {
    }

     * Wait queue node class.
    static final class Node {
   

     * Head of the wait queue, lazily initialized.  Except for
    private transient volatile Node head;

     * Tail of the wait queue, lazily initialized.  Modified only via
    private transient volatile Node tail;

     * The synchronization state.
    private volatile int state;

     * Returns the current value of synchronization state.
    protected final int getState() {
   

     * Sets the value of synchronization state.
    protected final void setState(int newState) {
   

     * Atomically sets synchronization state to the given updated
    protected final boolean compareAndSetState(int expect, int update) {
   
         
    ...
}         

18_AQS源码体系-下

AQS自身

AQS的int变量 - AQS的同步状态state成员变量

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
   

    ...

     * The synchronization state.
    private volatile int state;
    
    ...
}

state成员变量相当于银行办理业务的受理窗口状态。

  • 零就是没人,自由状态可以办理

  • 大于等于1,有人占用窗口,等着去

AQS的CLH队列

  • CLH队列(三个大牛的名字组成),为一个双向队列

  • 银行候客区的等待顾客

The wait queue is a variant of a “CLH” (Craig, Landin, and Hagersten) lock queue. CLH locks are normally used forspinlocks. We instead use them for blocking synchronizers, butuse the same basic tactic of holding some of the controlinformation about a thread in the predecessor of its node. A"status" field in each node keeps track of whether a threadshould block. A node is signalled when its predecessorreleases. Each node of the queue otherwise serves as aspecific-notification-style monitor holding a single waiting thread. The status field does NOT control whether threads aregranted locks etc though. A thread may try to acquire if it isfirst in the queue. But being first does not guarantee success;it only gives the right to contend. So the currently releasedcontender thread may need to rewait.

To enqueue into a CLH lock, you atomically splice it in as new tail. To dequeue, you just set the head field. 本段文字出自AbstractQueuedSynchronizer内部类Node源码注释

等待队列是“CLH”(Craig、Landin和Hagersten)锁队列的变体。CLH锁通常用于旋转锁。相反,我们使用它们来阻止同步器,但是使用相同的基本策略,即在其节点的前一个线程中保存一些关于该线程的控制信息。每个节点中的“status”字段跟踪线程是否应该阻塞。当一个节点的前一个节点释放时,它会发出信号。否则,队列的每个节点都充当一个特定的通知样式监视器,其中包含一个等待线程。状态字段并不控制线程是否被授予锁等。如果线程是队列中的第一个线程,它可能会尝试获取。但是,第一并不能保证成功,它只会给人争取的权利。因此,当前发布的内容线程可能需要重新等待。

要排队进入CLH锁,您可以将其作为新的尾部进行原子拼接。要出列,只需设置head字段。

小总结

  • 有阻塞就需要排队,实现排队必然需要队列

  • state变量+CLH变种的双端队列

AbstractQueuedSynchronizer内部类Node源码

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
   

    ...

     * Creates a new {
   @code AbstractQueuedSynchronizer} instance
    protected AbstractQueuedSynchronizer() {
    }

     * Wait queue node class.
    static final class Node {
   
        //表示线程以共享的模式等待锁
        /** Marker to indicate a node is waiting in shared mode */
        static final Node SHARED = new Node();
        
        //表示线程正在以独占的方式等待锁
        /** Marker to indicate a node is waiting in exclusive mode */
        static final Node EXCLUSIVE = null;

        //线程被取消了
        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;

        //后继线程需要唤醒
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        
        //等待condition唤醒
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        
        //共享式同步状态获取将会无条件地传播下去
        * waitStatus value to indicate the next acquireShared should     
        static final int PROPAGATE = -3;

        //当前节点在队列中的状态(重点)
        //说人话:
        //等候区其它顾客(其它线程)的等待状态
        //队列中每个排队的个体就是一个Node
        //初始为0,状态上面的几种
         * Status field, taking on only the values:
        volatile int waitStatus;

        //前驱节点(重点)
         * Link to predecessor node that current node/thread relies on
        volatile Node prev;

        //后继节点(重点)
         * Link to the successor node that the current node/thread
        volatile Node next;

        //表示处于该节点的线程
         * The thread that enqueued this node.  Initialized on
        volatile Thread thread;

        //指向下一个处于CONDITION状态的节点
         * Link to next node waiting on condition, or the special
        Node nextWaiter;

         * Returns true if node is waiting in shared mode.
        final boolean isShared() {
   

        //返回前驱节点,没有的话抛出npe
         * Returns previous node, or throws NullPointerException if null.
        final Node predecessor() throws NullPointerException {
   

        Node() {
       // Used to establish initial head or SHARED marker

        Node(Thread thread, Node mode) {
        // Used by addWaiter

        Node(Thread thread, int waitStatus) {
    // Used by Condition
    }
	...
}

AQS同步队列的基本结构

19_AQS源码深度解读01

从ReentrantLock开始解读AQS

Lock接口的实现类,基本都是通过聚合了一个队列同步器的子类完成线程访问控制的。

 * A reentrant mutual exclusion {
   @link Lock} with the same basic
public class ReentrantLock implements Lock, java.io.Serializable {
   
    private static final long serialVersionUID = 7373984872572414699L;
    /** Synchronizer providing all implementation mechanics */
    private final Sync sync;

     * Base of synchronization control for this lock. Subclassed
    abstract static class Sync extends AbstractQueuedSynchronizer {
   

     * Sync object for non-fair locks
    static final class NonfairSync extends Sync {
   

     * Sync object for fair locks
    static final class FairSync extends Sync {
   

     * Creates an instance of {
   @code ReentrantLock}.
    public ReentrantLock() {
   
        sync = new NonfairSync();
    }

     * Creates an instance of {
   @code ReentrantLock} with the
    public ReentrantLock(boolean fair) {
   
        sync = fair ? new FairSync() : new NonfairSync();
    }

     * Acquires the lock.
    public void lock() {
   
        sync.lock();//<------------------------注意,我们从这里入手
    }
        
    * Attempts to release this lock.
    public void unlock() {
   
        sync.release(1);
    }
    ...
}

从最简单的lock方法开始看看公平和非公平,先浏览下AbstractQueuedSynchronizer,FairSync,NonfairSync类的源码。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
   

	...

     * Acquires in exclusive mode, ignoring interrupts.  Implemented
    public final void acquire(int arg) {
   //公平锁或非公平锁都会调用这方法
        if (!tryAcquire(arg) &&//0.
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//1. 2.
            selfInterrupt();//3.
    }
    
    //0.
    * Attempts to acquire in exclusive mode. This method should query
    protected boolean tryAcquire(int arg) {
   //取决于公平锁或非公平锁的实现
        throw new UnsupportedOperationException();
    }
	
    
    //1.
    * Acquires in exclusive uninterruptible mode for thread already in
    final boolean acquireQueued(final Node node, int arg) {
   
        boolean failed = true;
        try {
   
            boolean interrupted = false;
            for (;;) {
   
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
   
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
   
            if (failed)
                cancelAcquire(node);
        }
    }
    
    //2.
    * Creates and enqueues node for current thread and given mode.
    private Node addWaiter(Node mode) {
   
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
   
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
   
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
    
    //3.
    static void selfInterrupt() {
   
        Thread.currentThread().interrupt();
    }
    
    //这个方法将会被公平锁的tryAcquire()调用
    * Queries whether any threads have been waiting to acquire longer
    public final boolean hasQueuedPredecessors() {
   
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return
  • 136
    点赞
  • 770
    收藏
    觉得还不错? 一键收藏
  • 24
    评论
评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值