1.6 锁

1.6 锁

1.6.1 Synchronized

image.png

1 Synchronized的使用方式

修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁

修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

2 底层原理

① synchronized 同步语句块的情况

public class Synchronized Demo {
	public void method() {
		synchronized (this) {
			System.out.println("synchronized 代码块");
		}
	}
}

复制代码

image.png

注意存在两次 moniterexit

② synchronized 修饰方法的的情况

public class Synchronize dDemo2 {
	public synchronized void method() {
		System.out.println("synchronized 方法");
	}
}

复制代码

image.png

3 锁升级

无锁状态,可偏向

JVM启动一会会后,才会开启,最开始生成的对象是可偏向状态

偏向锁

image-20210510183203731.png
偏向第一个拿到锁的线程。

当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

轻量级锁

轻量级锁就是通过 CAS 进行加锁的。

JVM 会给线程的栈帧中创建一个叫锁记录 Lock Record 的空间,把对象头 Mark Word 复制到该空间里(Displaced Mark Word),并通过 CAS 尝试把原对象头 Mark Word 中锁记录指针指向该锁记录。如果成功,表示线程拿到了锁。如果失败,则进行自旋(自旋锁),自旋超过一定次数时升级为重量级锁,这时该线程会被内核挂起。

image-20210510183135801.png

自旋锁

轻量级锁膨胀为重量级锁前,线程在执行 monitorenter 指令进入等待队列时,会通过自旋去尝试获得锁。

如果自旋超过一定次数时还未拿到锁(10次),就会进入阻塞状态,等待内核来调度。此时会发生内核态与用户态之间的上下文切换,所以会影响性能(引入自旋锁就是为了减少这个开销)。

因为后面的线程也先进行自旋尝试获取锁,所以这对于已被阻塞的那些线程来说,会不公平

重量级锁

重量级锁就是通过内核来操作线程。因为频繁出现内核态与用户态的切换,会严重影响性能。

升级为重量级锁时会在堆中创建 monitor 对象,并将 Mark Word 指向该 monitor 对象,并将线程加入 EntrySet 中。

image.png

image.png

1.6.2 ReentryLock

底层主要是一个 AQS 和 condition 节点

image.png

节点状态

  • int INITIAL = 0; // 初始状态
  • int CANCELLED = 1; // 当前节点从同步队列中取消
  • int SIGNAL = -1; // 后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继 节点的线程继续运行。
  • int CONDITION = -2; // 节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了 signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中。
  • int PROPAGATE = -3; // 表示下一次共享式同步状态获取将会无条件地被传播下去。

源码分析

锁获取
// 顶层入口
 public final void acquire(int arg) {
     if (!tryAcquire(arg) &&
         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
         selfInterrupt();
 }
/*
tryAcquire()尝试直接去获取资源,如果成功则直接返回(这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加塞一次,而CLH队列中可能还有别的线程在等待);
addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
acquireQueued()使线程阻塞在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
*/

// 产生获取锁
tryAcquire(int)
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
 }


// 添加进入等待队列
addWaiter(Node mode)
private Node addWaiter(Node mode) {
    //以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享)
    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) {
    //CAS"自旋",直到成功加入队尾
    for (;;) {
        Node t = tail;
        if (t == null) { // 队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {//正常流程,放入队尾
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
复制代码

上面的代码进入的等待队列中,需要排队拿号,去获得锁

// 接上文 第4行 的方法
// 在等待队列中排队拿号(中间没其它事干可以休息),直到拿到号后再返回
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;//标记是否成功拿到资源
    try {
        boolean interrupted = false;//标记等待过程中是否被中断过

        //又是一个“自旋”!
        for (;;) {
            final Node p = node.predecessor();//拿到前驱
            //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)。
            if (p == head && tryAcquire(arg)) { //tryAcquire 返回 ture 成功拿到锁
                setHead(node);//拿到资源后,将head指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null。
                p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!
                failed = false; // 成功获取资源
                return interrupted;//返回等待过程中是否被中断过
            }

            //如果自己可以休息了,就通过park()进入waiting状态,直到被unpark()。如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
        }
    } finally {
        if (failed) // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
            cancelAcquire(node);
    }
}


// 接 20 行
// 此方法主要用于检查状态,看看自己是否真的可以去休息了(进入waiting状态,如果线程状态转换不熟,可以参考本人上一篇写的Thread详解),万一队列前边的线程都放弃了只是瞎站着,那也说不定,对吧!
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;//拿到前驱的状态
    if (ws == Node.SIGNAL)
        //如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了
        return true;
    if (ws > 0) {
        /*
         * 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
         * 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)!
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
         //如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢!
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}


// 接上第 21 行  线程进入休息
private final boolean parkAndCheckInterrupt() {
     LockSupport.park(this);//调用park()使线程进入waiting状态
     return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。
 }
复制代码

到此可以进行锁的获取

主要过程

非公平状态下,首先尝试直接获取

不能直接获取,生成节点去纳入等待队列中,需要把前面节点的状态变成sign(通知唤醒我),如果找不到,就一直向前找

通过一个死循环的自旋操作去获取锁,如果前驱是 head 节点,就尝试去获取锁,如果前去不是,就通过park()的方法把自己阻塞

锁释放

非共享模式下

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;//找到头结点
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);//唤醒等待队列里的下一个线程
        return true;
    }
    return false;
}

// 上接第 5 行, 此方法用于唤醒等待队列中下一个线程
private void unparkSuccessor(Node node) {
    //这里,node一般为当前线程所在的结点。
    int ws = node.waitStatus;
    if (ws < 0)//置零当前线程所在的结点状态,允许失败。
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;//找到下一个需要唤醒的结点s
    if (s == null || s.waitStatus > 0) {//如果为空或已取消
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev) // 从后向前找。
            if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);//唤醒
}
复制代码

共享模式下

public final void acquireShared(int arg) {
     if (tryAcquireShared(arg) < 0)
         doAcquireShared(arg);
 }

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);//加入队列尾部
    boolean failed = true;//是否成功标志
    try {
        boolean interrupted = false;//等待过程中是否被中断过的标志
        for (;;) {
            final Node p = node.predecessor();//前驱
            if (p == head) {//如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的
                int r = tryAcquireShared(arg);//尝试获取资源
                if (r >= 0) {//成功
                    setHeadAndPropagate(node, r);//将head指向自己,还有剩余资源可以再唤醒之后的线程
                    p.next = null; // help GC
                    if (interrupted)//如果等待过程中被打断过,此时将中断补上。
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }

            //判断状态,寻找安全点,进入waiting状态,等着被unpark()或interrupt()
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}


// 上接第 16 行,将头节点设置位自己,唤醒后面的线程,
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head;
    setHead(node);//head指向自己
     //如果还有剩余量,继续唤醒下一个邻居线程
    if (propagate > 0 || h == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

// 释放锁
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {//尝试释放资源
        doReleaseShared();//唤醒后继结点
        return true;
    }
    return false;
}

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;
                unparkSuccessor(h);//唤醒后继
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;
        }
        if (h == head)// head发生变化
            break;
    }
}
复制代码

共享和非共享的主要区别在于:当head 节点释放了锁,唤醒后续节点的时候,共享模式会一直持续唤醒后面点,而非共享模式不会

1.6.3 相关组件

Semaphore (信号量):

允许多个线程同时访问: synchronized 和 ReentrantLock 都是⼀次只允许⼀个线程访问某个资源, Semaphore (信号量)可以指定多个线程同时访问某个资源。

CountDownLatch (倒计时器):

CountDownLatch 是⼀个同步⼯具类,⽤来协调多个线程之间的同步。这个⼯具通常⽤来控制线程等待,它可以让某⼀个线程等待直到倒计时结

束,再开始执⾏。

public class CountDownLatchExample1 {
    // 处理⽂件的数量
    private static final int threadCount = 6;

    public static void main(String[] args) throws InterruptedException {
        // 创建⼀个具有固定线程数量的线程池对象(推荐使⽤构造⽅法创建)
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
            final int threadnum = i;
            threadPool.execute(() -> {
                try {
                    //处理⽂件的业务操作
 				......
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //表示⼀个⽂件已经被完成
                    countDownLatch.countDown();
                }
            });
        }
        countDownLatch.await();
        threadPool.shutdown();
        System.out.println("finish");
    }
复制代码
CyclicBarrier (循环栅栏):

CyclicBarrier 和 CountDownLatch ⾮常类似,它也可以实现线程间的技术等待,但是它的功能⽐ CountDownLatch 更加复杂和强⼤。主要应⽤场景和

CountDownLatch 类似。 CyclicBarrier 的字⾯意思是可循环使⽤( Cyclic )的屏障( Barrier )。它要做的事情是,让⼀组线程到达⼀个屏障(也可以叫同步点)时被阻塞,直到最后⼀个线程到达屏障时,屏障才会开⻔,所有被屏障拦截的线程才会继续⼲活。 CyclicBarrier 默认的构造⽅法是 CyclicBarrier(int parties) ,其参数表示屏障拦截的线程数量,每个线程调⽤ await() ⽅法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

ReadWriteLock(读写锁)
public ReentrantReadWriteLock() {
    this(false);
 }

public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

复制代码

将锁的state 状态变成高16位和低16位,分别表示锁状态

image-20210511113816528.png

公平模式 ====》都是入队

非公平模式 ====》

由于获取读锁的逻辑比较复杂,我们在这里先简单进行归纳:

a. 如果当前全局处于无锁状态,则当前线程获取读锁

b. 如果当前全局处于读锁状态,且队列中没有等待线程,则当前线程获取读锁

c. 如果当前全局处于写锁占用状态(并且不是当前线程占有),则当前线程入队尾

d. 如果当前全局处于读锁状态,且等待队列中第一个等待线程想获取写锁,那么当前线程能够获取到读锁的条件为:当前线程获取了写锁,还未释放;当前线程获取了读锁,这一次只是重入读锁而已;其它情况当前线程入队尾。之所以这样处理一方面是为了效率,一方面是为了避免想获取写锁的线程饥饿,老是得不到执行的机会

e. 如果当前全局处于读锁状态,且等待队列中第一个等待线程不是写锁,则当前线程可以抢占读锁

获取写锁相对就比较简单了,规则如下:

h. 如果当前处于无锁状态,则当前线程获取写锁

i. 如果当前全局处于读锁状态,当前线程入队尾

j. 如果当前全局处于写锁状态,除非是重入获取写锁,否则入队尾

锁降级

假设A线程获得了写锁,在释放写锁之前A线程可以继续获得读锁。这个时候A线成同时拥有读锁和写锁。A线程的写业务完成后,先释放了写锁,但没有释放读锁。这个时候对于A线程来说就是锁降级了。需要注意的是,如果A线程首先获得的是读锁,这个时候是不能直接升级到写锁的。需要先把读锁释放了,再去尝试获得写锁

锁升级===》不可以

可重入===》 一旦发生可重入操作,可以直接获取,不需要排队

1.6.4 ReentrantLock 和 synchronized 的区别

1. 锁的实现

synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。

2. 性能

新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。

3. 等待可中断

当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。

ReentrantLock 可中断,而 synchronized 不行。

4. 公平锁

公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。

synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。

5. 锁绑定多个条件

一个 ReentrantLock 可以同时绑定多个 Condition 对象。

1.6.5 interrupted、interrupt、isInterrupted

interrupt():

方法是用于中断线程的,调用该方法的线程的状态将被置为”中断”状态

注意:调用interrupt()方法仅仅是在当前线程中打了一个停止的标记,并不是真的停止线程,需要用户自己去监视线程的状态为并做处理</font>。这一方法实际上完成的是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态

更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,那么,它将接收到一个中断异常(InterruptedException),从而提早地终结被阻塞状态。

interrupted():

测试当前线程(当前线程是指运行interrupted()方法的线程)是否已经中断,且清除中断状态。

isInterrupted():

isInterrupted():测试线程(*调用该方法的线程*)是否已经中断,不清除中断状态。

public class InterruptionSleepThread implements Runnable {

    @Override
    public void run() {
        try {
            System.out.println("本大爷要休眠50秒,你能奈我何?");
            Thread.sleep(50000);//休眠50秒
        } catch (InterruptedException e) {
            System.out.println("休眠的线程收到中断信号,利用抛异常的方式将本大爷从梦中叫醒,结束线程。");
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new InterruptionSleepThread());
        thread.start();
        Thread.sleep(2000);
        thread.interrupt();
        System.out.println("主线程结束");

    }
}
复制代码

image-20210511111133145.png

参考

锁升级和降级

锁升级

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享