JUC
- 1、线程和进程的区别:
- 2、 多线程实现的方式:
- 3、 为什么使用多线程呢
- 4、线程的五种状态(线程的生命周期)
- 5、线程阻塞的三种情况
- 6、 线程死亡的三种状态
- 7、 死锁:多个线程因为等待相互的资源,线程间等待,就会出现死锁
- 8、死锁具备的条件
- 9、如何避免死锁
- 10、 sleep和wait的区别:
- 11、 为什么调用start方法时run方法会执行,为什么不直接执行run方法
- 12、3种让线程等待唤醒的方式
- 13、为什么只能在同步锁或同步代码块中调用wait/nodify方法:因为wait/nodify是依赖于moniter对象的,而moniter对象只有加入sychnorized锁才会获得监视器锁(moniter)的执行权
- 14、 sychrnoized可以实现什么?sychrnoized的作用有哪些?
- 15、 sychrnoized可以保证三大特性,但是一个重量级的操作?
- 16、 sychrnoized可以作用在哪里:
- 17、sychrnoized的底层实现原理:
- 18、 sychrnoized锁升级的原理
- 19、 为什么要引入偏向锁和轻量级锁?
- 20、JVM对sychrnoized锁的优化有哪些?
- 21、 volatile可以保证可见性和有序性
- 22、 volatile无法保证像i++这种的原子操作,他能保证读是原子性,写也是原子性,但是没法保证读写都是原子性
- 23、 CAS完全依赖于硬件的原子操作,执行顺序连续,中途不允许打断,可以把一个看起来需要多步操作才会完成的行为通过一条指令就完成,像自增i++(CAS是一种无锁,非阻塞算法)(也是一把乐观锁)
- 24、CAS和sychrnoized的区别:
- 25、 CAS带来的问题
- 26、 Java中的引用类型:
- 27、threadLocal是什么:
- 28、 如何保证线程隔离,独享
- 29、 threadLocal的实现原理:
- 30、threadLocals是个map类型的,那么使用什么容器存储数据:
- 31、 threadLocal的应用场景:
- 32、 如何解决OOM的问题
- 33、为什么使用弱引用
- 34、 ThreadLocal的OOM问题:
- 35、在哪里可以使用
- 36、sych和threadlocal的区别:
- 37、 threadlocal只能当前线程可见么
- 38、 sychrnoized和ReentranLock都是可重入锁
- 39、可重入锁又分显示锁和隐式锁
- 40、 AQS是JUC的基石,是一种锁框架,就是用来管理因为没抢到锁的阻塞队列
- 41、AQS实现的基础
- 42、 和AQS相关的
- 43、
- 44、 在Java中有个类叫AbstractQueuedSychroized,里面有个内部类Node,Node中封装了各种参数
- 45、ReentrantLock
- 46、 ReentrantLock的加锁过程:
- 47、 详细的加入非公平锁的过程:
- 48、公平锁和非公平锁的底层差别:
- 49、 非公平锁体现在
- 50、AQS同步器是一个用来构建同步组件的基础框架,ReentrantLock就是基于AQS的
- 51、AQS设计模式使用的是模板方法:AQS中被protected修饰的方法要由继承AQS的子类来重写实现,同步状态的获取和释放,getState,setState,compareAndSetState等
- 52、AQS的原理
- 53、 acquired为独占式锁的同步状态的获取,这个方法中线程入队被中断后,线程也不会被移除
- 54、引申出来的知识点:
- 55、AcquiredInterruptedify独占式锁响应中断,(lock比sycn多了中断和超时)
- 56、 AcqiredNanos独占式锁超时等待,(除了响应中断还有超时等待)
- 57、 release独占式锁释放同步状态
- 58、结点的状态:根据结点的状态判断当前线程是否被阻塞
- 59、 同步组件--countdownlatch倒计时:锁的模式是共享锁
- 60、同步组件--cyclicbarrier循环屏障:锁的模式是共享锁
- 61、 countdownlatch和cyclicbarrier的区别:锁的模式是共享锁
- 62、同步组件--semaphore信号量:锁的模式是共享锁
- 63、线程池的好处:线程的创建销毁开销是很大的(线程切换,CPU调度),为了减少不必要的开销,可以用线程池,而且线程的执行效率也会加快
- 64、 线程池中的7个参数
- 65、 使用LinkedBlockingQueue这种队列的才可能导致OOM,长期占用CPU资源;反而SynchronousQueue会使得CPU100%
- 66、线程池的4种拒绝策略
- 67、 线程池的处理流程:(提交优先级)
- 68、提交任务时execute和submit的区别:(执行优先级)源码中的线程复用?
- 69、线程池的五种状态
- 70、在高并发操作线程池时,ThreadPoolExecutor的参数:
- 71、 阻塞队列:
- 72、 到底线程池的内部运行机制会是怎样的呢?
- 73、 线程池设计中得到的启发:
- 74、自定义拒绝策略:
- 75、手动创建线程池:
- 76、 关闭线程池:
- 77、Lock的特性
- 78、公平锁和非公平锁的区别
- 79、三个线程顺序打印abc
- 80、 Thread.currentThread.interrupted()表示中断线程
1、线程和进程的区别:
- 线程是进程中处理器任务调度的最小单位,进程是操作系统分配资源的基本单位,比如QQ运行起来,操作系统会分配资源给他
- 操作系统里会有多个进程,进程间相互不影响,进程又包含多个线程,cpu执行的是每个线程
- 进程都是相互独立的,线程间共享进程的全局变量和地址空间等资源
- 一个进程崩溃后,在保护模式下,别的进程不受影响;但是一个进程中的线程崩溃了,那这个进程就会崩溃
2、 多线程实现的方式:
-
实现Runable,Callable接口类,重写方法,,多个线程共享同一个对象,所以这种适合多个线程处理同一个资源的情况,要访问当前线程时,必须Thread.currentThread(),
其中runnable和callable的区别:
1)runnable重写的是run方法,callable重写的是call方法
2)callable必须要有返回值,runnable不需要返回值
3)callable需要捕获异常,runnable不需要
4)callable可以拿到future任务对象,了解任务的执行情况和执行结果等 -
继承Thread类,可以直接通过this拿到当前线程,重写run方法,调用start方法就可以启动线程
3、 为什么使用多线程呢
- 现在不都是千万级的并发量,使用多线程处理高并发才是主流
- 而且现在的多核时代,多个cpu同时处理线程,提高cpu的利用率
4、线程的五种状态(线程的生命周期)
- 新建状态new,new Thread就会进入新建状态
- runnable就绪状态:调用start方法线程就绪,等待cpu调度
- running运行状态:获得cpu的调度执行权
- waiting等待状态:由于几种原因放弃cpu的执行权
1.程序由running态执行sleep/join方法进入等待状态,sleep完之后进入就绪态
2.程序由running态执行wait方法,进入等待状态,执行notify接触wait状态后,拿到同步锁进入就绪态
3.程序由running态被sychroinzed锁定,得到同步锁后进入就绪态 - dead死亡状态:线程执行完或退出run方法后,线程生命周期结束
5、线程阻塞的三种情况
- 运行态的线程执行Object.wait方法后,进入等待状态,必须通过nodify方法才能去锁池中等待获得同步锁
- 运行态的线程要获得锁时,锁被其他线程使用,就进入锁池中等待获得同步锁
- 运行态的线程执行sleep或join方法后,进入等待状态,sleep完之后,主动进入就绪态
6、 线程死亡的三种状态
- run/call方法执行结束后,线程正常结束
- 抛出异常,线程被迫结束
- 调用线程的stop方法,线程结束
7、 死锁:多个线程因为等待相互的资源,线程间等待,就会出现死锁
8、死锁具备的条件
- 一个线程同一时刻只处理一个资源
- 当线程是因为获取资源而被阻塞时,是不会释放他已经获得的资源,并且系统不能强行剥夺已经获得的资源
- 进程间因为获取资源出现的循环等待的关系
9、如何避免死锁
- 可以一次性申请到所有的资源
- 当线程因为获取资源而被阻塞申请不到资源时,释放已经获得到的资源
- 锁排序:当线程在获得A锁,B锁后才会获得资源,那就规定好锁的顺序,只有获得A锁的线程才允许获得B锁,这样就会避免死锁的问题
10、 sleep和wait的区别:
- sleep是Thread的静态方法,线程进入阻塞态睡眠指定时间后,变为就绪态,sleep在睡眠期间不会释放锁
- wait是object的方法,被锁定之后,必须通过nodify去唤醒,wait方法要在sychroinzed代码块中使用,会释放锁,别的资源可以获得锁
- 因为释放锁的时机,所以sleep用于暂停线程的执行;wait用于线程间的通信
11、 为什么调用start方法时run方法会执行,为什么不直接执行run方法
- 线程通过new创建出来,调用start后进入就绪态,等待cpu的调度执行,当cpu调度时就会执行run方法中的内容,这就是多线程工作
- 直接调用run方法,会把run方法当成main线程下的普通方法进行执行,就不是多线程了
12、3种让线程等待唤醒的方式
- Object中的wait和nodify
- condition接口中的await和signal方法
- LockSupport类中的park和unpark方法
13、为什么只能在同步锁或同步代码块中调用wait/nodify方法:因为wait/nodify是依赖于moniter对象的,而moniter对象只有加入sychnorized锁才会获得监视器锁(moniter)的执行权
- 一开始moniter的进入数为0,然后有个线程获得了moniter的执行权,那moniter就设置为1
- 当该线程已经获得了锁,再次重新获得锁时,moniter在原来的基础上加1
- 所以sychrnoized是可重入锁
-
- moniterenter加锁;moniterexit解锁,解锁可能有两次是为了保证出现异常的时候,也释放锁
14、 sychrnoized可以实现什么?sychrnoized的作用有哪些?
- 原子性:用moniter监视器锁确保获得锁是互斥的
- 可见性:一个线程修改了变量会立即写回到主存中,确保对其他线程都是可见的
- 有序性:程序编译成字节码文件,操作系统执行时为了效率,有可能会指令重排,造成线程安全问题
15、 sychrnoized可以保证三大特性,但是一个重量级的操作?
16、 sychrnoized可以作用在哪里:
- sychrnoized可以作用在普通方法上:那么要想获得同步锁就需要获得当前类的实例化对象,拿到对象头中的moniter监视器锁,就能拿到sychrnoized锁了
- sychrnoized作用在静态方法上:要想获得同步锁就要竞争当前的类,虽然一个类可能有多个实例化对象
- sychrnoized作用在代码块上:sychrnoized(this)中的this指向的是当前类的class对象,要想获得锁就去竞争看谁拿到当前类
17、sychrnoized的底层实现原理:
- 给代码块加入sychrnoized锁后,反编译(javac将Java编译成class文件,再javap -v class文件)就可以看到Java虚拟机的指令
- 当线程获得锁之后,一开始moniter锁的进入数是0,当一个线程获得锁之后,就获得了moniter的执行权,moniter被设置为1(每个对象的对象头都有moniter指令,所以Java中的对象都可以作为锁)
- 当这个线程再次获得锁之后,moniter在原来的基础上+1,所以sychrnoized是可重入锁
- 释放锁的时候moniter-1直到为0,该线程失去了moniter的执行权
- 当线程要获得锁时,锁被其他线程占有了,那么该线程进入阻塞状态,被放到锁池中,直到获得了同步锁就进入就绪态,等待CPU的调度
- 给方法加入sychrnoized锁后,常量池中多个ACC_SYNCHRONIZED指令,当指令的访问标志被设置为可访问,就可以分配到moniter锁,其实和代码块是一样的,只不过多了ACC_SYNCHRONIZED指令
18、 sychrnoized锁升级的原理
- 无锁,偏向锁,轻量级锁,重量级锁
- 当一个线程获得了锁,该锁的对象头有个threadId用于记录当前线程的ID,把锁的状态变为偏向锁,当有别的线程再次请求锁时,只需要比较threadId和当前线程的ID是否一致,如果一致不需要申请锁,直接获得锁
- 如果threadId和当前线程的ID不一致,把锁的状态变为轻量级锁
- 通过自旋循环一定的次数去获得锁
- 如果拿不到,就升级为重量级锁(重量级锁存在竞争关系,同时申请锁的开销也变大)
- 这就是锁的升级
- 锁升级的目的:减少锁带来的性能消耗
19、 为什么要引入偏向锁和轻量级锁?
- 因为重量级锁的开销很大,要申请一把重量级锁时,必须要通过kernal内核,状态切换,上下文切换什么的
- 而很多情况下,可能只有一个线程,或者多个线程交替获得锁,这些线程的执行时间都不长,这时候申请重量级锁就不划算了,而且又没什么竞争关系
20、JVM对sychrnoized锁的优化有哪些?
- 锁消除:在编译期间,扫描上下文,去除掉那些不竞争的锁(只有重量级锁才会竞争)
- 锁粗化:扩大锁的范围,避免反复加锁和解锁
- 自旋锁:为了避免在操作系统层面线程挂起,就会用自旋锁,因为通常这些线程可能持续时间短,切换线程不值得,就循环等待锁的释放
21、 volatile可以保证可见性和有序性
- volatile保证可见性,一般为了效率每个线程4都有缓冲内存区,而每个线程并不与主内存直接交互,别的线程看不到更新后的操作,这就会造成变量不可见
- 用volatile修饰的变量,会即使的将改变的值更新到主内存中,并通知其他用到这个变量的线程
- volatile保证有序性,被volatile修饰的变量,她的读写,写写不是允许换位置的,在JVM底层是通过内存屏障来实现的,内存屏障是一条CPU汇编指令,在这条指令之前的指令必须全部执行完才会执行后面的指令
22、 volatile无法保证像i++这种的原子操作,他能保证读是原子性,写也是原子性,但是没法保证读写都是原子性
23、 CAS完全依赖于硬件的原子操作,执行顺序连续,中途不允许打断,可以把一个看起来需要多步操作才会完成的行为通过一条指令就完成,像自增i++(CAS是一种无锁,非阻塞算法)(也是一把乐观锁)
- 在Java中的unsafe类中就有CompareandSwap的方法
- 但是无法直接使用CAS的方法,必须通过反射的方式调用
- 所以在实际中,要实现原子操作,比如自增,她本身就不是原子操作,从主内存中读出来,自增,写回主内存,要实现原子操作就可以使用AutomicInteger,用set方法根据预期值设置更新值,然后incremet拿到自增的结果值,AutomaicInteger和Integer一样,只不过用CAS封装了原子操作
AutomaicInteger ai=new AutomaicInteger(1);
ai.compareAndSet(1,2);
ai.IncrementAndGet()
24、CAS和sychrnoized的区别:
- 既然可以通过加sychroinzed,为什么需要CAS
CAS是一把自旋锁,用来代替sychroinzed重量级锁的
25、 CAS带来的问题
- CAS是一把自旋锁,循环执行时间过长,开销大,CPU性能消耗大:开销大:i=0,i++写回主内存时会判断是否是原来的0,如果不是0,是2,就会读2,再加,依次类推,直到加到1为止
- CAS只能给一个变量设置原子操作(可以用互斥锁确保原子性;把多个变量封装成对象,通过AutomicReeference确保原子性)
- CAS的ABA问题:i=0;i++写回主内存时会判断是否是原来的0,问题出现了,此0非彼0,并发操作下,这个0被别的线程修改成了3,然后又将3修改为0,写回主内存了,这样可能会出现一些问题
- 如何解决ABA问题:
1.通过AutomicStampedreference控制版本号的方式,确保CAS的正确性
2.通过AtomicMarableReference设置布尔类型,确保CAS的正确性
26、 Java中的引用类型:
- 强引用:平常创建的对象,栈中的变量指向堆中的地址,这是强引用(内存泄露也不会被回收)
- 软引用: 内存不够时,gc就会被回收(适合做缓存,内存够用,缓存中的数据就待着,内存不够,清除缓存)
- 弱引用:gc就会被回收(用在ThreadLocal中的key上,避免OOM)
- 虚引用:随时被回收
27、threadLocal是什么:
- threadlocal相当于把变量copy到本地内存中做操作,在多线程操作共享数据时,一个线程拿到变量,可能这个变量已经被别的线程修改了,所以就要用到Threadlocal,让当前线程独享变量,作用域就是当前线程
- 很好的实现隔离
- 一个thread线程可能有多个threadlocal对象
28、 如何保证线程隔离,独享
- 创建threadlocal对象
- 在set值时,先拿到当前线程,在拿到线程对应的ThreadLocalMap,给key赋threadlocal的值,value赋值,先哈希&数组长度,拿到存储的索引,可能有哈希碰撞的问题出现,根据索引往后遍历,直到不碰撞为止
- 在get时,先拿到当前线程,在拿到线程对应的map,拿到getEntry,如果不为空,返回对应的value,如果为空,就初始化map,根据初始化容量创建entry,然后哈希存值(如果还没有set就get,就会触发initValue初*始化value的值)
29、 threadLocal的实现原理:
1.每个线程Thread都有成员变量threadlocals,这是个map类型的,key是ThreadLocal实例,value就是我们设置的值
2.但是没办法直接操作threadlocals,但是可以操作ThreadLocal,对ThreadLocal的get,set相当于threadlocals的get,set操作(get的是value)
30、threadLocals是个map类型的,那么使用什么容器存储数据:
- 先根据哈希魔数拿到哈希值,这个哈希魔数每次哈希后都加一,是为了让哈希码更加散列,和初始化容量-1做与运算,就可以拿到存储的下标位置(容器的初始化容量是16,要求容器必须是2的n次幂)
- 如果出现哈希碰撞,(索引+1直到不碰撞为止),假设数据长度是16,如果到了[15]还冲突,就会去[0]的为止,相当于环形数组
- 扩容的阈值是初始容量乘以2/3,这样应该是为了降低哈希碰撞的可能性
31、 threadLocal的应用场景:
- spring中的声明式事务的实现基础就是ThreadLocal
- 在开启事务后,事务可能在不同的方法中传递,都要使用同一个connection(事务可能在不同的方法中传递),使用Threadlocal可以保证是同一个connecion,实现隔离性
32、 如何解决OOM的问题
- threadLocal变量一定要是弱引用,因为强引用她回收不了,就OOM了么
- 当key为空时,要及时的remove调用整个Entry,否则找不到value,就OOM了
- 调用ThreadLocal的get,set,remove方法里都有ExpungeEntry这个方法,该方法里有if key=null,value=null,entry=null,就能被gc了
- 所以最好的就是在不使用ThreadLocal时,就调用remove方法,防止OOM问题
- 而且尤其对于线程池里的thread一定要remove,因为是线程复用的,有可能就get到了上一次的value值
33、为什么使用弱引用
- 当不使用threadLocal时,就把它置空,栈里的强引用就没了
- 如果堆里的threadLocal是强引用时,就会有OOM的问题
34、 ThreadLocal的OOM问题:
- 一般的thread执行完被回收,threadLocal没有了强引用,能被gc,不会有OOM问题
- 对于线程池里的核心thread时循环使用的,每个线程对应的ThreadLoal被强引用着,也就是栈里的强引用一直存在,堆里的ThreadLocal也不会被回收,就算是弱引用
- 好,如果我们使用完了ThreadLocal变量,手动释放ThreadLocal对象,也就是ThreadLocal=null,也就是把强引用干掉了,gc时,就会回收key会被回收
- 但是线程池里的线程一直存在,栈里的thread强引用一直在,强引用链就会一直存在,栈的Thread-堆的Thread-ThreadLocalmap-Entry-value,造成OOM
35、在哪里可以使用
- 频繁的操作一个共享变量,但是又不希望每次都分配这个变量
- 使用threadLocal搞个副本
36、sych和threadlocal的区别:
- sych保证的是多线程并发操作数据也可以拿到正确的结果
- 而threadlocal它虽然可以保证拿到争取的的结果,但是他是把线程赋值了一个副本
- 网站计数,使用sych可以,而threadlocal只能统计单个IP登录了几次
37、 threadlocal只能当前线程可见么
- InheritableThreadlocal可以让当前线程的子线程也可以访问当前线程的值
38、 sychrnoized和ReentranLock都是可重入锁
- 一开始获得sychrnoized锁对象,反编译后的Java虚拟机指令moniter的进入数被置为0
- 表示这个线程已经获得了锁,当这个线程中的内部方法也要获得锁时,moniter就在原来的基础上加1
- 可重入锁可以避免死锁
39、可重入锁又分显示锁和隐式锁
- sychrnoized是隐式锁JVM层面实现;
- ReentranLock是显示锁
40、 AQS是JUC的基石,是一种锁框架,就是用来管理因为没抢到锁的阻塞队列
41、AQS实现的基础
- CAS:状态被volatile修饰能实现可见性,为了保证线程安全用CAS
- 队列:用来存放没抢到锁的资源,封装成结点(这个结点是个内部类,封装了各种参数),放到FIFO队列中
1.同步队列/阻塞队列:双向链表,用在存储线程的状态,头部是一个哨兵结点,实际不存储数据,只是为了操作方便
2.条件队列/等待队列:condition - 状态:被volatile修饰的state,为0时表示没有线程获得锁,为1时表示有线程获得了锁,如果锁可重入时,state就可以加1
- lockSupport的park和unpark方法实现等待阻塞(不消耗CPU资源,线程安全)
- 封装的各种参数
- Thread:当前要封装的线程
- prev:结点的前指针
- next:结点的后指针
- head:头结点,tail:尾结点
- waitStates:每个结点线程的状态:
- cancelled:线程要获得锁的请求取消了
- condition:结点线程准备好了,等待被唤醒
- signal:结点线程等待锁的释放
42、 和AQS相关的
- ReentranLock,CountDoownLatch,ReentrantReadWriteLock,Semaphore
43、
- 当线程要拿到资源的前提必须要先获得锁,在并发的情况下,只有一个线程会获得锁,那别的线程就会被阻塞,被阻塞就一定会有队列去管理那些可能获得锁的线程,这个队列是CLH队列的变体FIFO(CLH是个单向的队列,道格李把塔改成了双向队列),将暂时获取不到资源的线程封装成结点Node实现锁的分配,通过CAS的方式分配锁
44、 在Java中有个类叫AbstractQueuedSychroized,里面有个内部类Node,Node中封装了各种参数
- Thread:表示被封装成结点的线程
- prev:结点的前指针
- next:结点的后指针
- head:队列的头;tail:队列的尾
- waitStates:表示结点的状态
- cancelled:线程获取锁的请求取消了
- condition:结点线程等待被唤醒
- signal:结点线程准备好了,就等待锁释放了
- shared:共享模式
- exclusive:独占模式
45、ReentrantLock
- ReentrantLock 是基于AQS的(AQS是队列同步器)
- 使用ReentrantLock 加锁,解锁方法时,其实用的sync的lock和release方法,sync是继承AQS的内部抽象类,sync中有公平锁和非公平锁(其实是AQS的),创建一个ReentrantLock实例。 这相当于使用ReentrantLock(false)
- 公平锁和非公平锁的源码几乎一样,不过公平锁多了个判断hasQueued(判断等待队列中是否有人排队,先到先得)
46、 ReentrantLock的加锁过程:
1.先尝试加锁
2.加锁失败后,进入等待队列中
3. 进入队列中,进入阻塞状态
47、 详细的加入非公平锁的过程:
1.用户层面用reentranlock的lock上锁,底层用的是SYNC类
- 进入Sync类中内部类Nofair类
- Nofair类中的加锁方法lock
- 会先通过CAS尝试获得锁CompareAndSetState(0,1)//就是尝试把AQS类内部类Node中的state置为1,是否有线程还在占有锁
- 如果加锁不成功,进入acquire方法进入同步队列
- acquire中调用了三个重要的方法;第一个tryAcquired再一次尝试加锁,判断当前State是否有人占用,判断当前有没有线程获得锁
- 第二次方法addWaiter执行排队逻辑,将线程封装成Node结点,这里有两种情况:队列未被初始化会调用enq方法,此时会默认生成一个node哨兵结点(用于占位),然后将传递的node参数,以CAS自旋的方式作为后继结点添加到队列中;第二种情况是已经有了队列直接将结点以尾插法的方式添加到队列中
- 第三个方法acquiredQueue执行阻塞park逻辑,用CAS自旋,先判断当前线程的前辈结点是否是头节点,如果是就尝试获得锁tryAcquired,如果获得的到就会对FIFO重新维护,把node结点置为head结点,把原来的head结点的指针断开,gc时,可达性算法达不到,就会被回收;如果获得不到锁或者前辈结点不是头结点,先判断前辈结点的waitSate状态,是不是Signal(默认是-1),如果是就park阻塞当前线程,如果是0,就设置为-1,如果大于0,表示前辈结点被取消了,重新维护下FIFO队列
- 当线程执行完后,要释放锁unlock,使用的是sync的release方法
9.首先会尝试释放锁,将当前的独占线程置为null,将states状态置为0,表示没有线程占用锁 - 然后通过unpark唤醒哨兵结点的后一个结点
- 然后acquiredQueue中的CAS自旋没有结束,就会将头节点的下一个结点置为头节点,
- 将state状态置为1,把头节点的下一个结点设置为当前占用线程
- 哨兵结点会被gc回收掉,下一个结点置为null,线程也置为空,把下一个结点置为哨兵结点
48、公平锁和非公平锁的底层差别:
- 进入Sync类中内部类fair类
- fair类中的加锁lock方法
- 一开始不会通过CAS尝试加锁,而是直接入队
- 在入队时进入tryAcquire方法也是先判断是否会获得锁,比非公平锁多了个判断操作,当前是否有队列hasQueuePredcessors
49、 非公平锁体现在
- 进入队列的顺序,可能先B后C,也可能先C后B(入队之前已经给过机会,去抢占了)
- 如果B先被唤醒,D来了会跟B抢占锁,这对C是不公平的
50、AQS同步器是一个用来构建同步组件的基础框架,ReentrantLock就是基于AQS的
- 锁主要是面向使用者的,不需要关注锁的具体实现,调用接口方法即可
- 同步器AQS是具体实现的,有同步状态和队列的管理,独占共享锁的占用和释放,线程的阻塞和唤醒等
51、AQS设计模式使用的是模板方法:AQS中被protected修饰的方法要由继承AQS的子类来重写实现,同步状态的获取和释放,getState,setState,compareAndSetState等
52、AQS的原理
- 线程要拿到共享的同步状态资源
- 如果资源空闲,该线程就是有效的工作资源,同时同步状态被锁定
- 如果资源被其他线程调用,那么这个线程就会被封装成结点,添加到CLH同步队列中
- 在队列中一直CAS尝试获取到同步状态,在队列中是会被阻塞的
53、 acquired为独占式锁的同步状态的获取,这个方法中线程入队被中断后,线程也不会被移除
- tryAcquired尝试获得锁
- addWaiter线程装成结点入队,以CAS自旋尾插的方式插入队列,否则创建队列
- 调用AcquiredQueue方法时:CAS判断当前结点的前辈结点是头结点并且拿到了同步状态,当前线程就可以获得锁,重构队列,断开当前节点的前辈结点的prev和next,然后被gc,把当前结点设置为head结点,thread置空
线程获得同步状态时:用CAS自旋把当前结点的状态设置为signal=-1,然后调用LockSupport.park方法阻塞当前线程
54、引申出来的知识点:
- 头节点的创建时机:判断tail是否为空
- 如果为空,调用enq方法,CAS自旋创建头节点,以CAS自旋把当前节点添加到head节点后面
55、AcquiredInterruptedify独占式锁响应中断,(lock比sycn多了中断和超时)
- 当线程被中断时,依然会存在CLH队列中,会等待同步状态的获取
- 使用响应中断时,中断就抛出异常InterruptException
- 如果没有中断,就会执行入队addWaiter和阻塞AcquiredQueue
56、 AcqiredNanos独占式锁超时等待,(除了响应中断还有超时等待)
- AcquiredNanos方法中有interrupt中断判断方法
- 还有doAcquiredNanos方法,判断是否timeout超时
- 如果timeout时间大于1L,就要休眠,如果<=1就会进入自旋入队,阻塞
57、 release独占式锁释放同步状态
- 先释放同步状态
- 当前节点的下一个结点不为空,且状态大于0,就会调用LockSupport.park来唤醒下一个结点的线程
58、结点的状态:根据结点的状态判断当前线程是否被阻塞
- cancelled=1:线程已经取消
- signal=-1后续线程被挂起,需要被唤醒
- condition=-2线程正在等待
- propagate=-3下一个应该无条件传播
59、 同步组件–countdownlatch倒计时:锁的模式是共享锁
- 相当于倒计时,火箭发射都准备好后,才会发射
- 多个并发线程操作数据,要求某一个线程必须在最后才会执行
- 使用new CountDownLatch(n),没执行一个countdown减一,直到0为止
- 调用await唤醒最后被执行的线程
- 不过countDownLatch只能被初始化一次,执行完毕后,就不会被使用
60、同步组件–cyclicbarrier循环屏障:锁的模式是共享锁
- 多个并发线程操作数据时,一组线程被阻塞,开会,人都到齐后,才会开会
- 直到最后一个线程达到屏障时
- 每个线程调用await都会被阻塞
61、 countdownlatch和cyclicbarrier的区别:锁的模式是共享锁
- countdownlatch做减法操作,计数为0时,才会释放等待线程;cyclicbarrier做加法操作,计数达到某一值时,才会释放等待线程
- countdownlatch只能被实例化一次使用过一次;cyclicbarrier可以重复使用
- countdownlatch的await方法,会唤醒最后的线程;cyclicbarrier的await方法会让每个线程阻塞到障碍处
62、同步组件–semaphore信号量:锁的模式是共享锁
- 并发操作下多个线程互斥的访问多个临界资源
- new semaphore(资源数)
- semaphore.accept允许获取资源
- semaphore.release释放资源
63、线程池的好处:线程的创建销毁开销是很大的(线程切换,CPU调度),为了减少不必要的开销,可以用线程池,而且线程的执行效率也会加快
- 不再频繁的创建销毁线程,降低资源消耗:线程切换,CPU调度
- 提高响应速度:线程处理任务时,没有了创建线程这一步,更快
- 提高线程的可管理性:把线程交给线程池同一调度
64、 线程池中的7个参数
- 每个线程池在源码中都返回了一个ThreadLocalExecutor类
- 这个类中就定义了7个参数
- corePoolSize:核心线程数
- maxiumPoolSize:最大线程数=核心线程数+非核心线程数
- KeepAliveTime:非核心线程存活的时间
- unit:上一个参数的单位
- workQueue:等待队列:用于存放等待线程执行的任务SychroinzedQueue同步队列
BlockingQueue:阻塞队列:这些都叫阻塞队列 - ThreadFactory:给线程池中创建新的线程
- RejectedHandler:拒绝策略
- newCachedThreadPool中ThreadPoolExecutor的参数(0,Integer.max,60L,SynchronousQueue):由此可见没有核心线程,空间换时间,要求多少个线程
- 线程复用: 线程执行完任务后,会回到池里面,再次拿到新的任务Task去执行
- newCachedThredPoolExecutors快的原因是:有多少的Task任务就会创建多少个Worker
- 当没有要处理的任务时就会线程自动终止
- 如果大量线程同时处理,CPU可能到达100%,长的线程长期占用CPU,可能有OOM的问题
- newFixedThreadPool中ThreadPoolExecutors中的参数(nThreads,nThreads,0L,LinkedBlockingQueue)由此可知,没有非核心线程,只有核心线程,而且核心线程数是自定义的,当核心线程被用完,依然有任务进来,就会进入队列等待
- 因为LinkedBlockingQueue这个队列的长度是Integer.maxValue也就是21亿,说明这个队列是无限长的
- 当没有任务要处理时,线程不会被回收,会一直占用资源
- 对于LinkedBlockingQueue,如果长时间没有线程去执行任务,可能有OOM的问题
- newSingleThreadExecutor中的ThreadPoolExecutors的参数(1,1,0L,LinkedBlockingQueue)由此可知,工作线程就一个,会顺序执行所有的任务
2.而且队列是LinkedBlockingQueue表示这个队列是无限长的,所以使用这个线程池会一个一个处理任务,这个的效率和创建单个线程去执行的效率差不多
- newSingleThreadLocal和其他的不一样是定时执行任务的线程池
- 过多长时间去打包:
65、 使用LinkedBlockingQueue这种队列的才可能导致OOM,长期占用CPU资源;反而SynchronousQueue会使得CPU100%
66、线程池的4种拒绝策略
- 当实际自定义的线程池不满足业务需求(最多能接收30个线程,但是业务有100个线程,这就需要拒绝策略)
- 任务提交给execute时,当都不满足条件,执行reject方法,方法中调用rejectedHadler接口,接口有4种实现类,也就是有四种拒绝策略
- 具体的实现类记不太清了,默认的就是丢弃任务抛出一个RejectedException
- 第二个就是默默丢弃,不会抛异常
- 第三个会丢弃队列中最前面的任务
- 第四个由调用线程处理任务
- 我们一般都自定义拒绝策略
67、 线程池的处理流程:(提交优先级)
- 用户提交任务,进入execute方法中
- 会用三个if判断规定了任务执行的优先级(这是提交优先级)
- 任务会优先交给核心线程去处理
- 再去任务队列中
- 然后去非核心线程
- 最后还有任务没被线程执行时,会进行拒绝策略
68、提交任务时execute和submit的区别:(执行优先级)源码中的线程复用?
- execute没有返回值,需要的参数是实现了runnable接口的任务
- submit参数可以是runnable和callable接口的任务;有返回值可以通过future查看任务的执行的执行情况和结果;对于异常,会被抛到最外层交给future捕获
69、线程池的五种状态
- running:运行态可以处理线程,接收任务
- shutdown:不再接收新的任务,把队列中的任务处理完即可
- stop:不再接收新的任务,队列里的任务也不会执行
- tidying:
- terminate:
70、在高并发操作线程池时,ThreadPoolExecutor的参数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit, 秒timeUnit.seconds
BlockingQueue<Runnable> workQueue, 阻塞队列
ThreadFactory threadFactory,
RejectedExecutionHandler handler,拒绝策略
71、 阻塞队列:
- sychrnoizedQueue同步队列:没有容量的,直接将任务交给线程取执行,当达到了max最大线程,就会触发拒绝策略
- linkedBlockingQueue无界队列:没有编辑额,总是可以把任务添加到队列中
- ArraysBlockingQueue有界队列:
72、 到底线程池的内部运行机制会是怎样的呢?
- 有任务需要被执行,先判断有没有核心线程空闲,如果达到了core数
- 就会放到阻塞队列中,等待工作线程处理
- 如果队列满了,就会判断是否当前线程数是否达到了max线程数
- 如果达到了就会执行拒绝策略
- 四种拒绝策略
- 直接抛出异常
- 抛弃当前任务
- 抛弃队列中最早的任务,并尝试入队
- 线程execute执行任务,可能当没有线程时,就会直接丢弃,也可能会被执行
73、 线程池设计中得到的启发:
- 可以有多个消费队列:
- 提交任务,把任务放到阻塞队列中
- 然后把任务分发给不同的子消费者对应的消费队列中
74、自定义拒绝策略:
- 继承RejectedExecutionHandler接口
- 重写方法,方法里的参数有任务Runnable,ThreadPoolExecutor
- 可以拿到线程池的core,max等属性
75、手动创建线程池:
- 阿里规约里不建议使用Executor直接用线程池
- 像fixThreadPool,singleThreadPool的阻塞队列都是无界队列,最大是Integer.max也就是21亿,容易造成OOM内存泄露
- 像cachedThreadPool和scheduleThreadPool的最大线程数是integer.max21亿,可能会有OOM内存泄露的问题
- 所以要用threadPoolExecutor自定义线程池
76、 关闭线程池:
- shutdown:关闭线程池,会让正在执行任务的线程执行完,并且把阻塞队列中的任务执行完
- shutdownNow:会立即关闭线程池,给正在执行任务的线程发送中断信号,把阻塞队列中未被执行的任务,包装成数组返回,不推荐使用
77、Lock的特性
- 可重入:ReentranLock和Sychronized都是可重入锁,就是同一个线程的不同方法可以获得同一把锁
- 设置尝试获得锁的超时时间:tryLock(时间)
- 公平锁和非公平锁:
- 公平锁:一开始就要入队,不会尝试获得锁;在抢占锁时,会判断队列中是否有结点
- 非公平锁:一开始就会先抢一下
- 丰富的API:tryLock,isQueuedThreads判断是否有线程获得锁
- 读写分离:读锁和写锁分离,一般读的情况多,可以有多个读锁
78、公平锁和非公平锁的区别
- 创建方式:new ReentranLock()参数中默认是false,就是非公平锁;传入true就是公平锁
- 调用非公平锁的方法时,一开始会先抢占锁(判断state的状态,判断当前获得锁的线程);而公平锁不会
- 公平锁会在执行排队逻辑之前,判断队列中是否有结点,如果有进入队列,如果没有直接抢占锁
79、三个线程顺序打印abc
1.线程的唤醒和阻塞
//三个线程顺序打印abc,使用阻塞唤醒wait和nodify
@Test
public void test1() throws InterruptedException {
Object a = new Object();
Object b = new Object();
Object c = new Object();
//waitnodify waitnodify1 = new waitnodify("A",(String) c,(String) a);
// new Thread(waitnodify1).start();
new waitnodify("A", c, a).start();
new waitnodify("B", a, b).start();
new waitnodify("C", b, c).start();
//使用睡,是因为可能子线程还没执行,主线程就执行完了,所以要睡
Thread.sleep(1000);
}
public static class waitnodify extends Thread{
private String name;
private Object pre;
private Object self;
public waitnodify(String name, Object pre, Object self) {
this.name = name;
this.pre = pre;
this.self = self;
}
private static int countWait=10;
@Override
public void run() {
while (countWait>0){
synchronized (pre){
synchronized (self){
System.out.println("我是线程"+name+",,,"+countWait);
countWait--;
self.notifyAll();//去唤醒哪些要拿到self锁的线程
}
try {
pre.wait();//让出锁,阻塞当前线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
//使用JDK的阻塞队列ArrayBlockQueue有界队列
public class BlockQueueShunxu {
@Test
public void test(){
//有界的阻塞队列
ArrayBlockingQueue queue = new ArrayBlockingQueue(10);
new Thread(new producer11(queue,"A")).start();
new Thread(new producer11(queue,"B")).start();
new Thread(new producer11(queue,"C")).start();
new Thread(new Coumster(queue)).start();
}
class producer11 implements Runnable{
private ArrayBlockingQueue queue;
private String name;
public producer11(ArrayBlockingQueue queue, String name) {
this.queue = queue;
this.name = name;
}
@Override
public void run() {
try {
//入队
queue.put(name);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Coumster implements Runnable{
private ArrayBlockingQueue queue;
public Coumster(ArrayBlockingQueue queue) {
this.queue = queue;
}
@Override
public void run() {
while(true){
try {
String o = (String) queue.take();
System.out.println("我是线程"+o);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
80、 Thread.currentThread.interrupted()表示中断线程
- 当前线程被阻塞时,直接报中断异常InterruptException
- 当前线程正在执行,线程中断标志置为true,但是当前线程可以正常运行