从应用到源码分析——聊一聊synchronized与ReentrantLock

synchronized 和 ReentrantLock 的实现原理 两者区别 synchronized 属于独占式悲观锁,是通过 JVM 隐式实现的,synchronized 只允许同一时刻只有一个线程操作资源。 在 Java 中每个对象都隐式包含一个 monitor(监视器)对象,加锁的过程其实就是竞争 monitor 的过程,当线程进入字节码 monitor enter 指令之后,线程将持有 monitor 对象,执行 monitor exit 时释放 monitor 对象,当其他线程没有拿到 monitor 对象时,则需要阻塞等待获取该对象。 ReentrantLoc 阅读详情

现在就来聊聊并发中两个重要的应用synchronized和ReentrantLock。synchronized是JVM提供的一种隐式锁,而ReentrantLock是通过java代码编写的显式锁。下面就来简单了解这两个锁的应用以及实现原理。

显式锁:需要自己通过手动加锁或解锁

隐式锁:不需要我们手动加解锁,由JVM自动帮我们实现

synchronized

synchronized是一种互斥锁,可以说是并发编程中元老级别的存在,在JDK1.6以前,很多人称之为重量级锁,性能不高。但是在JDK1.6之后,对synchronized进行了一些优化,引入了偏向锁,轻量级锁以及重量级锁。这个时候,synchronized会根据线程竞争的程度进行锁升级或降级。很多的文章或者书籍中都表示synchronized锁升级后就不会降级,其实synchronized存在降级机制,只是这个条件比苛刻,后面会讲到在什么情况下会进行锁升级和锁降级。

synchronized锁的是谁

synchronized在日常开发中有三种用法,分别为作用在普通方法,作用在静态方法,作用在同步代码块。下面就来简单测试一下这三种用法。

作用在普通方法上

public class SyncCommonMethod {

    public static void main(String[] args) throws InterruptedException {

        CommonMethod commonMethod = new CommonMethod();
        new Thread(() -> {
            try {
                commonMethod.method1();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "线程A").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            commonMethod.method2();
        }, "线程B").start();

    }

}

class CommonMethod {

    public synchronized void method1() throws InterruptedException {
        TimeUnit.SECONDS.sleep(2);
        System.out.println("===>method1<===" + Thread.currentThread().getName());
    }

    public synchronized void method2() {
        System.out.println("===>method2<===" + Thread.currentThread().getName());
    }
}

上段代码中使用两个线程去调用同一个对象的两个方法,两个方法都是用synchronized进行了修饰,无论怎么运行,结果都如下图。

这是因为synchronized锁住的是对象的实例,也就是说创建出的commonMethod实例只有一把锁,两个线程谁先获取这把锁,谁就先执行,另一个线程则进行阻塞。那么再次基础上,再次新建一个CommonMethod,继续执行并查看运行效果。

public class SyncCommonMethod {

    public static void main(String[] args) throws InterruptedException {

        CommonMethod commonMethod = new CommonMethod();
        CommonMethod commonMethod2 = new CommonMethod();
        new Thread(() -> {
            try {
                commonMethod.method1();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "线程A").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            commonMethod2.method2();
        }, "线程B").start();

        new Thread(() -> {
            commonMethod2.method3();
        }, "线程C").start();
    }

}

class CommonMethod {

    public synchronized void method1() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (Exception e) {

        }
        System.out.println("===>method1<===" + Thread.currentThread().getName());
    }

    public synchronized void method2() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (Exception e) {

        }
        System.out.println("===>method2<===" + Thread.currentThread().getName());
    }

    // 不使用synchronized 
    public void method3() {
        System.out.println("===>method3<===" + Thread.currentThread().getName());
    }
}

运行结果发生了变化,因为两个对象实例分别持有自己的锁,两个线程不存在竞争关系。那对于线程B和线程C来讲,两个线程中调用的是同一个对象实例中的不同方法,那为什么就没有出现竞争呢?这是因为线程C中调用的method3方法并没有使用synchronized,所以不会受到锁的影响。

===>method3<===线程C
===>method2<===线程B
===>method1<===线程A

作用在静态方法上

public class SyncCommonMethod {

    public static void main(String[] args) throws InterruptedException {

        StaticMethod staticMethod = new StaticMethod();
        StaticMethod staticMethod2 = new StaticMethod();
        new Thread(() -> {
            try {
                staticMethod.method1();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "线程A").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            staticMethod2.method2();
        }, "线程B").start();

    }

}

class StaticMethod {

    public synchronized static void method1() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (Exception e) {

        }
        System.out.println("===>method1<===" + Thread.currentThread().getName());
    }

    public synchronized static void method2() {
        System.out.println("===>method2<===" + Thread.currentThread().getName());
    }
}

因为方法被static修饰,所以在类加载的时候就已经有了锁,并且锁的是Class模板。一个类的Class模板是全局唯一的,所以无论创建了多少个对象,使用的都是同一把锁,所以上述代码的执行任然是谁先获取到锁,谁就先执行。与所属实例对象无关,与实例对象的class有关。所以输出结果为

===>method1<===线程A
===>method2<===线程B

作用在同步代码块

synchroized(this)

public static void main(String[] args) throws InterruptedException {

    SyncCommonMethod syncCommonMethod = new SyncCommonMethod();

    new Thread(()->{
        syncCommonMethod.method();
    },"线程A").start();

    TimeUnit.SECONDS.sleep(1);

    new Thread(()->{
        syncCommonMethod.method2();
    },"线程B").start();

}

public void method(){
    synchronized (this){
        System.out.println("=====>"+Thread.currentThread().getName()+"<======");
    }
}

public void method2(){
    synchronized (this){
        System.out.println("=====>"+Thread.currentThread().getName()+"<======");
    }
}

synchroized(this)代码块中的this表示当前对象实例,所以该作用与作用在普通方法上的用处一样。每个对象实例都会持有自己的一把锁。

synchroized(Object.class)

public static void main(String[] args) throws InterruptedException {
    SynClassMethod method = new SynClassMethod();
    new Thread(() -> {
        method.method1();
    }, "线程A").start();
    TimeUnit.SECONDS.sleep(1);
    new Thread(() -> {
        method.method2();
    }, "线程B").start();
}

public void method1() {
    synchronized (SynClassMethod.class) {
        System.out.println("======>method1<=======" + Thread.currentThread().getName());
    }
}

public void method2() {
    synchronized (SynClassMethod.class) {
        System.out.println("======>method1<=======" + Thread.currentThread().getName());
    }
}

synchroized(Object.class)中Object.class表示对Object的Class模板进行加锁,作用与静态同步方法一致,只是语法不同。经过上面几个例子可以总结出synchroized锁住的要么是实例对象,要么是Class类。

用法锁住目标
普通方法锁住的是实例对象
静态方法锁住的是Class类
synchroized(this) / synchroized(Object)锁住的是实例对象
synchroized(Object.class)锁住的是Class类

synchronized如何加锁

在虚拟机中,创建的对象在内存中可以三个部分:对象头,实例数据以及对其填充。对象头中可以分成三个部分:Mark Word,MetaData元数据指针以及数组长度(数组对象才会有数组长度)。synchronized加锁标识就存储在Mark Word中,除了锁的标志之外Mark Word还包含了偏向锁的标识,偏向锁的线程id,以及分代年龄和Epoch。

对象头信息是对对象本身自定义数据无关的额外存储,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构,从而达到以很小的内存空间存储更多的数据,Mark Word会随着程序的执行而发生变化。下面的表格就是对象中锁不同状态对应不同的标识。

锁的状态是否偏向锁(1bit)锁的标志位(2bit)
无锁001
偏向锁101
轻量级锁00
重量级锁10

如果想要查看这些内存信息,可以在pom中引入下面这个依赖包,这个依赖包为我们提供了ClassLayout类,通过这个类可以打印出对象的内存信息。

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

public static void main(String[] args) {
    Object o = new Object();
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
}

OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)

下面是上述代码的输出结果,其中00000001 00000000 00000000 00000000 标识对象的加锁信息。但是发现后两位的结果为00啊,与表格内的标志不符啊。这是因为JVM使用的是小端模式。小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址 * 的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。 所以实际应该是 00000000 00000000 00000000 00000001。那么这个时候就可以进行判定了后两个比特位为01,标识有可能是无锁状态或者是偏向锁状态。再看倒数第三位标识为0,那么就表示为非偏向锁。那就可以断定,此时对象是一个无锁状态。那么现在对Object对象进行加锁,再来查看内存数据,是否发生变化。

public static void main(String[] args) throws InterruptedException {
    TimeUnit.SECONDS.sleep(5);
    Object o = new Object();
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
    synchronized (o){
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)

      0     4        (object header)                           05 d0 e3 ea (00000101 11010000 11100011 11101010) (-354168827)
      4     4        (object header)                           38 01 00 00 (00111000 00000001 00000000 00000000) (312)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)

可以看出当对Object加锁后,同步代码块内的输出00000101,参照表格可以知道这是一个偏向锁的标识。但是你会发现未加锁之前的Object输出结果尽然也是00000101,这是因为Object知道会发生偏向,但是此时并没有发生,这种叫做匿名偏向或者叫做可偏向状态。如果输出结果为11101000,有可能是关闭了JVM偏向锁,也有可能JVM对偏向锁进行了延迟处理,这种情况下可以使用-XX:BiasedLockingStartupDelay=0 调整延迟处理时间,使用-XX:±UseBiasedLocking 开启或关闭偏向锁。

为什么要引入锁升级

在JDK1.6以前,synchronized加锁是依赖与对象的,当对象创建后会维护一个Monitor管程,而Monitor又依赖于底层操作系统,synchronized的互斥是由操作系统中Mutex互斥量来实现的。当JVM需要使用到Mutex时,CPU需要由用户态切换到内核态,这个操作是比较消耗性能的。这种方式适用于线程经常非常激烈的场景,但是如果只有一两个线程进行竞争 ,没必要使用这种模式。考虑到这个问题,所以在JDK1.6便进行了优化。

锁升级的过程

在解释synchronized加锁时,已经知道了锁的状态会随着程序的运行发生升级,由起初的无锁状态升级到重量级锁。在对象刚被创建时,此时处于一个无锁状态,如果有且仅有一个线程对对象进行了加锁,那么此时将会升级到偏向锁状态,记录当前线程id并通过CAS操作修改Mark Word中的标志位。当有第二个线程来竞争锁时,并且两个线程执行速度非常快并且竞争比较小,那么此时为抢到锁的线程将会一直处于自旋状态并等待对象所被释放。当有多个线程来竞争锁并且竞争非常强烈时,那么此时会升级到重量级锁。在很多文章中都有提到说一旦锁升级,则无法降级,其实锁降级是存在的,只是条件比较苛刻。具体的触发时机:在全局安全点(safepoint)中,执行清理任务的时候会触发尝试降级锁。

锁的粗化以及锁消除

锁粗化和锁消除是JVM对synchronized的一个优化。锁粗化是指将多个连续的同步代码块合并到同一个同步代码块中,从而减少加锁,解锁的次数。锁消除是指,JVM通过检测发现同步代码块内并不存在线程竞争,那么JVM会将该锁进行一个消除。锁粗化与锁消除都需要依赖于逃逸分析

Reentrantlock

ReentrantLock是java并发包下面的Lock接口的一个实现类,其丰富的功能与synchronized对比更加灵活。ReentrantLock的底层实现依赖于AQS,支持手动加解锁,支持手动设置公平与非公平锁,也支持线程中断。在ReentrantLock类中定义了一个抽象内部类Sync,而Sync继承了AQS。

private final Sync sync;

abstract static class Sync extends AbstractQueuedSynchronizer {...}

Sync有两个子类,分别为NonfairSync非公平锁实现类和FairSync公平锁实现类。如果在创建ReentrantLock对象并且没有指定是否公平时,此时默认为非公平锁。

// 非公平锁
static final class NonfairSync extends Sync{....}

// 公平锁
static final class FairSync extends Sync{....}

// 默认为非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}

// 指定公平锁或非公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

公平锁

公平锁指线程必须依靠先后顺序先获取锁,也就是常说的FIFO队列。公平锁的实现逻辑先判断当前AQS的CLH队列中是否正在排队的线程,如果队列为空,那么通过CAS加锁,如果加锁成功则继续向下执行。如果AQS有正在持有锁的线程,那么会判断持有锁的线程是否为当前线程,如果与当前线程是同一个那么就可以实现锁重入。如果获取锁失败,那么将会插入到CLH队列。


// 尝试加锁
final void lock() {
    acquire(1);
}

protected final boolean tryAcquire(int acquires) {
    // 当前线程
    final Thread current = Thread.currentThread();
    // AQS中判断当前是否有线程获取锁的状态值,如果为0表示没有加锁。
    int c = getState();  
    if (c == 0) {
    //hasQueuedPredecessors()判断当前队列里是否存在线程。
    //如果队列里没有线程,则compareAndSetState(0, acquires)从过CAS修改status值并获取锁。
    //如果获取锁成功,setExclusiveOwnerThread(current)将持有锁的线程设置为当前线程
        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;
}

非公平锁

非公平锁指线程比较暴力,上来就进行锁资源的争夺。如果获得锁,则继续向下执行,否则依然会插入到FIFO队列,等待被唤醒。一般情况下非公平锁的效率要优于公平锁。具体实现就是线程直接通过CAS抢锁,如果抢到锁,那么将持有锁的线程设置为自己。如果抢锁失败,那么就再次尝试获取锁,如果还是失败那只能老老实实的加入队列。

final void lock() {
    //先通过CAS抢锁
    if (compareAndSetState(0, 1))
        //抢锁成果,设置持有锁的线程为当前线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
    //抢锁失败,再次尝试获取锁,如果获取锁失败,则插入队列
        acquire(1);
}

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 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;
}

经过上面的源码,我们知道公平锁与非公平锁的区别在于:公平锁按照先到先获取锁的方式进行执行,这样的有点是每个线程都能够获取锁,但是几乎每个线程都要先被阻塞,在被唤醒,这样性能开销较大。非公平锁是按照暴力抢锁的方式,这种方式的优点在于减少线程的阻塞,提高吞吐量,但缺点在于可能会导致有些线程一直获取不到锁,造成线程饥饿。

CLH同步队列

上面提到的CLH队列,是 AQS内部实现的一个双向链表,用于存放抢锁失败的线程。当链表中前置节点执行完成并释放锁后,那么会唤醒下一个节点来尝试获取锁。在AQS中创建了一个Node静态内部类,在这个内部类中定义了前置节点,后置节点,抢锁失败的线程等成员属性。

static final class Node {

// 共享模式的节点
static final Node SHARED = new Node();

// 独占模式的节点
static final Node EXCLUSIVE = null;

// 节点状态
volatile int waitStatus;

// 前置节点
volatile Node prev;

// 后置节点
volatile Node next;

// 被阻塞的线程
volatile Thread thread;
}

基于Node类,在AQS类中有维护了一个head头节点和一个tail尾节点,用与构建链表的头部和尾部。

private transient volatile Node head;

/**
 * Tail of the wait queue, lazily initialized.  Modified only via
 * method enq to add new wait node.
 */
private transient volatile Node tail;

公平锁与非公平锁公用获取锁失败时,插入队列的方法在下面这块代码中

public final void acquire(int arg) {
// tryAcquire() 尝试获取锁
// 尝试获取失败,则调用addWaiter()方法像CLH队列里插入一个独占类型的Node节点
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

CLH插入Node节点

在抢锁失败时,会调用addWaiter方法,将失败线程封装到Node节点中。然后才会将Node节点插入到CLH队列中。

/**
* 将线程插入到队列
*/
private Node addWaiter(Node mode) {
    // 构建Node节点
    Node node = new Node(Thread.currentThread(), mode);
    // 首次插入 tail一定为null , 所以会走enq方法
    Node pred = tail;
    if (pred != null) { //如果尾节点不是空
        node.prev = pred; // 当前节点的前置节点指向尾节点
        if (compareAndSetTail(pred, node)) { 
            pred.next = node; wei'jie'dian
            return node;
        }
    }
    enq(node); // 自旋插入节点,直到节点插入成功
    return node;
}

/**
* 初始换空节点并插入新节点
*/
private Node enq(final Node node) {
    //自旋,直到节点插入成功为止
    for (;;) {
        Node t = tail;
        if (t == null) { // 首次插入前,必须要进行队列初始化一个空节点
            if (compareAndSetHead(new Node()))
                tail = head; // head,tail指向同一个空节点。
        } else {
            node.prev = t; // 插入节点的前置节点指向初始化的空节点
            if (compareAndSetTail(t, node)) {
                t.next = node; // 空节点的next后置节点指向给当前node
                return t;
            }
        }
    }
}

从源码中可以知道在首次插入节点时,AQS会初始化一个空节点插入到CLH队列中。当初始化一个空节点后,才是真正的将主节点插入到空节点后。

那为什么要新建一个空节点呢,这是因为每个节点都需要设置前置节点的waitstatus状态,如果值为1,则表示节点被释放,如果节点为-1表示可被唤醒节点,所以每个节点必须要有前置节点,而空节点存在了意义就是为了第一个入队的节点。当线程入队后,状态并没有被阻塞,那么什么是被阻塞呢。在节点入队完成后,会继续执行acquireQueued()方法。在该方法内线程可能会再次抢锁,如果抢锁成功,那么就将head节点由空节点重新指向自己,否则才会阻塞线程。

/**
* 尝试阻塞节点线程,如果节点为头节点,那么会再一次尝试获取锁
*/
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)) {
                // 将head指针指向当前节点,并将当前节点前置节点和线程属性置空
                setHead(node);
                p.next = null; // 原头指针指向的节点被置空
                failed = false;
                return interrupted;
            }
            // 阻塞线程 
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

/**
* 判断前置节点waitStatus状态,决定当前节点是否要阻塞 
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus; //前置节点的waitStatus状态
    if (ws == Node.SIGNAL) // 如果为-1 表示可以被唤醒,那么当前节点可以被阻塞
        return true;
    if (ws > 0) { //如果大于0,说明节点出现了问题,将节点丢弃。
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
    // 如果waitStatus为0,那么会将waitStatus状态更改为-1,这样的话下一个节点可以被唤醒。下一轮循环时ws=-1
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

好吧,坦白了!!!到这里我就扛不住了,这鬼东西太绕了,代码也不太好理解,我也懵逼了。

CLH唤醒下一节点

在调用unlock方法时,当前线程除了会释放锁,在内部方法中也会唤起下一个线程。在获取下一个唤醒的节点时,会校验下个节点的waitStatus是不是-1,如果不是,那么就会从后向前遍历找到离头节点最近的一个节点,并准备唤醒。


//释放锁的方法
public void unlock() {
    sync.release(1);
}

public final boolean release(int arg) {
//当前线程释放锁
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

/**
* 当前线程释放锁
*
*/
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

/**
*唤起下一个节点
*/
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
    // 将 head 节点的 ws 改成 0,清除信号。表示,他已经释放过了。不能重复释放。
        compareAndSetWaitStatus(node, ws, 0);
    // 找到下一个节点
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
    //如果下个节点为空,或者状态不是-1(被取消),那么从后往前寻找,直到找到最前面未被取消的节点。准备唤醒
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    //唤醒下一个节点
    if (s != null)
        LockSupport.unpark(s.thread);
}

总结

上述对并发中两个重要的应用synchronized和ReentrantLock做了简单的介绍以及源码分析。对于synchronized,简单介绍了锁的升级,如何加锁以及加锁的原理等。对于ReentrantLock,简单的介绍了公平锁与非公平锁的工作流程,节点的入队以及节点的唤醒等。当然,其中某些点来说可能存在一些疏漏,后续如有发现先会及时补充或修正。

【java】java wait 原理 synchronized ReentrantLock 唤醒顺序 学习一网友交流请问下,synchronized大并发的情况下,等待区的线程entrySet和waitSet里的线程分别对应Thread的哪个状态啊,都是wating吗?entryList是blocking状态吧,waitSet是waiting状态线程的生命周期,blocking和waiting不是同一种状态吗总结如下:synchronized关键字的调用wait方法进入等到的线程和因为拿不到锁而等待线程是否同一种状态?blocking?waiting? 阅读详情

相关推荐

PCB盲埋孔工艺全解析:从机械钻孔到激光钻孔的进阶之路

本文全面解析了PCB盲埋孔工艺,对比了机械钻孔与激光钻孔两种核心技术的工艺流程、优势与局限。机械钻孔成本低、工艺成熟,适用于常规孔径;激光钻孔则能实现微孔、叠孔等复杂HDI结构,精度更高。文章深入探讨了如何根据孔径、成本、板厚及产品需求选择合适工艺,并强调了设计与制造协同的重要性,为高密度PCB设计提供实用指南。

weixin_29306317的博客 685

ReentrantLock基本用法

1.相对于synchronized具备的特点: 1)可中断 2)可以设置超时时间 3)可以设置为公平锁 4)支持多个条件变量 2.

YyBook1s的博客 758

jmeter问题系列:Begin size 0 is not equal to fixed size 5

Begin size 0 is not equal to fixed size 5 ,问题的解决

雨水的早晨的博客 2411

ReentrantLock 的使用方式

 参考:https://javadoop.com/2017/06/16/AbstractQueuedSynchronizer/ public class OrderService { // 使用static,这样每个线程拿到的是同一把锁,当然,spring mvc中service默认就是单例,别纠结这个 private static ReentrantLock reentran...

区块链之美 3748

实操微信支付,适合第一次接通微信支付的同学

我的实际操作命令如下:就会生成图3中多出来的file文件夹,/Users/camel/cert/xxxxxx_cert/file/wechatpay_5A4xxxxxxxxxx9.pem,wechatpay_xxxxx5A4849E9.pem就是微信支付平台证书,也是程序真正需要的。•注意:我的操作说明,我是把下载好的 CertificateDownloader.jar 文件,放在了解压证书的目录里(图3)。//下面的静态参数要仔细看下面的解释,否则只会调用别人写好的方法,压根学不会0-1接入微信支付。

wjianwei666的专栏 525

IO Virtualization with Virtio.part 2 [十三]

通过对virtio_device_id 中的 device 和 vendor 的值进行比较,我们找到对应的 virtio 驱动,由于这里我们使用 i2c virtio 为例,所以调用 virtio_i2c_probe 进行 virtio i2c 设备的初始化。在和 virtio mmio 设备交互时我们会读取 VIRTIO_MMIO_DEVICE_ID 和 VIRTIO_MMIO_VENDOR_ID,并将其值写入 virtio_device_id 中,这个就会用作后面和 virtio 设备驱动的匹配参数。

cxjczy1990的博客 1211

使用ReentrantLock

使用ReentrantLock,最大的感触就是,一定要严格按照使用规范去写,否则各种多线程问题让人防不胜防。 大概几点吧: 1、无论是ReentrantLock还是Condition都是为了锁资源而存在的,没有资源的话,这些东西就没有意义。 2、ReentrantLock是为了保证操作资源的互斥性,Condition是为了消除等待资源的时间浪费。 3、ReentrantLock一定要loc

坐井观天的日常 736

SCADA|KingSCADA通过组合框选择修改变量的值

《SCADA项目中组合框控件的使用方法》 本文详细介绍了SCADA系统中组合框(下拉框)控件的应用方法。通过具体示例演示如何实现:当选择"雷工笔记"时变量值为100,选择"雷工杂谈"时为200,选择"小李食代"时为300的功能。主要实现步骤包括:1)创建项目并添加组合框控件;2)设置Items选项及对应数值;3)创建内存变量;4)使用GetCurrentIndex()和GetItemData()方法;5)在选项变化事件中编写脚本。该控件在SCADA

做有价值的事,积累有价值的笔记! 864

ReentrantLock(重入锁)功能详解和应用演示

ReentrantLock(重入锁)

比起日出,我更喜欢日落。我的意思是比起遇见你,我更喜欢与你终老 689

飞行机械臂设计

其后对飞行机械臂系统的机 理模型和基于 ADAMS 的物理模型进行了控制系统的搭建,对于机理模 型,设计了三个输入通道,分别是俯仰力矩,翻滚力矩与偏航力矩,控 制量为翻滚角,俯仰角,偏航角。四旋翼无人机自身具有独特的优势,在无人机上增加机械臂,整个 机体系统相当于一架空中行动自如的空中机器人,可以适应更多的应用场合,能够实现空中抓取,运输载荷,巡航监视等任务。的函数模块如图 4-8 所示,可见输入为 U1,U2,U3,U4,分别对应总力矩,俯仰力矩,翻滚力矩,偏航力矩,控制 量为俯仰角,翻滚角,偏航角。

coldmood77的博客 2916

【深度学习|多实例学习】多实例学习(Multiple Instance Learning, MIL)的起源、发展和应用,附代码(一)

【深度学习|多实例学习】多实例学习(Multiple Instance Learning, MIL)的起源、发展和应用,附代码(一)

2401_89898861的博客 1923

并发编程6-ReentrantLock

1.什么是可重锁ReentrantLock? 就是支持重新进入的锁,表示该锁能够支持一个线程对资源的重复加锁。底层实现原理主要是利用通过继承AQS来实现的,也是利用通过对volatile state的CAS操作+CLH队列来实现; 支持公平锁和非公平锁。 CAS:Compare and Swap 比较并交换。CAS的思想很简单:3个参数,一个当前内存值V、预期值A,即将更新的值B,当前仅当预期值A...

应学欣的博客 294

【CSP-S 提高组】考试大纲集合

【CSP-S提高组】考试大纲集合

weixin_42596089的博客 7255

java的两种同步方式, Synchronized与ReentrantLock的区别

java在编写多线程程序时,为了保证线程安全,需要同步,经常用到两种同步方式就是Synchronized和重入锁ReentrantLock。 相似点: 这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统在用户态与内核态之间来回

记忆力不好的博客 2万+

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

ReentrantLock 锁详解

ReentrantLock 锁详解 ReentrantLock 支持公平锁和非公平锁,可重入锁 ReentrantLock的底层是通过 **AQS[链接]**实现。 一、BAT 大厂的面试题 **【1】**什么是可重入,什么是可重入锁? 它用来解决什么问题? 【2】ReentrantLock 的核心是 AQS,那么它怎么来实现的,继承吗? 说说其类内部结构关系。 【3】ReentrantLock 是如何实现公平锁的? 【4】ReentrantLock 是如何实现非公平锁的? 【5】ReentrantL

程序猿进阶 9万+

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

ReentrantLock 与Synchronized 对比分析

ReentrantLock分析 及 与Synchronized对比分析ReentrantLock概念、公平性ReentrantLock 中的重要方法对比条件变量 Condition ReentrantLock概念、公平性 什么是再入? 它表示当一个线程试图获取一个它已经获取的锁时,这个获取动作就自动成功,这是对锁获取粒度的一个概念,也就是锁的持有是以线程为单位,而不是基于调用次数。Java锁实现强...

weixin_42610002的博客 352

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

【Java】多线程和高并发编程(三):锁(中)深入ReentrantLock

AQS就是AbstractQueuedSynchronizer抽象类,AQS其实就是JUC包下的一个基类,JUC下的很多内容都是基于AQS实现了部分功能,比如ReentrantLock,ThreadPoolExecutor,阻塞队列,CountDownLatch,Semaphore,CyclicBarrier等等都是基于AQS实现。首先AQS中提供了一个由volatile修饰,并且采用CAS方式修改的int类型的state变量。

专注上位机开发、机器视觉、机器学习、嵌入式软硬件开发 1万+

ReentrantLock 源码分析从入门到入土

回答一个问题 在开始本篇文章的内容讲述前,先来回答我一个问题,为什么 JDK 提供一个 synchronized 关键字之后还要提供一个 Lock 锁,这不是多此一举吗?难道 JDK 设计人员都是沙雕吗? 我听过一句话非常的经典,也是我认为是每个人都应该了解的一句话:你以为的并不是你以为的。明白什么意思么?不明白的话,加我微信我告诉你。 初识 ReentrantLock ReentrantLock...

程序员cxuan的个人主页 6157

Synchronized 与 ReentrantLock 的区别!

作者:jason cnblogs.com/baizhanshi/p/7211802.html 之前栈长分享了重入锁的概念:《到底什么是重入锁,拜托,一次搞清楚!》,今天现来深入了解下 Synchronized 与 ReentrantLock 的区别! 可重入性: 从名字上理解,ReenTrantLock的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也是可重入的,两者关...

Java技术栈,分享最主流的Java技术 1138

ReenTrantLock可重入锁(和synchronized的区别)总结

ReenTrantLock可重入锁(和synchronized的区别)总结 可重入性: 从名字上理解,ReenTrantLock的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也是可重入的,两者关于这个的区别不大。两者都是同一个线程没进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。   锁的实现: Synchronized是依赖于JVM实现的

qq838642798的博客 5万+

synchronized与ReentrantLock万字详解

Java并发编程中,synchronized和ReentrantLock是两种核心同步机制。synchronized作为关键字,提供简洁语法和自动锁管理,通过对象头和监视器实现,支持偏向锁、轻量级锁和重量级锁的升级过程。ReentrantLock基于AQS框架,提供更丰富的功能,包括公平/非公平锁、可中断获取、超时机制和多个条件变量。在高竞争环境下,ReentrantLock通常表现更好,而synchronized在简单场景下更简洁高效。开发时应根据需求选择:基础同步用synchronized,复杂场景用R

油墨香^-^的博客 2824

ReenTrantLock 与synchronized 区别

ReenTrantLock可重入锁(和synchronized的区别)总结 可重入性: 从名字上理解,ReenTrantLock的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也是可重入的,两者关于这个的区别不大。两者都是同一个线程没进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。 锁的实现: Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现的,有什么区别,说白了就类似于操作系统来控制实现和用户自己敲代码实现的.

FYHannnnnn的博客 909

并发编程-锁-ReentrantLock

简单的介绍这里有:跳转链接。AQS就是AbstractQueuedSynchronizer抽象类,AQS其实就是JUC包下的一个基类,JUC下的很多内容都是基于AQS实现了部分功能,比如ReentrantLock,ThreadPoolExecutor,阻塞队列,CountDownLatch,Semaphore,CyclicBarrier等等都是基于AQS实现。~~~重点!!重点!!重点!!~~1、AQS提供了一个由volatile修饰,并且才有CAS方式修改的int类型的state变量。

weixin_49622776的博客 2233

开发易忽视的问题:synchronized 和 ReentrantLock源码对比

状态字段:使用一个int类型的state字段来表示锁的状态。FIFO队列:使用一个FIFO等待队列来管理处于等待状态的线程。模板方法:通过继承AQS,实现特定资源的获取和释放逻辑。在非公平锁的环境中,哪个线程最终获得锁是由多个因素决定的,包括线程调度、CPU时间片以及CAS操作的竞争结果。由于新的线程可以在任何时候尝试获取锁,所以存在较大的不确定性。这种策略提高了并发性能,但可能导致某些线程长时间得不到锁,这就是所谓的“锁饥饿”现象。

2301_78976656的博客 963

ReentrantLock与synchronized 源码解析

一.概念及执行原理   在 JDK 1.5 之前共享对象的协调机制只有 synchronized 和 volatile,在 JDK 1.5 中增加了新的机制 ReentrantLock,该机制的诞生并不是为了替代 synchronized,而是在 synchronized 不适用的情况下,提供一种可以选择的高级功能。 二.synchronized 和 ReentrantLock 的实现和区别 1.实现的方式 synchronized synchronized 属于独占式悲观锁,是通过 JVM 隐式实现的

小豆的博客 272
上一篇: java类和对象——如何面向对象以及类的定义、使用和实例化
下一篇: 高级架构师纯手写MySQL笔记,已经熬夜学习了
Java技术那些事儿
博客等级 码龄5年 2306粉丝 2584原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值