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

Posted Dream_it_possible!

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试官: 如何用Java实现一个栈?相关的知识,希望对你有一定的参考价值。

        

目录

一、使用单链表结构实现

二、单链表+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: 线程安全 CASLock
 * @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操作的耗时 就越明显。

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

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

以上是关于面试官: 如何用Java实现一个栈?的主要内容,如果未能解决你的问题,请参考以下文章

用有限个栈模拟常数效率操作的队列

面试官:如何用Redis实现分布式锁?

今天聊:阿里巴巴面试官是如何用一道编程题考察候选人水平?

python3面试题:如何用python实现栈(Stack)的操作?

金三银四,H5前端开发如何用性能优化征服前端面试官?

面试官:如何用一段代码证明JVM加载类是懒加载模式