Java多线程常见面试题-第三节:线程安全集合类和死锁

Posted 快乐江湖

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多线程常见面试题-第三节:线程安全集合类和死锁相关的知识,希望对你有一定的参考价值。

文章目录

一:线程安全集合类

在Java集合框架中,大部分都是线程不安全的,当然也存在如Vector这样的线程安全类,但不推荐使用,因为效率太低。所以这里我们对之前学过的集合框架进行说明,以展示他们如何在多线程环境下使用

(1)多线程环境下使用ArrayList

方法一:自己使用同步机制完成,如SynchronizedReentrantLock

方法二:使用collections.synchronizedList(new ArrayList)synchronizedList是标准库提供的一个基于Synchronized进行线程同步的List

  • 此时synchronizedList所有关键操作都会带上synchronized,所以有点粗暴,是一种选择,但不太推荐使用

方法三:使用CopyOnWriteArrayList,它不涉及锁,适用于“一写多读”的场景(也即写的频率较低)

  • CopyOnWrite容器是指“写时拷贝”容器

    • 当我们向一个容器添加元素的时候,不直接添加,而是先把当前容器进行拷贝,拷贝出一个新的容器后,然后向新的容器中添加元素
    • 添加完元素之后,再让原容器的引用指向新的容器
  • 优缺点

    • 优点:可以对原容器进行并发的读,从而不需要加锁;因此在读多写少的情形下,性能很高

    • 缺点

      • 占用内存较多
      • 新写的数据不能在第一时间读到

(2)多线程环境使用队列

主要有以下几种

  • 基于数组实现的阻塞队列ArrayBlockingQueue
  • 基于链表实现的阻塞队列LinkedBlockingQueue
  • 基于堆实现的带优先级的阻塞队列PriorityBlockingQueue
  • 最多只包含一个元素的阻塞队列TransferQueue

(3)多线程使用哈希表

  • 前面说过,HashMap本身不是线程安全的,而在多线程环境下要想使用哈希表可以有以下两种选择

    • Hashtable:不推荐使用
    • ConcurrentHashMap:下面介绍

ConcurrentHashMap:相较于Hashtable来说,做了很多的优化,更便于使用,主要优化有

①:加锁的粒度变细

  • Hashtable直接对整个对象加锁,一个Hashtable只有一把锁,因此锁竞争非常激烈,只要线程访问Hashtable中的任意数据就会产生竞争

  • ConcurrentHashMap只对写操作加锁,并且不是锁整个对象,而是对每个哈希桶分别加锁,这大大降低了锁竞争发生的概率,只有两个线程访问同一个哈希桶时才有锁冲突

②:充分利用到了CAS特性

③:对扩容方式进行了优化:扩容过程类似于写时拷贝

  • 扩容过程中,会创建一个新的数组,它会和旧的数组同时存在一段时间
  • 后面每一个来操作ConcurrentHashMap的线程,都会负责将一小部分元素搬运至型数组
  • 在这个过程中,如果要查询元素,那么会在新数组和旧数组上同时进行
  • 在这个过程中,如果要插入元素,那么只在新数组上插入
  • 在搬运完最后一个元素时删除旧数组

二:死锁

(1)概念

死锁:所谓死锁,是指多个进程因竞争资源而造成的一种互相等待的局面,若无外力作用,这些进程将无法向前推进

  • 举例:我拿了你房间的钥匙,而我在自己的房间;你拿了我的房间的钥匙,而你又在自己的房间。如果我要从自己的房间走出去,必须要拿到你手中的钥匙,但是你要走出来又必须要拿到我手中的钥匙,于是形成了死锁

如下是典型的死锁代码

  • thread1首先尝试获取locker1,获取到之后再尝试获取locker2
  • thread2首先尝试获取locker2,获取到之后再尝试获取locker1
  • thread1thread2再尝试获取自己的第二把locker时发生死锁,因为自己想要的locker被对方持有
public class TestDemo6 
    public static void main(String[] args) 
        Object locker1 = new Object();
        Object locker2 = new Object();

        Thread thread1 = new Thread()
            @Override
            public void run()
                System.out.println("线程1在尝试获取locker1");
                synchronized (locker1)
                    try 
                        Thread.sleep(500);
                     catch (InterruptedException e) 
                        throw new RuntimeException(e);
                    
                    System.out.println("线程1已获取locker1现在尝试获取locker2");
                    synchronized (locker2)
                        System.out.println("线程1获取locker1和locker2成功");
                    

                
            
        ;

        Thread thread2 = new Thread()
            @Override
            public void run()
                System.out.println("线程2在尝试获取locker2");
                synchronized (locker2)
                    try 
                        Thread.sleep(500);
                     catch (InterruptedException e) 
                        throw new RuntimeException(e);
                    
                    System.out.println("线程2已获取locker2现在尝试获取locker1");
                    synchronized (locker1)
                        System.out.println("线程2获取locker1和locker2成功");
                    

                
            
        ;

        thread1.start();
        thread2.start();
    


(2)死锁产生的四个必要条件

死锁产生的四个必要条件:死锁必须同时满足以下四个条件才会发生

  • 互斥条件
  • 持有并等待条件
  • 不可剥夺条件
  • 循环等待条件(注意发生死锁一定有循环等待,但是发生循环等待未必死锁)

A:互斥条件

互斥条件:是指只有对必须互斥使用的资源抢夺时才可能导致死锁。比如打印机设备就可能导致互斥,但是像内存、扬声器则不会

  • 进程A已经获得资源,进程B只能等待

B:不可剥夺条件

不可剥夺条件:是指进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放

C:持有并等待条件

持有并等待条件:是指进程已经至少保持了一个资源,但又提出了新的资源请求,但是该资源又被其他进程占有,此时请求进程被阻塞,但是对自己持有的资源保持不放

D:循环等待条件

循环剥夺条件:是指存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求

(3)如何处理预防死锁

  • 发生死锁有4个必要条件,只要破坏其中之一就可以预防死锁。在这个4个必要条件中互斥、持有并等待和不可剥夺破坏起来都是不太现实的,所以我们一般会在循环等待这个条件上做文章

破坏循环等待条件:可以采用顺序资源分配方法。首先给系统中的资源进行编号,规定每个进程必须按照编号递增的顺序请求资源,编号相同的资源(也就是同类资源)一次申请完

  • 这是因为一个进程只有在已经占有小编号资源的同时,才有资格申请更大编号的资源。所以已经持有大编号资源的进程不可能逆向申请小编号的资源

例如对于上面的案例,我们可以让thread1thread2加锁的顺序一致,即都按照先locker1locker2的方式进行加锁,这样的话就不会发生死锁了

public class TestDemo7 
    public static void main(String[] args) 
        Object locker1 = new Object();
        Object locker2 = new Object();

        Thread thread1 = new Thread()
            @Override
            public void run()
                System.out.println("线程1在尝试获取locker1");
                synchronized (locker1)
                    try 
                        Thread.sleep(500);
                     catch (InterruptedException e) 
                        throw new RuntimeException(e);
                    
                    System.out.println("线程1已获取locker1现在尝试获取locker2");
                    synchronized (locker2)
                        System.out.println("线程1获取locker1和locker2成功");
                    

                
            
        ;

        Thread thread2 = new Thread()
            @Override
            public void run()
                System.out.println("线程2在尝试获取locker1");
                synchronized (locker1)
                    try 
                        Thread.sleep(500);
                     catch (InterruptedException e) 
                        throw new RuntimeException(e);
                    
                    System.out.println("线程2已获取locker1现在尝试获取locker2");
                    synchronized (locker2)
                        System.out.println("线程2获取locker1和locker2成功");
                    

                
            
        ;

        thread1.start();
        thread2.start();
    


以上是关于Java多线程常见面试题-第三节:线程安全集合类和死锁的主要内容,如果未能解决你的问题,请参考以下文章

JavaSE 常见面试题-线程篇

敲黑板!Java多线程常见面试题!!

Java多线程常见面试题

Java 并发常见面试题总结(上)

iOS 多线程 自动释放池常见面试题代码

Java多线程常见面试题-第二节:JUC(java.util.concurrent)