面试官: 如何用Java实现一个栈?

285 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。    

目录

一、使用单链表结构实现

二、单链表+ReentrantLock

三、使用CAS实现一个非阻塞的栈

统计耗时 


        有很多人在及技术面试的时候经常会被各种刁钻问题给灵魂拷问到,怎样去实现一个数据结构?  因为我们平时用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操作的耗时 就越明显。

面试官看到了最后,嗯嗯... 小伙子等后续通知把~

继续进行后面的面试。。。