目录
1、Thread.sleep(0)的作用
2、Synchronized
2.1、特性
2.2、说一说自己对于 synchronized 关键字的了解:
2.3、synchronized关键字的底层原理(JVM如何实现重量级锁)
2.4、Jdk1.6之后对synchronized做的优化
2.4.1、锁粗化
2.4.2、锁消除
2.5、线程池的isTerminated()
2.6、如何中断线程
2.7、线程的interrupted和isInterrupted
2.8、并发竞态条件
2.9、锁的种类
3、synchronized和ReentrantLock的区别
4、锁升级策略
4.1、锁升级过程(不可逆)
4.1.1、无锁
4.1.2、无锁->偏向锁
4.1.3、偏向锁升级为轻量级锁
4.1.4、轻量级锁->重量级锁
4.2、适用场景
4.3、锁的优缺点比较
5、volatile
5.1、Java的内存模型
5.2、volatile三重功效
5.3、说说 synchronized 关键字和 volatile 关键字的区别
5.4、volatile的原理是什么?volatile一定是线程安全的吗?
5.5、MESI(缓存一致性协议)
5.6、嗅探
5.7、嗅探->总线风暴
5.8、Volatile防止指令重排序:内存屏障
5.8.1、内存屏障作用
5.8.2、重排序规则
5.8.3、内存屏障插入策略
6、线程池
6.1、如何设计线程池
6.2、通过Executors创建线程池
6.3、ScheduledThreadPoolExecutor
6.3.1、延迟执行(schedule)
6.3.2、周期执行(scheduleWithFixedDelay)
6.4、使用ThreadPoolExecutor创建线程池
6.5、缓冲队列类型
6.6、拒绝策略
6.7、线程池原理
6.8、Shutdown和shutdownNow
7、AQS(抽象队列同步器)
7.1、原理
7.2、State访问方式
7.3、AQS主要成员变量
7.4、AQS实现
8、乐观锁和悲观锁
8.1、乐观锁(不加锁):
8.2、悲观锁(synchronized):
9、CAS的缺陷
9.1、ABA问题:
9.2、高竞争下的开销问题:
9.3、功能受限
10、死锁
10.1、死锁的条件
10.2、如何检测Java中的死锁
10.3、如何避免线程死锁
11、单例模式中volatile的作用
12、为什么要使用线程池(好处)
13、CAS中的ABA问题如何解决?
14、ThreadLocal是什么?它的原理是什么?
15、CountDowanLatch有没有用过?适合在什么样的场景下用?
15.1、闭锁(CountDownLatch)
15.2、栅栏(CyclicBarrier)
15.3、信号量(Semaphore)
15.4、RateLimiter(限制访问速率):令牌桶
16、如何检测一个线程是否拥有锁
17、同步块内的线程抛出异常会释放锁吗
18、线程间如何通信
19、线程池线程数量设置
19.1、IO密集型任务
19.2、计算密集型任务
20、阻塞队列
20.1、顶层接口(BlockingQueue)
20.1.1、入队操作
20.1.2、出队操作
20.2、ArrayBlockingQueue
20.3、LinkedBlockingQueue
20.4、PriorityBlockingQueue
20.5、SynchronousQueue
20.6、线程池中的阻塞队列一般会选择哪种队列?为什么?
21、Executors
22、wait(),notify(),notifyAll()必须加锁的原因
触发操作系统立刻重新进行一次CPU竞争。使得各线程重新去抢占CPU的使用权,避免一个线程长时间占用CPU资源,而导致其他线程进入假死状态。
synchronized作用域内的操作都是原子性的。
锁的状态对于其他线程来说是可见的,且释放锁后会立即将修改同步到主内存。
指令重排序影响的是多线程并发执行的顺序性;而synchronized保证同一时刻只有一个线程访问同步代码块,从而保证不会发生线程间的指令重排,保证有序性。
一个线程拥有了锁后还可以重复申请当前锁。
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者 代码块在任意时刻只能有一个线程执行。
早期的synchronized效率低下,早期,Java线程会映射到操作系统的原生线程上,线程的切换或状态改变会交由操作系统(用户态—>内核态,花费时间长)。
Jdk1.6后对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
对象:对象头、实例数据、对齐填充。(申请锁、上锁、释放锁)
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图 获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
锁升级策略:无锁->偏向锁->轻量级锁->重量级锁。
对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
此时,JVM会扩大锁的范围,把多次加锁释放锁操作,合成一次加锁释放锁的操作。
去除不可能存在竞争的锁。删除不必要的加锁操作,在代码上要求同步,但是被检测到不可能存在共享数据竞争情况”的锁进行消除。通俗的讲,为一个不会发生并发问题的代码块或方法加了锁,代码执行时加锁只会影响性能,此时JVM就会将该锁删除。
若线程处于正常状态,则会将该线程的中断标志设置为true,之后线程将继续正常运行,不受影响;若线程处于被阻塞状态,或由正常状态变为阻塞状态(sleep()、wait()),线程将立刻退出被阻塞状态,并抛出一个InterruptedException异常。可以通过捕获异常然后return。
interrupted()是static方法,调用的时候要用Thread.interrupted(),而isInterrupted()是实例方法,调用时要用线程的实例调用;
当某个正确性取决于多个线程的交替执行时序时,就会发生竟态条件。相当于正确的结果要取决于运气。
比如:(先检查后执行:通过一个可能失效的结果来决定下一步的动作。)
单例模式的双重锁检查,如果不加锁,可能就会发生竟态条件。是否发生取决于线程的调度状态。
ReentrantLock 是从jdk1.5提供的API层面的锁。通过利用CAS自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。
ReentrantLock则需要手动释放锁,一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。如果没有手动释放锁,就可能导致死锁现象。
锁升级:无锁->偏向锁->轻量级锁->重量级锁
Java对象头 = Mark Word + 指向类的指针 + 数组长度(只有数组对象才有)
Mark Word记录对象和锁有关的信息,根据锁标志位来确定当前对象是什么锁状态。
锁标志位 = 01,是否偏向锁 = 0,存储对象的hashcode值。
当对象没有被当成锁时,Mark Word记录的是对象的hashcode。
锁标志位 = 01,是否偏向锁 = 1,存储占有偏向锁线程的线程id。
当对象被当成同步锁并且线程A通过CAS抢到锁时,进入偏向锁状态,将线程A的线程id写入Mark Word。
当线程A再次试图获取锁时,JVM发现同步锁为偏向锁,且偏向锁中的线程id值等于线程A的id,就可以直接执行同步锁的代码块;
当线程B来试图获取锁时,JVM发现同步锁为偏向锁,且偏向锁中的线程id值不等于线程B的id,线程B就会通过CAS去尝试获取锁:如果获取成功,则将Mark Word中的偏向锁的线程id修改为线程B的id;如果获取失败,则将同步锁升级为轻量级锁;
出现锁竞争(CAS获取锁失败)时,偏向锁升级为轻量级锁。
锁标志位 = 00,存储指向栈中锁记录的指针。
JVM在当前线程的栈帧中开辟一块独立空间,用以保存指向对象锁Mark Word的指针,同时Mark Word保存指向栈帧中独立空间的指针。
如果保存成功,则当前线程获取该对象的轻量级锁,执行同步代码;
如果保存失败,则当前线程自旋,不断尝试获取锁(尝试N次,由JVM决定),如果抢锁成功,执行同步代码;如果抢锁失败,则将轻量级锁升级为重量级锁。
轻量级锁自旋超过JVM指定次数时,升级为重量级锁。
锁标志位 = 10,存储指向重量级锁的指针。
未抢占到锁的线程会被阻塞,抢到锁的线程在释放锁的同时会唤醒那些阻塞的线程。
重量级锁下,线程之间的切换需要从用户态到内核态,成本较高。
偏向锁:只有一个线程在临界区执行,无需操作系统介入;
轻量级锁:多个线程交替进入临界区,竞争不激烈,就算有冲突,线程自旋几次就能获取锁;
重量级锁:多个线程出现激烈竞争。
线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。(多线程环境下导致数据的不一致)。
Volatile修饰变量的内存模型
当一个线程对volatile修饰的变量进行写操作时,JMM会立即把该线程对应的本地内存中的共享变量刷新到主内存。
当一个线程对volatile修饰的变量进行读操作时,JMM会立即将当前线程对应的本地内存设置为无效,从主内存中读取共享变量的值。
64位写入的原子性、内存可见性和禁止重排序。
保证共享变量的可见性。当一个线程修改一个共享变量时,另一个线程能读到这个修改的值。
每个线程都有自己的高速缓存区,线程本身并不与主内存进行直接交互。
保证可见性:
(1)修改volatile变量时会强制将修改后的值刷新的主内存中。
(2)修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。
保证有序性:
Happen-before
如果a在b之前执行,则a的执行结果必然对b是可见的。(不代表a一定在b之前执行)
没保证原子性,不一定线程安全
当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存值置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
每个CPU通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
MESI缓存一致性协议,导致CPU需要不断地从主内存嗅探和cas不断循环,会导致总线带宽达到峰值。
内存栅栏,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点。
硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。
对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;
对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。
volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障。
a、newFixedThreadPool(int Threads):创建固定数目线程的线程池。
b、newCachedThreadPool():创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果没有可用的线程,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
c、newSingleThreadExecutor()创建一个单线程化的Executor。
d、newScheduledThreadPool(int corePoolSize)创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
缺陷:
使用newFixedThreadPool和newSingleThreadExecutor:使用LinkedBlockingQueue无界阻塞队列,允许请求的队列长度为 Integer.MAX_VALUE,可能堆积 大量的请求,从而导致OOM。
使用newCachedThreadPool和newScheduledThreadPool:允许创建的线程数量为 Integer.MAX_VALUE ,可能 会创建大量线程,从而导致OOM。
new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue
->
new ThreadPoolExecutor(核心线程数,最大线程数,非核心线程存活时间,时间单位,缓冲队列,创建线程使用的工厂,任务拒绝策略)。
new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, TimeUnit unit, BlockingQueue
核心线程数->缓冲队列->最大线程数->拒绝策略。
shutdown只是将线程池的状态设置为SHUTWDOWN状态,停止接收新线程,正在执行的任务会继续执行下去,没有被执行的则中断。
shutdownNow则是将线程池的状态设置为STOP,停止接收新线程,正在执行的任务则被停止,没被执行任务的则返回。
调用shutdown/shutdown方法后,如果线程再遇到wait、sleep等阻塞方法,线程会抛异常。
AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
AQS是一个多线程访问同步资源的的同步器框架。该框架维护了一个volatile int state(共享资源)和一个FIFO(CLH双向队列)线程等待队列(多线程争夺共享资源时会进入此队列)。
AQS一般以继承的方式被使用,重入锁ReentrantLock、闭锁、栅栏等都用到了AQS。
AQS的实现依赖内部的同步队列(FIFO双向队列),如果当前线程获取同步状态失败,AQS会将当前线程以及等待状态等信息构造成一个Node,将其加入同步队列的尾部,同时阻塞当前线程,当同步状态释放时,唤醒队列的头节点。
getState()、setState()、compareAndSetState(),线程通过尝试获取共享变量state的结果来对线程的状态作出处理。
State变量可以理解成同步资源的锁状态,值为0时表示当前资源未被线程占用,值不为0时表示当前共享资源已被线程获取锁。
实现类只需要实现共享资源state的获取和释放方式即可。
独占:只有一个线程能执行->ReentrantLock
共享:多个线程可以同时执行->Semaphore、CountDownLatch、CyclicBarrier
独占和共享:ReentrantReadWriteLock
在操作数据时比较乐观,认为别人不会同时修改数据,所以不会为数据加锁,只是会在更新数据时判断一下是否有其他人修改了数据,如果别人修改了数据,则放弃当前更新操作,否则执行更新。
乐观锁的两种实现方式:
CAS机制(例如AtomicInteger、AtomicBoolean等原子类):
CAS操作包括了3个操作数(多个操作,原子性由硬件层面保证):
CAS操作逻辑如下:如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。许多CAS的操作是自旋的:如果操作不成功,会一直重试,直到操作成功为止。
版本号机制:
给数据增加一个字段,作为判断数据变化的状态标记,读取数据的时候取出该字段,更新数据时判断数据此时的标记是否与之前取出的值相同,如果一致才进行更新操作。
在操作数据时比较悲观,认为别人会同步修改数据,所以在更新数据时会把数据锁住,更新完后才会释放锁。
假设有两个线程——线程1和线程2,两个线程按照顺序进行以下操作:
第4步中的A已经不是第1步中的A值了。
比如:栈顶问题—>一个栈的栈顶经过两次(或多次)变化又恢复了原值,但是栈可能已发生了变化
解决方法:加入版本号
在并发冲突概率大的高竞争环境下,CAS一直失败,会一直重试,导致CPU开销较大。
解决方法:增加重试阈值。
CAS只能保证单个变量(单个内存值)操作的原子性;
普通Java用户不能使用,只能借助atomic下的原子类使用。
互斥:共享资源同一时刻只能被一个线程占用。
持有并等待:线程1持有A锁,等待B锁;线程2持有B锁,等待A锁。
资源不可剥夺:已经获取到的锁,不会被其他线程强制抢占。
循环等待:线程1和线程2形成环形等待,死循环链。
查看进程pid信息:jps
查看指定pid的线程信息:jstack -l pid
显示结果:
Volatile修饰的变量保证可见性,禁止重排序。
在第7步创建对象的时候,实际上执行了三个操作:
1、分配对象的内存空间;
2、初始化对象;
3、设置instance引用指向分配的内存地址。
在2、3步可能发生指令的重排序,可能出现,instance已经指向了一个地址,但是对象还没有初始化,此时另一个线程就可能获取到一个没有初始化的对象。而volatile修饰的变量,禁止重排序,以此来解决这个问题。
CAS (compareAndSwap),中文叫比较交换,一种无锁原子算法。过程是这样:它包含 3 个参数 CAS(V,E,N),V表示要更新变量的值,E表示预期值,N表示新值。仅当 V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做两个更新,则当前线程则什么都不做。最后,CAS 返回当前V的真实值。CAS 操作时抱着乐观的态度进行的,它总是认为自己可以成功完成操作。
ABA问题:
考虑如下操作:
并发1(上):获取出数据的初始值是A,后续计划实施CAS乐观锁,期望数据仍是A的时候,修改才能成功
并发2:将数据修改成B
并发3:将数据修改回A
并发1(下):CAS乐观锁,检测发现初始值还是A,进行数据修改
上述并发环境下,并发1在修改数据时,虽然还是A,但已经不是初始条件的A了,中间发生了A变B,B又变A的变化,此A已经非彼A,数据却成功修改,可能导致错误,这就是CAS引发的所谓的ABA问题。
解决:
线程的局部变量,每一个线程所单独持有,其他线程不能对其进行访问。
当使用ThreadLocal维护变量的时候 为每一个使用该变量的线程提供一个独立的变量副本,即每个线程内部都会有一个该变量,这样同时多个线程访问该变量并不会彼此相互影响,因此他们使用的都是自己从内存中拷贝过来的变量的副本, 这样就不存在线程安全问题,也不会影响程序的执行性能。
ThreadLocal提供了set和get访问器用来访问与当前线程相关联的线程局部变量。
一个线程的继续运行需要先完成其他线程的初始化。
Countdown()、await()
CountDownLatch是闭锁的一个实现,允许一个或多个线程等待某个事件的发生。其内部是一个正数计数器,初始化时指定一个正数,countDown()方法对计数器进行减操作,await()方法就是这个闭锁的大门,计数器不为0时,await()方法的线程会被阻塞,当计数器为0时,await()方法的线程才会继续执行。
使用场景:
await()
栅栏类似于闭锁,内部也有一个正数计数器,初始化时指定一个正数,每当有一个线程使用await()方法时,计数器加1,当前线程阻塞,直到计数器的值为初始化的正数时,打开栅栏。
区别:
闭锁是一个或多个线程等待一个外部条件的发生,await()阻塞,countDown()为条件;而栅栏是线程与线程间相互等待,await()既是条件,也会阻塞。
举例:
闭锁:三个工人,一个老板,三个工人都完成任务后,老板才来检查;
栅栏:三个工人,三个工人都完成任务后,一起出去嗨皮。
限流:用于限制并发访问的数量。
信号量通过AQS的state变量去维护“许可证”的计数,而线程去访问共享资源前,必须先拿到许可证。线程可以从信号量中去“获取”一个许可证,一旦线程获取之后,信号量持有的许可证就转移过去了,所以信号量手中剩余的许可证要减一。
限流:使用访问速率进行共享资源的访问限流。
RateLimiter会按照一定的频率往桶里扔令牌,线程拿到令牌才能执行。
设置每秒访问速率:RateLimiter rateLimiter = RateLimiter.create(n);
请求访问资源:rateLimiter.acquire();
Thread.holdsLock()
如果使用的synchronized,同步块内抛出异常,会自动释放锁。
而如果是Lock,则需要在finally块里手动释放锁。
任务的主要处理为IO操作,比如文件读写、网络通信等,此类任务占用CPU资源很少,则线程数 = (CPU内核数)/(1-阻塞系数)-----阻塞系数一般为0.8或0.9;
任务的主要处理为算法计算等,占用CPU资源多,一般设置线程数=CPU核数*2。
add:队列满时抛出异常
offer:队列满时返回false
put:队列满时阻塞
remove:队列为空抛出异常
poll:队列为空返回null
take:队列为空时阻塞
一个数组 + 一把锁 + 两个条件,读写互斥。直接操作数据,性能好。
两把锁,分别控制队头和队尾的操作。读读互斥、写写互斥。
Put通知take,take通知put。
非满时,put会通知其他put;非空时,take会通知其他take。
类似于ArrayBlockingQueue,区别在于用数组实现一个二叉堆,实现按自然顺序或者自定义比较器顺序出队列。(在插入数据时,会按照顺序写入数组)
没有notFull条件,当元素个数超出数据长度时,执行扩容操作。
直接提交,不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。
可以认为SynchronousQueue是一个缓存值为1的阻塞队列,但是 isEmpty()方法永远返回是true,remainingCapacity() 方法永远返回是0,remove()和removeAll() 方法永远返回是false,iterator()方法永远返回空,peek()方法永远返回null。
newFixedThreadPool(int nThreads):固定大小线程池;LinkedBlockingQueue;
newCachedThreadPool():无界线程池;SynchronousQueue;
newSingleThreadExecutor():大小为1的固定线程池;
newScheduledThreadPool() :定长线程池,支持定时和周期性任务执行。
防止notify先于wait执行,导致wait线程永远处于阻塞状态(饥饿线程)。
一个线程调用了wait()之后, 必然需要由另外一个线程调用notify()来唤醒该线程, 所以本质上, wait()与notify()的成对使用, 是一种线程间的通信手段。
以上内容为个人学习理解,如有问题,欢迎在评论区指出。
部分内容截取自网络,如有侵权,联系作者删除。