本文已参与「新人创作礼」活动,一起开启掘金创作之路。
目录
有很多人在及技术面试的时候经常会被各种刁钻问题给灵魂拷问到,怎样去实现一个数据结构? 因为我们平时用java现成的数据结构屡试不爽,这个时候聪明的小脑袋可能会卡壳~
由于栈是一个先进后出的数据结构,我们要实现它得从入栈和出栈两个方面重点入手。
一、使用单链表结构实现
主要思想: 入栈采用尾插法,出栈直接取最后一个节点,然后将最后一个节点从链表中移除即可。
如图:
package collection;
/**
* @Desc:
* @Author: bingbing
* @Date: 2022/4/20 0020 17:37
*/
public class Entry<T> {
Entry<T> next;
T data;
public Entry(Entry<T> next, T data) {
this.next = next;
this.data = data;
}
public T getData() {
return data;
}
}
MyUnSafeStack:
package collection;
import cn.hutool.core.thread.ExecutorBuilder;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Desc: 非线程安全
* @Author: bingbing
* @Date: 2022/4/20 0020 17:37
*/
public class MyUnSafeStack<T> {
private AtomicInteger size = new AtomicInteger(0);
/**
* 采用尾插法
*/
private Entry<T> lastNode;
public int size() {
return size.get();
}
public boolean isEmpty() {
return size() <= 0;
}
public void push(T element) {
Entry<T> node;
if (lastNode == null) {
node = new Entry<>(null, element);
} else {
node = new Entry<>(lastNode, element);
}
lastNode = node;
size.incrementAndGet();
}
/**
* 弹出最后一个节点
*
* @return
*/
public T pop() {
if (size.get() <= 0) {
return null;
}
size.decrementAndGet();
// 移除末尾节点
T data = lastNode.getData();
lastNode = lastNode.next;
return data;
}
public static void main(String[] args) {
MyUnSafeStack<Integer> stack = new MyUnSafeStack<>();
for (int i = 0; i < 20; i++) {
stack.push(i);
}
while (!stack.isEmpty()) {
System.out.println("出栈:" + stack.pop());
}
}
}
打印结果:
面试官看了笑了笑,实现了基本的功能,但是多线程环境下,对单链表的读写存在线程安全的问题,你考虑到了嘛?
我机灵的小脑袋灵光一闪,给它加把锁!
二、单链表+ReentrantLock
在push和pop操作前使用reentrantlock加锁。
package collection;
import cn.hutool.core.thread.ExecutorBuilder;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @Desc: 线程安全 reentrantLock
* @Author: bingbing
* @Date: 2022/4/20 0020 17:37
*/
public class MyLockStack<T> {
private AtomicInteger size = new AtomicInteger(0);
private ReentrantLock lock = new ReentrantLock();
/**
* 采用尾插法
*/
private Entry<T> lastNode;
public int size() {
return size.get();
}
public boolean isEmpty() {
return size() <= 0;
}
public void push(T element) {
lock.lock();
try {
Entry<T> node;
if (lastNode == null) {
node = new Entry<>(null, element);
} else {
node = new Entry<>(lastNode, element);
}
System.out.println("入栈:" + node.getData());
lastNode = node;
size.incrementAndGet();
} finally {
lock.unlock();
}
}
/**
* 弹出最后一个节点
*
* @return
*/
public T pop() {
lock.lock();
try {
if (size.get() <= 0) {
return null;
}
size.decrementAndGet();
// 移除末尾节点
T data = lastNode.getData();
lastNode = lastNode.next;
return data;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
MyLockStack<Integer> stack = new MyLockStack<>();
ExecutorService executorService = ExecutorBuilder.create().build();
for (int i = 0; i < 20; i++) {
final int index = i;
executorService.execute(() -> {
stack.push(index);
});
}
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
while (!stack.isEmpty()) {
System.out.println("出栈:" + stack.pop());
}
}
}
打印结果:
发现入栈的顺序并不是按照i的递增进行入栈的, 因为出现竞争锁的情况!
面试官看了后点了点头,嗯嗯..... 加锁可以避免线程安全的问题,但是会损耗一定的竞争性能,请问还有其他方式实现替代加锁嘛?
于是我又想到了另外一个方法,使用CAS操作就能避免加锁了。
三、使用CAS实现一个非阻塞的栈
主要思想: 在入栈时,取到lastNode为期望的数据,插入的数据为要更新的数据,如果期间又其他线程修改掉了lastNode,那么cas不成功,会重新进行CAS!
public void push(T element) {
Entry<T> node = new Entry<>(null, element);
Entry<T> old;
do {
old = lastNode.get();
node.next = old;
} while (!lastNode.compareAndSet(old, node));
size.incrementAndGet();
}
出栈时,我们先拿到lastNode, 因为有要移除节点的操作,因此需要重新将lastNode的next重新赋值给lastNode。
public T pop() {
if (size.get() <= 0) {
return null;
}
Entry<T> top;
Entry<T> topNext;
do {
top = lastNode.get();
topNext = top.next;
} while (!lastNode.compareAndSet(top, topNext));
size.decrementAndGet();
return top.getData();
}
完整代码:
package collection;
import cn.hutool.core.thread.ExecutorBuilder;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Desc: 线程安全 CASStack
* @Author: bingbing
* @Date: 2022/4/20 0020 17:37
*/
public class MyCASStack<T> {
private AtomicInteger size = new AtomicInteger(0);
/**
* 采用尾插法
*/
private AtomicReference<Entry<T>> lastNode = new AtomicReference<>();
public int size() {
return size.get();
}
public boolean isEmpty() {
return size() <= 0;
}
/**
* @param element
*/
public void push(T element) {
Entry<T> node = new Entry<>(null, element);
Entry<T> old;
do {
old = lastNode.get();
node.next = old;
} while (!lastNode.compareAndSet(old, node));
size.incrementAndGet();
}
/**
* 弹出最后一个节点
*
* @return
*/
public T pop() {
if (size.get() <= 0) {
return null;
}
Entry<T> top;
Entry<T> topNext;
do {
top = lastNode.get();
topNext = top.next;
} while (!lastNode.compareAndSet(top, topNext));
size.decrementAndGet();
return top.getData();
}
public static void main(String[] args) {
MyCASStack<Integer> stack = new MyCASStack<>();
ExecutorService executorService = ExecutorBuilder.create().build();
for (int i = 0; i < 20; i++) {
final int index = i;
executorService.execute(() -> {
stack.push(index);
});
}
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
while (!stack.isEmpty()) {
System.out.println("出栈:" + stack.pop());
}
executorService.shutdown();
}
}
打印结果:
统计耗时
将Lock和CAS做比较,设置任务数为5000
Lock 耗时:
CAS耗时:
当任务数越多时,CAS操作耗时< Lock操作的耗时 就越明显。
面试官看到了最后,嗯嗯... 小伙子等后续通知把~
继续进行后面的面试。。。