锁的对比
java中的锁一共有4种状态,级别从低到高分别是:
- 无锁状态
- 偏向锁
- 轻量级锁
- 重量级锁
锁只能升级,不能降级
偏向锁
顾名思义,为了让线程获得锁的代价更低,引入了偏向锁。
加锁
当一个线程访问同步块并且获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程id,这样,这个线程便获取了这个对象的偏向锁,之后这个线程进入和退出就不需要通过CAS操作,也就是原子操作,来进行加锁和解锁,只需要简单的测试下对象头存储的偏向锁的线程id是否和自身的id一致,如果一致,那么已经获取锁,直接进入。否则,判断对象中是否已经存储了偏向锁,如果没有锁,那么使用CAS竞争锁,如果设置了,那么尝试使用CAS将对象头的偏向锁指向当前线程。
解锁
偏向锁的解锁时机是在竞争时才会释放锁,撤销时需要等待全局安全点,这个时间点没有正在执行的字节码,首先会暂停拥有偏向锁的线程,然后检查偏向锁的线程是否存活,如果不活动,那么直接设置为无锁状态。否则要么偏向其他锁,要么恢复到无锁或者标记对象不适合偏向锁。
轻量锁
会自旋尝试获取锁,消耗cpu资源
加锁
一旦多线程发起了锁竞争,并且释放了偏向锁之后,线程通过CAS修改Mark Word,如果当前没有对象持有同步体的锁,那么直接将同步体的锁修改的轻量锁,否则,该线程将自旋获取锁,直到膨胀为重量级锁,修改同步体的Mark Word为重量级锁,然后阻塞
解锁
一旦有其他线程因想获取当前锁而膨胀为重量级锁,那么这个线程将会通过CAS替换Mark Word,然后失败,解锁,并且唤醒其他等待线程。
重量级锁
会阻塞,不消耗cpu资源,但是响应时间较慢
原子操作
处理器实现原子操作
总线锁
通过总线锁保证只有单一处理器占有共享内存
缓存锁
只对某个内存地址进行加锁,保证其他的内存地址不受影响。
java实现原子操作
循环使用CAS操作
需要解决以下三个问题
- ABA问题
如果一个值最开始为1,中间修改为2,最后再修改为1,其实这个值是修改过的,所以只通过基本的CAS操作是有问题的,这个时候就需要通过引用来处理。 - 耗时
通过pause指令解决 - 只能保证一个共享变量
将多个共享变量合成一个共享变量
锁机制也是通过循环CAS操作
volatile原理
在jvm内存模型上,即每一个线程都持有一份对于内存的拷贝,volatile保证了对于一个volatile变量的读,总能看到任意线程对这个volatile变量最后的写入。
我们可以这么理解,对一个volatile变量,它的set和get都将加锁,只要是一个volatile变量,对该变量的读写就具有原子性,但是复合操作,比如说volatile操作整体上不具有原子性。
- 可见性:对于java内存模型,每一个线程都拥有一个主存的副本,在一个共享变量被某个线程修改之后,将会将其他线程中内存的副本标志为无效,强制从主存中取值。
- 原子性:对于被volatile修饰的变量,其get和set操作都加了锁,
- 不可重排:对于被volatile修饰的变量,保证编译器不会对一些代码时序做重排
synchronized
内部也是利用了锁。
每一个对象都有一个自己的monitor,必须先获取这个monitor对象才能够进入同步块或同步方法,而这个monitor对象的获取是排他的,也就是同一时刻只能有一个线程获取到这个monitor
修饰同步块
主要通过monitorenter和monitorexit指令来进行获取
修饰方法
主要是通过修饰符ACC_SYMCHRONIZED来修饰
如果获取对象的monitor失败,线程将会切换为BLOCK状态,
synchronized作为java中用得最广泛的同步关键字。实际上内部实现的依靠着两个关键的命令,monitorenter和monitorexit。synchronized同样是通过互斥量和临界区来实现。monitorenter指令表示进入临界区,互斥量+1,monitorexit表示退出临界区,互斥量减一。在jdk1.5之前,synchroize操作都是重操作,所加的锁都是自旋锁。即,线程在执行了monitorenter时,发现互斥量已经为0了,则必须在外面循环等待,而不会进行阻塞,加入等待队列。而在jdk1.6之后,对synchronized做了很多优化操作,锁粗化,锁消除,轻量级锁,偏向锁,适应性自旋锁等来减少操作的开销。下面我们分别来介绍几个优化操作。
- 锁膨胀(Lock Coarsening):如果系统检测到有对同一个对象反复加锁或者解锁,那么系统在保证加锁的正确性之后,会将锁扩张,保证不必要的加锁操作和解锁操作。
- 锁削除(Lock Elimination):在虚拟机运行时,通过JIT编译器的逃逸分析检测出部分上锁的代码不可能出现数据多线程数据共享的状态,那么自然会将这个锁给去除。
- 轻量级锁(Lightweight Locking):轻量级锁是相对于重量级锁来说的,它的本意是在没有多线程竞争的情况下,减少重量级锁使用的互斥量产生的性能消耗,轻量级锁不用使用操作系统的互斥量,仅仅只是在hotspot对象头记录,在这里,我们必须先介绍一下Hotspot的对象头,其中又个markWork,用来记录当前的锁状态:
01: 表示未锁定
00: 表示轻量级锁定
10: 表示重量级锁定 - 偏向锁:偏向第一个持有锁的对象,一旦持有,则不会再进行同步。直到有下一个线程尝试获取这个锁。
- 适应性自旋锁:在线程首次尝试轻量级锁失败时,会进入忙等待一段时间,然后再次重试,在重试了一定次数失败之后,就会进入阻塞态。
和lock的区别
synchronized将会隐式的获取锁,但是它把锁给固化了,没有办法比较灵活的利用锁,比如说非阻塞式的获取锁,超时获取锁等等。
synchronized和lock方式两种加锁的方式的最底层实现是一致的,都是通过临界区的互斥量来判断的。只是synchronized加锁及解锁操作都是由jvm为我们实现好了。而lock加锁方式是基于我们代码的实现,我们必须要手动的解锁。这就让lock方式能够有一些相对于synchronized有一些优点了。
提供了tryLock方法,可以让我们提前预知能否加锁而不会让该线程进入阻塞状态。synchronized加锁之后,一旦不能加锁便会进入阻塞状态。
lockInterruptibly方法:提供了立即响应线程中断的方法,通常来说,线程并不会是在任何状态下都会被立即中断的,只有在线程处于阻塞状态才会被立即中断。线程在获取锁的时候,会处于一种不可中断的状态,比如synchonized或者单纯的lock操作就是这样的。
提供了获取锁超时的返回选项,对于竞争激烈的临界区,这种机制显得格外重要。这个机制也是通过tryLock来实现,多加了一个超时参数判断。
在我们代码的实现要求中,假如没有明确的要求,我们可以利用synchronized关键字,sunchronized关键字在竞争不是很激烈的情况下,性能要优于lock操作,但是在竞争激烈的情况下,性能将会下降特别多。
队列同步器 AbstractQueuedSynchronizer
提供了3类模版方法:
- 独占式获取与释放同步状态
- 共享式获取与释放同步状态
- 查询同步队列的等待线程
通过下面3个方法来访问或修改同步状态:
- getState
- setState
- compareAndSetState
同步器内部依赖于同步队列来完成同步状态的管理,当前线程获取同步状态失败时,同步器将会把这个线程构造成一个node添加到同步队列,并且阻塞线程,当同步状态释放时,将会把首节点的线程唤醒,让其再次尝试获取同步状态。 对于入队操作,同样也需要CAS操作进行入队
- 可重入锁: 线程可以进入它已经加上锁的同步代码块。即假如这个线程在某个位置加上可重入锁,在后面这个线程仍然可以再次加锁。进入同样的同步代码块,也就是临界区。
- 不可重入锁: 一旦线程在某个地方加上锁了,在没解锁前,这个锁就是该线程唯一一次进入该临界区的通道。而且,在这个临界区中,有且仅能有一个这样的锁。
重入锁
ReetrantLock,顾名思义,它表示该锁能够支持一个线程对资源的重复加锁。
symchronized默认支持的是可重入锁。
java中锁主要是在util.concurrent.locks包下,最常用的就是ReentrantLock类,这个锁被成为可重入锁。既然有可重入锁,也就会有不可重入锁,这种锁的区别主要是针对于同一个线程来说的,下面简单聊聊这两种锁的区别:
在java中,ReentrantLock和synchronized所加上的锁都是可以重入.
在介绍ReentrantLock之前,我们必须先了解其内部实现的两种锁,公平锁和非公平锁。
- 公平锁: 所有的尝试加锁的对象,都会添加到一个队列中,先来的排在队列前头,到时候可以加锁时,从队首取第一个加锁对象。
- 非公平锁: 所有尝试加锁的对象能够加上锁的顺序不能保证,随机。
我们先来看看ReentrantLock对象的创建:
Lock fairLock = new ReentrantLock(true); //创建公平锁
Lock noFairLock = new Reentrantlock(); //创建非公平锁
=====>两种构造函数的实现
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
{%endcodeblock%}
我们先只针对于非公平锁来分析,我们平时调用这个锁并不会特意的指定锁类型。对于加锁,我们只需要调用lock方法即可。
{%codeblock%}
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
{%endcodeblock%}
compareAndSetState中,0时我们期待的值,1是我们现在想要更新的值,这个值表示的是当前临界区的互斥量。加入返回true的话,表示当前没有线程给这个临界区上锁,则直接上锁,设置持有线程,否则,现在已经有锁了,调用acquire(1).
{%codeblock%}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
========>添加队列节点
private Node addWaiter(Node mode) {
//新建队列节点,节点是需要记录线程的
Node node = new Node(Thread.currentThread(), mode);
//获取尾部元素
Node pred = tail;
if (pred != null) {
//插入在尾部后面
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//加入没有尾部元素,执行enq操作,
enq(node);
return node;
}
==========>
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
//没有元素,插入头部
if (compareAndSetHead(new Node()))
tail = head;
} else {
//有元素,插入尾部
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
=========> 利用断言,判断是否能够加锁
public boolean tryAcquire(long acquires) {
assertEquals(LOCKED, acquires);
return compareAndSetState(UNLOCKED, LOCKED);
}
=======> 在尝试获取锁失败之后,会添加到等待队列中
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//找到前一个节点
final Node p = node.predecessor();
//如果这个节点是头节点的话,那么在再次尝试进入临界区
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//shouldParkAfterFailedAcquire返回这个线程是否需要阻塞等待,
//parkAndCheckInterrupt()用于阻塞这个线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
通过上面这个加锁操作,我们可以了解当一个线程尝试加锁失败时,会判断是否需要阻塞,如果需要阻塞,则直接阻塞该线程.
上面操作,是直接执行lock方法,我们可以先尝试执行tryLock方法:
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
=======>尝试获取非公平锁,尝试加锁,加锁是以线程的基础
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//state表示获取的当前状态的互斥量值
int c = getState();
if (c == 0) {
//为0表示没上锁,重新尝试进入临界区
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;
}
下面我们来看看解锁操作:
public void unlock() {
sync.release(1);
}
=======>
public final boolean release(long arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
======>在tryRelease中利用断言解锁,假如现在没有锁,那么则会抛异常
public boolean tryRelease(long releases) {
if (getState() != LOCKED) throw new IllegalMonitorStateException();
setState(UNLOCKED);
return true;
}
======>unparkSuccessor(h)让队列头进入临界区
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
//寻找第一个合法的等待节点
if (s == null || s.waitStatus > 0) {
s = null;
for (Node p = tail; p != null && p != node; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
if (s != null)
//打断阻塞,进入临界区
LockSupport.unpark(s.thread);
}
加锁
如果当前对象没有加锁,那么直接加锁,如果已经加锁,判断之前加锁的线程与当前要加锁线程是否是同一个,如果是,那么再次获取。
解锁
如果一个线程获取了n次锁,那么就需要解锁n次。
读写锁
在同一时刻允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程都被阻塞。其实内部是维护了一对锁