CAS----->UnSafe----->CAS底层思想----->ABA----->原子引用更新----->如何规避ABA问题
1.volatile 是什么? volatile 是 Java 虚拟机提供的轻量级的同步机制 * 保证可见性 * 禁止指令排序 * 不保证原子性 禁止指令重排小总结: volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象 先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个: 一是保证特定操作的执行顺序; 二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性); 由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。 内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。 对volatile变量进行写操作时,会在写操作后加入一条store屏障指令,将工作内存中的共享变量值刷新回到主内存。 对Volatile变量进行读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。
2.CAS是什么? CAS的全称为Compare-And-Swap(比较并交换),它是一条CPU并发原语。 它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。 底层原理? 自旋锁、UnSafe类 小结: CAS(CompareAndSwap) 比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止。 CAS应用 CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。 缺点: 1.循环时间长开销很大 我们可以看到getAndAddInt方法执行时,有个do while,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。 2.只能保证一个共享变量的原子操作 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。 3.引发ABA问题(狸猫换太子)
3.ABA问题怎么产生的? CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。 比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。 尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。 ABA问题的解决(AtomicStampedReference版本号原子引用) 原子引用 + 新增一种机制,那就是修改版本号(类似时间戳),它用来解决ABA问题。
4.集合类不安全问题? List安全问题 扩容是1.5倍 List<String> list = new Vector<>();//解决一 List<String> list = Collections.synchronizedList(new ArrayList<>());//解决二 List<String> list = new CopyOnWriteArrayList<>();//解决三 (推荐) CopyOnWrite容器即写时复制的容器。待一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行copy,复制出一个新的容器Object[] newELements,然后新的容器Object[ ] newELements里添加元素,添加完元素之后,再将原容器的引用指向新的容器setArray (newELements)。 这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁(区别于Vector和Collections.synchronizedList()),因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的 容器。 HashSet安全问题 Set<String> set = Collections.synchronizedSet(new HashSet<>());//解决一 List<String> list = new CopyOnWriteArraySet<>();//解决二 (推荐) HashMap安全问题 Map<String,String> map = Collections.synchronizedMap(new HashMap<>());//解决一 Map<String,String> map = new ConcurrentHashMap<>();//解决二
5.ConcurrentHashMap1.7和1.8的区别? 去除 1.7 Segment + HashEntry + Unsafe 的实现, 改为 1.8 Synchronized + CAS + Node + Unsafe 的实现 其实 Node 和 HashEntry 的内容一样,但是HashEntry是一个内部类。 用 Synchronized + CAS 代替 Segment ,这样锁的粒度更小了,并且不是每次都要加锁了,CAS尝试失败了在加锁。 put()方法中 初始化数组大小时,1.8不用加锁,因为用了个 sizeCtl 变量,将这个变量置为-1,就表明table正在初始化。
6.HashMap扩容机制? HashMap的底层有数组 + 链表(红黑树)组成,数组的大小可以在构造方法时设置,默认大小为16,数组中每一个元素就是一个链表,jdk7之前链表中的元素采用头插法插入元素,jdk8之后采用尾插法插入元素,由于插入的元素越来越多, 查找效率就变低了,所以满足某种条件时,链表会转换成红黑树。随着元素的增加,HashMap的数组会频繁扩容,如果构造时不赋予加载因子默认值,那么负载因子默认值为0.75,数组扩容的情况如下: 1:当添加某个元素后,数组的总的添加元素数大于了 数组长度 * 0.75(默认,也可自己设定),数组长度扩容为两倍。(如开始创建HashMap集合后,数组长度为16,临界值为16 * 0.75 = 12,当加入元素后元素个数超过12,数组长 度扩容为32,临界值变为24) 2:在没有红黑树的条件下,添加元素后数组中某个链表的长度超过了8,数组会扩容为两倍.(如开始创建HashMAp集合后,假设添加的元素都在一个链表中,当链表中元素为8时,再在链表中添加一个元素,此时若数组中不存在红黑树, 则数组会扩容为两倍变成32,假设此时链表元素排列不变,再在该链表中添加一个元素,数组长度再扩容两倍,变为64,假设此时链表元素排列还是不变,则此时链表中存在10个元素,这是HashMap链表元素数存在的最大值,此时,再加 入元素,满足了链表树化的两个条件(1:数组长度达到64, 2:该链表长度达到了8),该链表会转换为红黑树 HashMap扩容分为两步: 扩容:创建一个新的Entry空数组,长度是原数组的2倍。 ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。 头插法和尾插法: 当HashMap要在链表里插入新的Entry时,在Java 8之前是将Entry插入到链表头部,在Java 8开始是插入链表尾部(Java 8用Node对象替代了Entry对象)。 Java 7插入链表头部,是考虑到新插入的数据,更可能作为热点数据被使用,放在头部可以减少查找时间。 Java 8改为插入链表尾部,原因就是防止环化。因为resize的赋值方式,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置,在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放 到了新数组的不同位置上。 1.7和1.8有什么不同? JDK1.7 是先扩容,在添加。具体put是否扩容需要两个条件: 1、 存放新值的时候当前已有元素的个数必须大于等于阈值 2、 存放新值的时候当前存放数据发生hash碰撞(当前key计算的hash值换算出来的数组下标位置已经存在值) JDK1.8 是先添加,在扩容。具体put是否扩容需要满足一个条件: 当table中存储值的个数大于等于threshold的时候,进行扩容。容量为原来的2倍。 红黑树转化条件:数组的长度大于64的时候,链表长度大于8才会从链表转换为红黑树 红黑树是一种含有红黑结点并能自平衡的二叉查找树。它必须满足下面性质: 性质1:每个节点要么是黑色,要么是红色。 性质2:根节点是黑色。 性质3:每个叶子节点(NIL)是黑色。 性质4:每个红色结点的两个子结点一定都是黑色。 性质5:任意一结点到每个叶子结点的路径都包含数量相同的黑结点。
7.锁的分类? 公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。在高并发的情况下,有可能会造成优先级反转或者饥饿现象 并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认是非公平锁。 关于两者区别: 公平锁: Threads acquire a fair lock in the order in which they requested it. 公平锁就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。 非公平锁: a nonfair lock permits barging: threads requesting a lock can jump ahead of the queue of waiting threads if the lockhappens to be available when it is requested. 非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。 非公平锁的优点在于吞吐量比公平锁大。 对于Synchronized而言,也是一种非公平锁 可重入锁(也叫做递归锁): 指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。 也即是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。 ReentrantLock/synchronized就是一个典型的可重入锁。 可重入锁最大的作用是避免死锁。 自旋锁(Spin Lock) 是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。 提到了互斥同步对性能最大的影响阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态完成,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段 时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程 “稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会 释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。 独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁 共享锁:指该锁可被多个线程所持有。
8.CountDownLatch、CyclicBarrier、Semaphore? CountDownLatch: 让一线程阻塞直到另一些线程完成一系列操作才被唤醒。 CountDownLatch主要有两个方法(await(),countDown())。 当一个或多个线程调用await()时,调用线程会被阻塞。其它线程调用countDown()会将计数器减1(调用countDown方法的线程不会阻塞),当计数器的值变为零时,因调用await方法被阻塞的线程会被唤醒,继续执行。 假设一个自习室里有7个人,其中有一个是班长,班长的主要职责就是在其它6个同学走了后,关灯,锁教室门,然后走人,因此班长是需要最后一个走的,那么有什么方法能够控制班长这个线程是最后一个执行,而其它线程是随机执行的 CyclicBarrier: CyclicBarrier的字面意思就是可循环(Cyclic)使用的屏障(Barrier)。它要求做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续 干活,线程进入屏障通过CyclicBarrier的await方法。 CyclicBarrier与CountDownLatch的区别:CyclicBarrier可重复多次,而CountDownLatch只能是一次。 Semaphore: 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。 正常的锁(concurrency.locks或synchronized锁)在任何时刻都只允许一个任务访问一项资源,而 Semaphore允许n个任务同时访问这个资源。
9.阻塞队列? 阻塞队列,顾名思义,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致如下图所示: 线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素。 当阻塞队列是空时,从队列中获取元素的操作将会被阻塞。 当阻塞队列是满时,往队列里添加元素的操作将会被阻塞。 试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。 同样试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程从列中移除一个或者多个元素或者完全清空队列后使队列重新变得空闲起来并后续新增 为什么用?有什么好处? 在多线程领域:所谓阻塞,在某些情况下余挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒 为什么需要BlockingQueue? 好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了 在Concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。 架构介绍 种类分析: ArrayBlockingQueue:由数组结构组成的有界阻塞队列。 LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列。 PriorityBlockingQueue:支持优先级排序的无界阻塞队列。 DelayQueue:使用优先级队列实现妁延迟无界阻塞队列。 SynchronousQueue:不存储元素的阻塞队列。 LinkedTransferQueue:由链表结构绒成的无界阻塞队列。 LinkedBlockingDeque:由链表结构组成的双向阻塞队列。 BlockingQueue的核心方法: 性质 抛出异常: 当阻塞队列满时:在往队列中add插入元素会抛出 IIIegalStateException:Queue full 当阻塞队列空时:再往队列中remove移除元素,会抛出NoSuchException element()获取队首元素 特殊值: 插入方法,成功true,失败false 移除方法:成功返回出队列元素,队列没有就返回空 阻塞: 当阻塞队列满时,生产者继续往队列里put元素,队列会一直阻塞生产线程直到put数据or响应中断退出。 当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用。 超时退出: 当阻塞队列满时,队里会阻塞生产者线程一定时间,超过限时后生产者线程会退出
10.synchronized 和 Lock 有什么区别? 1.synchronized属于JVM层面,属于java的关键字 monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象 只能在同步块或者方法中才能调用 wait/ notify等方法) monitorexit Lock是具体类(java.util.concurrent.locks.Lock)是api层面的锁 2.使用方法: synchronized:不需要用户去手动释放锁,当synchronized代码执行后,系统会自动让线程释放对锁的占用。 ReentrantLock:则需要用户去手动释放锁,若没有主动释放锁,就有可能出现死锁的现象,需要lock() 和 unlock() 配置try catch语句来完成 3.等待是否中断 synchronized:不可中断,除非抛出异常或者正常运行完成。 ReentrantLock:可中断,可以设置超时方法 设置超时方法,trylock(long timeout, TimeUnit unit) lockInterrupible() 放代码块中,调用interrupt() 方法可以中断 4.加锁是否公平 synchronized:非公平锁 ReentrantLock:默认非公平锁,构造函数可以传递boolean值,true为公平锁,false为非公平锁 5.锁绑定多个条件Condition synchronized:没有,要么随机,要么全部唤醒 ReentrantLock:用来实现分组唤醒需要唤醒的线程,可以精确唤醒,而不是像synchronized那样,要么随机,要么全部唤醒
11.线程池? 线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。 它的主要特点为:线程复用,控制最大并发数,管理线程。 优点: 降低资源消耗。通过重复利用己创建的线程降低线程创建和销毁造成的消耗。 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 3个常用方式? Executors.newFixedThreadPool(int) 1.创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 2.newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的 LinkedBlockingQueue。 (适用:执行长期任务,性能好很多) Executors.newSingleThreadExecutor() 1.创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。 2.newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,它使用的 LinkedBlockingQueue。 (适用:一个任务一个任务执行的场景) Executors.newCachedThreadPool() 1.创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 2.newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。 (适用:执行很多短期异步的小程序或者负载较轻的服务器) 7大参数? 1.corePoolSize:线程池中的常驻核心线程数 在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程。 当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。 2.maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1 3.keepAliveTime:多余的空闲线程的存活时间。 当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止 4.unit:keepAliveTime的单位。 5.workQueue:任务队列,被提交但尚未被执行的任务。 6.threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可。 7.handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)。 *******************************************代码******************************************* ExecutorService threadPool = new ThreadPoolExecutor( 2,//核心线程池大小 5,//最大核心线程池大小 3,//超时了没有人调用就会释放 TimeUnit.SECONDS,//超时单位 new LinkedBlockingDeque<>(3),//阻塞队列 Executors.defaultThreadFactory(),//线程工厂,创建线程池的,一般不用变 new ThreadPoolExecutor.DiscardOldestPolicy());//队列满了,尝试去和最早的竞争,竞争不到也会丢掉 *******************************************代码******************************************* 四种拒绝策略? 等待队列也已经排满了,再也塞不下新任务了同时,线程池中的max线程也达到了,无法继续为新任务服务。这时候我们就需要拒绝策略机制合理的处理这个问题。 JDK拒绝策略: 1.AbortPolicy(默认):直接抛出 RejectedExecutionException异常阻止系统正常运知。 2.CallerRunsPolicy:"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。 3.DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。 4.DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。 以上内置拒绝策略均实现了RejectedExecutionHandler接口。 1.在创建了线程池后,等待提交过来的任务请求。 2.当调用execute()方法添加一个请求任务时,线程池会做如下判断: 1.如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务; 2.如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列; 3.如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务; 4.如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。 3.当一个线程完成任务时,它会从队列中取下一个任务来执行。 4.当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断: 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小。
12.线程池实际中使用哪一个? (超级大坑警告)你在工作中单一的/固定数的/可变的三种创建线程池的方法,你用那个多? 答案是一个都不用,我们生产上只能使用自定义的 Executors 中JDK已经给你提供了,为什么不用? 3.【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。 4.【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors 返回的线程池对象的弊端如下: 1) FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2) CachedThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。 阿里巴巴《Java 开发手册》
13.合理配置线程池你是如何考虑的? CPU密集型 CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。 CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程), 而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些。 CPU密集型任务配置尽可能少的线程数量: 一般公式:(CPU核数+1)个线程的线程池 lO密集型 由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数 * 2。 IO密集型,即该任务需要大量的IO,即大量的阻塞。 在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。 所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。 IO密集型时,大部分线程都阻塞,故需要多配置线程数: 参考公式:CPU核数/ (1-阻塞系数) 阻塞系数在0.8~0.9之间 比如8核CPU:8/(1-0.9)=80个线程数
14.死锁是什么? 死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而 陷入死锁。 产生死锁主要原因: 系统资源不足 进程运行推进的顺序不合适 资源分配不当 发生死锁的四个条件: 互斥条件,线程使用的资源至少有一个不能共享的。 至少有一个线程必须持有一个资源且正在等待获取一个当前被别的线程持有的资源。 资源不能被抢占。 循环等待。 如何解决死锁问题: 破坏发生死锁的四个条件其中之一即可。
15.线程等待唤醒的实现方法? 1.Object对象中的wait()方法可以让线程等待,使用Object中的notify()方法唤醒线程; 1.必须都在同步代码块内使用; 2.调用wait,notify的对象是加锁对象; 3.notify必须在wait后执行才能唤醒; 4.wait后能释放锁对象,线程处于wait状态; 2.使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程; 1.必须在lock同步代码块内使用; 2.signal必须在await后执行才能唤醒; 3.LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程; 1.不需要锁块; 2.unpark()可以在park()前唤醒;
16.介绍一下LockSupport? 概念:用于创建锁和其他同步类的基本线程阻塞原语;是线程等待唤醒的升级版本;他是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞后也有对应的唤醒方法。 特点:无需在加锁环境中;park()和unpark()的作用分别是阻塞线程和解除线程;可先唤醒后等待; 原理:LockSupport使用了一种许可证机制(Permit)来实现阻塞和唤醒,每个线程都有一个许可证(Permit);其中,许可证只有两个值,1和0;默认是0;我们可以将许可证视为信号量,只是许可证不能超过1; 常用方法: park():permit默认为0,一旦调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park()方法会被唤醒;然后会将permit重新设置为0后返回。 *******************************************代码******************************************* //LockSupport public static void park() { UNSAFE.park(false, 0L); } // Unsafe public native void park(boolean var1, long var2); *******************************************代码******************************************* unpark(thread): 会将线程的许可证设置为1(多次调用unpark,许可证不会累加)并自动唤醒park()的等待线程,被阻塞的线程将立即返回; *******************************************代码******************************************* //LockSupport public static void unpark(Thread thread) { if (thread != null) UNSAFE.unpark(thread); } // Unsafe public native void unpark(Object var1); *******************************************代码******************************************* 常见问题: LockSupport为什么可以先唤醒线程后阻塞线程但不会阻塞? 答:因为unpark()方法获得了一个许可证,许可证值为1,再调用park()方法,就可消费这个许可证,所以不会阻塞; 为什么唤醒两次后阻塞两次,最终还是会阻塞? 答:如果线程A调用两遍park(),线程B调用两边unpark(),那么只会解锁一个park(),因为许可证最多只能为1,不能累加;
17.AQS是什么? AQS(AbstractQueuedSynchronizer):抽象的队列同步器 一般我们说的 AQS 指的是 java.util.concurrent.locks 包下的 AbstractQueuedSynchronizer,但其实还有另外三种抽象队列同步器: AbstractOwnableSynchronizer、AbstractQueuedLongSynchronizer 和 AbstractQueuedSynchronizer AQS 抽象的队列同步器,是用来构建锁或者其他同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型的变量表示持有锁的状态;当有线程获取不到锁时,就将线程加 入该队列中,通过CAS自旋和LockSupport.part()的方式,维护state变量,达到并发同步的效果。其中,ReentranLock,CountDownLatch,ReentrantReadWriteLock,Semaphore底层都是AQS AQS的原理图如下: 其中:CLH是Craig,Landin andHagersten队列,是一个单向链表,AQS中的队列是CLH扩展的虚拟双向FIFO队列; AQS的核心原理? 原理:AQS使用一个violatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,将每条要抢占资源的线程封装成一个Node节点来完成锁的分配,通过CAS完成对state值的修改; 核心:state+CLH带头节点的双端队列 其内部属性与继承关系如下: AbstractQueuedSynchronizer: AQS本身是由state和CLH双端队列组成,CLH原理是一个Node节点的双向循环链表 *******************************************代码******************************************* public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { // 同步state成员变量,0表示无人占用锁,大于1表示锁被占用需要等待; private volatile int state; /*CLH队列 * <pre> * +------+ prev +-----+ +-----+ * head | | <---- | | <---- | | tail * +------+ +-----+ +-----+ * </pre> */ // 通过state自旋判断是否阻塞,阻塞的线程放入队列,尾部入队,头部出队 static final class Node{} private transient volatile Node head; private transient volatile Node tail; } *******************************************代码******************************************* AbstractQueuedSynchronizer.Node *******************************************代码******************************************* static final class Node { // 表示线程以共享的模式等待锁 static final Node SHARED = new Node(); // 表示线程以独占的方式等待锁 static final Node EXCLUSIVE = null; //表示线程获取锁的请求已经取消了 static final int CANCELLED = 1; //表示线程准备解锁 static final int SIGNAL = -1; // 表示节点在等待队列红,节点线等待唤醒 static final int CONDITION = -2; // 表当前节点线程处于共享模式,锁可以传递下去 static final int PROPAGATE = -3; // 表示节点在队列中的状态 volatile int waitStatus; volatile Node prev;// 前序指针 volatile Node next;// 后序指针 volatile Thread thread; // 当前节点的线程 Node nextWaiter;// 指向下一个处于Condition状态的节点 final Node predecessor();//一个方法,返回前序节点prev,没有的话抛出NPE(空指针异常) } *******************************************代码******************************************* 公平锁与非公平锁的区别? 公平锁:多个线程按照线程调用lock()的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。 优点:所有的线程都能得到资源,不会饿死在队列中。 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。 非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。 缺点:这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。 *******************************************代码******************************************* // 非公平锁 final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } // 公平锁 final void lock() { acquire(1); } *******************************************代码******************************************* 总结:我们发现,底层lock()方法,非公平锁多了一个compareAndSetState(),其不公平的原因,就是线程调用lock()的时候,如果发现当前没人占用的时候,直接就抢占过来,否则就进入CLH队列,等待调用。 因此非公平锁就是每次调用lock的线程和CLH队列头节点后的第一个线程进行竞争,竞争成功,调用lock的线程抢到锁,否则就是头节点后的第一个线程抢占成功。公平锁的话,就是所有线程都进入CLH队列,然后FIFO抢占依次获得锁。 讲这个的目的就是为了后面讲加锁解锁原理的时候,不再解释为什么是依次获取锁而不是抢占;
18.从 ReentrantLock 进入 AQS? ReentrantLock 锁? ReentrantLock 类是 Lock 接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类完成线程访问控制的 ReentrantLock 的原理? ReentrantLock 实现了 Lock 接口,在 ReentrantLock 内部聚合了一个 AbstractQueuedSynchronizer 的实现类 如图:ReentrantLock 的原理.png 公平锁 & 非公平锁 ? 在 ReentrantLock 内定义了静态内部类,分别为 NoFairSync(非公平锁)和 FairSync(公平锁)。如图 :ReentrantLock1.png ReentrantLock 的构造函数:不传参数表示创建非公平锁;参数为 true 表示创建公平锁;参数为 false 表示创建非公平锁。如图:ReentrantLock2.png 在 ReentrantLock 中,NoFairSync 和 FairSync 中 tryAcquire() 方法的区别,可以明显看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors() 如图:ReentrantLock3.png hasQueuedPredecessors() 方法是公平锁加锁时判断等待队列中是否存在有效节点的方法 执行流程: 先来看看线程 A(客户 A)的执行流程: A:第一次执行 lock() 方法: lock().png 由于第一次执行 lock() 方法,state 变量的值等于 0,表示 lock 锁没有被占用,此时执行 compareAndSetState(0, 1) CAS 判断,可得 state == expected == 0,因此 CAS 成功,将 state 的值修改为 1 再来看看 setExclusiveOwnerThread() 方法做了啥:将拥有 lock 锁的线程修改为线程 A setExclusiveOwnerThread().png 再来看看线程 B(客户 B)的执行流程: 第二次执行 lock() 方法: 由于第二次执行 lock() 方法,state 变量的值等于 1,表示 lock 锁没有被占用,此时执行 compareAndSetState(0, 1) CAS 判断,可得 state != expected,因此 CAS 失败,进入 acquire() 方法 acquire() 方法主要包含如下几个方法,下面我们一个一个来讲解 acquire().png tryAcquire(arg) 方法的执行流程 tryAcquire()1.png 先来看看 tryAcquire() 方法,诶,怎么抛了个异常?别着急,仔细一看是 AbstractQueuedSynchronizer 抽象队列同步器中定义的方法,既然抛出了异常,就证明父类强制要求子类去实现 tryAcquire()2.png tryAcquire()3.png 这里以非公平锁 NonfairSync 为例,在 tryAcquire() 方法中调用了 nonfairTryAcquire() 方法,注意,这里传入的参数都是 1 nonfairTryAcquire(acquires) 正常的执行流程: nonfairTryAcquire(acquires)1.png 在 nonfairTryAcquire() 方法中,大多数情况都是如下的执行流程:线程 B 执行 int c = getState() 时,获取到 state 变量的值为 1,表示 lock 锁正在被占用;于是执行 if (c == 0) { 发现条件不成立, 接着执行下一个判断条件 else if (current == getExclusiveOwnerThread()) {,current 线程为线程 B,而 getExclusiveOwnerThread() 方法返回正在占用 lock 锁的线程,为线程 A,因此 tryAcquire() 方法最后会 return false,表示并没有抢占到 lock 锁 补充:getExclusiveOwnerThread() 方法返回正在占用 lock 锁的线程(排他锁,exclusive) 如图:getExclusiveOwnerThread().png nonfairTryAcquire(acquires) 比较特殊的执行流程: nonfairTryAcquire(acquires)2.png 第一种情况是,走到 int c = getState() 语句时,此时线程 A 恰好执行完成,让出了 lock 锁,那么 state 变量的值为 0,当然发生这种情况的概率很小,那么线程 B 执行 CAS 操作成功后,将占用 lock 锁的线程 修改为自己,然后返回 true,表示抢占锁成功。其实这里还有一种情况,需要留到 unlock() 方法才能说清楚 第二种情况为可重入锁的表现,假设 A 线程又再次抢占 lock 锁(当然示例代码里面并没有体现出来),这时 current == getExclusiveOwnerThread() 条件成立,将 state 变量的值加上 acquire,这种情况下也 应该 return true,表示线程 A 正在占用 lock 锁。因此,state 变量的值是可以大于 1 的 继续往下走,执行 addWaiter(Node.EXCLUSIVE) 方法: acquire().png 在 tryAcquire() 方法返回 false 之后,进行 ! 操作后为 true,那么会继续执行 addWaiter() 方法 来看看 addWaiter() 方法做了些啥? addWaiter().png 之前讲过,Node 节点用于封装用户线程,这里将当前正在执行的线程通过 Node 封装起来(当前线程正是抢占 lock 锁没有抢占到的线程) 判断 tail 尾指针是否为空,双端队列此时还没有元素呢~肯定为空呀,那么执行 enq(node) 方法,将封装了线程 B 的 Node 节点入队 enq(node) 方法:构建双端同步队列 enq(node).png 也许看到这里的代码有点蒙,需要有些前置知识,在双端同步队列中,第一个节点为虚节点(也叫哨兵节点),其实并不存储任何信息,只是占位。 真正的第一个有数据的节点,是从第二个节点开始的。 第一次执行 for 循环:如图:第一次执行 for 循环.png 现在解释起来就不费劲了,当线程 B 进来时,双端同步队列为空,此时肯定要先构建一个哨兵节点。此时 tail == null,因此进入 if(t == null) { 的分支,头指针指向哨兵节点,此时队列中只有一个节点,尾节点即是 头结点,因此尾指针也指向该哨兵节点 第二次执行 for 循环:如图:第二次执行 for 循环.png 现在该将装着线程 B 的节点放入双端同步队列中,此时 tail 指向了哨兵节点,并不等于 null,因此 if (t == null) 不成立,进入 else 分支。以尾插法的方式,先将 node(装着线程 B 的节点)的 prev 指向之前 的 tail,再将 node 设置为尾节点(执行 compareAndSetTail(t, node)),最后将 t.next 指向 node,最后执行 return t结束 for 循环 补充:compareAndSetTail(t, node) 方法的实现 如图:compareAndSetTail(t, node).png 注意:哨兵节点和 nodeB 节点的 waitStatus 均为 0,表示在等待队列中 acquireQueued() 方法的执行: 执行完 addWaiter() 方法之后,就该执行 acquireQueued() 方法了,这个方法有点东西,我们放到后面再去讲它 最后来看看线程 C(客户 C)的执行流程: 线程 C 和线程 B 的执行流程很类似,都是执行 acquire() 中的方法 acquire().png 但是在 addWaiter() 方法中,执行流程有些区别。此时 tail != null,因此在 addWaiter() 方法中就已经将 nodeC 添加至队尾了 addWaiter()2.png 执行完 addWaiter() 方法后,就已经将 nodeC 挂在了双端同步队列的队尾,不需要再执行 enq(node) 方法 nodeC入队后.png 补前面的坑:acquireQueued() 方法的执行逻辑 acquireQueued().png 先来看看看看 acquireQueued() 方法的源代码,其实这样直接看代码有点懵逼,我们接下来举例来理解。注意看:两个 if 判断中的代码都放在 for( ; ; ) 中执行,这样可以实现自旋的操作 线程 B 的执行流程 线程 B 执行 addWaiter() 方法之后,就进入了 acquireQueued() 方法中,此时传入的参数为封装了线程 B 的 nodeB 节点,nodeB 的前驱结点为哨兵节点,因此 final Node p = node.predecessor() 执行完后, p 将指向哨兵节点。哨兵节点满足 p == head,但是线程 B 执行 tryAcquire(arg) 方法尝试抢占 lock 锁时还是会失败,因此会执行下面 if 判断中的 shouldParkAfterFailedAcquire(p, node) 方法,该方法的 代码如下: *******************************************代码******************************************* private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) /* * This node has already set status asking a release * to signal it, so it can safely park. */ return true; if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } *******************************************代码******************************************* 哨兵节点的 waitStatus == 0,因此执行 compareAndSetWaitStatus(pred, ws, Node.SIGNAL) ,CAS 操作将哨兵节点的 waitStatus 改为 Node.SIGNAL(-1) 注意:compareAndSetWaitStatus(pred, ws, Node.SIGNAL) 调用 unsafe.compareAndSwapInt(node, waitStatusOffset, expect, update); 实现,虽然 compareAndSwapInt() 方法内无自旋,但是在 acquireQueued() 方法中的 for( ; ; ) 能保证此自选操作成功(另一种情况就是线程 B 抢占到 lock 锁) 执行完上述操作,将哨兵节点的 waitStatus 设置为了 -1 将哨兵节点的 waitStatus 设置为了 -1.png 执行完毕将退出 if 判断,又会重新进入 for( ; ; ) 循环,此时执行 shouldParkAfterFailedAcquire(p, node) 方法时会返回 true,因此此时会接着执行 parkAndCheckInterrupt() 方法 acquireQueued()2.png parkAndCheckInterrupt().png 线程 B 调用 park() 方法后被挂起,程序不会然续向下执行,程序就在这儿排队等待 线程 C 的执行流程: 线程 C 最终也会执行到 LockSupport.park(this); 处,然后被挂起,进入等待区 总结: 如果前驱节点的 waitstatus 是 SIGNAL 状态(-1),即 shouldParkAfterFailedAcquire() 方法会返回 true,程序会继续向下执行 parkAndCheckInterrupt() 方法,用于将当前线程挂起 根据 park() 方法 API 描述,程序在下面三种情况会继续向下执行: 1.被 unpark 2.被中断(interrupt) 3.其他不合逻辑的返回才会然续向下执行 因上述三种情况程序执行至此,返回当前线程的中断状态,并清空中断状态。如果程序由于被中断,该方法会返回 true 可总算要 unlock() 了: 线程 A 执行 unlock() 方法: A 线程终于要 unlock() 了吗?真不容易啊! unlock() 方法调用了 sync.release(1) 方法 unlock().png release() 方法的执行流程: release().png 其实主要就是看看 tryRelease(arg) 方法和 unparkSuccessor(h) 方法的执行流程,这里先大概说以下,能有个印象:线程 A 即将让出 lock 锁,因此 tryRelease() 执行后将返回 true,表示礼让成功,head 指针指 向哨兵节点,并且 if 条件满足,可执行 unparkSuccessor(h) 方法 tryRelease(arg) 方法的执行逻辑: 又是 AbstractQueuedSynchronizer 类中定义的方法,又是抛了个异常 tryRelease(arg)1.png tryRelease(arg)2.png 线程 A 只加锁过一次,因此 state 的值为 1,参数 release 的值也为 1,因此 c == 0。将 free 设置为 true,表示当前 lock 锁已被释放,将排他锁占有的线程设置为 null,表示没有任何线程占用 lock 锁 tryRelease(arg)3.png unparkSuccessor(h) 方法的执行逻辑: 在 release() 方法中获取到的头结点 h 为哨兵节点,h.waitStatus == -1,因此执行 CAS操作将哨兵节点的 waitStatus 设置为 0,并将哨兵节点的下一个节点(s = node.next = nodeB)获取出来,并唤醒 nodeB 中封装的线程(if (s == null || s.waitStatus > 0) 不成立,只有 if (s != null) 成立) unparkSuccessor(h).png 执行完上述操作后,当前占用 lock 锁的线程为 null,哨兵节点的 waitStatus 设置为 0,state 的值为 0(表示当前没有任何线程占用 lock 锁) 哨兵节点的 waitStatus 设置为 0.png 杀个回马枪:继续来看 B 线程被唤醒之后的执行逻辑 再次回到 lock() 方法的执行流程中来,线程 B 被 unpark() 之后将不再阻塞,继续执行下面的程序,线程 B 正常被唤醒,因此 Thread.interrupted() 的值为 false,表示线程 B 未被中断 parkAndCheckInterrupt().png 回到上一层方法中,此时 lock 锁未被占用,线程 B 执行 tryAcquire(arg) 方法能够抢到 lock 锁,并且将 state 变量的值设置为 1,表示该 lock 锁已经被占用 acquireQueued()3.png 接着来研究下 setHead(node) 方法:传入的节点为 nodeB,头指针指向nodeB节点;将 nodeB 中封装的线程置为 null(因为已经获得锁了);nodeB 不再指向其前驱节点(哨兵节点)。这一切都是为了将 nodeB 作为新的哨兵节点 setHead(node).png 执行完 setHead(node) 方法的状态如下图所示 状态图.png 将 p.next 设置为 null,这是原来的哨兵节点就是完全孤立的一个节点,此时 nodeB 作为新的哨兵节点 状态图2.png 哇哦,通透,线程 C 也是类似的执行流程 第一个考点:我相信你应该看过源码了,那么AQS里面有个变量叫State,它的值有几种? 答:3个状态:没占用是0,占用了是1,大于1是可重入锁 第二个考点:如果锁正在被占用,AB两个线程进来了以后,请问这个总共有多少个Node节点? 答:答案是3个,分别是哨兵节点、nodeA、nodeB
19、线程的五种状态? 1) 新建 当用new关键字创建一个线程时,还没调用start 就是新建状态。 2) 就绪 调用了 start 方法之后,线程就进入了就绪阶段。此时,线程不会立即执行run方法,需要等待获取CPU资源。 3) 运行 当线程获得CPU时间片后,就会进入运行状态,开始执行run方法。 4) 阻塞 当遇到以下几种情况,线程会从运行状态进入到阻塞状态。 调用sleep方法,使线程睡眠。 调用wait方法,使线程进入等待。 当线程去获取同步锁的时候,锁正在被其他线程持有。 调用阻塞式IO方法时会导致线程阻塞。 调用suspend方法,挂起线程,也会造成阻塞。 需要注意的是,阻塞状态只能进入就绪状态,不能直接进入运行状态。因为,从就绪状态到运行状态的切换是不受线程自己控制的,而是由线程调度器所决定。只有当线程获得了CPU时间片之后,才会进入运行状态。 5) 死亡 当run方法正常执行结束时,或者由于某种原因抛出异常都会使线程进入死亡状态。另外,直接调用stop方法也会停止线程。但是,此方法已经被弃用,不推荐使用。
20、synchronized锁升级? synchronized锁有四种状态:无锁,偏向锁、轻量级锁和重量级锁,下面介绍四种状态和其之间的转换。 1.无锁 当一个对象被创建之后,还没有线程进入,这个时候对象处于无锁状态,其Mark Word中的信息如上表所示。 2.偏向锁 当锁处于无锁状态时,有一个线程A访问同步块并获取锁时,会在对象头和栈帧中的锁记录记录线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来进行加锁和解锁,只需要简单的测试一下对象头中的线程ID和当前线程是否 一致。 3.轻量级锁 在偏向锁的基础上,又有另外一个线程B进来,这时判断对象头中存储的线程A的ID和线程B不一致,就会使用CAS竞争锁,并且升级为轻量级锁,会在线程栈中创建一个锁记录(lock Record),将Mark Word复制到锁记录中,然后线程 尝试使用CAS将对象头的Mark Word替换成指向锁记录的指针,如果成功,则当前线程获得锁;失败,表示其他线程竞争锁,当前线程便尝试CAS来获取锁。 4.重量级锁 当线程没有获得轻量级锁时,线程会CAS自旋来获取锁,当一个线程自旋10次之后,仍然未获得锁,那么就会升级成为重量级锁。 成为重量级锁之后,线程会进入阻塞队列(EntryList),线程不再自旋获取锁,而是由CPU进行调度,线程串行执行。