zoukankan      html  css  js  c++  java
  • 并发编程相关面试题四

    一、Java开发中用过哪些锁

    1、乐观锁

      乐观锁顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS(Compare and Swap 比较并交换)实现的

      乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升;  

      乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

    2、悲观锁

      悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。比如Java里面的同步原语synchronized关键字的实现就是悲观锁。

      悲观锁适合写操作非常多的场景;

      悲观锁在Java中的使用,就是利用各种锁;

    3、独享锁

      独享锁是指该锁一次只能被一个线程所持有。

      独享锁通过AQS来实现的,通过实现不同的方法,来实现独享锁。

      对于Synchronized而言,当然是独享锁。

    4、共享锁

      共享锁是指该锁可被多个线程所持有。

      读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。

      共享锁也是通过AQS来实现的,通过实现不同的方法,来实现共享锁。

    5、互斥锁

      互斥锁在Java中的具体实现就是ReentrantLock。

    6、读写锁

      读写锁在Java中的具体实现就是ReadWriteLock。

    7、可重入锁    

      重入锁也叫作递归锁,指的是同一个线程外层函数获取到一把锁后,内层函数同样具有这把锁的控制权限;
      synchronized和ReentrantLock就是重入锁对应的实现;
      synchronized重量级的锁 ;
      ReentrantLock轻量级的锁;

    8、公平锁

      公平锁是指多个线程按照申请锁的顺序来获取锁。

      对于Java ReetrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。

    9、非公平锁

      非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。  

      对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

    10、分段锁

      分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

      我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7和JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。

      当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。

      但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。

      分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

    11、偏向锁  

      偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

    12、轻量级锁

      轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

    13、重量级锁

      重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。

    14、自旋锁

      在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

    二、synchronized关键字理解

      使用了synchronized关键字可以轻松地解决多线程共享数据同步问题。

      synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。 

      synchronized取得的锁都是对象;每个对象只有一个锁(lock)与之相关联;实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

      synchronized的4种用法:

        1. 方法声明时使用,线程获得的是成员锁;

        2. 对某一代码块使用,synchronized后跟括号,括号里是变量,线程获得的是成员锁;

        3. synchronized后面括号里是一对象,此时,线程获得的是对象锁;

        4. synchronized后面括号里是类,此时,线程获得的是对象锁;

    三、CAS无锁机制

      CAS:Compare and Swap,即比较交换;

      jdk1.5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronized同步锁的一种乐观锁。jdk1.5之前java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是悲观锁;

      本身无锁,采用乐观锁的思想,在数据操作时对比数据是否一致,如果一致代表之前没有线程操作该数据,那么就会更新数据,如果不一致代表有县城更新则重试;

      CAS当中包含三个参数CAS(V,E,N),V标识要更新的变量,E标识预期值,N标识新值  

      运行过程:

        1.线程访问时,先会将主内存中的数据同步到线程的工作内存当中;

        2.假设线程A和线程B都有对数据进行更改,那么假如线程A先获取到执行权限;

        3.线程A先会对比工作内存当中的数据和主内存当中的数据是否一致,如果一致(V==E)则进行更新,不一致则刷新数据,重新循环判断;

        4.这时更新完毕后,线程B也要进行数据更新,主内存数据和工作内存数据做对比,如果一致则进行更新,不一致则将主内存数据重新更新到工作内存,然后循环再次对比两个内存中的数据,直到一致为止; 

      CAS无锁机制存在一个问题

        ABA问题,如果将原来A的值改为了B,然后又改回了A,虽然最终结果没有发生改变,但是在过程中是对该数据进行了修改操作

        解决该问题:在Java中并发包下有一个原子类:AtomicStampedReference,在该类当中通过版本控制判断值到底是否被修改

        解释:如果对值进行了更改则版本号+1,那么在CAS当中不仅仅对比变量的值,还要对比版本号,如果值和版本号都相等则代表没有被修改,如果有一方不相等代表进行过更改

        那么就从主内存中重新刷新数据到工作内存然后循环对比,直到成功为止~

    四、AQS

      AQS:全称AbstractQueueSynchronizer,抽象队列同步器,这个类在java.util.concurrent.locks包下

      它是一个底层同步工具类,比如CountDownLatch,Sammphore,ReentrantLock,ReentrantReadWriteLock等等都是基于AQS

        底层三个内容:

          1.state(用于计数器)

           2.线程标记(哪一个线程加的锁)

           3.阻塞队列(用于存放阻塞线程)

      AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架,如下图所示。AQS为一系列同步器依赖于一个单独的原子变量(state)的同步器提供了一个非常有用的基础。子类们必须定义改变state变量的protected方法,这些方法定义了state是如何被获取或释放的。

        

      J.U.C是基于AQS实现的,AQS是一个同步器,设计模式是模板模式。

      核心数据结构:双向链表 + state(锁状态)

      底层操作:CAS

        

    五、ReentrantLock底层实现

      ReentrantLock是基于AQS的,AQS是Java并发包中众多同步组件的构建基础,它通过一个int类型的状态变量state和一个FIFO队列来完成共享资源的获取,线程的排队等待等。AQS是个底层框架,采用模板方法模式,它定义了通用的较为复杂的逻辑骨架,比如线程的排队,阻塞,唤醒等,将这些复杂但实质通用的部分抽取出来,这些都是需要构建同步组件的使用者无需关心的,使用者仅需重写一些简单的指定的方法即可(其实就是对于共享变量state的一些简单的获取释放的操作)。

      无参构造器(默认为非公平锁) 

    public ReentrantLock() {
           sync = new NonfairSync();//默认是非公平的
    }

      synchronized是ReentrantLock内部实现的一个同步组件,它是Reentrantlock的一个静态内部类,继承于AQS;

      带布尔值的构造器(是否公平)

    public ReentrantLock(boolean fair) {
            sync = fair ? new FairSync() : new NonfairSync();//fair为true,公平锁;反之,非公平锁
    }

      此处可以指定是否采用公平锁,FailSync和NonFailSync亦为Reentrantlock的静态内部类,都继承于synchronized;

    六、ReentrantLock和synchronized之间的区别

    • synchronized 竞争锁时会一直等待;ReentrantLock 可以尝试获取锁,并得到获取结果
    • synchronized 获取锁无法设置超时;ReentrantLock 可以设置获取锁的超时时间
    • synchronized 无法实现公平锁;ReentrantLock 可以满足公平锁,即先等待先获取到锁
    • synchronized 控制等待和唤醒需要结合加锁对象的 wait() 和 notify()、notifyAll();ReentrantLock 控制等待和唤醒需要结合 Condition 的 await() 和 signal()、signalAll() 方法
    • synchronized 是 JVM 层面实现的;ReentrantLock 是 JDK 代码层面实现
    • synchronized 在加锁代码块执行完或者出现异常,自动释放锁;ReentrantLock 不会自动释放锁,需要在 finally{} 代码块显示释放

    七、ReentrantReadWriteLock(读写锁)

      相比Java中的锁(Locks in Java)里Lock实现,读写锁更复杂一些。

      假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写(译者注:也就是说:读-读能共存,读-写不能共存,写-写不能共存)。这就需要一个读/写锁来解决这个问题。

      Java5在java.util.concurrent包中已经包含了读写锁。

    package com.zn.lockTest;
    
    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    public class ReadWriteLock {
        //创建一个集合
        static Map<String,String> map=new HashMap<String,String>();
        //创建一个读写锁
        static ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
        //获取读锁
        static Lock readLock=lock.readLock();
        //获取写锁
        static Lock writeLock=lock.writeLock();
        //写操作
        public Object put(String key,String value){
            writeLock.lock();
            try {
                System.out.println("Write正在执行写操作~");
                Thread.sleep(100);
                String put = map.put(key, value);
                System.out.println("Write写操作执行完毕~");
                return put;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                writeLock.unlock();
            }
            return null;
    
        }
    
        //写操作
        public Object get(String key){
            readLock.lock();
            try {
                System.out.println("Read正在执行读操作~");
                Thread.sleep(100);
                String value = map.get(key);
                System.out.println("Read读操作执行完毕~");
                return value;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                readLock.unlock();
            }
            return null;
    
        }
    
        public static void main(String[] args) {
            ReadWriteLock lock=new ReadWriteLock();
            for (int i = 0; i < 10; i++) {
                int finalI = i;
                new Thread(()->{
                    try {
                        //写操作
                        lock.put(finalI +"","value"+finalI);
                        //读操作
                        System.out.println(lock.get(finalI+""));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }).start();
            }
    
        }
    }

    控制台效果:

      

    八、BlockingQueue阻塞队列的实现方式

      阻塞队列(BlockingQueue)是一个支持两个附加操作的队列,这两个附加的操作是:

        在队列为空时,获取元素的线程会等待队列变为非空;

        当队列满时,存储元素的线程会等待队列可用;

      阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器拿元素;

      在java中,BlockingQueue的接口位于java.util.concurrent包中,阻塞队列是线程安全的;

      在新增呢的concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题,通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利;

      常用的队列主要由以下两种:

        先进先出(FIFO)先插入的队列的元素也最先出队列,类似于排队的功能,从某种程度上来说这种队列也体现了一种公平性;

        后进后出(LIFO)后插入队列的元素最先出队列,这种队列优先处理最近发生的事件;

    1.ArrayBlockingQueue

      ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组,有边界意思就是它的容量是有限的,我们必须在其初始化的时候执行它的容量大小,容量大小一旦执行就不可改变;

      ArrayBlockingQueue是以先进先出的方式存储数据,最新插入的对象是尾部,最新移除的对象是头部;

    package com.zn.queueTest;
    
    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.TimeUnit;
    
    public class ArrayBlockingQueueTest {
        public static void main(String[] args) throws InterruptedException {
            ArrayBlockingQueue<String> arrays=new ArrayBlockingQueue<String>(3);
            arrays.add("张三");
            arrays.add("李四");
            arrays.add("王五");
    
            //添加阻塞队列
            arrays.offer("赵六",1, TimeUnit.SECONDS);
    
            //poll方法相当于消费了队列中的数据,队列的数据就会删除
            System.out.println(arrays.poll());
            System.out.println(arrays.poll());
            System.out.println(arrays.poll());
            System.out.println(arrays.poll());
        }
    }

    控制台效果:

      

    如果先出队一条数据,此时被阻塞的数据就可以添加进来:

    package com.zn.queueTest;
    
    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.TimeUnit;
    
    public class ArrayBlockingQueueTest {
        //出队一条数据
        public static void main(String[] args) throws InterruptedException {
            ArrayBlockingQueue<String> arrays=new ArrayBlockingQueue<String>(3);
            arrays.add("张三");
            arrays.add("李四");
            arrays.add("王五");
            //出队一条数据
            System.out.println(arrays.poll());
    
            //添加阻塞队列
            arrays.offer("赵六",1, TimeUnit.SECONDS);
    
            //poll方法相当于消费了队列中的数据,队列的数据就会删除
            System.out.println(arrays.poll());
            System.out.println(arrays.poll());
            System.out.println(arrays.poll());
        }
    }

    控制台效果:

      

    2.LinkedBlockingQueue

      LinkedBlockingQueue阻塞队列大小的配置时可选的,如果我们初始化时指定大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE容量,它的内部是一个链表;

      和ArrayBlockingQueue一样,LinkedBlockingQueue也是以先进先出的方式存储数据,最新插入的对象是尾部,最新移除的对象是头部;

    package com.zn.queueTest;
    
    import java.util.concurrent.LinkedBlockingQueue;
    
    public class LinkedBlockingQueueTest {
        public static void main(String[] args) throws InterruptedException {
            LinkedBlockingQueue linkedBlockingQueue=new LinkedBlockingQueue(3);
            linkedBlockingQueue.add("A");
            linkedBlockingQueue.add("B");
            linkedBlockingQueue.add("C");
            System.out.println(linkedBlockingQueue.poll());
            System.out.println(linkedBlockingQueue.size());
        }
    }

    控制台效果:

      

    3.PriorityBlockingQueue

      PriorityBlockingQueue是一个没有边界的队列,它的排序规则和java.util.PriorityQueue一样。需要注意,PriorityBlockingQueue中国允许插入null对象;

      所有插入PriorityBlockingQueue的对象必须实现java.lang.Comparable接口,队列优先级的排序规则就是按照我们对这个接口的实现来定义的;

      另外,我们可以从PriorityBlockingQueue获得一个迭代器Iterator,但这个迭代器并不保证按照优先级顺序进行迭代;

    package com.zn.queueTest;
    
    import java.util.concurrent.PriorityBlockingQueue;
    
    public class PriorityBlockingQueueTest {
        public static void main(String[] args) throws InterruptedException {
            PriorityBlockingQueue<String> priorityBlockingQueue=new PriorityBlockingQueue<String>(3);
            priorityBlockingQueue.add("AA");
            priorityBlockingQueue.add("BB");
            priorityBlockingQueue.add("CC");
            System.out.println(priorityBlockingQueue.poll());
            System.out.println(priorityBlockingQueue.size());
        }
    }

    控制台效果:

       

    4.SynchronousQueue

      SynchronousQueue队列内部仅容纳一个元素,当一个线程插入一个元素后会被阻塞,除非这个元素被另一个线程消费;

    九、ConcurrentLinkedQueue

      ConcurrentLinkedQueue:是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue性能好于BlockingQueue,它是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。头是最先加入的,尾是最近加入的,该队列不允许null元素;

      ConcurrentLinkedQueue重要方法:

        add()和offer()都是加入元素的方法(在ConcurrentLinkedQueue中这两个方法没有任务区别);

        poll()和peek()都是取头元素节点,区别在于前者会删除元素,后者不会;

    package com.zn.queueTest;
    
    import java.util.concurrent.ConcurrentLinkedQueue;
    
    public class ConcurrentLinkedQueueTest {
        public static void main(String[] args) {
            //准备队列
            ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
            //存放数据
            queue.offer("张三");
            queue.offer("李四");
            queue.offer("王五");
    
            //获取队列中数据个数
            System.out.println("队列中当前有:"+queue.size()+"个数据~");
            //获取队列中头数据  poll()方法相当于消费了队列中的数据,队列数据会随之删除
            System.out.println("获取队列中的数据:"+queue.poll());
            System.out.println("队列中当前有:"+queue.size()+"个数据~");
            //获取队列中数据,但是不会删除
            System.out.println("获取队列中的数据:"+queue.peek());
            System.out.println("获取队列中的数据:"+queue.peek());
            System.out.println("队列中当前有:"+queue.size()+"个数据~");
    
        }
    }

    控制台效果:

      

  • 相关阅读:
    【leetcode】Binary Search Tree Iterator
    【leetcode】Palindrome Partitioning II
    【leetcode】Best Time to Buy and Sell Stock III
    【leetcode】Best Time to Buy and Sell Stock II
    【leetcode】Longest Consecutive Sequence
    【leetcode】Factorial Trailing Zeroes
    【leetcode】Simplify Path
    【leetcode】Generate Parentheses
    【leetcode】Combination Sum II
    【leetcode】Combination Sum
  • 原文地址:https://www.cnblogs.com/Zzzzn/p/12586656.html
Copyright © 2011-2022 走看看