一、 synchronized
1.synchronized关键字介绍
synchronized块是java提供的一种原子性内置锁,java中的每个对象都可以把它当作一个同步锁来使用。
这些java内置的使用者看不到的锁被称为内部锁,也叫做监视器锁。
java中内置锁包括:
无锁、偏向锁、轻量级锁、重量级锁(同步锁)
synchronized就是重量级锁。
线程的执行代码在进入synchronized代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起
。拿到了内部锁的线程会在正常退出同步代码块
或者抛出异常后
或者在同步块内调用了wait系列方法时
释放该内置锁。
另外,由于java中的线程与操作系统的原生线程是一一对应的(java线程非用户线程),所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作
,也就是很耗时的上下文切换
。
2.synchronized的内存语义
synchronized可以解决共享变量内存可见性问题。
进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的语义就是把在synchronized块内对共享变量的修改刷新到主内存。
关于共享变量内存的可见性
根据java内存模型可知,线程在对共享变量进行操作时,会先将其从主内存中加载到工作内存(比如cpu的L1、L2和L3缓存),然后操作完成后将修改后的值同步回主内存。假如有两个线程AB同时读取了共享变量,A修改了变量的值并刷回了主内存,但是B由于自己的工作内存中有共享变量的值,所以不会去读取主内存了,此时A对共享变量的修改对于B就是不可见的。volatile就是用来解决这个问题的。
3.三种使用方式
- 修饰实例方法:给对象实例加锁。
- 修饰静态方法:给当前类加锁。
- 修饰代码块:指定加锁对象(类或者指定对象)。
4.底层原理(jvm层面)
(1)修饰代码块
synchronized
同步语句块使用的是 monitorenter
(对应JMM模型lock指令) 和 monitorexit
(unlock)指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。
**当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor
的持有权。**如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。
(2)修饰方法
synchronized
修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取得代之的确实是 ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法。如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。
总结:本质都是对对象监视器monitor的获取。
加锁时,对锁计数器+1;释放锁时,对锁计数器-1;锁计数器为0时,表示可以获取该锁。
补充:lock和unlock
lock 和 unlock 在Java内存模型中是必须满足下面四条规则的:
① 一个变量同一时刻只允许一条线程对其进行 lock ,但 lock 操作可以被同一个线程执行多次,多次执行 lock 后,只有执行相同次数的 unlock ,变量才能被解锁。
② 如果对一个变量执行 lock ,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值;
③ 如果一个变量没有被 lock 锁定,则不允许对其执行 unlock 操作,也不允许 unlock 一个其它线程锁定的变量;
④ 对一个变量执行 unlock 之前,必须先把此变量同步回主内存中,即执行 store 和 write 操作;
5.锁优化
首先明确锁有四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
在jdk1.6以后,synchronized 有如下三种状态:
- 偏向锁,是指一段同步代码一直被一个线程访问,那么这个线程会自动获取锁,降低获取锁的代价。
- 轻量级锁,是指当锁是偏向锁时,被另一个线程所访问,偏向锁会升级为轻量级锁,这个线程会通过自旋的方式尝试获取锁,不会阻塞,提高性能。
- 重量级锁,是指当锁是轻量级锁时,当自旋的线程自旋了一定的次数后,还没有获取到锁,就会进入阻塞状态,该锁升级为重量级锁,重量级锁会使其他线程阻塞,性能降低。
6.总结
(1)synchronized 是可重入锁;
(2)synchronized 是非公平锁;
(3)synchronized 可以同时保证原子性、可见性、有序性;
(4)synchronized 有三种状态:偏向锁、轻量级锁、重量级锁;
(5)synchronized 是悲观锁;
synchronized的不⾜之处:
- 如果临界区是只读操作,其实可以多线程⼀起执⾏,但使⽤synchronized的话,同⼀时间只能有⼀个线程执⾏。
- synchronized⽆法知道线程有没有成功获取到锁。
- 使⽤synchronized,如果临界区因为IO或者sleep⽅法等原因阻塞了,⽽当前 线程⼜没有释放锁,就会导致所有线程等待。
为解决以上问题,可以使用lock包下的锁。
二、ReentrantLock
juc.locks包下共有三个接⼝: Condition 、 Lock 、 ReadWriteLock 。其中,Lock 和ReadWriteLock从名字就可以看得出来,分别是锁和读写锁的意思。Lock接⼝⾥ ⾯有⼀些获取锁和释放锁的⽅法声明,⽽ReadWriteLock⾥⾯只有两个⽅法,分别 返回“读锁”和“写锁”。
Lock接⼝中有⼀个⽅法是可以获得⼀个 Condition :
Condition newCondition();
Condition:await、signal、signalAll。
Object:wait、notify、notifyAll()。
但Condition类似于Object的等待/通知机制的加强版,可以唤醒指定线程。
1.概述:
ReentrantLock是⼀个非抽象类,它是Lock接⼝的JDK默认实现,实现了锁的基本功能。
2.ReentrantLock的高级特性(相比synchronized )
(1)等待可中断:ReentrantLock
提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly()
来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
(2)可实现公平锁(先等待先获得):默认是非公平锁,可以通过修改构造方法参数来决定是否公平。
(3)可实现选择性通知(锁可以绑定多个条件):通过Condition
接口与newCondition()
方法实现等待/通知机制。
3.源码分析
(1)ReentrantLock实现了Lock接口,里面定义了java中锁应该实现的几个方法:
//获取锁
void lock();
//获取锁可中断
void lockInterruptibly() throws InterruptedException;
// 尝试获取锁,如果没获取到锁,就返回false
boolean tryLock();
// 尝试获取锁,如果没获取到锁,就等待一段时间,这段时间内还没获取到锁就返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//释放锁
void unlock();
//条件锁
Condition newCondition();
(2)主要属性
//在构造方法中初始化,决定使用公平锁还是非公平锁,默认非公平锁
private final Sync sync;
(3)主要内部类
//实现了AQS的部分方法
abstract static class Sync extends AbstractQueuedSynchronizer{}
//非公平锁的获取
static final class NonfairSync extends Sync {}
//公平锁的获取
static final class fairSync extends Sync {}
(4)主要构造方法
//默认非公平
public ReentrantLock() {
sync = new NonfairSync();
}
//传入参数实现公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
(5)公平锁实现
//ReentrantLock.lock()
public void lock() {
//调用sync的lock方法,sync是继承了AQS的静态内部类
//这里是公平锁,调用FairSync的实例
sync.lock();
}
//ReentrantLock.FairSync.lock()
final void lock() {
//调用AQS的acquire()方法获取锁,传值为1
acquire(1);
}
// AbstractQueuedSynchronizer.acquire()
public final void acquire(int arg) {
// 尝试获取锁
// 如果失败了,就排队
if (!tryAcquire(arg) &&
// 注意addWaiter()这里传入的节点模式为独占模式
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// ReentrantLock.FairSync.tryAcquire()
protected final boolean tryAcquire(int acquires) {
//当前线程
final Thread current = Thread.currentThread();
//查看当前状态变量的值
int c = getState();
//如果状态变量的值为0,说明暂时还没有人占有锁
if (c == 0) {
// 如果没有其它线程在排队,那么当前线程尝试更新state的值为1
// 如果成功了,则说明当前线程获取了锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {d
当前线程获取了锁,把自己设置到(AQS)dexclusiveOwnerThread变量中
setExclusiveOwnerThread(current);
//返回true说明成功获取锁
return true;
}
}
//如果当前线程本身就占有着锁,现在又尝试获取锁
// 那么,直接让它获取锁并返回true
else if (current == getExclusiveOwnerThread()) {
// 状态变量state的值加1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 设置到state中
// 这里不需要CAS更新state
// 因为当前线程占有着锁,其它线程只会CAS,把state从0更新成1是不会成功的
setState(nextc);
//获取成功
return true;
}
//获取失败
return false;
}
}
//AbstractQueuedSynchronizer.addWaiter()
// 调用这个方法,说明上面尝试获取锁失败了
private Node addWaiter(Node mode) {
// 新建一个节点
Node node = new Node(Thread.currentThread(), mode);
// 这里先尝试把新节点加到尾节点后面
// 如果成功了就返回新节点,如果没成功再调用enq()方法不断尝试
Node pred = tail;
// 如果尾节点不为空
if (pred != null) {
// 设置新节点的前置节点为现在的尾节点
node.prev = pred;
// CAS更新尾节点为新节点
if (compareAndSetTail(pred, node)) {
// 如果成功了,把旧尾节点的下一个节点指向新节点,并返回新节点
pred.next = node;
return node;
}
}
// 如果上面尝试入队新节点没成功,调用enq()处理
enq(node);
return node;
}
公平锁情况下获取锁的步骤总结:
(a)尝试获取锁,如果获取到了就直接返回了;
(b)尝试获取锁失败,再调用addWaiter()构建新节点并把新节点入队;
(c)然后调用acquireQueued()再次尝试获取锁,如果成功了,直接返回;
(d)如果再次失败,再调用shouldParkAfterFailedAcquire()将节点的等待状态置为等待唤醒(SIGNAL);
(e)调用parkAndCheckInterrupt()阻塞当前线程;
(f)如果被唤醒了,会继续在acquireQueued()的for()循环再次尝试获取锁,如果成功了就返回;
(g)如果不成功,再次阻塞,重复(c)(d)(e)直到成功获取到锁。
(6)非公平锁的实现
// ReentrantLock.lock()
public void lock() {
sync.lock();
}
// ReentrantLock.NonfairSync.lock()
final void lock() {
// 直接尝试CAS更新状态变量
if (compareAndSetState(0, 1))
// 如果更新成功,说明获取到锁,把当前线程设为独占线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// AbstractQueuedSynchronizer.acquire()
public final void acquire(int arg) {
//尝试获取,失败就排队
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// ReentrantLock.NonfairSync.tryAcquire()
protected final boolean tryAcquire(int acquires) {
//调用父类sync的方法(默认非公平锁)
return nonfairTryAcquire(acquires);
}
// ReentrantLock.Sync.nonfairTryAcquire()
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 如果状态变量的值为0,再次尝试CAS更新状态变量的值
// 相对于公平锁模式少了!hasQueuedPredecessors()条件
//!hasQueuedPredecessors()是为了检查前面是否有排队的线程
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
总结:
相对于公平锁,非公平锁在一开始就多了两次直接尝试获取锁的过程。
(a)一开始就尝试CAS更新状态变量state的值,如果成功了就获取到锁了;
(b)在tryAcquire()的时候没有检查是否前面有排队的线程,直接上去获取锁才不管别人有没有排队呢;
(7)tryLock()方法
以非公平模式尝试获取一次锁,成功了就返回true,没成功就返回false,不会继续尝试。
public boolean tryLock() {
//直接调用Sync的nonfairTryAcquire()方法,不排队直接获取
return sync.nonfairTryAcquire(1);
}
(8)unlock方法
//ReentrantLock.unlock()
public void unlock() {
sync.release(1);
}
//AbstractQueuedSynchronizer.release
public final boolean release(int arg) {
//调用AQS实现类的tryRelease()方法释放锁
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
//ReentrantLock.Sync.tryRelease(同tryAcquire,调用子类)
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 如果当前线程不是占有着锁的线程,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果状态变量的值为0了,说明完全释放了锁
// 这也就是为什么重入锁调用了多少次lock()就要调用多少次unlock()的原因
// 如果不这样做,会导致锁不会完全释放,别的线程永远无法获取到锁
if (c == 0) {
free = true;
// 清空占有线程
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
释放锁的过程大致为:
(1)将state的值减1;
(2)如果state减到了0,说明已经完全释放锁了,唤醒下一个等待着的节点;