指令重排,不是java代码重排,因为一行java代码,可能对应了多行指令
)管程是一种通用的同步原语,synchronized就是管程的实现
)Thread.interrupted()
检测到是否发生中断实现多线程的方式
本质上也是Runnable
)Java对象头
标记字段 (Mark Work
)
默认存储对象的HashCode
、分代年龄
和锁标志位
信息。这些信息都是与对象自身定义无关
的数据,所以Mark Word
被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的数据。它会根据对象的状态复用自己的储存空间,也就是说运行期间,Mark Word里储存的数据会随着锁标志位的变化而变化
类型指针 (Klass Pointer
)
对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
Monitor
可以理解为一个同步工具
、或一种同步机制
,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁
或Monitor锁
Monitor是线程
私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局可用列表。每一个被锁住的对象都会和一个monitor
关联,同时monitor中有一个Owner
字段存放拥有该锁的线程的唯一标识
,表示该锁被这个线程占用。
Monitor描述为对象监视器,可以类比一个特殊的房间,这个房间中有一些被保护的数据,Monitor保证每次只能有一个线程能进入这个房间进行访问被保护的数据,进入房间即为持有Monitor,退出房间即为释放Monitor。使用synchronized加锁的同步代码块,主要就是通过锁对象的monitor的取用和释放来实现的。
- 被synchronized修饰过的程序块,在编译前后被编译器生成了monitorenter和monitorexit两个字节码指令
- 虚拟机执行到monitorenter指令时,首先要尝试获取对象的锁
- 如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁,把锁的计数器+1;当执行monitorexit指令时,将锁的计数器-1;当计数器为0时,锁就被释放了
所有请求锁的线程将被首先放置到该竞争队列
Contention List中那些有资格成为候选人的线程被移到Entry List
调用wait方法被阻塞的线程,被放置到wait set
任何时刻最多只有一个线程正在竞争锁,该线程称为OnDeck
释放锁的线程
synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的
Mutex Lock (互斥锁)
来实现的线程同步
同步方法或同步代码块
当JVM
执行一个同步方法时,执行中的线程识别该方法的method_info
结构是否有ACC_SYNCHRONIZED
标记设置,然后它自动获取对象锁,调用方法,最后释放锁。如果有异常发生,线程自动释放锁。
字节代码
:创建同步代码块
产生了16行字节码,而创建同步方法
仅产生了5行。
同步实例方法,则当前实例加锁。同步静态方法,则当前类加锁。同步方法块,则给定对象加锁
volatile
关键字
Java里只有
volatile
关键字是能实现禁止指令重排序的
volatile
关键字修饰的变量被一个线程修改的时候,其它线程可以立刻得到修改之后的结果。当一个线程向被volatile
关键字修饰的变量写入数据的时候,虚拟机会强制它被值刷新到主内存中。当一个线程用到被volatile
关键字修饰的值的时候,虚拟机会强制要求它从主内存中读取。非常经典的例子是在单例方法中同时对字段加入
volatile
,就是为了防止指令重排序。
synchronized
和volatile
的有序性与可见性是两个角度来看的
synchronized
是因为块与块之间看起来是原子操作,块与块之间有序可见volatile
是在底层通过内存屏障防止指令重排的,变量前后之间的指令与指令之间有序可见synchronized
和volatile
有序性不同也是因为其实现原理不同
synchronized
靠操作系统内核互斥锁实现的,相当于JMM中的lock
和unlock
。退出代码块时一定会刷新变量回主内存。volatile
靠插入内存屏障指令防止其后面的指令跑到它前面去了简言之,volatile
关键字识别一个变量,意味着这个变量的值会被不同的线程修改
为了提高性能,Java
语言规范允许JRE
在引用变量的每个线程
中维护该变量的一个本地副本
。您可以将变量的这些“线程局部”副本看作是与缓存类似,在每次线程需要访问变量的值时帮助它避免检查主存储器。
两个线程启动,第一个线程将变量A读取为5,第二个线程将变量A读取为10。如果变量A从5变到10,第一个线程将不会知道这个变化,因此会拥有错误的变量A的值。但是如果将变量A标记为volatile
,那么不管线程何时读取A的值,它都会回头查阅A的原版拷贝
并读取当前值
。
易失性变量与同步化
如果一个变量被声明为volatile
,这意味着它预计会由多个线程修改。当然,您会希望JRE会为易失性变量施加某种形式的同步
。幸运的是,JRE在访问易失性变量时确实隐式地提供同步,但是有一条重要提醒:读取易失性变量是同步的
,写入易失性变量也是同步的
,但非原子操作不同步
。
像AtomicIntegerFieldUpdater
之类的原子字段更新程序基本上是应用于易失性字段的封装器。Java
类库在内部使用它们。虽然它们没有在应用程序代码中得到广泛应用,但是也没有不能使用它们的理由。
换言之,如果一个易失性变量得到更新,这样其值就会在底层被读取、修改并分配一个新值,结果将是一个在两个同步操作
之间执行的非线程安全操作
。然后您可以决定是使用同步化
还是依赖于JRE的支持
来自动同步易失性变量。更好的方法取决于您的用例:如果分配给易失性变量的值取决于当前值(比如在一个递增操作期间),要想该操作是线程安全的,那么您必须使用同步化。
// 线程不安全
myVolatile++
// 上述语句也可写成
int temp = 0;
synchronized (myVolatileVar) {
temp = myVolatileVar;
}
temp++;
synchronized (myVolatileVar) {
myVolatileVar = temp;
}
// 在一个多线程环境中递增或递减一个原语类型时,使用在java.util.concurrent.atomic包中找到的其中一个新原子类比自己编写同步代码块要好得多。
Java
提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是JVM
实现的synchronized
,另一个是JDK
实现的ReentrantLock
。
两种同步机制的比较
synchronized
基于JVM
实现,ReentrantLock
基于JDK
实现
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock
可中断,synchronized
不行
公平锁是指多个线程在等待同一个锁的时候,必须按照申请锁的时间顺序来一次获取锁。线程直接进入队列中排队,队列中的第一个线程才能获得锁。
synchronized
中的锁是非公平的,ReentrantLock
默认情况下也是非公平,但是也可以是公平的。
公平锁的优点是等待锁的线程不会饿死
。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大
一个ReentrantLock
可以同时绑定多个Condition
对象
除非需要使用
ReentrantLock
的高级功能,否则优先使用synchronized
。这是因为synchronized
是JVM
实现的一种锁机制,JVM
原生的支持它,而ReentrantLock
不是所有的JDK版本都支持。并且使用synchronized
不用担心没有释放锁而导致死锁问题,因为JVM
会确保锁的释放。
synchronized
,应尽可能使用同步块
而不是同步方法
wait
和notify
。CountDownLatch
、CyclicBarrier
,Semaphore
和Exchanger
这些同步类简化了编码操作,而用wait
和notify
很难实现复杂控制流。BlockingQueue
实现生产者消费者问题并发集合
,少用同步集合
本地变量
和不可变类
来保证线程安全线程的静态方法,默认对当前执行的线程起作用。因此,这些静态方法,如果写在main
方法里面,则一定作用到主线程,只有把它写到自定义线程里面,才一定是作用到自定义线程。
join
在线程中调用另一个线程的join
方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。
await/signal/signalAll
JUC
(java.util.concurrent
) 类库中提供了Condition
类来实现线程之间的协调,可以在Condition
上调用await
方法使线程等待,其他线程调用signal
或者signalAll
方法唤醒等待的线程。当前线程进入等待状态直到被通知(signal
)或中断,当前线程进入后台运行状态且从await()
方法返回其他线程调用该Condition
的signal
或者signalAll
方法,而当前线程被选中唤醒
interrupt
)中断当前线程await
方法返回,那么表明当前线程已经获取了Condition
对象的锁相比
wait
这种等待方式,await
可以指定等待的条件,因此更加灵活。
wait
方法与notify
调用Thread.sleep()
方法使当前线程
进入限期等待状态时,常常用"使一个线程睡眠
"进行描述
调用Object.wait()
方法使当前线程
进入限期等待或者无限等待状态,常常用"挂起一个线程
"进行描述
睡眠
和挂起
用来描述行为,阻塞
和等待
用来描述状态。阻塞
是被动的,等待获取一个排他锁,等待
是主动的,通过调用Thread.sleep()
和Object.wait()
等方法进入
wait
: 调用wait
之前,线程必须获得该对象的锁(即必须拥有该对象的监视器),因此只能在同步方法/同步代码块中调用wait
方法。只要该线程在等待队列中,就会一直处于闲置状态,不会被调度执行。此方法使当前线程(称为T )将自己置于该对象的等待集中,然后放弃对该对象的任何和所有同步声明。 出于线程调度的目的,线程T被立即禁用,并且处于休眠状态,不再往下执行其他代码。
synchronized(first) {
try {
// 表示执行这段代码的线程, 获得了first对象的监视器,同时把自己添加到first对象的等待集中,等待2秒。这期间,该线程处于TIMED_WAITING状态。
// 虽然是first对象调用了wait方法,但实际上对first对象并无影响,哪怕first对象也是个线程对象。
// wait方法仅作用到当前线程,表示线程挂起,等待获取first对象。
first.wait(2000);
}
}
notify
: 和wait
一样,notify
也要在同步方法/同步代码块
中调用。notify
的作用是,如果有多个线程等待,那么线程规划器随机
挑选出一个wait
的线程,对其发出通知
,并使它等待获取
该对象的对象锁,注意,即使收到了通知,wait
的线程也不会马上获取对象锁
,必须等待notify
方法的线程释放锁
才可以。进入方法 | 退出方法 |
---|---|
Thread.sleep()方法 | 时间结束 |
设置了Timeout参数的Object.wait()方法 | 时间结束/Object.notify()/Object.notifyAll() |
设置了Timeout参数的Thread.join()方法 | 时间结束/被调用的线程执行完毕 |
interrupted
与isInterrupted
与interrupt
方法阻塞式方法抛出InterruptedException
时,会清除中断中断状态。(会抛出InterruptedException
异常的方法,称为阻塞式方法)
中断是一种合作机制。当一个线程打断另一个线程时,被中断的线程不一定立即停止它正在做的事情。相反,中断是一种礼貌地要求另一线程在方便时停止它正在做的事情的方式。一些方法,如Thread.sleep()
,会认真对待此请求,但方法不需要注意中断。不阻塞但执行时间仍可能需要很长时间的方法可以通过轮询中断状态来尊重中断请求,如果中断,则提前返回。您可以自由地忽略中断请求,但这样做可能会损害响应。
中断的合作性质的好处之一是,它为安全构建可取消的活动提供了更大的灵活性。我们很少希望活动立即停止;如果活动在更新中被取消,程序数据结构可能会处于不一致的状态。中断允许可取消的活动清理任何正在进行的工作,恢复不变量,将取消通知其他活动,然后终止。
如果一个线程的run
方法执行一个无限循环,并且没有执行sleep
等会抛出InterruptedException
的操作,那么调用线程的interrupt
方法就无法使线程提前结束。但是调用interrupt
方法会设置线程的中断标记,此时调用interrupted
方法会返回true
,因此可以在循环体中使用interrupted()
方法来判断线程是否处于中断状态,从而提前结束线程。
public class InterruptExample {
private static class MyThread extends Thread {
@Override
public void run() {
try{
while (!interrupted()) {
// ..
}
} catch (InterruptedException e) {
}
System.out.println("Thread end");
}
}
}
@Override
public void run() {
try {
// do something...
} catch (InterruptedException e) {
// do something cleanup
throw e;
}
}
ThreadPoolExecutor
),工作线程的实现是响应中断,因此中断在线程池中执行的任务,可能有取消任务
和通知执行线程该线程池正在关闭
的作用。如果任务吞下中断请求,则工作线程可能不会得知请求了中断,这会延迟应用程序或服务的关闭。catch
住并打log
的操作,完全没作用 public Task getNextTask(BlockingQueue<Task> queue) {
boolean interrupted = false;
try {
while (true) {
try {
return queue.take();
} catch (InterruptedException e) {
interrupted = true;
}
}
} finally {
if (interrupted) {
Thread.currentThread().interrupt();
}
}
}
@Override
public void run() {
try {
while (!Thread.currentThread.isInterrupted()) {
queue.put(...)
}
} catch (InterruptedException e) {
// allow thread to exit
}
}
@Override
public void run () {
try {
// do something...
} catch (InterruptedException e) {
Thread.currentThread.interrupt();
}
}
新建
-New:创建后尚未启动
可运行
-Runnable:可能正在运行,也可能正在等待CPU时间片 (Running/Ready
)。当Thread
实例的yield
方法被调用时,或者由于线程调度器的原因,相应线程的状态会由RUNNING
转换为READY
。
阻塞
-Blocked:等待获取一个排他锁,如果其线程释放了锁就会结束此状态。等待监控锁进入synchronized
块或方法,或者在调用Object.wait
方法后(源码注释)。该状态下不占用CPU资源。结束此状态后,转回RUNNABLE
状态
无限期等待
-Waiting:等待其他线程显式地唤醒,否则不会分配CPU时间片。因为调用了以下三种方法
Object.wait
未设置 timeout
Thread.join
未设置 timeout
LockSupport.park
同样的道理,执行以下方法可以使WAITING
转RUNNABLE
Object.notify
Object.notifyAll
LockSupport.unpark(thread)
限期等待
-Timed Waiting:无需等待其他线程显式的唤醒,在一定时间之后会被系统自动唤醒(但如果其他线程在指定时间内执行该线程所期望的特定操作后,能提早转换为RUNNABLE
)。因为调用了以下方法
Thread.sleep
Object.wait
设置了 timeout
Thread.join
设置了 timeout
LockSupport.parkNanos
LockSupport.parkUntil
执行结束
-Terminated:线程执行完成
多线程编程
Dealing with InterruptedException
【基本功】不可不说的Java“锁”事
并发程序正确的执行,必须要保证原子性
、可见性
、有序性
。只要有一个没有被保证,就有可能会导致程序运行不正确。
原子性 (synchronized
、Lock
实现)
一个或多个操作要么全部执行完成、且执行过程不被中断。要么就不执行
可见性 (volatile
、synchronized
、Lock
保证)
多个线程同时访问同一个变量时,一个线程修改了值,其他线程能够立即看得到新值
有序性 (volatile
、synchronized
、Lock
保证)
程序执行的顺序按照代码的先后顺序执行 (Java内存模型的happen-before
原则(即先行发生原则
)保证有序性)
独享锁,又叫排他锁
,是指该锁一次只能被一个线程所持有。如果线程T
对数据A
加上排他锁后,则其他线程
不能再对A加任何类型的锁。获得排他锁的线程既能读数据又能修改数据。
JDK的synchronized和JUC的Lock的实现类就是互斥锁,排他锁
共享锁,指该锁可被多个线程所持有。如果线程T
对数据A
加上共享锁后,则其他线程只能对A再加共享锁
,不能加排他锁
。获得共享锁
的线程只能读取数据,不能修改数据。
模式 | 含义 |
---|---|
SHARED | 表示线程以共享的模式等待锁 |
EXCLUSIVE | 表示线程正在以独占的方式等待锁 |
阻塞
或唤醒
一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还长。
许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。
为了让当前线程稍等一下
,让当前线程自旋,如果在自旋完成后,前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销
。这就是自旋锁。
自旋锁本身是有缺点的,它不能代替阻塞
。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。
可重入锁,又名递归锁
。是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁 (前提锁对象得是同一个对象或class
),不会因为之前已经获取过还没释放而阻塞。(解决自己锁死自己的情况)
ReentrantLock与synchronized都是可重入锁
方法嵌套调用
:因为内置锁是可重入的,所以同一个线程在调用doOthers
时可以直接获得当前对象的锁,进入doOthers
进行操作。如果是一个不可重入锁,那么当前线程在调用doOthers
之前需要将执行doSomething
时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。
没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码代码块时能够提高性能。
当一个线程访问同步代码块并获取锁时,会在Mark Word
里存储锁偏向的线程ID
。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁
。而是检测Mark Word
里是否存储着指向当前线程的偏向锁。
引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径
,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上,没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。
撤销偏向锁后恢复到无锁(标志位为01
)或轻量级锁(标志位为00
)的状态。
偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:
-XX:-UseBiasedLocking=false
,关闭之后,程序默认会进入轻量级锁状态。
当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
在代码进入同步块的时候,如果同步对象锁状态为无锁状态,虚拟机首先将在当前线程的栈帧中建立一个名为锁记录 (Lock Record
)的空间,用于储存锁对象目前的Mark Word
的拷贝,然后拷贝对象头中的Mark Word
复制到锁记录中。
拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word
更新为指向Lock Record
的指针,并将Lock Record
里的owner
指针指向对象的Mark Word
。
如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word
的锁标志位设置,表示此对象处于轻量级锁定状态。
如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word
是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。
若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
Mark Word
中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。
偏向锁
通过对比Mark Word
解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作
和自旋
来解决加锁问题,避免线程阻塞
和唤醒
而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞
公平锁是指多个线程在等待同一个锁的时候,必须按照申请锁的时间顺序来一次获取锁。线程直接进入队列中排队,队列中的第一个线程才能获得锁。
synchronized
中的锁是非公平的,ReentrantLock
默认情况下也是非公平,但是也可以是公平的。
公平锁的优点是等待锁的线程不会饿死
。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大
多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁
的场景。
优缺点与公平锁相反。
ReentrantLock中,公平锁与非公平锁的lock()方法的唯一区别就在于
公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()
。这个方法主要是判断当前线程是否位于同步队列中的第一个,如果是则返回true,否则返回false
。
根据锁的添加到Java的时间,Java中的锁,可以分为同步锁
和JUC包中的锁
。即通过synchronized
来进行同步,实现对竞争资源的互斥访问的锁。
一种思想。在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁
,只是在执行更新的时候判断一下在此期间别人是否修改了数据;如果别人修改了数据则放弃操作,否则执行操作
CAS机制
如果内存的值等于预期的值,则将该值更新,否则不进行操作。许多CAS操作是自旋
的:如果操作不成功,会一直重试,直到成功为止。(CAS只能保证单个变量操作的原子性,当涉及多个变量时,CAS无能为力
)
版本号机制
在数据中增加一个字段version
,每当数据被修改,版本号+1。查询数据时,将版本号一起查出来;当更新数据时,判断当前版本号与读取的版本号是否一致,一致才进行操作。(当query针对表1,update针对表2,很难通过简单的版本号实现乐观锁
)
一种思想。在操作数据时非常悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住
,直到操作完成后才会释放锁;上锁期间其他人不能修改数据
代码块加锁
、数据加锁 (MySQL的排它锁)
)# 该查询语句会为这条数据加上排它锁,直到 事务提交 或 回滚 时才会释放排它锁;在此期间,如果其他线程试图 更新这条数据 或执行 select for update ,会被阻塞
select ... for update
出现并发冲突的概率小
)时,乐观锁更有优势。悲观锁需要锁住代码块或数据
,而且加锁和释放
需要消耗额外资源出现并发冲突的概率大
)时,悲观锁更有优势。乐观锁执行更新失败时需要不断重试
,浪费CPU资源 (可引入退出机制,重试次数超过一定阈值后失败退出
)。悲观锁
适合写
操作多的场景,先加锁可以保证写操作时数据正确。乐观锁
适合读
操作多的场景,不加锁的特点是能够使其操作的性能大幅提升。AQS (AbstractQueuedSynchronizer
),队列同步器,用来构建锁
和其他同步组件
的基础框架。AQS是一个抽象类
,仅仅只是定义了同步状态的获取和释放的方法
来供自定义的同步组件使用。一般是同步组件的静态内部类
,即通过组合
的方式使用。(常用的ReentrantLock/Semaphore/CountDownLatch
的实现都依赖AQS)
AQS维护了一个volatile int state (代表共享资源)
和FIFO (线程等待队列,多线程争用资源被阻塞时进入此队列)
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
...
}
}
AQS框架架构图如下:有颜色的是方法,无颜色的是属性
AQS核心思想是,如果被请求的共享资源
空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态
;如果共享资源
被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用到的是CLH队列的变体 (Craig、Landin and Hagersten队列,单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配)
实现的,将暂时获取不到锁的线程加入到队列中。
AQS使用一个volatile的int类型的成员变量state
来表示同步状态
,通过内置的FIFO队列来完成资源获取的排队工作,通过CAS
完成对state
值的修改。
通过修改state字段表示的同步状态来实现多线程的独占模式:
通过修改state字段表示的同步状态来实现多线程的共享模式:
protected final int getState()
使用final修饰的方法无法被重写,类无法被继承
protected final void setState(int newState)
protected final boolean compareAndSetState(int expetct, int update)
protected boolean isHeldExclusively()
该线程是否正在独占资源。只有用到Condition才需要去实现它。
同步工具 | 同步工具与AQS的关联 |
---|---|
ReentrantLock | 使用AQS 保存锁重复持有的次数 。当一个线程获取锁时,ReentrantLock记录当前获得锁的线程标识,用于检测是否重复获取,以及错误线程试图解锁操作时异常情况的处理。 |
Semaphore | 使用AQS同步状态来保存信号量的当前计数 。tryRelease会增加计数,acquireShared会减少计数。 |
CountDownLatch | 使用AQS同步状态来表示计数 。计数为0时,所有的Acquire操作(CountDownLatch的await方法)才可以通过。 |
ReentrantReadWriteLock | 使用AQS同步状态中的16位保存写锁持有的次数 ,剩下的16位用于保存读锁的持有次数 。 |
ThreadPoolExecutor | Worker利用AQS同步状态实现对独占线程变量的设置 (tryAcquire和tryRelease)。 |
CAS是一种乐观锁思想。涉及到三个操作数
数据库读写
场景,这个一般是待写入新值时,之前读取出来的内存值,但CAS是通用思想,因此用这种说法表示)功能限制
CAS只能保证单个变量操作的原子性,这意味着
除此之外,CAS的实现需要硬件层面处理器
的支持。在Java中普通用户无法直接使用,只能借助atomic
包下的原子类
使用,灵活性收到限制。
循环时间长,开销大
CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
ABA问题
线程1读取内存中数据为A
线程2修改内存中数据为B
线程2修改内存中数据为A
线程1对数据进行CAS操作
最后一步,由于内存中数据仍然为A,因此CAS操作成功,但实际上该数据已经被线程2修改过。
在AtomicInteger的例子中,ABA似乎无危害。在某些场景下,ABA却会带来隐患,如栈顶问题:一个栈的栈顶经过多次变化又恢复了原值,但是栈可能已发生了变化。
对于ABA问题,比较有效的方案是引入版本号,内存中的值每一次发生变化,版本号+1;进行CAS操作时,不仅比较内存中的值,也会比较版本号,只有当二者都没有变化时,CAS才能执行成功。
Java中的AtomicStampedReference类就是用版本号解决ABA问题。
支持语义不同(重入
、公平
等)的锁规则
可重入
同一个锁能够被一个线程多次获取
公平机制
不同线程获取锁的机制是公平的
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
读取者可以共享,写入者独占的锁
。JUC中仅ReentrantReadWriteLock
实现了该接口。
LockSupport的功能与Thread
中的Thread.suspend()
和Thread.resume()
有点类似。LockSupport中的park()
与unpark()
的作用分别是阻塞线程
和解除阻塞线程
。但是park()
和unpark()
不会遇到可能引发的死锁
问题。
ReentrantLock
实现wait
和notify
的功能。await
、signal
、signalAll
原理和synchronized
锁对象的wait
、notify
、notifyAll
是一致的。wait、notify、notifyAll的功能,需要synchronized锁定住一个资源对象,设为obj。则通过调用obj.wait来使当前线程挂起。
但是ReentrantLock特殊,其功能实现需要两个对象,lock本身和condition。lock本身只用来锁定资源,condition只用来协调线程。所以condition一定需要绑定一个Lock,表示被锁定住。
class TaskQueue {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void addTask(...) {
lock.lock();
try {
...
condition.signalAll();
} finally {
lock.unlock();
}
}
public String getTask() {
lock.lock();
try {
while (...) {
condition.await();
}
return ...
} finally {
lock.unlock();
}
}
}
独占锁
,基于AQS
)ReentrantLock
用于替代synchronized
,和synchronized
不同的是,ReentrantLock
可以尝试获取锁
// 线程在tryLock失败的时候,不会导致死锁
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
...
} finally {
lock.unlock();
}
}
AQS
)AQS
)可以协同多个线程,让多个线程在这个屏障前等待,直到所有线程都达到了这个屏障时,再一起继续执行后面的动作。
CyclicBarrier适用于多个线程有固定的多步需要执行,线程间互相等待,当都执行完了,再一起执行下一步
。
上图7个线程各有一个barrier.await
,任何一个线程在执行到await
时就会进入阻塞等待状态,直到7个线程都到了await
时才会同时从await
返回,继续后面的工作。
这个类比CountDownLatch
和Semaphore
两个类的一个好处是,有点类似切面编程,可以让我们在同类线程的某个切面切入一块逻辑,并且可以同步所有的线程的执行速度。
如果在构造CyclicBarrier时设置了一个Runnable实现,那么最后一个到await的线程会执行这个Runnable
设置了5个等待线程,结果await被调用了超过5次,会发生什么?
仅5个以内的await有效?其余的一定无效吗?
AQS
)信号量对象管理的信号就像令牌,构造时传入个数,总数就是控制并发的数量。
执行前先获取信号(通过acquire
获取信号),执行后归还信号(通过release
归还信号)。
每次acquire
成功返回后,可用的信号量就会减少一个,如果没有可用的信号量,acquire调用就会阻塞,等待有release
调用释放信号后,acquire
才会得到信号并返回。
semaphore.acquire();
try {
// 调用
} finally {
semaphore.release();
}
共享锁
,基于AQS
)在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。CountDownLatch只能触发一次,计数值不能被重置。
当多个线程都达到了预期状态时触发事件,其他线程可以等待这个事件来触发自己的后续工作。等待的线程可以是多个,即CountDownLatch可以唤醒多个等待的线程
。达到自己预期状态
的线程调用CountDownLatch的countDown
方法,而等待
的线程会调用CountDownLatch的await
方法。
void await()
使当前线程在锁存器倒计数到零之前一直等待,除非线程被中断。
boolean await(long timeout, TimeUnit unit)
同上,加入了超时时间
void countDown()
递减锁存器的计数,如果计数到达零,则释放所有等待的线程。
CountDownLatch
的作用是允许1或N个线程等待其他线程完成执行
;CyclicBarrier
则是`允许N个线程相互等待``CountDownLatch
的计数器无法被重置
;CyclicBarrier
的计数器可以被重置后使用
public class MyRunnable implements Runnable {
private CyclicBarrier barrier;
public MyRunnable(CyclicBarrier barrier) {
this.barrier = barrier;
}
@Override
public void run(){
try {
barrier.await();
} catch (...)
}
}
...
int count = 10;
final CyclicBarrier barrier = new CyclicBarrier(count + 1);
for(int i = 0;i < count; i++) {
}
##阻塞队列的实现
阻塞队列 (BlockingQueue
)是util.concurrent
包下重要的数据结构,BlockingQueue
提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空
。并发包下很多高级同步类的实现都是基于BlockingQueue
实现的。阻塞队列被广泛应用在生产者-消费者
问题中。
插入元素
add(E e)
往队列插入数据。当队列满时,会抛出IllegalStateException
异常
offer(E e)
往队列插入数据。成功返回true
,否则返回false
。不会因为队列满就抛出IllegalStateException
异常
offer(E e, long timeout, TimeUnit unit)
往队列插入数据。当队列满时,在规定时间内阻塞插入数据的线程
。超出时间,线程退出
。
put
往队列插入数据。当队列满时,插入数据的线程
会被阻塞,直至队列有容量
删除元素
remove(Object o)
从队列中删除数据,成功返回true
,否则返回false
poll
从队列中删除数据,当队列为空时,返回null
take
从队列中删除数据,当队列为空时,获取队头数据的线程
会被阻塞。
poll(long timeout, TimeUnit unit)
从队列中删除数据,当队列为空时,获取数据的线程
会在给定时间内被阻塞。超出时间,线程退出
。
查看元素
element
获取队头元素,队列为空则抛出NoSuchElementException
异常
peek
获取队头元素,队列为空则抛出NoSuchElementException
异常
有容量大小限制
)默认情况下,不保证线程访问队列的公平性。即队列有容量时,长时间阻塞的线程仍然无法访问到队列。如果保证公平性,通常会降低吞吐量。
// 可使用如下方式获取保证公平性的队列
ArrayBlockingQueue<Integer> block = new ArrayBlockingQueue<Integer>(10, true);
Delayed
接口可选容量,默认为无界,即Integer.MAX_VALUE,所以默认创建的该队列有容量危险
)为了防止容量迅速增大,通常在创建该对象时,会指定大小。如未指定,则容量等于Integer.MAX_VALUE
线程池的优点
线程池的运行状态
运行状态 | 状态描述 |
---|---|
RUNNING | 能接受新提交的任务,并且也能处理阻塞队列中的任务 |
SHUTDOWN | 关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务 |
STOP | 不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程 |
TIDYING | 所有的任务都已经终止了,workerCount(有效线程数 )为0 |
corePoolSize
创建了线程池后,默认初始没有任何线程,等待任务来后才创建线程。超过核心线程数后,放入阻塞队列。
maxPoolSize
当线程数大于等于corePoolSize
,并且任务阻塞队列已满时,线程池会创建新的线程,直到线程数达到maxPoolSize
。当线程数达到maxPoolSize
,且任务阻塞队列已满,这时候线程池会根据拒绝策略
来处理该任务,默认的处理方式是直接抛异常。
keepAliveTime
当线程空闲时间达到keepAliveTime
,该线程会退出,直到线程数等于corePoolSize
。如果设置了allowCoreThreadTimeout
为true,则所有线程都会退出。
tasks
每秒的任务数,假设为500-1000taskcost
每个任务花费的时间,假设为0.1sresponsetime
系统允许容忍的最大响应时间,假设为1scorePoolSize
:每秒需要处理多少个任务 => tasks * taskcost
queueCapaticy
: 每秒可以缓存多少个任务 =>
maxPoolSize
: 每秒可以缓存多少个任务 => tasks-corePoolSize
网上的例子没有权威性,全TM是国内的码农抄来抄去的代码和博客。
这里使用固定线程数线程池的创建,即corePoolSize
与maxPoolSize
数一致,该值使用如下获取
Runtime.getRuntime().availableProcessors() * 50;
这种设置线程数的方式,在StackOverFlow
看得有点多,而且hsweb
开源项目也是使用这种方法的
package ...
import java.util.concurrent.TimeUnit;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
public class Main {
public static void main(String[] args) {
int coreSize = 10, maxSize = 40, keepAliveTime = 1000;
TimeUnit keepAliveUnit = TimeUnit.SECONDS;
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(100_00);
ExecutorService service = new ThreadPoolExecutor(coreSize, maxSize, keepAliveTime, keepAliveUnit, queue, r -> {
Thread temp = new Thread(r);
temp.setName("GROUP-"+System.currentTimeMillis());
return temp;
}, (runnable, executor)->{
System.out.println(runnable+"直接放弃");
});
}
}
从ReentrantLock的实现看AQS的原理及应用
死锁发生的情形
一个线程两次申请锁 (第一次申请成功,第二次重复申请,这时候无法获取到被第一次时申请到的锁,造成拥有锁的线程挂起
)
两个线程互相申请对方的锁,但是对方都不释放锁
死锁产生的必要条件
处理死锁的四种方法
避免死锁的方法
线程安全的两个方面:执行控制
和内存可见
Java内存模型
的实现,线程在具体执行时,会先拷贝主存数据到线程本地,操作完成后再把结果从线程本地刷新到主存。synchronized
解决的是执行控制的问题,他会阻止其他线程获取当前对象的监控锁,这样就使得当前对象中被synchronized
关键字保护的代码块无法被其他线程访问,也就无法并发执行。更重要的是,synchronized
还会创建一个内存屏障
,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作,都happens-before
于随后获得这个锁的线程的操作。
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在
happens-before
关系
happens-before
原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们解决在并发环境下两操作之间是否可能存在冲突的所有问题。下面就一个简单的例子稍微了解
i = 1; // 线程A执行
j = i; // 线程B执行
j是否等于1呢?假定线程A的操作happens-before
于线程B的操作,那么可以确定线程B执行后j=1
一定成立。
happens-before
另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。happens-before
关系,并不意味着一定要按照happens-before
原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before
关系来执行的结果一致,那么这种重排序并不非法。volatile
解决的是内存可见性的问题,会使得所有对volatile
变量的读写都会直接刷到主存,即保证了变量的可见性。
使用
volatile
关键字仅能实现对原始变量(如boolean、short、int、long等
)操作的原子性,但需要特别注意,volatile
不能保证复合操作的原子性,即使只是i++
,实际上也是由多个原子性操作组成。
对于volatile
关键字,当且仅当满足以下所有条件时可使用:
volatile
和synchronized
的区别
volatile
本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取;synchronized
则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile
仅能使用在变量级别;synchronized
则可以使用在变量、方法和类级别的
volatile
仅能实现变量的修改可见性,不能保证原子性;而synchronized
则可以保证变量的修改可见性和原子性
volatile
不会造成线程的阻塞,synchronized
可能会造成线程的阻塞
volatile
标记的变量不会被编译器优化,synchronized
标记的变量可以被编译器优化。
理论上,用map
存放线程对象和值的键值对就可以实现ThreadLocal
的功能,但是性能上不是最优的,多线程访问ThreadLocal
的map
对象会导致并发冲突,用synchronized
加锁会导致性能上的损失。因此,JDK7
里是将map对象保存在线程里,这样每个线程去取自己的数据,就不需要加锁保护的。
使得所有对volatile
变量的读写都会直接刷到主存,即保证了变量的可见性。
使用
volatile
关键字仅能实现对原始变量(如boolean、short、int、long等
)操作的原子性,但需要特别注意,volatile
不能保证复合操作的原子性,即使只是i++
,实际上也是由多个原子性操作组成。
对于volatile
关键字,当且仅当满足以下所有条件时可使用:
volatile
和synchronized
的区别
volatile
本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取;synchronized
则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile
仅能使用在变量级别;synchronized
则可以使用在变量、方法和类级别的
volatile
仅能实现变量的修改可见性,不能保证原子性;而synchronized
则可以保证变量的修改可见性和原子性
volatile
不会造成线程的阻塞,synchronized
可能会造成线程的阻塞
volatile
标记的变量不会被编译器优化,synchronized
标记的变量可以被编译器优化。
理论上,用map
存放线程对象和值的键值对就可以实现ThreadLocal
的功能,但是性能上不是最优的,多线程访问ThreadLocal
的map
对象会导致并发冲突,用synchronized
加锁会导致性能上的损失。因此,JDK7
里是将map对象保存在线程里,这样每个线程去取自己的数据,就不需要加锁保护的。