- 它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步
- 同步代码块原理
- 具体实现是在编译之后在同步方法调用前加入一个 monitorenter 指令,在退出方法和异常处插入 monitorexit 的指令
monitorenter
每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
- 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;
monitorexit
执行monitorexit的线程必须是objecterf所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
- 具体实现是在编译之后在同步方法调用前加入一个 monitorenter 指令,在退出方法和异常处插入 monitorexit 的指令
- 方法的同步并没有通过指令 monitorenter 和 monitorexit 来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符
- 当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。
作用于整个类。
Synchronized是可重入锁
可重入锁
默认是非公平锁
Sync
Sync继承自AQS实现了解锁tryRelease()方法NonfairSync
继承自Sync,实现了获取锁的tryAcquire()方法
代码
/**
* 非公平锁的实现
*/
static final class NonfairSync extends Sync {
/**
* 加锁 首先CAS尝试把state的状态从0置为1,成功则获得锁,否则执行 acquire(1)方法,为AQS中的方法
*/
final void lock() {
//执行CAS操作,获取同步状态
if (compareAndSetState(0, 1))
//成功则将独占锁线程设置为当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
//否则再次请求同步状态
acquire(1);
}
}
public final void acquire(int arg) {
//再次尝试获取同步状态
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- 这里传入参数arg表示要获取同步状态后设置的值(即要设置state的值),因为要获取锁,而status为0时是释放锁,1则是获取锁,所以这里一般传递参数为1,进入方法后首先会执行tryAcquire(arg)方法,在前面分析过该方法在AQS中并没有具体实现,而是交由子类实现,因此该方法是由ReentrantLock类内部实现的
//NonfairSync
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
abstract static class Sync extends AbstractQueuedSynchronizer {
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//判断同步状态是否为0,并尝试再次获取同步状态
if (c == 0) {
//执行CAS操作
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果当前线程已获取锁,属于重入锁,再次获取锁后将status值加1
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//设置当前同步状态,当前只有一个线程持有锁,因为不会发生线程安全问题,可以直接执行setState(nextc);
setState(nextc);
return true;
}
return false;
}
}
从代码执行流程可以看出,这里做了两件事
- 一是尝试再次获取同步状态,如果获取成功则将当前线程设置为OwnerThread,否则失败
- 二是判断当前线程current是否为OwnerThread,如果是则属于重入锁,state自增1,并获取锁成功,返回true,反之失败,返回false,也就是tryAcquire(arg)执行失败,返回false。需要注意的是nonfairTryAcquire(int acquires)内部使用的是CAS原子性操作设置state值,可以保证state的更改是线程安全的,因此只要任意一个线程调用nonfairTryAcquire(int acquires)方法并设置成功即可获取锁,不管该线程是新到来的还是已在同步队列的线程,毕竟这是非公平锁,并不保证同步队列中的线程一定比新到来线程请求(可能是head结点刚释放同步状态然后新到来的线程恰好获取到同步状态)先获取到锁,这点跟后面还会讲到的公平锁不同
在acquire中,如果tryAcquire(arg)返回true,acquireQueued自然不会执行,这是最理想的,因为毕竟当前线程已获取到锁,如果tryAcquire(arg)返回false,则会执行addWaiter(Node.EXCLUSIVE)进行入队操作,由于ReentrantLock属于独占锁,因此结点类型为Node.EXCLUSIVE
addWaiter
private Node addWaiter(Node mode) {
//将请求同步状态失败的线程封装成结点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
//如果是第一个结点加入肯定为空,跳过。
//如果非第一个结点则直接执行CAS入队操作,尝试在尾部快速添加
if (pred != null) {
node.prev = pred;
//使用CAS执行尾部结点替换,尝试在尾部快速添加
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果第一次加入或者CAS操作没有成功执行enq入队操作
enq(node);
return node;
}
如果是第一个结点,则为tail肯定为空,那么将执行enq(node)操作
enq
private Node enq(final Node node) {
//死循环
for (;;) {
Node t = tail;
//如果队列为null,即没有头结点
if (t == null) { // Must initialize
//创建并使用CAS设置头结点
if (compareAndSetHead(new Node()))
tail = head;
} else {
//队尾添加新结点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
这里做了两件事,一是如果还没有初始同步队列则创建新结点并使用compareAndSetHead设置头结点,tail也指向head,二是队列已存在,则将新结点node添加到队尾
添加到同步队列后,结点就会进入一个自旋过程,即每个结点都在观察时机待条件满足获取同步状态,然后从同步队列退出并结束自旋,回到之前的acquire()方法,自旋过程是在acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法中执行的
acquireQueued
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//自旋,死循环
for (;;) {
//获取前驱结点
final Node p = node.predecessor();
//当且仅当p为头结点才尝试获取同步状态
if (p == head && tryAcquire(arg)) {
//将node设置为头结点
setHead(node);
//清空原来头结点的引用便于GC
p.next = null; // help GC
failed = false;
return interrupted;
}
//如果前驱结点不是head,判断是否挂起线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
//最终都没能获取同步状态,结束该线程的请求
cancelAcquire(node);
}
}
当前线程在自旋(死循环)中获取同步状态,当且仅当前驱结点为头结点才尝试获取同步状态,这符合FIFO的规则,即先进先出,其次head是当前获取同步状态的线程结点,只有当head释放同步状态唤醒后继结点,后继结点才有可能获取到同步状态,因此后继结点在其前继结点为head时,才进行尝试获取同步状态,其他时刻将被挂起
setHead
//设置头结点
private void setHead(Node node) {
head = node;
//清空节点数据
node.thread = null;
node.prev = null;
}
设置为node结点被设置为head后,其thread信息和前驱结点将被清空,因为该线程已获取到同步状态(锁),正在执行了,也就没有必要存储相关信息了,head只有保存指向后继结点的指针即可,便于head结点释放同步状态后唤醒后继结点
如果前驱结点不是head
shouldParkAfterFailedAcquire
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取当前结点的等待状态
int ws = pred.waitStatus;
//如果为等待唤醒(SIGNAL) 状态则返回true
if (ws == Node.SIGNAL)
return true;
//如果ws>0则说明是结束状态,
//遍历前驱结点直到找到没有结束状态的结点
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果ws小于0又不是SIGNAL状态,
//则将其设置为SIGNAL状态,代表该结点的线程正在等待唤醒。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
shouldParkAfterFailedAcquire()方法的作用是判断当前结点的前驱结点是否为SIGNAL状态(即等待唤醒状态),如果是则返回true
如果结点的ws为CANCELLED状态(值为1>0),即结束状态,则说明该前驱结点已没有用应该从同步队列移除,执行while循环,直到寻找到非CANCELLED状态的结点
倘若前驱结点的ws值不为CANCELLED,也不为SIGNAL(当从Condition的条件等待队列转移到同步队列时,结点状态为CONDITION因此需要转换为SIGNAL),那么将其转换为SIGNAL状态,等待被唤醒
若shouldParkAfterFailedAcquire()方法返回true,即前驱结点为SIGNAL状态同时又不是head结点,那么使用parkAndCheckInterrupt()方法挂起当前线程,称为WAITING状态,需要等待一个unpark()操作来唤醒它
public void unlock() {
sync.release(1);
}
//AQS类的release()方法
public final boolean release(int arg) {
//尝试释放锁
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
//唤醒后继结点的线程
unparkSuccessor(h);
return true;
}
return false;
}
//ReentrantLock类中的内部类Sync实现的tryRelease
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//判断状态是否为0,如果是则说明已释放同步状态
if (c == 0) {
free = true;
//设置Owner为null
setExclusiveOwnerThread(null);
}
//设置更新同步状态
setState(c);
return free;
}
private void unparkSuccessor(Node node) {
//这里,node一般为当前线程所在的结点
int ws = node.waitStatus;
if (ws < 0)
//置零当前线程所在的结点状态,允许失败
compareAndSetWaitStatus(node, ws, 0);
//找到下一个需要唤醒的结点s
Node s = node.next;
//如果为空或已取消
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
//从这里可以看出,<=0的结点,都还是有效的结点
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒
LockSupport.unpark(s.thread);
}
继承自Sync,实现了获取锁的tryAcquire()方法
不同点
//公平锁FairSync
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//主要,这里先判断同步队列是否存在结点
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
该方法与nonfairTryAcquire(int acquires)方法唯一的不同是在使用CAS设置尝试设置state值前,调用了hasQueuedPredecessors()判断同步队列是否存在结点,如果存在必须先执行完同步队列中结点的线程,当前线程进入等待状态。这就是非公平锁与公平锁最大的区别,即公平锁在线程请求到来时先会判断同步队列是否存在结点,如果存在先执行同步队列中的结点线程,当前线程将封装成node加入同步队列等待。而非公平锁呢,当线程请求到来时,不管同步队列是否存在线程结点,直接尝试获取同步状态,获取成功直接访问共享资源,但请注意在绝大多数情况下,非公平锁才是我们理想的选择,毕竟从效率上来说非公平锁总是胜于公平锁。
- 循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态
- 需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
为了保证数据的完整性,在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除
- 用vector举例
- StringBuffer
- 如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。
- 上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。
- vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。
- 偏向锁
- 在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁
- 当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里储存锁偏向的线程ID,再次进入和退出同步块时不需要CAS来加锁或解锁,只需简单测试下对象头里是否储存着锁偏向的线程ID,如果成功则获得锁,失败则查看是否是偏向锁,是,尝试把偏向的线程ID改为自己,否,CAS竞争锁
- 偏向锁是在单线程执行代码块时使用的机制,如果在多线程并发的环境下(即线程A尚未执行完同步代码块,线程B发起了申请锁的申请),则一定会转化为轻量级锁或者重量级锁
- 锁的撤销
- 出现多线程竞争
- 在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁
- 轻量级锁
- 首先会在当前线程的栈帧中创建用于储存锁记录的空间Lock Record,并把对象头里的mark word复制到锁记录中,然后线程尝试用CAS把对象头里的mark word替换为指向锁记录Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word,成功,则获取锁,否则自旋来获取锁,若自旋超过一定次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁
- 重量级锁
- 所有未获取到锁的线程都阻塞而不是自旋
是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去
- 因为系统资源不足。
- 进程运行推进顺序不合适。
- 资源分配不当等。 如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
public class DeadLock {
//创建两个对象,用两个线程分别先后独占
private Boolean flag1 = true;
private Boolean flag2 = false;
public static void main(String[] args) {
DeadLock deadLock = new DeadLock();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程1开始,作用是当flag1 = true 时,将flag2也改为 true");
synchronized (deadLock.flag1){
if(deadLock.flag1){
try{
//睡眠1s ,模拟业务执行耗时,并保证两个线程进入死锁状态
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("flag1 = true,准备锁住flag2...");
synchronized (deadLock.flag2){
deadLock.flag2 = true;
}
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程2开始,作用是当flag2 = false 时,将flag1也改为 false");
synchronized (deadLock.flag2){
if(!deadLock.flag2){
try{
//睡眠1s ,模拟业务执行耗时,并保证两个线程进入死锁状态
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("flag2 = false,准备锁住flag1...");
synchronized (deadLock.flag1){
deadLock.flag1 = false;
}
}
}
}
}).start();
}
}
以上代码,可以用一个死锁的图解释。线程1独占对象1,想要访问对象2,而对象2此时已经独占对象2,在等待对象1的资源释放,此时线程1因无法获取到对象2而无法向下执行,因此没法释放对象1,线程2同理,造成了死锁状态,两个线程都阻塞在等待资源处
死锁的预防基本思想打破产生死锁的四个必要条件中的一个或几个,保证系统不会进入死锁状态。
- 比如
- 打破互斥条件:允许进程同时访问某些资源
- 打破不剥夺条件:允许进程从占有者占有的资源中强行剥夺一些资源
- 打破请求与保持条件:进程在运行前一次性地向系统申请它所需要的全部资源
- 打破循环等待条件:实行资源有序分配策略
- 加锁顺序(线程按照一定的顺序加锁)
- 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
- 死锁检测
- jps+jstack方式排查
- jconsole方式排查
- 选择线程,监测死锁。会将死锁的线程信息都展示出来
- jvisualvm
- 选择对应进程即可直观看到死锁的存在
- Lock是一个接口而synchronized是java中的关键字,synchronized是内置的语言实现,synchronized是在JVM层面上是实现的,出异常时JVM会自动释放锁定,但是Lock不行,Lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中;
- synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生。
- Lock可以让等待锁的线程响应中断,线程可以中断去干别的事务,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断
- 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到
- Lock可以提高多个线程进行读操作的效率 总结:当资源竞争激烈时,Lock的性能要远远优于synchronized