Java进阶笔试&&面试经验Java学习笔记

面试小结之并发篇

2017-06-14  本文已影响931人  ginobefun

最近面试一些公司,被问到的关于Java并发编程的问题,以及自己总结的回答。

Java线程的状态及如何转换。

线程状态及其转换图

多个线程之间如何协调?

// 等待方
synchronized(lockObj){
    while(condition is false){
        lockObj.wait();
    }
    
    // do business
}

// 通知方
synchronized(lockObj){
    // change condition
    
    lockObj.notifyAll();
}

说说Java的线程池是如何实现的?

线程池 线程池处理流程
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

关于BlockingQueue和TransferQueue的异同。

谈谈HashMap的实现。

HashMap
从源码可知,HashMap类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组;
Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。
HashMap就是使用哈希表来存储的。为解决冲突,Java中HashMap采用了链地址法,简单来说,就是数组加链表的结合。在每个数组元素上都一个链表结构,当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表上。
Node[] table的初始化长度默认值是16,Load factor为负载因子(默认值是0.75),threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold = length * Load factor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
数组索引位置 put方法执行过程
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 步骤①:tab为空则创建
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 步骤②:计算index,并对null做处理 
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 步骤③:节点key存在,直接覆盖value
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 步骤④:判断该链为红黑树
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            // 步骤⑤:该链为链表
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //链表长度大于8转换为红黑树进行处理
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // key已经存在直接覆盖value
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        // 步骤⑥:超过最大容量 就扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            // 超过最大值就不再扩充了,就只好随你碰撞去吧
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 没超过最大值,就扩充为原来的2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        
        // 计算新的resize上限
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            // 把每个bucket都移动到新的buckets中
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            // 原索引
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            // 原索引+oldCap
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        
                        // 原索引放到bucket里
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        // 原索引+oldCap放到bucket里
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

谈谈线程安全的ConcurrentHashMap的实现原理。

1.若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
2.若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
3.任意节点的左、右子树也分别为二叉查找树。
4.没有键值相等的节点(no duplicate nodes)。
1.每个结点要么是红的要么是黑的。  
2.根结点是黑的。  
3.每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。  
4.如果一个结点是红的,那么它的两个儿子都是黑的。  
5.对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。

什么是一致性哈希?

一致性哈希

Java有哪些实现锁的方式?

// 对于非公平锁,会执行该方法:
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();//获取状态变量
    if (c == 0) {//表明没有线程占有该同步状态
        if (compareAndSetState(0, acquires)) {//以原子方式设置该同步状态
            setExclusiveOwnerThread(current);//该线程拥有该FairSync同步状态
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {//当前线程已经拥有该同步状态
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);//重复设置状态变量(锁的可重入特性)
        return true;
    }
    
    return false;
}

// 而对于公平锁,该方法则是这样:
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        //先判断该线程节点是否是队列的头结点
        //是则以原子方式设置同步状态,获取锁
        //否则失败返回
        if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {//重入
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    
    return false;
}
public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
    
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分支其实是一种优化:CAS操作失败的话才进入enq中的循环。
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
} 

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

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);
    }
}
class Point {
   private double x, y;
   private final StampedLock sl = new StampedLock();
   void move(double deltaX, double deltaY) { // an exclusively locked method
     long stamp = sl.writeLock();
     try {
       x += deltaX;
       y += deltaY;
     } finally {
       sl.unlockWrite(stamp);
     }
   }
   
   //下面看看乐观读锁案例
   double distanceFromOrigin() { // A read-only method
     long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁
     double currentX = x, currentY = y; //将两个字段读入本地局部变量
     if (!sl.validate(stamp)) { //检查发出乐观读锁后同时是否有其他写锁发生?
        stamp = sl.readLock(); //如果没有,我们再次获得一个读悲观锁
        try {
          currentX = x; // 将两个字段读入本地局部变量
          currentY = y; // 将两个字段读入本地局部变量
        } finally {
           sl.unlockRead(stamp);
        }
     }
     return Math.sqrt(currentX * currentX + currentY * currentY);
   }

    //下面是悲观读锁案例
   void moveIfAtOrigin(double newX, double newY) { // upgrade
     // Could instead start with optimistic, not read mode
     long stamp = sl.readLock();
     try {
       while (x == 0.0 && y == 0.0) { //循环,检查当前状态是否符合
         long ws = sl.tryConvertToWriteLock(stamp); //将读锁转为写锁
         if (ws != 0L) { //这是确认转为写锁是否成功
           stamp = ws; //如果成功 替换票据
           x = newX; //进行状态改变
           y = newY; //进行状态改变
           break;
         }
         else { //如果不能成功转换为写锁
           sl.unlockRead(stamp); //我们显式释放读锁
           stamp = sl.writeLock(); //显式直接进行写锁 然后再通过循环再试
         }
       }
     } finally {
       sl.unlock(stamp); //释放读锁或写锁
     }
   }
 }

AtomicLong、LongAdder和LongAccumulator的实现有何不同?

/** 
 * 存放Cell的hash表,大小为2的幂。 
 */  
transient volatile Cell[] cells;  

/** 
 * 基础值,没有竞争时会使用(更新)这个值,同时做为初始化竞争失败的回退方案。 
 * 原子更新。 
 */  
transient volatile long base;  

/** 
 * 自旋锁,通过CAS操作加锁,用于保护创建或者扩展Cell表。 
 */  
transient volatile int cellsBusy;  

CompletableFuture对比Future有哪些改进,怎么用?

supplyAsync/runAsync -- 创建CompletableFuture对象;
whenComplete/whenCompleteAsync/exceptionally -- 计算完成或者抛出异常的时可以执行特定的Action;
thenApply/thenApplyAsync -- 对数据进行一些处理或变换;
thenAccept/thenAcceptAsync -- 纯消费,不返回新的计算值;
thenAcceptBoth/thenAcceptBothAsync/runAfterBoth -- 当两个CompletionStage都正常完成计算的时候,就会执行提供的Action;
thenCompose/thenComposeAsync -- 这个Function的输入是当前的CompletableFuture的计算值,返回结果将是一个新的CompletableFuture。
记住,thenCompose返回的对象并不一是函数fn返回的对象,如果原来的CompletableFuture还没有计算出来,
它就会生成一个新的组合后的CompletableFuture。可以用来实现异步pipline;
thenCombine/thenCombineAsync - 并行执行的,它们之间并没有先后依赖顺序,和thenAcceptBoth的区别在于有返回值;
allOf/anyOf -- 所有的/其中一个CompletableFuture都执行完后执行计算;

其他面试小结

扫一扫 关注我的微信公众号
上一篇 下一篇

猜你喜欢

热点阅读