2020金三银四冲击BAT必备面试题(上篇):集合类+阻塞队列+锁

最新互联网大厂面试真题、Java程序员面试攻略(面试前的准备、面试中的技巧)请访问GitHub


一、集合类

1. ArrayList的扩容机制

  1. 每次扩容是原来容量的1.5倍,通过移位的方法实现。
  2. 使用copyOf的方式进行扩容。

扩容算法是首先获取到扩容前容器的大小。然后通过oldCapacity + (oldCapacity >> 1) 来计算扩容后的容器大小newCapacity。这里用到了>> 右移运算,即容量增大原来的1.5倍。还要注意的是,这里扩充容量时,用的时Arrays.copyOf方法,其内部也是使用的System.arraycopy方法。

区别:

  • arraycopy()需要目标数组,将原数组拷贝到你自己定义的数组里,而且可以选择拷贝的起点和长度以及放入新数组中的位置。
  • copyOf()是系统自动在内部新建一个数组,并返回该数组。

2. 数组和ArrayList的区别

  • 数组可以包含基本类型,ArrayList成员只能是对象。
  • 数组大小是固定的,ArrayList可以动态扩容。

3. ArrayList和LinkedList的区别

  • 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
  • 数据结构:LinkedList 是基于双向链表实现的,ArrayList 是基于数组实现的。
  • 快速随机访问:ArrayList 支持随机访问,所以查询速度更快,LinkedList 添加、插入、删除元素速度更快。
  • 内存空间占用:ArrayList的空间浪费主要体现在在list列表的结尾会预留一定的容量空间,LinkedList使用Node来存储数据每个Node中不仅存储元素的值,还存储了前一个 Node 的引用和后一个 Node 的引用,占用内存更多。
  • 遍历方式选择:实现了RandomAccess接口的list,优先选择普通for循环 ,其次foreach,未实现RandomAccess接口的list, 优先选择iterator遍历(foreach遍历底层也是通过iterator实现的),大size的数据,千万不要使用普通for循环。

4. 如何创建同步的List

可以通过Collections.sychronizeList将list转换成同步list,或者直接使用CopyOnWriteArrayList。

5. CopyOnWriteArrayList

  • 读时不加锁,写入时加锁,写入时创建一个新入组将老数组拷贝进入新数组,并将数据加入新数组。
  • 只能保证最终一致性。

6. Vector

ArrayList线程安全的一个版本,底层通过synchronize加锁实现线程安全。

7. HashMap扩容机制

HashMap使用resize()方法来进行扩容,计算table数组的新容量和Node在新数组中的新位置,将旧数组中的值复制到新数组中,从而实现自动扩容。

  • 当空的HashMap实例添加元素时,会以默认容量16为table数组的长度扩容,此时 threshold = 16 * 0.75 = 12。
  • 当不为空的HashMap实例添加新元素数组容量不够时,会以旧容量的2倍进行扩容,当然扩容也是大小限制的,扩容后的新容量要小于等于规定的最大容量,使用新容量创建新table数组,然后就是数组元素Node的复制了,计算Node位置的方法是 index = (n-1) & hash,这样计算的好处是,Node在新数组中的位置要么保持不变,要么是原来位置加上旧数组的容量值,在新数组中的位置都是可以预期的(有规律的),并且链表上Node的顺序也不会发生改变。

8. HashMap为什么不是线程安全的

  • 没有锁操作,两个线程操作同一个hashMap会出现线程安全的问题,可能会导致数据丢失。
  • resize的时候会出现死锁,以为hash冲突之后采用链地址法解决hash冲突,但是两个线程都进行扩容的时候,链表使用头插法,导致出现循环引用,出现死锁。1.8之后 链表都是采用尾插法。避免了死循环的问题。

9. 为什么HashMap的hashCode要高16位异或hashCode

因为元素所处位置只与低n位相关,高16位与hashcode进行异或是为了减少碰撞。

异或是两者相同返回0 不相同返回1。

10. 为什么HashMap的容量要是2的N次幂

  • 取模时分配更均匀。
  • 扩容成本更低。

2^n下有特性:x%2^n=x&(2^n-1)只有2的幂次方才有此特性。

11. ConcurrentHashMap的实现

  • jdk1.7之前,使用分段锁来实现,默认支持的并发度为16,segment继承自reetrantlock,segment充当锁角色。每个segment中包含一个小的hash表。size方法将segment的count相加,计算两次,如果两次结果相同,说明计算准确,否则每个segment重新加锁计算。
  • jdk1.8之后取消分段锁的设计,采用CAS+Synchronized保证线程安全。主要是锁住链表的头结点。size方法使用一个volatile变量baseCount记录元素个数,当插入新数据或者删除数据的时候会更新baseCount的值。

12. ConcurrentHashMap1.7与1.8异同

  • 1.8取消了分段锁,锁的粒度更小,减少并发冲突的概率。
  • 1.8采用了链表+红黑树的实现方式,对查询的提升很大。

13. 为什么ConcurrentHashMap读操作不加锁

  • ConcurrentHashMap只保证最终一致性,并不能保证强一致性。
  • 对于value使用valitile关键字,保证内存可见,能够被多线程同时读,并且不会读到过期的值。根据java内存模型的hanpends-befor原则,对volatitle的写入操作先于读操作,即使两个线程同时读取和写入同一个变量,也能是get操作拿到最新值
  • Node使用volatitle关键字标识是为了数组扩容时的可见性。

14. LinkedHashMap的实现

基于hashMap和双向链表实现的,线程不安全。

15. HashSet的实现

  • 底层是通过hashMap实现的。
  • 判断两个对象是否相等,先判断hashCode是否相等,如果相等再判断equals,这就是为什么重写equals方法要重写hashCode方法。

16. TreeMap的实现

底层使用红黑树实现。根据键值进行排序,key必须实现Compareable接口或者构造TreeMap时传入Comparetor。

17. TreeSet的实现

底层使用TreeMap实现,即使用红黑树进行实现。
Set判断两个元素是否相等,先判断hashCode再使用equals

18. 解决Hash冲突的方法

  • 开放定址法
  • 链地址法
  • 再hash法

19. List、Map、Set存储的null值

  • list null值,加几个存几个。
  • set null值 只存一个。
  • map只存在一个null值对。

20. 平衡二叉树AVL与红黑树的区别

  • 平衡二叉树是高度平衡的,每次的插入和删除,都要进行rebalance操作。
  • 红黑树不是高度平衡的。

红黑树定义:

  • 节点是红色或黑色。
  • 根节点是黑色。
  • 每条路径上的黑色节点数目相同。
  • 子节点和父节点的颜色不相同。

二. 阻塞队列

1. 五种阻塞队列介绍

  • ArrayBlockingQueue:有界队列,底层使用数组实现,并发控制使用ReentrantLock控制,不管是插入操作还是读取操作,都需要获取锁之后才能执行。
  • LinkedBlockingQueue:底层基于单向链表实现,既可以当做有界队列,也可以当做无界队列使用。使用两个ReentrantLock实现并发控制:takelock和putlock。
  • SynchronousQueue:底层使用单向链表实现,只有一个元素,同步的意思是一个写操作必须等到一个读操作之后才返回,指的是读写线程的同步。
  • PriorityBlockingQueue:带排序的阻塞队列的实现,使用数组进行实现。并发控制使用ReentrantLock,队列为无界队列。
    有初始化参数指定队列大小,但是会自动扩容。使用最小堆来实现排序。
  • DelayedQueue:DelayedQueue是使用PriorityBlockingQueue和Delayed实现的,内部定义了一个优先级队列,当调用offer的时候,把Delayed对象加入队列中,使用take先把first对象拿出来(peek),如果没有到达阈值,进行await处理。

2. poll和peek的区别

都用于取队列的头结点,poll会删除头结点,peek不会删除头结点。

3. LinkedBlockingQueue

  • 是先进先出队列FIFO。
  • 采用ReentrantLock保证线程安全

3.1、功能

3.1.1、增加

增加有三种方式,前提:队列满

3.1.2、删除
删除有三种方式,前提:队列为空

3.2、简单分析

  • LinkedBlockingQueue是一个阻塞队列,内部由两个ReentrantLock来实现出入队列的线程安全,由各自的Condition对象的await和signal来实现等待和唤醒功能。
  • 基于单向链表的、范围任意的(其实是有界的)、FIFO 阻塞队列。
  • 头结点和尾结点一开始总是指向一个哨兵的结点,它不持有实际数据,当队列中有数据时,头结点仍然指向这个哨兵,尾结点指向有效数据的最后一个结点。这样做的好处在于,与计数器 count 结合后,对队头、队尾的访问可以独立进行,而不需要判断头结点与尾结点的关系。

2020金三银四冲击BAT必备面试题(上篇):集合类+阻塞队列+锁_第1张图片

3.2.1、节点与属性

//链表节点内部类
static class Node {
    //节点元素
    E item;
    Node next;
    Node(E x) {
         item = x;
    }
}
//容量界限,如果未设定,则为Integer最大值
 private final int capacity;
//当前元素个数
private final AtomicInteger count = new AtomicInteger();
//链表的头:head.item == null
transient Node head;
//链表的尾:last.next == null
private transient Node last;
//take,poll等获取锁
private final ReentrantLock takeLock = new ReentrantLock();
//等待任务的等待队列
private final Condition notEmpty = takeLock.newCondition();
//put,offer等插入锁
private final ReentrantLock putLock = new ReentrantLock();
//等待插入的等待队列
private final Condition notFull = putLock.newCondition();

3.2.2、插入线程与获取线程的相互通知

signalNotEmpty()方法,在插入线程发现队列为空时调用,告知获取线程需要等待。
signalNotFull()方法,在获取线程发现队列已满时调用,告知插入线程需要等待。

//表示等待take。put/offer调用,否则通常不会锁定takeLock。
private void signalNotEmpty() {
    //获取takeLock
    final ReentrantLock takeLock = this.takeLock;
    //锁定takeLock
    takeLock.lock();
    try {
        //唤醒take线程等待队列
        notEmpty.signal();
    } finally {
       //释放锁
       takeLock.unlock();
    }
}
//表示等待put,take/poll 调用
private void signalNotFull() {
    //获取putLock
    final ReentrantLock putLock = this.putLock;
    //锁定putLock
    putLock.lock();
    try {
        //唤醒插入线程等待队列
        notFull.signal();
    } finally {
        //释放锁
        putLock.unlock();
    }
}

3.2.3、入队与出队操作

enqueue()方法只能在持有 putLock 锁下执行,dequeue()在持有 takeLock 锁下执行。

//在队列尾部插入
private void enqueue(Node node) {
    // assert putLock.isHeldByCurrentThread();
    // assert last.next == null;
    //last.next指向当前node
    //尾指针后移
    last = last.next = node;
}
//移除队列头
private E dequeue() {
    // assert takeLock.isHeldByCurrentThread();
    // assert head.item == null;
    //保存头指针
    Node h = head;
   //获取当前链表第一个元素
   Node first = h.next;
   //头指针的next指向自己
   h.next = h; // help GC
   //头指针指向第一个元素
   head = first;
   //获取第一个元素的值
   E x = first.item;
   //将第一个元素的值置空
   first.item = null;
   //返回第一个元素的值
   return x;
}

3.2.4、对两把锁的加锁与释放

在需要对两把锁同时加锁时,把加锁的顺序与释放的顺序封装成方法,确保所有地方都是一致的。而且获取锁时都是不响应中断的,一直获取直到加锁成功,这就避免了第一把锁加锁成功,而第二把锁加锁失败导致锁不释放的风险。

//锁定putLock和takeLock
void fullyLock() {
    putLock.lock();
    takeLock.lock();
}
//与fullyLock的加锁顺序相反,先解锁takeLock,再解锁putLock
void fullyUnlock() {
    takeLock.unlock();
    putLock.unlock();
}

3.3、源码解读
简单介绍一下LinkedBlockingQueue中API的源码,如构造方法,新增,获取,删除,drainTo。

3.3.1、构造函数
LinkedBlockingQueue有三个构造方法,其中无参构造尽量少用,因为容量为Integer的最大值,操作不当会出现内存溢出。

public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
    //参数校验
    if (capacity <= 0) throw new IllegalArgumentException();
    //设置容量
    this.capacity = capacity;
    //首尾节点指向一个空节点
    last = head = new Node(null);
}
public LinkedBlockingQueue(Collection c) {
    this(Integer.MAX_VALUE);
    //获取putLock
    final ReentrantLock putLock = this.putLock;
    //锁定
    putLock.lock(); // Never contended, but necessary for visibility
    try {
        int n = 0;
        for (E e : c) {
            if (e == null)
                throw new NullPointerException();
            if (n == capacity)
                throw new IllegalStateException("Queue full");
            enqueue(new Node(e));
            ++n;
        }
        count.set(n);
    } finally {
        putLock.unlock();
    }
}

3.3.2、offer(E e)

将给定的元素设置到队列中,如果设置成功返回true, 否则返回false。 e的值不能为空,否则抛出空指针异常。

//如果可以在不超过队列容量的情况下立即插入指定的元素到队列的尾部,成功后返回true,如果队列已满,返回false。当使用容量受限的队列时,此方法通常比方法BlockingQueue#add更可取,后者只能通过抛出异常才能插入元素。
public boolean offer(E e) {
    //非空判断
    if (e == null) throw new NullPointerException();
    //计数器
    final AtomicInteger count = this.count;
    //如果队列已满,直接返回插入失败
    if (count.get() == capacity)
        return false;
    int c = -1;
    //新建节点
    Node node = new Node(e);
    //获取插入锁
    final ReentrantLock putLock = this.putLock;
    //锁定
    putLock.lock();
    try {
        //如果队列未满
        if (count.get() < capacity) {
        //插入队列
        enqueue(node);
        //计数
        c = count.getAndIncrement();
        //还有空余空间
        if (c + 1 < capacity)
             //唤醒插入线程
             notFull.signal();
        }
    } finally {
        //解锁
        putLock.unlock();
    }
    //如果队列为空
    if (c == 0)
        //通知获取线程阻塞
        signalNotEmpty();
    //返回成功或者插入失败
    return c >= 0;
}

3.3.3、put(E e)

将元素设置到队列中,如果队列中没有多余的空间,该方法会一直阻塞,直到队列中有多余的空间。

public void put(E e) throws InterruptedException {
    //不可以插入空元素
    if (e == null) throw new NullPointerException();

    //所有put/take/etc中的约定都是预先设置本地var
    //除非设置,否则保持计数为负数表示失败。
    int c = -1;
    //新建节点
    Node node = new Node(e);
    //获取putLock
    final ReentrantLock putLock = this.putLock;
    //获取计数器
    final AtomicInteger count = this.count;
    //可中断加锁,即在锁获取过程中不处理中断状态,而是直接抛出中断异常,由上层调用者处理中断。
    putLock.lockInterruptibly();
    try {
        /*
        * 注意count在wait守卫线程中使用,即使它没有被锁保护。
        * 这是因为count只能在此时减少(所有其他put都被锁定关闭),
        * 如果它从容量更改,我们(或其他一些等待put)将收到信号。
        * 类似地,count在其他等待守卫线程中的所有其他用途也是如此。
        */
        //只要当前队列已满
        while (count.get() == capacity) {
            //通知插入线程等待
            notFull.await();
        }
        //插入队列
        enqueue(node);
        //数量加1
        c = count.getAndIncrement();
        //如果队列增加1个元素还未满
        if (c + 1 < capacity)
            //唤醒插入进程
            notFull.signal();
    } finally {
        //解锁
        putLock.unlock();
    }
    //如果队列中没有元素了
    if (c == 0)
        //通知获取线程等待
        signalNotEmpty();
}

3.3.4、peek()

非阻塞的获取队列中的第一个元素,不出队列。

public E peek() {
    //队列为空,直接返回
    if (count.get() == 0)
        return null;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        //获取第一个元素,非哨兵
        Node first = head.next;
        //元素为空,返回null
        if (first == null)
            return null;
        else
            //返回第一个元素值
            return first.item;
    } finally {
        takeLock.unlock();
    }
}

3.3.5、poll()
非阻塞的获取队列中的值,未获取到返回null。

public E poll() {
    final AtomicInteger count = this.count;
    //队列为空,直接返回
    if (count.get() == 0)
        return null;
    E x = null;
    int c = -1;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        //队列非空,获取队列中元素
        if (count.get() > 0) {
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        }
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)
        signalNotFull();
    return x;
}

3.3.6、remove(Object o)
从队列中移除指定的值。将两把锁都锁定。

public boolean remove(Object o) {
    //不支持null
    if (o == null) return false;
    //锁定两个锁
    fullyLock();
    try {
        //迭代队列
        for (Node trail = head, p = trail.next;
            p != null;
            trail = p, p = p.next) {
            //通过equals方法匹配待删除元素
            if (o.equals(p.item)) {
                //移除p节点
                unlink(p, trail);
                //成功
                return true;
            }
        }
        //失败
        return false;
    } finally {
        //解锁
        fullyUnlock();
    }
}
// 将内部节点p与前一个跟踪断开连接
void unlink(Node p, Node trail) {
    // assert isFullyLocked();
    // p.next is not changed, to allow iterators that are
    // traversing p to maintain their weak-consistency guarantee.
    //p节点内容置空
    p.item = null;
    //trail节点的next指向p的next
    trail.next = p.next;
    //如果p是队尾
    if (last == p)
        //trail变为队尾
        last = trail;
    //如果队列已满
    if (count.getAndDecrement() == capacity)
        //通知插入线程阻塞
        notFull.signal();
}

3.3.7、clear()

清空队列。

//原子性地从队列中删除所有元素。此调用返回后,队列将为空。
public void clear() {
    //锁定
    fullyLock();
    try {
        //清空数据,帮助垃圾回收
        for (Node p, h = head; (p = h.next) != null; h = p) {
             h.next = h;
             p.item = null;
        }
        head = last;
        // assert head.item == null && head.next == null;
        //如果容量为0
        if (count.getAndSet(0) == capacity)
            //唤醒插入线程
            notFull.signal();
    } finally {
        //解锁
        fullyUnlock();
    }
}

3.3.8、drainTo(Collection c)
将队列中值,全部移除,并发设置到给定的集合中。

public int drainTo(Collection c, int maxElements) {
    //各种判断
    if (c == null)
        throw new NullPointerException();
    if (c == this)
        throw new IllegalArgumentException();
    if (maxElements <= 0)
        return 0;
    boolean signalNotFull = false;
    //锁
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        //获取要转移的数量
        int n = Math.min(maxElements, count.get());
        // count.get provides visibility to first n Nodes
         Node h = head;
        int i = 0;
        try {
            //组装集合
            while (i < n) {
                Node p = h.next;
                c.add(p.item);
                p.item = null;
                h.next = h;
                h = p;
                ++i;
            }
            return n;
        } finally {
            // Restore invariants even if c.add() threw
            if (i > 0) {
                // assert h.item == null;
                head = h;
                signalNotFull = (count.getAndAdd(-i) == capacity);
            }
        }
    } finally {
        takeLock.unlock();
        if (signalNotFull)
            signalNotFull();
    }
}

三. 锁

1. 锁状态
锁的状态只能升级不能降级。

  • 无锁:没有锁对资源进行锁定,所有线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。其他修改失败的线程会不断重试,直到修改成功,如CAS原理和应用是无锁的实现。
  • 偏向锁:偏向锁是指一段同步代码一直被一个线程访问,那个该线程会自动获取锁,降低获取锁的代价。
  • 轻量级锁:是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。通过cas操作和自旋来解决加锁问题,自旋超过一定的次数或者已经有一个线程在自旋,又来一个线程获取锁时,轻量级锁会升级为重量级锁。
  • 重量级锁:升级为重量级锁,等待锁的线程都会进入阻塞状态。

2. 乐观锁与悲观锁

  • 乐观锁,每次拿数据的时候认为别人都不会修改,在更新的时候再判断在此期间有没有更新数据,可以使用版本号等机制,适合读取多场景,提高性能。
  • 悲观锁,每次拿数据都认为别人会修改,都会上锁,可以使用synchronized、独占锁Lock、读写锁等机制,适合写多的场景,保证写入操作正确。

3. 自旋锁与适应性自旋锁

  • 自旋锁:指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断判断锁是否能获取成功,直到获取到锁才退出循环。
  • 优点:线程不进行上下文切换,减少了上下文切换的时间。
    存在的问题:如果线程持有锁的时间较长,其他线程进入循环,消耗cpu。
  • 自适应自旋锁:指的是自旋的时间不固定,由前一个在同一个锁上自旋的时间和锁拥有者的状态来决定。如果在同一个对象上,刚刚通过自旋成功获取过锁,且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋很有可能再次成功。反之自旋操作很少成功获取锁,那么后面获取这个锁可能直接省略掉自旋的过程,直接阻塞线程。

4. 公平锁与非公平锁

  • 公平锁是指多个线程按照申请锁的顺序直接进入队列排队,队列中的第一个线程才能获取锁。
  • 非公平锁是指线程先尝试获取锁,获取不到进入队列中排队,如果能获取到,则无需阻塞直接获取锁。

5. 重入锁与非重入锁

重入锁:同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,前提是锁对象是相同的。

6. 共享锁与排他锁

  • 共享锁是指一个锁可以被多个线程锁持有。
  • 排它锁或者叫独享锁或者互斥锁 指锁一次只能被一个线程所持有。

7. 读写锁

  • 读锁是共享的,写锁是独占的。
  • 读读之间不会互斥,读写互斥,写写互斥,读写锁提高了读的性能。

8. CAS

CompareAndSwap比较与交换,是一种无锁算法,原子类使用了CAS实现了乐观锁。

带来的问题:

  • ABA问题:解决思路在变量前面加版本号,每次变量更新的时候都将版本号+1,每次更新的时候要求版本>=当前版本(AtomicStampedReference)

循环时间长开销大,CAS操作如果长时间执行不成功,会导致其一直自旋,cpu消耗大。

  • 只能保证一个共享变量的原子操作。
  • 可以把多个变量放在一个对象里面进行CAS操作。

9. 锁优化

9.1 锁升级

  • 偏向锁的升级:线程A获取锁对象时,会在java对象头和栈帧中记录偏向的线程A的id,线程A再次获取锁时,只需要比较java头中的线程id与当前Id是否相等,如果一致则无需通过cas加锁解锁。如果不一致,说明有线程B来获取锁,那么要判断java头中偏向锁的线程是否存活,如果没有存活,锁对象被置为无锁状态,线程B可将锁对象置为B的偏向锁。如果存活,则查看A是否还需要继续持有对当前锁,如果不需要持有,则将锁置为无锁状态,偏向新的线程,如果还继续持有锁对象,则暂停A线程,撤销偏向锁,将锁升级为轻量级锁。
  • 轻量级锁的升级:线程A获取轻量级锁时会把锁的对象头复制到自己的线程栈针中,然后通过cas把对象头中的内容替换为A所记录的地址。此时线程B也想获取锁,发现A已经获取锁,那么线程B就自旋等待。等到自旋次数到了或者线程A正在执行,线程B自旋等待,此时来了线程C来竞争锁对象,这个时候轻量级锁就会膨胀为重量级锁。重量级锁会把未获得到锁对象的线程全部变为阻塞状态该,防止cpu空转。

9.2 锁粗化

将多个连续的加锁,解锁操作连接在一起,扩展成为一个范围更大的锁,避免频繁的加解锁操作。

9.3 锁消除

通过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁。

10. synchronized底层实现

  • synchronized通过Monitor实现同步,Monitor依赖于底层操作系统互斥锁来实现线程同步。
  • java对象头是由markword(标记字段)和klass point(类型指针)组成。markword存储对象的hashcode,分代年龄和锁标志位信息。Klass point 指向对象元数据的指针,虚拟机通过这个指针来确定对象是哪个类的实例。
  • synchronized修饰同步代码块,是使用monitorenter和monitorexit来控制的,通过java对象头中的锁计数器。
  • 修饰方法时会将方法标识为ACCSYNCHRONIZE,JVM通过这个标志来判断方法是不是同步方法。

11. synchronized与ReentrantLock的区别

  • 两者都是悲观锁,可重入锁。
  • ReentrantLock 可中断,可以实现公平锁,可以绑定多个条件。
  • ReentrantLock需要显示的调用锁和释放锁,synchronized属于java关键字,不需要显式的释放。

12. volatile关键字

  • 保证变量内存可见。
  • 禁止指令重排序。

volatile和synchronized的区别:

  • volatile不会阻塞,synchronized会阻塞。
  • volatile保证数据的内存可见性但不能保证原子性,synchronized两者都能保证。
  • volatile主要解决变量在线程之间的可见性,而synchronized主要解决多线程访问资源的同步性。

13. Atomic原子类实现

使用cas操作 + volatile + native方法保证同步。

14. AQS

AQS(AbstractQueuedSynchronizer)内部维护的是一个FIFO的双向同步队列,如果当前线程竞争锁失败,AQS会把当前线程以及等待状态信息构造成一个Node加入到同步队列中,同时在阻塞该线程。当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点线程。使用内部的一个state来控制是否获取锁,当state=0时表示无锁状态,state>0时表示已经有线程获取了锁。

15. AQS的组件

  • semaphore 可指定多个线程同时访问某个共享资源。
  • countDownLatch 一个线程A等待其他线程执行完成之后才继续执行。
  • cyclicBarrier 一组线程等待至某个状态之后同时执行。

countDownLatch和CyclicBarrier的区别

  • countDownLatch是一个线程等一组线程执行完成之后才执行, cyclicBarrier是一组线程互相等待至某个状态之后,同时执行。
  • countDownLatch不能复用,cyclicBarrier可以重用。

16. 锁降级

锁降级是指将写锁降级为读锁,这个过程就是当前线程已经获取到写锁的时候,再获取到读锁,随后释放写锁的过程,这么做的目的为的就是保证数据的可见性。

17. 逃逸分析

  • 逃逸分析就是分析对象的动态作用域,当一个对象在方法中被定义后,他可能被外部方法所引用,作为参数传递到其他方法中,成为方法逃逸,赋值给类变量或者可以被其他线程访问的实例变量成为线程逃逸。
  • 使用逃逸分析,编译器可以对代码做优化。比如:同步省略(锁消除),将堆分配转化为栈分配,标量替换。
  • 使用逃逸分析的缺点,没法保证逃逸分析的性能一定高于其他性能。极端的话经过逃逸分析后,所有的对象都逃逸了,那么逃逸分析的过程就浪费了。

最新互联网大厂面试真题、Java程序员面试攻略(面试前的准备、面试中的技巧)请访问GitHub

你可能感兴趣的:(程序人生,JVM)