Java多线程、高并发面试总结


1、CAS(Compare and Swap):Unsafe类+CAS思想(自旋)
        是一条CPU并发原语。是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地〈native)方法来访问,
        Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像c的找针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
        AtomicInteger.compareAndSet(int expect,int update)
2、CAS缺点:(synchronized 加锁保证了数据一致性,但并发性下降)
    2.1、循环时间长,开销打(由于自旋)
    2.2、只能保证一个共享变量的原子操作
    2.3、引出来ABA问题?
3、CAS--->Unsafe--->CAS底层思想--->ABA--->原子引用更新--->如何规避ABA问题
    3.1、ABA问题:狸猫换太子!
            CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
            比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
            尽管线程otie的CAS操作成功,但是不代表这个过程就是没有问题的。
    3.2、如何解决ABA问题???  理解原子引用 + 新增一种机制,那就是修改版本号(类似时间戳标记)
    
4、ArrayList不安全:并发环境下,
        1.1.故障现象
         *  java.util.ConcurrentModificationException(并发修改异常)
        4.2.导致原因
            并发争抢修改导致  一个线程正在写入,另外一个线程进来抢夺,导致数据不一致异常
        4.3.解决方案
            3.1 List<String> list = new Vector<>();        ArrayList的前身     Vector.add()使用了synchronized修饰
           3.2 Collections.synchronizedList(new ArrayList<>());            Collection接口有一个Collections(提供一系列静态方法实现对各种集合的搜索、排序、线程安全等操作)辅助工具类,
           3.3 new CopyOnWriteArrayList<E>:复制出一个新容器,然后新的容器Object[] newElements里添加元素,添加完元素后再将原容器的引用指向新的容器setArray(newElements)
               这样做的好处是可以对CopyWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想
        4.4.优化建议(同样的错误不犯第二次)
        
        4.5.ArrayList 和 LinkedList、Vector 的区别:List接口一共有三个实现类,分别是ArrayList、Vector和LinkedList
            ①ArrayList基于动态数组实现的非线程安全的集合;LinkedList基于链表实现的非线程安全的集合。
                    Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,但实现同步需要很高的花费,因此,访问它比访问ArrayList慢。
            ②对于随机index访问的get和set方法,一般ArrayList的速度要优于LinkedList。因为ArrayList直接通过数组下标直接找到元素;LinkedList要移动指针遍历每个元素直到找到为止。
            ③新增和删除元素,一般LinkedList的速度要优于ArrayList。因为ArrayList在新增和删除元素时,可能扩容和复制数组;LinkedList实例化对象需要时间外,只需要修改指针即可。
            ④LinkedList集合不支持 高效的随机随机访问(RandomAccess)
            ⑤ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间
5、HashSet不安全:解决方法和上面类似:(HashSet底层结构为HashMap)----->public HashSet() {  map = new HashMap<>();    }  传进了容量初始值为16  负载因子为0.75: 16*0.75=12 也就是说,当容量达到了12的时就会执行扩容操作
        Map的value值固定为了Object类型的常量--->就是Set。
        为什么负载因子设置为0.75?负载因子是0.75的时,空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度比较低,提升了空间效率。
    解决方法:        
        Set<String> set = Collections.synchronizedSet(new HashSet<>());
        Set<String> set = new CopyOnWriteArraySet<>();
5、Map不安全:
    解决方法:①Map<String, String> map = new ConcurrentHashMap<>();
                    ②Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
6、原子更新应用:
    AtomicReference:用于对引用的原子更新
    AtomicMarkableReference:带版本戳的原子引用类型,版本戳为boolean类型。
    AtomicStampedReference:带版本戳的原子引用类型,版本戳为int类型。
    
7、公平锁/非公平锁/可重入锁/递归锁/自旋锁的理解,手写自旋锁。
    7.1 公平锁和非公平锁
            公平锁:多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到;
            非公平锁:多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能造成优先级反转或者饥饿现象;
        区别:
            并发环境中,每个线程获取锁时先查看此锁维护的等待队列,为空或者当前线程是等待队列的第一个,就占有锁,否则加入等待队列。以后按照FIFO的规则中取到自己。
            非公平锁上来就直接尝试占有锁,如果尝试失败再采用类似公平锁那种方式。
        对Java ReentrantLock()而言:通过构造函数指定该锁是公平锁还是非公平锁,默认是非公平锁(false)。非公平锁的优点在于吞吐量比公平锁大。
        对synchronized而言,也是一种非公平锁。
    7.2 可重入锁(又名递归锁):同一线程外层获得锁之后,内层递归函数仍能获取该锁的代码,在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。也就是说:线程可以进入任意一个它已经拥有的锁所同步着的代码块。
        ReentrantLock/synchronized就是典型的可重入锁
        最大的作用:避免死锁
    7.3 自旋锁:指尝试获取锁的线程不会立即阻塞,而是采用循环的方式尝试获取锁。
            好处:减少线程上下文切换的消耗
            缺点:循环会消耗CPU
    7.4 独占锁(写锁)/共享锁(读锁)/互斥锁:
        独占锁:指该锁只能被一个线程所持有。对ReentrantLock和Synchronized而言就是独占锁。
        共享锁:该锁可被多个线程所持有。对ReentrantReadWriteLock 其读锁是共享锁(--ReentrantReadWriteLock.readLock()--),写锁是独占锁(--ReentrantReadWriteLock.writeLock()--)。      
                读-读  能共存
                读-写  不能共存
                写-写  不能共存
    加锁的方法:synchronized/Lock/ReadWriteLock/Semaphore
8、CountDownLatch/CyclicBarrier/Semaphore的使用?
    8.1 CountDownLatch:让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒,(减到0才继续执行,类似所有人都离开后班长才能锁门)
         CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,调用线程会被阻塞。其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),
         当计数器的值变为零时,因调用await方法被阻塞的线程会被唤醒,继续执行。
    8.2 CyclicBarrier (加到指定值才继续干活,类似所有人都到齐后才能开会)的字面意思是可循环(Cyclic)使用的屏障( Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,
         线程进入屏障通过cyclicBarrier的await(方法。
    8.3 Semaphore 信号量(可加可见,类似抢停车位)。两个目的:一是用于多个共享资源的互斥使用,另一个是用于并发线程数的控制。
        acquire():抢占一个许可,
        release():释放一个许可
9、阻塞队列
    9.1 队列+阻塞队列  
            阻塞队列:首先是一个队列,而一个阻塞队列在数据结构中起的作用:线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素。
            当阻塞队列是空时,从队列获取元素将会被阻塞。直到其他线程往空的队列里插入新的元素。
            当阻塞队列是满的时,往队列里添加元素将会被阻塞。知道其他线程从队列中移除一个或者多个元素或者完全清空队列。
    9.2 为什么用?有什么好处?
            在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒
        为什么需要BlockingQueue(阻塞队列)?
            好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了
            在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
    9.3 BlockingQueue的核心方法
        ★ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
        ★LinkedBlockingQueue:由链表结构组成的有界(但默认大小为Integer.MAX_VALUE)阻塞队列。
        ★SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
        PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
        DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
        LinkedTransferQueue:由链表组成的无界阻塞队列。
        LinkedBlockingDeque:由链表组成的双向阻塞队列。
        
                        抛出异常            特殊值            阻塞                超时
        插入             add(e)                offer(e)            put(e)            offer(e,time,unit)
        移除            remove(e)            poll()            take()            poll(time,unit)
        检查            element()            peek()            不可用            不可用
        
        抛出异常:队列满时,add插入元素抛IllegalStateException:Queue full异常
                        队列空时,remove移除元素抛NoSuchElementException
        特殊值:   插入:成功返回true,失败false
                        移除:成功返回出队列的元素,队列里没有就返回null
        一直阻塞:队列满,生产者线程继续往队列里put元素,队列一直阻塞直到put数据or响应中断推出。    
                        队列空时,消费者线程往队列里take元素,队列会一直阻塞消费者线程直到队列可用。
        超时退出:当队列满时,队列会阻塞生产者线程一定时间,超过时限后生产者线程会退出。
    9.4 阻塞队列用在哪里
        1、生产者消费者模式
        2、线程池
        3、消息中间件
10、synchronized和Lock有什么区别?用新的Lock有什么好处?
    10.1 原始构成
        synchronized是关键字,属于JVM层面,monitorenter和monitorenterexit
            底层通过monitor对象完成,其实wait/notify等方法也依赖于monitor对象只有在同步代码块或者方法中才能调用wait/notify等方法。
        Lock是具体的类:java.util.concurrent.locks.Lock  是api层面的锁。
    10.2 使用方法
        synchronized  不需要用户去手动释放锁,当synchronized代码块执行完后系统会自动让线程释放对锁的占用。
        Lock  需要用户手动释放锁,若没有主动释放锁会可能导致死锁的出现。需要lock()和unlock()方法配合try/catch/finally语句块完成。
    10.3 等待是否可中断
        synchronized不可中断,除非抛异常或者正常运行完成。
        ReentrantLock 可中断,1.设置超时方法,tryLock(long timeout,TimeUnit unit)
                                            2.lockInterruptibly()放代码块中,调用interrupt()方法可中断
    10.4 加锁是否公平
        synchronized 非公平锁
        ReentrantLock 两者都可以,默认非公平锁,构造方法可以传入boolean值,true为公平锁,false为非公平锁。
    10.5 锁绑定多个条件Condition
        synchronized  没有
        ReentrantLock  用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是想synchronized要么随即唤醒一个线程要么唤醒全部线程。
11、线程池?ThreadPoolExecutor的理解
    11.1 为什么使用线程池,优势是什么?
        ①new Thread的弊端
            a. 每次new Thread新建对象性能差。
            b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
            c. 缺乏更多功能,如定时执行、定期执行、线程中断。
        线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量 超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
        特点:线程复用;控制最大并发数;管理线程。
        优势:
            第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
            第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
            第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
    11.2 架构说明
        Java中线程池是通过Executor框架实现的,该框架中用到了Executor、Executors、ExecutorService、ThreadPoolExecutor这几个类
        重点:
        ★★Executors.newFixedThreadPool(int )   执行长期的任务,性能好很多   指定一个线程池有几个处理线程
        ★★Executors.newSingleThreadExecutor() 一池一个处理线程  适用于一个任务一个任务执行的场景
        ★★Executors.newCachedThreadPool() 一池N个处理线程   适用于执行很多短期异步的小程序或者负载较轻的服务
        Executors.newScheduledThreadPool(int ) 创建一个定长线程池,支持定时及周期性任务执行。
        Executors.newWorkStealingPool(int )  java8新增的  使用在目前机器上可用的处理器作为他的并行级别
        
        源代码:(5个参数)
        public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
        }
        
        public static ExecutorService newSingleThreadExecutor() {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
        }
        
        public static ExecutorService newCachedThreadPool() {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
        }        
    11.3 重要参数的介绍(七大参数)前五个是上面源码表现出来的  后两个是底层的
        corePoolSize(int):线程池中的常驻核心线程数
        maximumPoolSize(int):线程池中能够同时容纳的最大线程数,此值必须大于等于1
        keepAliveTime(long):多余的空闲线程的存活时间。当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止。
        unit:(TimeUnit)keepAliveTime的单位
        workQueue(BlockingQueue<Runnable>):任务队列,被提交但尚未被执行的任务
        threadFactory(ThreadFactory):表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可
        handler(RejectedExecutionHandler):拒绝策略,当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)
    11.4 线程池的底层工作原理
        1.在创建了线程池后,等待提交过来的任务请求。
        2.当调用execute()方法添加一个请求任务时,线程池会做如下判断:
            ① 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
            ② 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
            ③ 如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
            ④ 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
        3.当一个线程完成任务时,它会从队列中取下一个任务来执行。
        4.当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:
            如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
            所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小。
    11.5 线程池的拒绝策略
        等待队列也已经排满了,再也塞不下新任务了,同时,线程池中的max线程也达到了,无法继续为新的任务服务。这时我们就需要拒绝策略机制合理地处理这个问题。
        AbortPolicy(默认):直接抛出RejectedExecutionException异常 阻止系统正常运行
        CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者
        DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
        DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常,如果允许任务丢失,这是最好的一种解决方案。
        以上内置策略均实现了RejectedExecutionHandler接口
    11.6 对于固定数目的(newFixedThreadPool)/单一的(newSingleThreadExecutor)/可变的(newCachedThreadPool)三种创建线程池的方法,用的哪个多?超级大坑!
        答案是一个都不用,我们生产上只能使用自定义的
        Executors中JDK已经提供了 为什么不用?
            1) FixedThreadPool和 singleThreadPool:
                允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM(Out Of Memory 内存用完了)。
            2) cachedThreadPool和scheduledThreadPool:
                允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
    11.7 合理配置线程池是如何考虑的?
        获取当前服务器CPU的核数:Runtime.getRuntime().availableProcessors() 
        1.CPU密集型
            即该任务需要大量的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程)
            任务配置尽可能少的线程数量:CPU核数+1个线程的线程池
        2.IO密集型
            ①:IO密集型任务线程并不是一直执行,则应该配置尽可能多的线程,如CPU核数 * 2
            ②:IO密集型时,大部分线程都阻塞,故需要多配置线程数:
                参考公式:CPU核数 / (1 - 阻塞系数)      阻塞系数在0.8~0.9之间
                比如8核CPU:8 /(1-0.9) = 80个线程数
    11.8 死锁编码及定位分析
        1.产生死锁的原因:两个或两个以上的进程在执行过程中,因抢夺资源而造成的互相等待的现象,若无外力干涉那它们都无法推进下去。
            如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
        2.如何解决?
            jps命令定位进程号--->jstack找到死锁查看
        
        

    
    

    

    
    
    
    
    
    
    
    
    
    

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值