多线程
1、线程是程序运行的最小单位,是操作系统调度的最小单位。
2、一个进程包含多个线程,多个线程共享所属进程的资源(CPU、IO等)和内存空间,但是每个线程有自己独立的栈空间。
3、使用多线程可以提高CPU利用率,提高系统响应速度。
4、降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
5、提高响应速度。当任务到达时,可以不需要等到线程创建就能立即执行。
6、提高线程的可管理性。统一管理线程,避免系统创建大量同类线程而导致消耗完内存。
进程和线程的区别
1、系统会为进程分配地址空间,不会为线程分配地址空间,但是线程有自己的堆栈和局部变量表。
2、系统会为进程分配内存,不会为线程分配内存。
3、进程和线程切换时都会切换上下文,但是进程切换比线程切换更耗时,更耗资源。
4、线程相对进程并发性较高。
5、进程有自己的程序运行入口可以独立运行,线程得依赖程序的调度运行。
6、在保护模式下进程崩溃之后不会对其它进程产生影响,但是线程崩溃了所属的进程也会崩溃掉。
线程创建方式
1、继承Thread类。
2、实现Runnable接口。
3、实现Callable接口(Future、FutureTask配合可以用来获取异步执行的结果, Callable 接口 call 方法允许抛出异常,可以获取异常信息 注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到)。
线程状态
线程有五种状态:1.准备,新建线程,没有执行start方法。2.就绪,执行start方法,初始化线程,准备时间片。3.运行,执行run方法。4.阻塞。(造成线程阻塞原因:sleep,wait,使用阻塞IO,同步锁,suspend悬挂)5.销毁。
线程通信
主要包含两种方式Monitor和Condition
1、notify/notifyAll
2、signal/signalAll
线程停止
1、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
2、使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。
3、使用interrupt方法中断线程。调用interrupt方法,可以通过isInterrupt方法判断线程终止。
线程共享数据
1、如果多个线程执行同一个Runnable实现类中的代码,此时共享的数据放在Runnable实现类中;
2、如果多个线程执行不同的Runnable实现类中的代码,此时共享数据和操作共享数据的方法封装到一个对象中,在不同的Runnable实现类中调用操作共享数据的方法。
线程异常
如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候,JVM 会使用 Thread.getUncaughtExceptionHandler()来查询线程的 UncaughtExceptionHandler 并将线程和异常作为参数传递给 handler 的 uncaughtException()方法进行处理。
线程顺序执行
假设有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?可以使用join方法解决这个问题。比如在线程A中,调用线程B的join方法表示的意思就是:A等待B线程执行完毕后(释放CPU执行权),在继续执行。
start()、run() 方法
1、new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。
2、而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
sleep()、 yield()方法
(1) sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
(2) 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;
(3)sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;
(4)sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。
sleep和sleep(0)的区别
1、当 timeout = 0, 即 Sleep(0),如果线程调度器的可运行队列中有大于或等于当前线程优先级的就绪线程存在,操作系统会将当前线程从处理器上移除,调度其他优先级高的就绪线程运行;如果可运行队列中的没有就绪线程或所有就绪线程的优先级均低于当前线程优先级,那么当前线程会继续执行,就像没有调用 Sleep(0)一样。
2、当 timeout > 0 时,如:Sleep(1),会引发线程上下文切换:调用线程会从线程调度器的可运行队列中被移除一段时间,这个时间段约等于 timeout 所指定的时间长度。
线程等待、阻塞
线程等待是主动释放CPU资源,等待某个条件满足后再继续执行,而线程阻塞是被动等待某个条件的满足,期间会一直占用CPU资源。
线程调度
1、分时调度:是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。
2、抢占式调度:Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。
JMM内存模型
每个线程运行时,都会创建一个工作内存(也叫栈空间),来保存线程所有的私有变量。而JMM内存模型规范中规定所有的变量都存储在主内存中,而主内存中的变量是所有的线程都可以共享的,而对主内存中的变量进行操作时,必须在线程的工作内存进行操作,首先将主内存的变量拷贝到工作内存,进行操作后,再将变量刷回到主内存中。所有线程只有通过主内存来进行通信。
原子类
用过哪些原子类,他们的原理是什么?
1、Atomic基本原子类:AtomicInteger、AtomicLong、AtomicBoolean
2、AtomicArray数组类型原子类:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
3、AtomicReference引用类型原子类:AtomicReference、AtomicStampedReference、AtomicMarkableReference
4、AtomicFieldUpdater升级类型原子类:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
5、Adder累加器:LongAdder、DoubleAdder
原子类实现原理: 原子类是通过自旋CAS操作volatile变量实现的。
volatile
1、变量可见性:JMM会把该线程本地内存中的变量强制刷新到主内存中去,每次读取前必须先从主存刷新最新的值。
2、保证不了原子性:替代不了锁。
3、禁止指令重排:避免指令并行运行。
ThreadLocal
ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
ThreadLocal使用场景
1、管理Session会话:将Session保存在ThreadLocal中,使线程处理多次会话时始终是同一个Session。
2、JDBC 连接 Connection,这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection。
ThreadLocal与Synchronized的区别
ThreadLocal<T>其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。
但是ThreadLocal与synchronized有本质的区别:
1、Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
2、Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
ThreadLocal内存泄露
每个线程都有⼀个ThreadLocalMap的内部属性,map的key是ThreaLocal,定义为弱引用,value是强引用类型。垃圾回收的时候会自动回收key,而value的回收取决于Thread对象的生命周期。一般会通过线程池的方式复用线程节省资源,这也就导致了线程对象的生命周期比较长,这样便一直存在一条强引用链的关系:Thread --> ThreadLocalMap-->Entry-->Value,随着任务的执行,value就有可能越来越多且无法释放,最终导致内存泄漏。
ThreadLocal正确使用方法
1、每次使用完ThreadLocal都调用它的remove()方法清除数据
2、将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
FutureTask
FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。
synchronized、Lock
1、synchronized是Java关键字,在JVM层面实现加锁和解锁;Lock是一个接口,在代码层面实现加锁和解锁。
2、synchronized可以用在代码块上、方法上;Lock只能写在代码里。
3、synchronized在代码执行完或出现异常时自动释放锁;Lock不会自动释放锁,需要在finally中显示释放锁。
4、synchronized会导致线程拿不到锁一直等待;Lock可以设置获取锁失败的超时时间。
5、synchronized无法得知是否获取锁成功;Lock则可以通过tryLock得知加锁是否成功。
6、synchronized锁可重入、不可中断、非公平;Lock锁可重入、可中断、可公平/不公平,并可以细分读写锁以提高效率。
1、公平性:lock支持公平锁,sync不支持公平锁
2、锁状态:lock可以获取锁状态,sync无法获取锁状态
3、性能:资源竞争激烈时lock性能优异,资源竞争不激烈时sync性能优异。
synchronized实现原理
1、synchronized作用在代码块时,它的底层是通过monitorenter、monitorexit指令来实现的。
monitorenter:每个对象都是一个监视器锁(monitor),当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit:执行monitorexit的线程必须是objectref所对应的monitor持有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
2、方法的同步并没有通过 monitorenter 和 monitorexit 指令来完成,不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
synchronized和CAS的区别?什么场景使用CAS?什么场景使用synchronized
简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少)
synchronized适用于写比较多的情况下(多写场景,冲突一般较多)
1.对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
2.对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
synchronized是如何实现可重入性的
synchronized关键字经过编译后,会在同步块的前后分别形成monitorenter和monitorexit两个字节码指令。每个锁对象内部维护一个计数器,该计数器初始值为0,表示任何线程都可以获取该锁并执行相应的方法。根据虚拟机规范要求,在执行monitorenter指令时,首先要尝试获取对象的锁,如果这个对象没有被锁定,或者当前线程已经拥有了对象的锁,把锁的计数器+1,相应的在执行monitorexit指令后锁计数器-1,当计数器为0时,锁就被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。
使用synchronized修饰静态方法和非静态方法的区别
1、Synchronized修饰非静态方法,实际上是对调用该方法的对象加锁,俗称“对象锁”。
<1> 同一个对象在两个线程中分别访问该对象的两个同步方法,会产生互斥。
<2> 不同对象在两个线程中调用同一个同步方法,不会产生互斥。
2、Synchronized修饰静态方法,实际上是对该类对象加锁,俗称“类锁”。
<1> 用类直接在两个线程中调用两个不同的同步方法,会产生互斥。
<2> 用一个类的静态对象在两个线程中调用静态方法或非静态方法,会产生互斥。
<3> 一个对象在两个线程中分别调用一个静态同步方法和一个非静态同步方法,不会产生互斥。
synchronized加在this和class区别
synchronized 加锁 class 时,无论共享一个对象还是创建多个对象,它们用的都是同一把锁,而使用 synchronized 加锁 this 时,只有同一个对象会使用同一把锁,不同对象之间的锁是不同的。
线程安全集合
1、HashTable/Vector(性能较差已经弃用)
2、Collections.synchroniedXXX方法包装的集合。
3、JUC下面的以Concurrent/CopyOnWrite开头的集合
4、BlockingQueue: BlockingQueue是阻塞队列,内部维护了一个RetrantLock和2个Condition来实现生产者和消费者线程同步。当队列为空时消费者线程阻塞等待生产者线程往队列中添加元素,当队列满时生产者线程阻塞等待消费者线程从队列中取元素。
<1> DelayQueue:基于时间优先级的队列,延期阻塞队列
<2> ArrayBlockingQueue:基于数组的并发阻塞队列
<3> LinkedBlockingQueue:基于链表的FIFO阻塞队列
<4> PriorityBlockingQueue:带优先级的无界阻塞队列
<5> SynchronousQueue:并发同步阻塞队列
AQS
抽象队列同步器AbstractQueuedSynchronizer ,为构建锁或者其他同步组件提供了一个框架,AQS采用模板方法模式,子类只需要实现特定的几个方法来实现锁或者同步逻辑。AQS内部维护了一个int成员变量来表示同步状态,通过内置的FIFO(first-in-first-out)同步队列来控制获取共享资源的线程。基于AQS实现的有RetrantLock、Semaphore、CyclicBarria,CountDownLatch。
四种同步器
1、CountDownLatch:就是等待其他线程执行完任务,并且必要时可以对各个任务的结果进行汇总,然后主线程继续往下执行;
2、CyclicBarrier:可以让一组线程达到一个屏障时被阻塞,直到最后一个线程到达屏障时,所有线程才可以继续执行;根据字面意思就是回环。
3、信号量:信号量核心作用是控制并发执行的线程数量。定义信号量的时候会初始化一个预设值n,即最多有n个线程同时执行,如果将n设为1,就达到了互斥锁的效果。
AQS加解锁流程
1、加锁:先通过tryAcquire来尝试获取锁,如果获取成功就可以执行你的代码逻辑了。如果获取失败,那么就创建一个 Node 节点,并把当前 线程 设置到Node节点中,最后把Node节点添加到内部维护的一个队列尾部,并阻塞住Node对应线程运行。
2、解锁:AQS内部会从队列头部开始获取下一个节点,然后解除节点对应的线程阻塞状态,然后把该节点设置成队列的头节点(这样实现队列头节点出栈),最后该节点线程对应的代码逻辑得以继续执行。后面如此循环,AQS队列中所有节点对应线程中锁住的代码块得以顺序执行。
JUC
JUC是java提供的并发操作实现线程安全的包,主要包含以下类:
1、原子类
2、Lock锁
3、线程池(ThreadPoolExecutor)
4、并发容器(Concurrent开头的容器、CopyOnWrite开头的容器、BlockingQueue)
5、同步器(CountDownLatch(允许一个或者多个线程等待其它线程操作完成)、信号量(控制同时访问资源的数量)、CyclicBarrier(控制一组线程达到一个屏障时被阻塞,先到达的线程等待后到达的线程,当所有的线程都到达时才会继续执行))
线程池
常见的几种线程池
1、newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。
2、newFixedThreadPool(int nThreads):创建一个可重用的、具有固定线程数的线程池。
3、newSingleThreadExecutor():创建一个只有单线程的线程池,它相当于调用newFixedThread Pool()方法时传入参数为1。
4、newScheduledThreadPool(int corePoolSize):创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。
5、corePoolSize指池中所保存的线程数,即使线程是空闲的也被保存在线程池内。
6、newSingleThreadScheduledExecutor():创建只有一个线程的线程池,它可以在指定延迟后执行线程任务。
线程池的工作流程
1、判断核心线程池是否已满,没满则创建一个新的工作线程来执行任务。
2、判断任务队列是否已满,没满则将新提交的任务添加在工作队列。
3、判断整个线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则执行饱和(拒绝)策略。
线程池的拒绝策略
1、AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
2、DiscardPolicy:也是丢弃任务,但是不抛出异常。
3、DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复该过程)。
4、CallerRunsPolicy:由调用线程处理该任务。
线程池参数
1、corePoolSize(核心工作线程数):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时。
2、maximumPoolSize(最大线程数):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
3、keepAliveTime(多余线程存活时间):当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
4、workQueue(队列):用于传输和保存等待执行任务的阻塞队列。
5、threadFactory(线程创建工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
6、handler(拒绝策略):当线程池和队列都满了,再加入线程会执行此策略
线程池的生命周期
1、Running:线程池正在运行中。
2、SHUTDOWN:线程池中不再接收新任务,等待队列中所有任务执行完毕。
3、STOP:立马停止,清空队列中的所有任务,已有任务也不再执行。
4、TIDING:线程池中队列为空。
5、TERMINATE:线程池死亡。
线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好的吗?
每当我们调用execute()方法添加一个任务时,线程池会做如下判断:·如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;·如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会抛出异常RejectExecutionException。当一个线程完成任务时,它会从队列中取下一个任务来执行。当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断。
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
线程池中阻塞队列的长度如何设置
要结合具体的业务场景要求,如果系统要求相应块的话,可以将队列大小设置小一点,比如0,如果系统要求没那么快的话,可以设置大一点。建议采用tomcat的处理方式,core与max一致,先扩容到max再放队列,不过队列长度要根据使用场景设置一个上限值,如果响应时间要求较高的系统可以设置为0。
线程池中submit()和execute() 方法有什么区别
1、相同点:都可以开启线程执行池中的任务。
2、不同点:
接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务。
返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()没有
异常处理:submit()方便Exception处理
提交任务时线程池队列已满
有俩种可能:
1、如果使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务。
2、如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy。
线程池的关闭方式
1、shutdownNow:会首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程。
2、shutdown:将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
线程池的任务执行完成判断
1、使用线程池的原生函数isTerminated();executor提供一个原生函数isTerminated()来判断线程池中的任务是否全部完成。如果全部完成返回true,否则返回false。
2、使用重入锁,维持一个公共计数。所有的普通任务维持一个计数器,当任务完成时计数器加一(这里要加锁),当计数器的值等于任务数时,这时所有的任务已经执行完毕了。
3、使用CountDownLatch。它的原理跟第二种方法类似,给CountDownLatch一个计数值,任务执行完毕后,调用countDown()执行计数值减一。最后执行的任务在调用方法的开始调用await()方法,这样整个任务会阻塞,直到这个计数值为零,才会继续执行。这种方式的缺点就是需要提前知道任务的数量。
4、submit向线程池提交任务,使用Future判断任务执行状态。使用submit向线程池提交任务与execute提交不同,submit会有Future类型的返回值。通过future.isDone()方法可以知道任务是否执行完成。
线程池阻塞队列选择
1、ArrayBlockingQueue(常用):都是共用同一个锁对象,由此也意味着两者无法真正并行运行。
2、LinkedBlockingQueue(常用):生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发
的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
3、DelayQueue:DelayQueue 中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue 是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻
塞。
4、PriorityBlockingQueue:不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。
因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。
5、SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
6、LinkedTransferQueue:由链表组成的无界阻塞队列。
7、LinkedBlockingDeque:由链表组成的双向阻塞队列。
锁
ReentrantLock实现原理
ReentrantLock是基于AQS实现的,AQS即AbstractQueuedSynchronizer的缩写,这个是个内部实现了两个队列的抽象类,分别是同步队列和条件队列。其中同步队列是一个双向链表,里面储存的是处于等待状态的线程,正在排队等待唤醒去获取锁,而条件队列是一个单向链表,里面储存的也是处于等待状态的线程,只不过这些线程唤醒的结果是加入到了同步队列的队尾,AQS所做的就是管理这两个队列里面线程之间的等待状态-唤醒的工作。
在同步队列中,还存在2中模式,分别是独占模式和共享模式,这两种模式的区别就在于AQS在唤醒线程节点的时候是不是传递唤醒,这两种模式分别对应独占锁和共享锁。
AQS是一个抽象类,所以不能直接实例化,当我们需要实现一个自定义锁的时候可以去继承AQS然后重写获取锁的方式和释放锁的方式还有管理state,而ReentrantLock就是通过重写了AQS的tryAcquire和tryRelease方法实现的lock和unlock。
ReentrantLock是如何实现可重入性的
ReentrantLock使用内部类Sync来管理锁,所以真正的获取锁是由Sync的实现类控制的。Sync有两个实现,分别为NonfairSync(非公公平锁)和FairSync(公平锁)。Sync通过继承AQS实现,在AQS中维护了一个private volatile int state来计算重入次数,避免频繁的持有释放操作带来的线程问题。
自旋锁
自旋锁的定义:当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。这种采用循环加锁 -> 等待的机制被称为自旋锁
自旋锁的原理比较简单,如果持有锁的线程能在短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞状态,它们只需要等一等(自旋),等到持有锁的线程释放锁之后即可获取,这样就避免了用户进程和内核切换的消耗
读写锁
与传统锁不同的是读写锁的规则是可以共享读,但只能一个写,总结起来为:读读不互斥、读写互斥、写写互斥,而一般的独占锁是:读读互斥、读写互斥、写写互斥,而场景中往往读远远大于写,读写锁就是为了这种优化而创建出来的一种机制。 注意是读远远大于写,一般情况下独占锁的效率低来源于高并发下对临界区的激烈竞争导致线程上下文切换。因此当并发不是很高的情况下,读写锁由于需要额外维护读锁的状态,可能还不如独占锁的效率高。因此需要根据实际情况选择使用。
在Java中ReadWriteLock的主要实现为ReentrantReadWriteLock,其提供了以下特性:
公平性选择:支持公平与非公平(默认)的锁获取方式,吞吐量非公平优先于公平。
可重入:读线程获取读锁之后可以再次获取读锁,写线程获取写锁之后可以再次获取写锁。
可降级:写线程获取写锁之后,其还可以再次获取读锁,然后释放掉写锁,那么此时该线程是读锁状态,也就是降级操作。
分段锁
容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率。这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
乐观锁和悲观锁
1、悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。Java中悲观锁是通过synchronized关键字或Lock接口来实现的。
2、乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于多读的应用类型,这样可以提高吞吐量。在JDK1.5 中新增 java.util.concurrent (J.U.C)就是建立在CAS之上的。相对于对于 synchronized 这种阻塞算法,CAS是非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。
偏向锁
顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
公平锁与非公平锁
1、公平锁:每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的,最前面的线程总是最先获取到锁。在java中RetarntLock可以设置为公平锁。实现原理:先将线程自己添加到等待队列的队尾并休眠,当某线程用完锁之后,会去唤醒等待队列中队首的线程尝试去获取锁,锁的使用顺序也就是队列中的先后顺序,在整个过程中,线程会从运行状态切换到休眠状态,再从休眠状态恢复成运行状态,但线程每次休眠和恢复都需要从用户态转换成内核态,而这个状态的转换是比较慢的,所以公平锁的执行速度会比较慢。
2、非公平锁:每个线程获取锁的顺序是随机的,并不会遵循先来先得的规则,所有线程会竞争获取锁。在Java中synchronized和RetrantLock可以设置为非公平锁。实现原理:当线程获取锁时,会先通过 CAS 尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入等待队列,等待下次尝试获取锁。这样做的好处是,获取锁不用遵循先到先得的规则,从而避免了线程休眠和恢复的操作,这样就加速了程序的执行效率。
共享式与独占式锁
同一时刻独占式只能有一个线程获取同步状态,而共享式在同一时刻可以有多个线程获取同步状态。例如读操作可以有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其他操作都会被阻塞。
synchronized 锁升级
1、synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
2、锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
死锁
1、定义:多个进程或者线程在竞争资源时产生了资源依赖而导致互相等待的现象,如果没有外力介入,就会一直等待下去无法继续运行。
2、条件:<1>资源在一段时间只能被一个进程占据 <2>当某个资源被某个进程占用之后其它进程只能等待 <3>某个资源被占用之后,只能等待该进程释放 <4>循环等待
如何避免线程死锁
1、避免一个线程同时获得多个锁
2、避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
3、尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
CAS
1、CAS 是 compare and swap 的缩写,即我们所说的比较交换。
2、cas 是一种基于锁的操作,而且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加 version 来获取数据,性能较悲观锁有很大的提高。
3、CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和 A 的值是一样的,那么就将内存里面的值更新成 B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行。
CAS问题
1、ABA 问题:
比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。从 Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。
2、循环时间长开销大:对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。
3、只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。
LongAdder
1、LongAdder采用了分段锁的思想,内部维护了一组计数单元,不同的线程可以操作不同的计数单元,减少了线程竞争,提高了效率。最终LongAdder的数值等于base值+所有计数单元的值之和。
2、在线程数量比较少时AtomicLong效率要高一点,在线程数量很多时LongAdder效率高一点。
其它
假如有一个第三方接口,有很多个线程去调用获取数据,现在规定每秒钟最多有10个线程同
时调用它,如何做到?
可以使用ScheduledThreadPool的scheduleAtFixedRate方法。
用三个线程按顺序循环打印abc三个字母,比如abcabcabc?
如果让你实现一个并发安全的链表,你会怎么做?
有哪些无锁数据结构,他们实现的原理是什么?
CAS实现的
简述ConcurrentLinkedQueue和LinkedBlockingQueue的用处和不同之处?
BlockingQue哪些操作时阻塞的
take和put是阻塞的。
poll和offer是非阻塞的。