AQS source code in-depth analysis of conditional queue - do you know how to implement blocking queue in Java?

Diao Ye 2020-11-09 17:44:59
aqs source code in-depth depth


This article is based on JDK-8u261 Source code analysis


1 brief introduction

img

because CLH Threads in the queue , What thread gets the lock , What threads are queued , What thread releases the lock , These are beyond our control . So the emergence of conditional queues provides us with the initiative to 、 Only when the specified conditions are met can the thread block and wake up . For conditional queues, we first need to explain some concepts : The condition queue is AQS In addition to CLH A queue other than a queue , For every one created Condition It's actually creating a conditional queue , And every time it's called await The method is actually to join the conditional queue , Every call signal The method is actually to queue up in the conditional queue . Unlike CLH There are multiple states of nodes on the queue , There is only one state of the node on the conditional queue :CONDITION. So if the node on the conditional queue is no longer CONDITION In the state of , It means that this node should be out of the team . It should be noted that , Conditional queues can only run in exclusive mode .

In general, when using conditional queue as blocking queue, two conditional queues will be created :notFull and notEmpty.notFull When the condition queue is full ,put The method will be in a wait state , Until the queue is not full ;notEmpty When the condition queue is empty ,take The method will be in a wait state , Until the queue has data .

and notFull.signal Methods and notEmpty.signal Method will move the node on the conditional queue to CLH In line ( Transfer only one at a time ). in other words , There is a node that is transferred from the conditional queue to CLH The situation in the queue . It also means , Lock resource contention will not occur on conditional queues , All of the lock competition happens in CLH On the queue .

Some other conditional queues and CLH The differences between queues are as follows :

  • Conditional queues use nextWaiter Pointer to the next node , Is a one-way linked list structure , differ CLH Double linked list structure of queue ;
  • Conditional queues use firstWaiter and lastWaiter To point to the head and tail , differ CLH Queued head and tail;
  • The first node in the conditional queue will not look like CLH The lines are the same , It's a special empty node ;
  • differ CLH There will be a lot of CAS Operation to control concurrency , The premise of conditional queue entering the queue is that the exclusive lock resource has been obtained , So many places don't need to consider concurrency .

The following is the specific source analysis . Conditional queue with ArrayBlockingQueue For example :


2 Constructors

 1 /**
2 * ArrayBlockingQueue:
3 */
4 public ArrayBlockingQueue(int capacity) {
5 this(capacity, false);
6}
7
8 public ArrayBlockingQueue(int capacity, boolean fair) {
9 if (capacity <= 0)
10 throw new IllegalArgumentException();
11 // An array of actual data
12 this.items = new Object[capacity];
13 // Exclusive lock use ReentrantLock To achieve (fair Is it fair lock or unfair lock , Default to unfair lock )
14 lock = new ReentrantLock(fair);
15 //notEmpty Condition queue
16 notEmpty = lock.newCondition();
17 //notFull Condition queue
18 notFull = lock.newCondition();
19 }

3 put Method

 1 /**
2 * ArrayBlockingQueue:
3 */
4 public void put(E e) throws InterruptedException {
5 // Non empty verification
6 checkNotNull(e);
7 final ReentrantLock lock = this.lock;
8 /*
9 Get exclusive lock resource , Response interrupt mode . In fact, modern code and lock There are ways Semaphore Of acquire The method is similar
10 Because this is the conditional queue , So the details of the method are no longer analyzed
11 */
12 lock.lockInterruptibly();
13 try {
14 while (count == items.length)
15 // If the array is full , It's just notFull A new node in the Chinese team , And block the current thread
16 notFull.await();
17 // Add array elements and wake up notEmpty
18 enqueue(e);
19 } finally {
20 // Release lock resource
21 lock.unlock();
22 }
23 }

4 await Method

If in put The array is full , Or in take The array is empty , Will call await Method to put the current node in the conditional queue :

 1 /**
2 * AbstractQueuedSynchronizer:
3 */
4 public final void await() throws InterruptedException {
5 // If the current thread is interrupted, an exception is thrown
6 if (Thread.interrupted())
7 throw new InterruptedException();
8 // Add the current node to the conditional queue
9 Node node = addConditionWaiter();
10 // Release the lock resource obtained before , Because the thread will be blocked later , So if you don't release it , Other threads will wait for the thread to wake up
11 int savedState = fullyRelease(node);
12 int interruptMode = 0;
13 // If the current node is not in CLH The queue is blocked , wait for unpark Wake up the
14 while (!isOnSyncQueue(node)) {
15 LockSupport.park(this);
16 /*
17 It may be normal to be awakened here signal The operation may have been interrupted . But in either case , Will insert the current node into CLH Queue tail ,
18 And exit the loop ( Be careful , It's awakened here except in the two cases above , There is also a false wake-up at the operating system level (spurious wakeup),
19 That is, the current thread will be awakened for no reason , So you need to use while To avoid this situation )
20 */
21 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
22 break;
23 }
24 // Go here to show that the current node has been inserted into CLH In line ( By signal Awakened or interrupted ). And then in CLH The operation of getting lock resource in the queue
25 if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
26 /*
27 <<<THROW_IE and REINTERRUPT For an explanation, see transferAfterCancelledWait Method >>>
28
29 The previous analysis if acquireQueued Method returns true, Indicates that the current thread has been interrupted
30 return true It means in acquireQueued Method will be interrupted again ( Be careful , This means that there are two code points that determine whether the thread is interrupted :
31 One is at the 15 Line code , The other is in acquireQueued Method inside ), If it hasn't been interrupted before , be interruptMode=0,
32 And in the acquireQueued Method in which the thread is interrupted and returned , This time will be interruptMode It is revised as REINTERRUPT that will do
33 As for why not amend it to THROW_IE Because in this case , The first 15 Line code has been called by signal The method wakes up normally ,
34 The node has been put in CLH In line . And the interruption at this time is signal After the operation , In the 25 Line code to grab lock resources occurred when
35 At this time, it doesn't matter whether you interrupt or not , So there's no need to throw InterruptedException
36 */
37 interruptMode = REINTERRUPT;
38 /*
39 This indicates that the current node has obtained the lock resource ( If you can't get it, you'll be blocked again acquireQueued In the method )
40 If interruptMode=REINTERRUPT Words , Indicates that you have called signal The method , In other words, the node has been removed from the conditional queue ,
41 nextWaiter The pointer must be empty , So in this case, there is no need to execute unlinkCancelledWaiters Methodical
42 And if the interruptMode=THROW_IE Words , Description has not been called before signal Method to remove the node from the conditional queue . At this point, you need to call
43 unlinkCancelledWaiters Method to remove this node ( Prior to transferAfterCancelledWait In the method
44 The state of the node has been changed to the initial state 0), By the way, all the others are not CONDITION State nodes are also removed . Be careful : If the current node is in the conditional queue
45 The last node , It's not going to be cleaned up . It's ok , Wait until the next time you add a node or call signal Methods will be cleaned up
46 */
47 if (node.nextWaiter != null)
48 unlinkCancelledWaiters();
49 // Processing interrupts according to different modes ( Normal mode doesn't need to deal with )
50 if (interruptMode != 0)
51 reportInterruptAfterWait(interruptMode);
52 }

5 addConditionWaiter Method

Logic to add a node to the conditional queue :

 1 /**
2 * AbstractQueuedSynchronizer:
3 */
4 private Node addConditionWaiter() {
5 Node t = lastWaiter;
6 /*
7 If the last node is not CONDITION state , Delete all not in the conditional queue CONDITION Nodes of state
8 As for why we only need to judge the state of the last node to know whether there is no in the whole queue CONDITION The node of , It will be explained later
9 */
10 if (t != null && t.waitStatus != Node.CONDITION) {
11 // Delete all not CONDITION Nodes of state
12 unlinkCancelledWaiters();
13 t = lastWaiter;
14 }
15 // Create a type of CONDITION The new node
16 Node node = new Node(Thread.currentThread(), Node.CONDITION);
17 if (t == null)
18 //t by null It means that the condition queue is empty at this time , Just point the head pointer to the new node
19 firstWaiter = node;
20 else
21 //t Not for null This means that there are nodes in the conditional queue , Add this new node directly to the end
22 t.nextWaiter = node;
23 // The tail pointer points to the new node , Finished adding nodes
24 lastWaiter = node;
25 /*
26 Be careful , It doesn't have to look like CLH Queue enq The method is the same , If the insertion fails, it spins until the insertion is successful
27 Because the exclusive lock has not been released yet
28 */
29 return node;
30 }
31
32 /**
33 * The first 12 Line code :
34 * Delete all not in the conditional queue CONDITION Nodes of state
35 */
36 private void unlinkCancelledWaiters() {
37 Node t = firstWaiter;
38 /*
39 In each of the following cycles ,trail It points from the beginning to the node of the loop , The last one is CONDITION Nodes of state
40 This is done because the middle of the queue is not CONDITION The node of , You need to keep the last one CONDITION Pointer to node ,
41 And then directly trail.nextWaiter = next You can disconnect
42 */
43 Node trail = null;
44 while (t != null) {
45 Node next = t.nextWaiter;
46 if (t.waitStatus != Node.CONDITION) {
47 t.nextWaiter = null;
48 if (trail == null)
49 firstWaiter = next;
50 else
51 trail.nextWaiter = next;
52 if (next == null)
53 lastWaiter = trail;
54 } else
55 trail = t;
56 t = next;
57 }
58 }

6 fullyRelease Method

Release lock resource , All lock resources including reentrant locks :

 1 /**
2 * AbstractQueuedSynchronizer:
3 */
4 final int fullyRelease(Node node) {
5 boolean failed = true;
6 try {
7 int savedState = getState();
8 /*
9 Release lock resource . Notice that this is to release all locks , Including reentrant locks with multiple locks , It will be released all at once . Because on the previous line
10 Code savedState All lock resources are stored , And here's how to release all these resources , This is the method name “fully” The meaning of
11 */
12 if (release(savedState)) {
13 failed = false;
14 return savedState;
15 } else {
16 /*
17 Throw exception if release fails , That is to say, it is not released clean , Maybe in the context of concurrency state The reason for the change ,
18 It could be something else . Note that if an exception is thrown here, it will go first 166 Line code
19 */
20 throw new IllegalMonitorStateException();
21 }
22 } finally {
23 /*
24 If the release of the lock fails , Set the node to CANCELLED state . One of the more subtle things is , Before addConditionWaiter Regulation in method 10 Line code ,
25 Determine whether there is no in the conditional queue CONDITION The nodes of , Just determine whether the state of the last node is CONDITION That's it
26 As a general rule , You need to traverse the entire queue to know . But every time a new node is added to the conditional queue, it is inserted at the end , And if the release of the lock fails ,
27 Will add this new 、 Set the new node at the end of the queue to CANCELLED state . And before CONDITION The nodes must be at the head of the team
28 Because if there are new nodes in the queue at this time , First in addConditionWaiter Regulation in method 12 Line code will not be all CONDITION The nodes of the
29 That is to say, in any case , If there's not CONDITION The node of , Then it must be at the end of the team , So just judge it
30 */
31 if (failed)
32 node.waitStatus = Node.CANCELLED;
33 }
34 }

7 isOnSyncQueue Method

Determine whether the node is in CLH In line , To judge when awakened signal Whether the method is completed . Of course , stay transferAfterCancelledWait This method will also be called in the method :

 1 /**
2 * AbstractQueuedSynchronizer:
3 * Determine whether the node is in CLH In line
4 */
5 final boolean isOnSyncQueue(Node node) {
6 /*
7 If the state of the current node is CONDITION Or nodes don't have prev The pointer (prev Pointer only in CLH The nodes in the queue have ,
8 Tail insertion guarantees prev The pointer must have ) Words , Just go back to false
9 */
10 if (node.waitStatus == Node.CONDITION || node.prev == null)
11 return false;
12 // If the current node has next The pointer (next Pointer only in CLH The nodes in the queue have , The nodes in the conditional queue are nextWaiter) Words , Just go back to true
13 if (node.next != null)
14 return true;
15 // If it can't be judged quickly , Only from CLH Traversing the queue , Judge one by one
16 return findNodeFromTail(node);
17 }
18
19 /**
20 * Traverse to determine whether the current node is in CLH In the queue
21 */
22 private boolean findNodeFromTail(Node node) {
23 Node t = tail;
24 for (; ; ) {
25 if (t == node)
26 return true;
27 if (t == null)
28 return false;
29 t = t.prev;
30 }
31 }

8 checkInterruptWhileWaiting Method

Determine the state that you belong to when you wake up (0 / THROW_IE / REINTERRUPT):

 1 /**
2 * AbstractQueuedSynchronizer:
3 * If the current thread has not been interrupted , Then return to 0
4 * If the current thread is not interrupted by signal too , Then return to THROW_IE
5 * If the current thread has been interrupted signal After that , Then return to REINTERRUPT
6 */
7 private int checkInterruptWhileWaiting(Node node) {
8 return Thread.interrupted() ?
9 (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
10 0;
11 }
12
13 /**
14 * This method is used to determine whether the current thread has been interrupted signal, In order to distinguish THROW_IE and REINTERRUPT. The basis of judgment is :
15 * If ever signal, Then the status of the current node is no longer CONDITION 了 , And in CLH The node can also be found in the queue . See transferForSignal Method
16 * <p>
17 * THROW_IE: Indicates that the thread interrupt has not been called signal Method , At this time, we put this node into CLH In the queue to grab resources ,
18 * Until you grab the lock resource , And then take this node from CLH Delete from both the queue and the conditional queue , Finally, throw out InterruptedException
19 * <p>
20 * REINTERRUPT: Indicates that... Has been called when a thread interrupt occurs signal The method , At this time, it doesn't really make sense to interrupt ,
21 * Because the node has been put into CLH In line . And in signal This node has been removed from the conditional queue in the method
22 * Now we put this node into CLH In the queue to grab resources , Until you grab the lock resource ( This node will be removed from the node when it grabs resources CLH Delete... From the queue ),
23 * Interrupt the current thread again , It doesn't throw InterruptedException
24 */
25 final boolean transferAfterCancelledWait(Node node) {
26 // Determine whether the current node state is CONDITION
27 if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
28 /*
29 If CAS Successful means that the current node is CONDITION state , This means interruptMode by THROW_IE
30 And then there will be CLH Line up , Then, the operation of robbing the lock resource is carried out
31 */
32 enq(node);
33 return true;
34 }
35 /*
36 If CAS If it fails, it means that the current node is not CONDITION Status quo , This indicates that... Has been called at this time signal The method ,
37 But because the lock resource has been released before ,signal Methods transferForSignal Method to change the node state to CONDITION
38 And put the node into CLH These two operations of the queue are not atomic operations , So there may be concurrency problems . That is to say, there may be a change of node state to CONDITION after ,
39 But not yet CLH Queue this point in time . The following code considers this scenario . At this time, you only need to constantly transfer the current thread resources ,
40 wait for signal Method to add the node CLH When the queue is finished
41 */
42 while (!isOnSyncQueue(node))
43 Thread.yield();
44 return false;
45 }

9 reportInterruptAfterWait Method

Interrupt wakes up the final processing :

 1 /**
2 * AbstractQueuedSynchronizer:
3 */
4 private void reportInterruptAfterWait(int interruptMode)
5 throws InterruptedException {
6 if (interruptMode == THROW_IE)
7 // If it is THROW_IE It's going to end up throwing out InterruptedException abnormal
8 throw new InterruptedException();
9 else if (interruptMode == REINTERRUPT)
10 // If it is REINTERRUPT It's just “ interrupt ” The current thread is just ( Just set the interrupt flag bit to true)
11 selfInterrupt();
12 }

10 enqueue Method

ArrayBlockingQueue The logic of joining the team :

 1 /**
2 * ArrayBlockingQueue:
3 */
4 private void enqueue(E x) {
5 final Object[] items = this.items;
6 // insert data
7 items[putIndex] = x;
8 //putIndex The next insertion is recorded . If putIndex It's the last one , Reset to 0, It means that the data may be covered
9 if (++putIndex == items.length)
10 putIndex = 0;
11 // The number in the current array +1
12 count++;
13 /*
14 If notEmpty If the condition queue is not empty , Wake up the notEmpty The first node in the conditional queue goes to CLH Queue up to grab resources
15 If notEmpty If there is no node in it , The array is not empty at this time .signal The method will not work , Because there's no blocking at this time take Threads
16 */
17 notEmpty.signal();
18 }

11 signal Method

See if you need to wake up the nodes in the condition queue , Wake up when you need to ( Transfer the node from the conditional queue to CLH In line ):

 1 /**
2 * AbstractQueuedSynchronizer:
3 */
4 public final void signal() {
5 // If the current thread is not the thread of locking , Throw an exception
6 if (!isHeldExclusively())
7 throw new IllegalMonitorStateException();
8 Node first = firstWaiter;
9 if (first != null)
10 // If notEmpty If there are nodes in the conditional queue , I'll inform you to CLH Queue up for resources
11 doSignal(first);
12 }
13
14 private void doSignal(Node first) {
15 do {
16 if ((firstWaiter = first.nextWaiter) == null)
17 // be equal to null This means that the condition queue is empty by this time of the loop , Then put the lastWaiter Also set to null
18 lastWaiter = null;
19 // To break off notEmpty Of the current node in the conditional queue nextWaiter The pointer , It is equivalent to eliminating the current node , wait for GC
20 first.nextWaiter = null;
21 } while (!transferForSignal(first) &&
22 // If the current node is no longer CONDITION In terms of state ( It means that the current node has failed ), Just select the next node and try to put it in CLH In line
23 (first = firstWaiter) != null);
24 }
25
26 /**
27 * take notEmpty The nodes in the conditional queue move from the conditional queue to CLH In the queue
28 * The first 21 Line code :
29 */
30 final boolean transferForSignal(Node node) {
31 /*
32 If notEmpty The node in the conditional queue is no longer CONDITION In state , Go straight back false,
33 Skip the node , It is equivalent to removing the node from the conditional queue
34 */
35 if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
36 return false;
37
38 // Go here to show that the state of the node has been changed to the initial state 0. Add it to CLH Queue tail , And return to the previous node
39 Node p = enq(node);
40 int ws = p.waitStatus;
41 /*
42 Let's review ,SIGNAL State indicates that the current node is blocked , The last node is SIGNAL.notEmpty In the conditional queue
43 The node is still in a blocked state , So move this node to CLH After the queue, you need to change the status of the previous node to SIGNAL
44 If CAS If the modification fails , It wakes up the thread where the node is located to compete for lock resources , It must end up missing ( Because the lock resource is
45 The current thread holds ), So in the acquireQueued Methods continue to be blocked , One of the nodes will be fixed again
46 Of SIGNAL state ( It must be modified successfully , If the modification is not successful , It will always be acquireQueued Method in the cycle to CAS modify )
47 Of course, if the previous node is CANCELLED In terms of state , To wake up the node, too . such acquireQueued There is a chance to get rid of
48 these CANCELLED node , It's equivalent to cleaning up
49 It should be mentioned that , It's where the awakening is blocked take Threads ( The array has been empty before , Now add a node
50 After the array is not empty , So you need to wake up a fetching thread that was blocked before . Suppose the awakened thread is a thread 2, Perform a wake-up action
51 It's threads 1). As mentioned earlier , Threads 2 Will enter the acquireQueued The method is blocked again . Until the thread 1 Go to the put Methods
52 The last step unlock It will be awakened again when unlocking ( It doesn't have to be this time to wake up , It's also possible to wake up other threads ( If say
53 Is the thread 3). But as long as the thread 3 Finally, execute unlock Method time , Will continue to wake up , It's equivalent to passing on the wake-up action
54 So thread 2 Eventually there will be a chance to be awakened ( When it becomes CLH When the first node in the queue ))
55 */
56 if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
57 LockSupport.unpark(node.thread);
58 return true;
59 }

12 take Method

ArrayBlockingQueue Of take Method :

 1 /**
2 * ArrayBlockingQueue:
3 */
4 public E take() throws InterruptedException {
5 final ReentrantLock lock = this.lock;
6 // Lock in response to interrupt mode
7 lock.lockInterruptibly();
8 try {
9 while (count == 0)
10 // If the array is empty , It's just notEmpty A new node in the Chinese team , And block the current thread
11 notEmpty.await();
12 // Delete array elements and wake up notFull
13 return dequeue();
14 } finally {
15 // Unlock
16 lock.unlock();
17 }
18 }
19
20 /**
21 * The first 13 Line code :
22 */
23 private E dequeue() {
24 final Object[] items = this.items;
25 // Record the old value and return it
26 @SuppressWarnings("unchecked")
27 E x = (E) items[takeIndex];
28 // Empty array elements
29 items[takeIndex] = null;
30 //takeIndex It's the location of the next pick . If takeIndex It's the last one , Reset to 0
31 if (++takeIndex == items.length)
32 takeIndex = 0;
33 // The number in the current array -1
34 count--;
35 //elementDequeued Method is called when data is removed from an array , In order to make sure Itrs Consistency of iterator and queue data
36 if (itrs != null)
37 itrs.elementDequeued();
38 /*
39 If notFull If the condition queue is not empty , Wake up the notFull The first node in the conditional queue goes to CLH Queue up to grab resources
40 If notFull If there is no node in it , The array is not full at this time .signal The method will not work , Because there's no blocking at this time put Threads
41 */
42 notFull.signal();
43 return x;
44 }

More content, please pay attention to WeChat official account. : Geek time

版权声明
本文为[Diao Ye]所创,转载请带上原文链接,感谢

  1. 【计算机网络 12(1),尚学堂马士兵Java视频教程
  2. 【程序猿历程,史上最全的Java面试题集锦在这里
  3. 【程序猿历程(1),Javaweb视频教程百度云
  4. Notes on MySQL 45 lectures (1-7)
  5. [computer network 12 (1), Shang Xuetang Ma soldier java video tutorial
  6. The most complete collection of Java interview questions in history is here
  7. [process of program ape (1), JavaWeb video tutorial, baidu cloud
  8. Notes on MySQL 45 lectures (1-7)
  9. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  10. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  11. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  12. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  13. 【递归,Java传智播客笔记
  14. [recursion, Java intelligence podcast notes
  15. [adhere to painting for 386 days] the beginning of spring of 24 solar terms
  16. K8S系列第八篇(Service、EndPoints以及高可用kubeadm部署)
  17. K8s Series Part 8 (service, endpoints and high availability kubeadm deployment)
  18. 【重识 HTML (3),350道Java面试真题分享
  19. 【重识 HTML (2),Java并发编程必会的多线程你竟然还不会
  20. 【重识 HTML (1),二本Java小菜鸟4面字节跳动被秒成渣渣
  21. [re recognize HTML (3) and share 350 real Java interview questions
  22. [re recognize HTML (2). Multithreading is a must for Java Concurrent Programming. How dare you not
  23. [re recognize HTML (1), two Java rookies' 4-sided bytes beat and become slag in seconds
  24. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  25. RPC 1: how to develop RPC framework from scratch
  26. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  27. RPC 1: how to develop RPC framework from scratch
  28. 一次性捋清楚吧,对乱糟糟的,Spring事务扩展机制
  29. 一文彻底弄懂如何选择抽象类还是接口,连续四年百度Java岗必问面试题
  30. Redis常用命令
  31. 一双拖鞋引发的血案,狂神说Java系列笔记
  32. 一、mysql基础安装
  33. 一位程序员的独白:尽管我一生坎坷,Java框架面试基础
  34. Clear it all at once. For the messy, spring transaction extension mechanism
  35. A thorough understanding of how to choose abstract classes or interfaces, baidu Java post must ask interview questions for four consecutive years
  36. Redis common commands
  37. A pair of slippers triggered the murder, crazy God said java series notes
  38. 1、 MySQL basic installation
  39. Monologue of a programmer: despite my ups and downs in my life, Java framework is the foundation of interview
  40. 【大厂面试】三面三问Spring循环依赖,请一定要把这篇看完(建议收藏)
  41. 一线互联网企业中,springboot入门项目
  42. 一篇文带你入门SSM框架Spring开发,帮你快速拿Offer
  43. 【面试资料】Java全集、微服务、大数据、数据结构与算法、机器学习知识最全总结,283页pdf
  44. 【leetcode刷题】24.数组中重复的数字——Java版
  45. 【leetcode刷题】23.对称二叉树——Java版
  46. 【leetcode刷题】22.二叉树的中序遍历——Java版
  47. 【leetcode刷题】21.三数之和——Java版
  48. 【leetcode刷题】20.最长回文子串——Java版
  49. 【leetcode刷题】19.回文链表——Java版
  50. 【leetcode刷题】18.反转链表——Java版
  51. 【leetcode刷题】17.相交链表——Java&python版
  52. 【leetcode刷题】16.环形链表——Java版
  53. 【leetcode刷题】15.汉明距离——Java版
  54. 【leetcode刷题】14.找到所有数组中消失的数字——Java版
  55. 【leetcode刷题】13.比特位计数——Java版
  56. oracle控制用户权限命令
  57. 三年Java开发,继阿里,鲁班二期Java架构师
  58. Oracle必须要启动的服务
  59. 万字长文!深入剖析HashMap,Java基础笔试题大全带答案
  60. 一问Kafka就心慌?我却凭着这份,图灵学院vip课程百度云