Java并发编程2(锁-Sychronized)

目录

认识Java对象头

sychronized锁原理

基本概念

工作原理 

1.作用在方法上

2.作用在代码块上

工作机制

JVM优化锁

Monitor锁

wait/notify

park/unpark

线程状态转换案例 

死锁

概念

死锁发生的必要条件

哲学家问题

活锁

饥饿 

概念

饥饿的原因

ReentrantLock 

基本锁操作

查看锁状态的方法

条件变量相关的方法

可重入锁

可打断(lockInterruptibly)

不可打断 

锁超时(tryLock)

立刻返回

 ​编辑

超时释放 

公平锁/非公平锁 

非公平锁

公平锁

 ​编辑

 await/Condition

等待条件(await方法系列):

通知条件(signal方法系列):


认识Java对象头

32位虚拟机对象头:

64位虚拟机对象头: 

1.Mark Word(标记字):

  • Mark Word是对象头的一部分,用于存储对象自身的哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID(或偏向时间戳)、偏向模式以及锁的状态等信息。
  • 标记字的大小和具体内容可能因JVM实现的不同而有所变化。例如,在64位JVM上,默认情况下Mark Word占用64位(8字节),而在32位JVM上则是32位(4字节)。

2.Class Pointer(类指针):

  • 这是指向该对象对应类(Class)的指针,通过这个指针可以访问到对象所属类的元数据(如方法表、字段描述等)。类指针的大小依赖于JVM的具体实现及其是否开启了压缩指针(Compressed Oop)选项。
  • 在某些情况下,比如当使用了-XX:+UseCompressedClassPointers选项时,类指针会被压缩以节省内存。

3.Array Length(数组长度,仅适用于数组对象):

  • 如果对象是一个数组,则对象头还需要额外的空间来存储数组的长度。这是因为非数组对象不需要知道其“长度”,但数组需要这个信息以便进行边界检查等操作。

sychronized锁原理

基本概念

  • 对象头(Object Header):每个Java对象都有一个对象头,其中包含了一些元数据信息。对于普通对象来说,这部分包括Mark Word和Class Pointer。Mark Word存储了对象的哈希码、GC分代年龄、锁状态标志等信息。
  • Monitor(监视器/管程):这是JVM内部的一种同步机制。每个对象都关联有一个监视器锁。当一个线程获取到这个锁时,它可以进入临界区;其他试图进入同一临界区的线程必须等待,直到第一个线程释放锁。

工作原理 

1.作用在方法上

  • 使用synchronized修饰一个实例方法时,该方法被调用时会自动获取当前实例(即this)的monitor锁。
  • 如果是静态方法,则锁定的是该类的Class对象。

2.作用在代码块上

  • 使用synchronized代码块可以更灵活地指定要锁定的对象,获取的是括号中的对象。

工作机制

字节码层面:使用synchronized关键字时,编译器会将同步块或方法转换为特定的字节码指令。对于同步方法,编译器会在方法信息中设置一个标志位(ACC_SYNCHRONIZED)。对于同步代码块,则会生成monitorenter和monitorexit指令。

monitorenter 和 monitorexit 指令:

  • monitorenter:每个对象都有一个与之关联的monitor(监视器锁)。当线程执行到monitorenter指令时,它尝试获取该对象的monitor。如果monitor未被其他线程持有,则当前线程成功获取monitor并继续执行;否则,线程将被阻塞直到获得monitor。
  • monitorexit:当线程执行完同步代码块后,会执行monitorexit指令以释放monitor,允许其他等待获取该monitor的线程继续执行。
public class Test2 {static final Object lock = new Object();static int cnt = 0;public static void main(String[] args) {synchronized (lock) {cnt ++;}}
}

上段代码main方法生成的字节码指令如下:

public static void main(java.lang.String[]);Code:0: getstatic     #2                  // Field lock:Ljava/lang/Object;3: dup4: astore_15: monitorenter                     // 获取锁6: getstatic     #3                  // Field cnt:I9: iconst_110: iadd11: putstatic     #3                  // Field cnt:I14: aload_115: monitorexit                      // 释放锁16: goto          2419: astore_220: aload_121: monitorexit                      // 确保在异常情况下也释放锁22: aload_223: athrow24: returnException table:from    to  target type6    16    19   any19    22    19   any

    JVM优化锁

    1.无锁状态:

    • 这是对象的初始状态,没有任何线程持有该对象的Monitor锁。

    2.偏向锁(Biased Locking):

    • 偏向锁是针对单线程访问场景的一种优化。它假设如果一个线程获得了某个对象的锁,那么接下来很可能还是这个线程继续访问该对象。
    • 当一个线程第一次进入同步块时,会尝试获取偏向锁,并将线程ID记录在对象头中。如果之后还是同一个线程进入同步块,则不需要再次获取锁,减少了锁获取的开销。
    • 如果有其他线程试图获取偏向锁,则需要撤销偏向锁并升级为轻量级锁

    3.轻量级锁(Lightweight Locking):

    • 存在少量锁竞争时,使用轻量级锁来代替重量级锁,以提高性能。
    • 轻量级锁通过CAS(Compare And Swap)操作尝试原子性地获得锁。如果成功,则线程可以进入临界区;如果失败,表示存在锁竞争,锁可能会升级为重量级锁。

    4.重量级锁(Heavyweight Locking):

    • 当锁竞争加剧时,JVM会将锁升级为重量级锁。重量级锁涉及到操作系统层面的线程挂起和恢复操作,因此开销较大。
    • 在这种状态下,所有竞争锁的线程都会被阻塞,直到当前持有锁的线程释放锁为止

    5.自旋锁(Spin Lock):

    • 自旋锁不是一种独立的锁类型,而是一种策略。当一个线程尝试获取锁时,它可以选择“自旋”一段时间,而不是立即进入阻塞状态。这样可以在等待锁释放的过程中减少上下文切换的开销。
    • Java中的自旋锁通常与轻量级锁结合使用,在某些情况下可以作为一种优化手段。

    6.可重入锁(Reentrant Lock):

    • synchronized机制本身就是可重入的,这意味着如果一个线程已经持有了某个对象的锁,它可以再次获取相同的锁而不会导致死锁。
    • 每次重新获取锁时,计数器会增加;每次释放锁时,计数器会减少,直到计数器归零才真正释放锁。

    在Java中默认第一次给对象加锁就是偏向锁(默认延迟加载),假如t1线程已经给obj加上锁(偏向锁)了,当有另外一个线程t2也试图获取同一把锁时,偏向锁升级为轻量级锁。

    轻量级锁通过使用CAS操作尝试将对象头中的Mark Word更新为指向当前线程的Lock Record的指针。如果成功,则线程获得了轻量级锁,并继续执行;如果失败(意味着存在锁竞争),则会尝试进行锁膨胀,轻量级锁尝试升级为重量级锁。

    重量级锁的核心是Monitor(监视器锁),如果Monitor已被其他线程持有,则试图获取锁的线程将被放入Entry Set中等待。此时,线程会进入阻塞状态,即线程会被挂起,不会占用CPU资源,直到Monitor锁被释放并且该线程有机会重新尝试获取锁,这允许Entry Set中的一个或多个线程重新竞争Monitor锁。如果有线程处于Wait Set中,那么根据调用的是notify()还是notifyAll()方法,相应的线程会被移动到Entry Set中,重新参与Monitor锁的竞争。

    Monitor锁

    • Owner(所有者):指向当前持有Monitor的线程。
    • Entry Set(入口集):等待获取Monitor锁的线程队列。如果一个线程尝试进入一个已经被其他线程持有的同步块或方法,则该线程会被放入这个队列中,直到Monitor被释放并且该线程有机会重新尝试获取锁。
    • Wait Set(等待集):调用了wait()方法并正在等待某个条件满足的线程集合。这些线程暂时释放了Monitor,并且不会参与锁的竞争,直到另一个线程调用notify()或notifyAll()方法通知它们可以继续执行。

    wait/notify

    • wait():使当前线程等待,直到另一个线程调用同一个对象上的notify()或notifyAll()方法。调用wait()时,当前线程会释放它持有的对象锁,并进入该对象的“等待集”。只有当其他线程调用了相应的通知方法并且当前线程被选中(对于notify())或所有等待的线程都被唤醒(对于notifyAll()),当前线程才能重新获取锁并继续执行。
    • notify():随机唤醒一个正在等待该对象监视器的单个线程。如果多个线程都在等待,则选择其中一个线程进行唤醒。被唤醒的线程并不能立即执行,而是要等到当前线程退出同步代码块并释放锁之后。
    • notifyAll():唤醒所有正在等待该对象监视器的线程。尽管所有线程都会被唤醒,但只有一个线程能够成功获得锁并继续执行;其余线程将再次进入等待状态或者竞争锁。
        final static Object lock = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{synchronized (lock) {log.debug("running {}", System.currentTimeMillis());try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}log.debug("end... {}", System.currentTimeMillis());}},"t1");Thread t2 = new Thread(()->{synchronized (lock) {log.debug("running {}", System.currentTimeMillis());try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}log.debug("end... {}", System.currentTimeMillis());}}, "t2");t1.start();t2.start();Thread.sleep(2000);synchronized (lock) {log.debug("唤醒线程");// lock.notify(); // 唤醒一个线程lock.notifyAll(); // 唤醒所有线程}}

     调用wait就是当前线程得到了对象锁了,但是我不往下执行了,我把锁释放给其他线程先去执行,我自己到Wait Set(等待集)中去,有人(线程)叫我(notify)我再去排队进入Entry Set(入口集),每有人叫我我就继续等着。(感觉很像坐高铁,我虽然进了高铁站,但是车次没有轮到我,那么我就继续等着,如果叫到我了我就去排队等待坐车)

    park/unpark

    它们是 LockSupport 类中的方法

    // 暂停当前线程

    LockSupport.park();

    // 恢复某个线程的运行

    LockSupport.unpark(暂停线程对象)

    @Slf4j(topic = "c.thread")
    public class Test4 {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {log.debug("t1 running {}", System.currentTimeMillis());LockSupport.park();log.debug("t1 end {}", System.currentTimeMillis());}, "t1");t1.start();sleep(1000);LockSupport.unpark(t1);}
    }
    

     执行结果:

    16:10:58 [t1] c.thread - t1 running 1742631058230
    16:10:59 [t1] c.thread - t1 end 1742631059240

    线程状态转换案例 

    
    @Slf4j(topic = "c.thread")
    public class Test5 {private static final Object obj = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {synchronized (obj) {log.debug("running t1");try {sleep(1000);obj.notify();} catch (InterruptedException e) {e.printStackTrace();}}});Thread t2 = new Thread(() -> {synchronized (obj) {log.debug("running t2");try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}}}, "t2");Thread t3 = new Thread(() -> {synchronized (obj) {log.debug("running t3");try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}}}, "t2");t1.start();t2.start();t3.start();log.debug("t1 {} t2 {} t3 {}", t1.getState(), t2.getState(), t3.getState());sleep(100);log.debug("t1 {} t2 {} t3 {}", t1.getState(), t2.getState(), t3.getState());sleep(2000);log.debug("t1 {} t2 {} t3 {}", t1.getState(), t2.getState(), t3.getState());}
    }

    运行结果:

    21:08:27 [main] c.thread - t1 RUNNABLE t2 RUNNABLE t3 RUNNABLE
    21:08:27 [t2] c.thread - running t2
    21:08:27 [t2] c.thread - running t3
    21:08:27 [Thread-0] c.thread - running t1
    21:08:27 [main] c.thread - t1 TIMED_WAITING t2 WAITING t3 WAITING
    21:08:29 [main] c.thread - t1 TERMINATED t2 TERMINATED t3 WAITING 

    第一次打印分析:三个线程都启动了

    第二次打印分析:此时t1在sleep,t2和t3都在wait

    第三次打印分析:t1sleep完了,然后随机唤醒一个waiting的线程,所以t1和t2都执行完了,t3没有人唤醒它

    死锁

    概念

    死锁(Deadlock)是指两个或更多线程在执行过程中,由于争夺资源而造成的一种互相等待的状态。在这种状态下,没有任何线程能够继续前进,整个系统或部分系统因此陷入停滞。死锁是并发编程中常见的问题之一,尤其是在多线程环境下对共享资源进行访问时。

    死锁发生的必要条件

    1. 互斥条件:至少有一个资源必须处于非共享模式,即只能被一个线程占用。例如,文件写入权限通常就是互斥的。
    2. 占有并等待条件:一个线程已经占有了至少一个资源,并且正在等待其他线程占有的资源。也就是说,该线程不会释放自己已获得的资源直到它获得了所有需要的资源
    3. 不可剥夺条件:资源不能被强制从某个线程中抢走,只能由拥有它的线程主动释放。这意味着一旦一个线程获得了某个资源,它可以在不自愿的情况下保持对该资源的控制权,直到它完成任务并释放资源。
    4. 循环等待条件:存在一组等待资源的线程链,其中每个线程都在等待下一个线程当前占有的资源。例如,线程A等待线程B持有的资源,而线程B又在等待线程C持有的资源,以此类推,直到某一线程又在等待线程A持有的资源,形成一个闭环。 

    一个线程同时获取多把锁就容易导致死锁

    @Slf4j(topic = "c.Test6")
    public class Test6 {static Object lockA = new Object();static Object lockB = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {synchronized (lockA) {log.debug("lockA {}", Thread.currentThread().getName());synchronized (lockB) {log.debug("lockB {}", Thread.currentThread().getName());try {sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}, "t1");Thread t2 = new Thread(() -> {synchronized (lockB) {log.debug("lockB {}", Thread.currentThread().getName());synchronized (lockA) {log.debug("lockA {}", Thread.currentThread().getName());try {sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}, "t2");t1.start();t2.start();log.debug("t1 {} t2 {}", t1.getState(), t2.getState());sleep(100);log.debug("t1 {} t2 {}", t1.getState(), t2.getState());sleep(2000);log.debug("t1 {} t2 {}", t1.getState(), t2.getState());}}

    分析:

    t1获得lockA的锁,t2获得lockB的锁,t1、t2都上锁成功,然后t1想要获得lockB,但是lockB被t2占有,t2只有等到全部执行完才能释放锁,但是t2中又要获得lockA的锁,但是lockA的锁被t1占有,又回到t1,t1也只能是执行完才能释放锁,然后t1和t2就互相等待对方放锁,造成死锁。 

     ----> 定位死锁的工具:jconsole

    哲学家问题

    • 有五位哲学家,围坐在圆桌旁。
    • 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
    • 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
    • 如果筷子被身边的人拿着,自己就得等待
    package cn.itcast.sychronized;import lombok.extern.slf4j.Slf4j;@Slf4j(topic = "c.Test7")
    public class Test7 {public static void main(String[] args) {ChopSticks c1 = new ChopSticks("1");ChopSticks c2 = new ChopSticks("2");ChopSticks c3 = new ChopSticks("3");ChopSticks c4 = new ChopSticks("4");ChopSticks c5 = new ChopSticks("5");Philosopher p1 = new Philosopher("p1", c1, c2);Philosopher p2 = new Philosopher("p2", c2, c3);Philosopher p3 = new Philosopher("p3", c3, c4);Philosopher p4 = new Philosopher("p4", c4, c5);Philosopher p5 = new Philosopher("p5", c5, c1);p1.start();p2.start();p3.start();p4.start();p5.start();}
    }
    class ChopSticks {String name;public ChopSticks(String name) {this.name = name;}@Overridepublic String toString() {return "筷子 { " + name + "}";}
    }
    @Slf4j(topic = "c.Philosopher")
    class Philosopher extends Thread{final ChopSticks left;final ChopSticks right;public Philosopher(String name, ChopSticks left, ChopSticks right) {super(name); // 线程名字this.left = left;this.right = right;}public void eat() {log.debug("{} eat", Thread.currentThread().getName());}@Overridepublic void run() {while (true) {synchronized (left) {synchronized (right) {eat();}}}}
    }

    活锁

    活锁(Livelock)是并发编程中的一种问题,类似于死锁,但它涉及的是线程虽然没有被阻塞,但却无法继续向前推进的情况。在活锁的情况下,线程不断地尝试执行某项操作但总是失败,并且不断地重复相同的操作,导致它们实际上没有取得任何进展。这种现象通常发生在试图避免死锁的过程中,特别是当多个线程相互改变状态以试图解决另一个线程的问题时。

    活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束。
    @Slf4j(topic = "c.Test8")
    public class Test8 {static volatile int cnt = 0;public static void main(String[] args) {new Thread(() -> {while (cnt > 0) {cnt --;try {sleep(100);} catch (InterruptedException e) {e.printStackTrace();}log.debug("t1 : {}", cnt);}}, "t1").start();new Thread(() -> {while (cnt < 100) {cnt ++;try {sleep(100);} catch (InterruptedException e) {e.printStackTrace();}log.debug("t2 : {}", cnt);}}, "t2").start();}
    }
    

    饥饿 

    概念

     “饥饿”(Starvation)是指一个线程由于无法获得所需的资源而无法继续执行的情况。尽管该线程没有被阻塞,但由于其他线程频繁地抢占资源,导致这个线程得不到足够的CPU时间或其他必要的资源来推进其任务

    饥饿的原因

    1. 资源竞争:当多个线程竞争有限的资源时,如果没有公平的调度机制,某些线程可能会始终得不到资源,从而导致饥饿。例如,如果系统总是优先处理高优先级的任务,那么低优先级的任务可能永远得不到执行的机会。
    2. 不恰当的锁机制:在使用锁进行同步控制时,如果某个线程长时间持有锁而不释放,其他等待该锁的线程就会被阻塞,可能导致这些线程出现饥饿现象。
    3. 忙等待(Busy-waiting):如果一个线程通过忙等待的方式来等待某个条件成立,而这个条件很少或几乎不会成立,那么这个线程就会一直消耗CPU资源,同时无法完成实际的工作。
    4. 不公平的调度器:操作系统或运行时环境中的线程调度器如果没有实现公平调度算法,也可能导致某些线程长期得不到执行机会。 

    ReentrantLock 

    ReentrantLock reentrantLock = new ReentrantLock(true);

    reentrantLock.lock();

    try {

        // 临界区代码

    } finally {

        reentrantLock.unlock()

    }

    ReentrantLock(false) : 非公平锁

    ReentrantLock(true) : 公平锁

    @Slf4j(topic = "c.Test")
    public class Test {private static final ReentrantLock reentrantLock = new ReentrantLock(true);static int cnt = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {reentrantLock.lock();try {for (int i = 0; i < 10000; i++) {cnt++;}} finally {reentrantLock.unlock();}}, "t1");Thread t2 = new Thread(() -> {reentrantLock.lock();try {for (int i = 0; i < 10000; i++) {cnt--;}} finally {reentrantLock.unlock();}}, "t2");t1.start();t2.start();t1.join();t2.join();log.debug("{}",cnt);}
    }

    基本锁操作

    lock():

    • 获取锁。
    • 如果锁已经被其他线程持有,则当前线程将被阻塞,直到获取到锁为止。

    lockInterruptibly() throws InterruptedException:

    • 获取锁,但可以响应中断。
    • 如果当前线程在等待获取锁的过程中被中断,则抛出 InterruptedException 异常并退出等待状态。

    tryLock():

    • 尝试获取锁,如果锁未被其他线程持有,则立即返回 true;否则立即返回 false,不会阻塞当前线程。

    tryLock(long timeout, TimeUnit unit) throws InterruptedException:

    • 在给定的时间内尝试获取锁。
    • 如果在指定时间内成功获取锁,则返回 true;如果超时或者当前线程被中断,则返回 false 或抛出 InterruptedException。

    unlock():

    • 释放锁。
    • 每次调用 lock() 方法获得一次锁后,必须对应地调用一次 unlock() 来释放锁。通常在 finally 块中调用以确保即使发生异常也能正确释放锁。

    查看锁状态的方法

    • isHeldByCurrentThread():判断当前线程是否持有该锁。
    • getHoldCount():返回当前线程获取这个锁的次数(重入计数)。每次调用 lock() 方法都会增加计数,每次调用 unlock() 方法都会减少计数。
    • isLocked():判断锁是否被任意一个线程持有。
    • hasQueuedThreads():查询是否有线程正在等待获取此锁。

    条件变量相关的方法

    newCondition():

    • 创建一个新的 Condition 实例,与当前锁关联。
    • 可以使用条件变量来实现类似于 Object.wait() 和 Object.notify() 的功能,但更加灵活和强大。

    可重入锁

    这是指一个线程可以尝试多次获取同一个锁,并且不会发生死锁的情况。每次获取锁(调用lock()方法)都需要相应的释放锁(调用unlock()方法)。锁获取次数与锁释放次数需要匹配。

    // ReentrantLock 可重入锁
    @Slf4j(topic = "c.Test2")
    public class Test2 {final static ReentrantLock reentrantLock = new ReentrantLock();public static void method1() {reentrantLock.lock();try {log.debug("method1 running");method2();} finally {reentrantLock.unlock();}}public static void method2() {reentrantLock.lock();try {log.debug("method2 running");method3();} finally {reentrantLock.unlock();}}public static void method3() {reentrantLock.lock();try {log.debug("method3 running");} finally {reentrantLock.unlock();}}public static void main(String[] args) {method1();}
    }
    

    执行结果:

    10:17:12 [main] c.Test2 - method1 running
    10:17:12 [main] c.Test2 - method2 running
    10:17:12 [main] c.Test2 - method3 running 

    可打断(lockInterruptibly)

    这个方法允许你以一种可以响应中断的方式获取锁。如果当前线程在调用lockInterruptibly()方法时没有被打断,并且能够获取到锁,则它将继续执行;但如果此时有另一个线程已经持有了这个锁,那么当前线程将被阻塞,直到锁可用或当前线程被中断。
    如果在等待获取锁的过程中收到了中断请求(即另一个线程调用了当前线程的interrupt()方法),则会抛出InterruptedException异常,从而允许你捕获这个异常并进行相应的处理。

    // 可打断
    @Slf4j(topic = "c.Test3")
    public class Test3 {static ReentrantLock lock =  new ReentrantLock();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {log.debug("t1 running");try {lock.lockInterruptibly();} catch (InterruptedException e) {log.debug("t1:在等锁过程中被打断");e.printStackTrace();return;}try {log.debug("t1 获取锁 {}", System.currentTimeMillis());} finally {lock.unlock();}}, "t1");lock.lock();log.debug("主线程获得锁");t1.start();try {sleep(1000);t1.interrupt(); // 打断t1log.debug("执行打断");} finally {log.debug("主线程释放锁");lock.unlock();}}
    }
    

    不可打断 

    在ReentrantLock中,默认的锁获取方法lock()是不可中断的。这意味着如果一个线程尝试获取一个已经被其他线程持有的锁,它将会一直阻塞直到成功获取到锁,即使这个线程在这期间被中断(即调用了该线程的interrupt()方法),也不会抛出InterruptedException,而是继续等待直至获得锁。这种行为可以被视为“不可打断”。

    // 不可打断
    @Slf4j(topic = "c.Test4")
    public class Test4 {static ReentrantLock lock = new ReentrantLock();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {log.debug("t1 启动 {}", System.currentTimeMillis());lock.lock();try {log.debug("t1 获得锁 {}", System.currentTimeMillis());} finally {log.debug("t1 释放锁 {}", System.currentTimeMillis());lock.unlock();}}, "t1");lock.lock();log.debug("主线程获得锁 {}", System.currentTimeMillis());t1.start();try {log.debug("执行打断 {}", System.currentTimeMillis());sleep(1000);t1.interrupt();} finally {log.debug("主线程释放锁 {}", System.currentTimeMillis());lock.unlock();}}
    }

    执行结果:

    10:18:41 [main] c.Test4 - 主线程获得锁 1742696321911
    10:18:41 [main] c.Test4 - 执行打断 1742696321917
    10:18:41 [t1] c.Test4 - t1 启动 1742696321917
    10:18:42 [main] c.Test4 - 主线程释放锁 1742696322931
    10:18:42 [t1] c.Test4 - t1 获得锁 1742696322931
    10:18:42 [t1] c.Test4 - t1 释放锁 1742696322931

    锁超时(tryLock)

    • tryLock():尝试立即获取锁。如果锁当前没有被其他线程持有,则该方法会成功获取锁并返回true;如果锁已经被其他线程持有,则该方法不会阻塞,而是立刻返回false。
    • tryLock(long timeout, TimeUnit unit):尝试在指定的时间内获取锁。如果在这个时间段内成功获取到了锁,则返回true;如果在这个时间段内未能获取到锁(即锁一直被其他线程持有),则返回false。此外,如果在等待期间当前线程被中断,它将抛出InterruptedException。

    立刻返回

    // 锁超时 --- 立刻返回
    @Slf4j(topic = "c.Test5")
    public class Test5 {static ReentrantLock lock = new ReentrantLock();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {log.debug("t1 running");if (!lock.tryLock()) {log.debug("t1 获取锁失败, 立刻返回");return;}try {log.debug("t1 获得锁成功");} finally {lock.unlock();}}, "t1");lock.lock();t1.start();try {sleep(1000);} finally {lock.unlock();}}
    }
    

     

    超时释放 

    @Slf4j(topic = "c.Test5")
    public class Test6 {static ReentrantLock lock = new ReentrantLock();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {log.debug("t1 running");try {// 超时释放if (!lock.tryLock(1, TimeUnit.SECONDS)) {log.debug("t1 1s尝试获得锁失败");return;}} catch (InterruptedException e) {e.printStackTrace();}try {log.debug("t1 获得锁成功");} finally {lock.unlock();}}, "t1");lock.lock();t1.start();try {sleep(1500);} finally {log.debug("主线程释放锁");lock.unlock();}}
    }
    

     例子:哲学家问题

    @Slf4j(topic = "c.Test7")
    public class Test7 {public static void main(String[] args) {ChopSticks c1 = new ChopSticks("1");ChopSticks c2 = new ChopSticks("2");ChopSticks c3 = new ChopSticks("3");ChopSticks c4 = new ChopSticks("4");ChopSticks c5 = new ChopSticks("5");Philosopher p1 = new Philosopher("p1", c1, c2);Philosopher p2 = new Philosopher("p2", c2, c3);Philosopher p3 = new Philosopher("p3", c3, c4);Philosopher p4 = new Philosopher("p4", c4, c5);Philosopher p5 = new Philosopher("p5", c5, c1);p1.start();p2.start();p3.start();p4.start();p5.start();}
    }class ChopSticks extends ReentrantLock {String name;public ChopSticks(String name) {this.name = name;}@Overridepublic String toString() {return "筷子{" + name + '}';}
    }
    @Slf4j(topic = "c.Philosopher")
    class Philosopher extends Thread {ChopSticks left;ChopSticks right;public Philosopher(String name, ChopSticks left, ChopSticks right) {super(name);this.left = left;this.right = right;}public void eat(){log.debug("{} eating", Thread.currentThread().getName());try {sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}@Overridepublic void run() {while (true) {// 尝试获得左边的筷子if (left.tryLock()) {try {// 尝试获得右边的筷子if (right.tryLock()) {try {eat();} finally {right.unlock();}}} finally {left.unlock();}}}}
    }
    

    公平锁/非公平锁 

    非公平锁

    • 定义:在非公平锁机制下,锁不会保证按照请求的先后顺序来分配。当锁可用时,任何一个尝试获取锁的线程都有机会获得锁,包括那些刚刚释放了这个锁并且立刻再次尝试获取它的线程。
    • 默认行为:如果不特别指定,ReentrantLock默认采用的是非公平锁策略(即new ReentrantLock()等同于new ReentrantLock(false))。
    • 性能:通常情况下,非公平锁可以获得更高的吞吐量,因为它减少了线程上下文切换的次数,并且允许线程在锁刚被释放时迅速重新获取锁,而不需要排队等待。
    // 非公平锁
    @Slf4j(topic = "c.Test8")
    public class Test8 {static ReentrantLock lock = new ReentrantLock(false);public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 500; i ++) {new Thread(() -> {lock.lock();try {log.debug("{} 获得锁成功", Thread.currentThread().getName());} finally {lock.unlock();}}, "t" + i).start();}Thread.sleep(1);new Thread(()->{log.debug("start-----------------------");lock.lock();try {log.debug("{} 获得锁成功 running", Thread.currentThread().getName());} finally {lock.unlock();}}, "强行抢夺").start();}
    }
    

     

    公平锁

    • 定义:当使用公平锁时,锁会按照线程请求的顺序来分配。也就是说,最早提出请求的线程将最先被授予锁。这有助于避免“饥饿”现象,即某些线程永远无法获得锁。
    • 实现:要创建一个公平锁的ReentrantLock实例,可以在构造函数中传入参数true,例如new ReentrantLock(true)。
    • 性能:由于需要维护线程请求锁的顺序,公平锁可能会导致较低的吞吐量。因为每当有线程释放锁后,系统必须检查等待队列中的下一个线程是否可以获取锁,而不是简单地允许当前正在尝试获取锁的线程直接获取它。
    // 公平锁
    @Slf4j(topic = "c.Test8")
    public class Test8 {static ReentrantLock lock = new ReentrantLock(true);public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 500; i ++) {new Thread(() -> {lock.lock();try {log.debug("{} 获得锁成功", Thread.currentThread().getName());} finally {lock.unlock();}}, "t" + i).start();}Thread.sleep(1);new Thread(()->{log.debug("start-----------------------");lock.lock();try {log.debug("{} 获得锁成功 running", Thread.currentThread().getName());} finally {lock.unlock();}}, "强行抢夺").start();}
    }
    

     

     await/Condition

    等待条件(await方法系列):

    当一个线程需要等待某个特定条件发生时,可以调用Condition对象的await()方法进入等待状态。这会导致当前线程释放锁并等待,直到另一个线程调用了同一个Condition对象的signal()或signalAll()方法。常见的await方法包括:

    • await():使当前线程进入等待状态,直到被通知或中断。
    • awaitUninterruptibly():类似于await(),但是不会响应中断。
    • awaitNanos(long nanosTimeout):尝试等待指定的时间长度,如果超时则返回剩余时间。
    • awaitUntil(Date deadline):等待直到指定的时间点,如果到达截止时间还未被通知,则继续执行。

    通知条件(signal方法系列):

    当一个线程修改了共享资源并且认为可能满足了一个或多个正在等待的线程的条件时,它可以调用Condition对象的signal()或signalAll()方法来唤醒等待的线程。

    • signal():唤醒一个等待此Condition的线程。如果有多个线程在等待,则选择其中的一个进行唤醒。
    • signalAll():唤醒所有等待此Condition的线程。

    可以把Condition实例看作对应的等待队列,不同的线程可以调用不同的等待队列的await方法,使得当前线程进入当前的等待队列中 

    ​
    // 等待队列 Condition实例
    // 唤醒线程:signal或signalAll
    @Slf4j(topic = "c.Test9")
    public class Test9 {static ReentrantLock lock = new ReentrantLock();static Condition waitBreadQueue = lock.newCondition();static Condition waitCoffeeQueue = lock.newCondition();static volatile boolean hasBread = false;static volatile boolean hasCoffee = false;public static void main(String[] args) throws InterruptedException {new Thread(() -> {try {lock.lock();while (!hasCoffee) {try {log.debug("等待咖啡");waitCoffeeQueue.await(); // 等咖啡队列} catch (InterruptedException e) {e.printStackTrace();}}log.debug("t1 收到咖啡");} finally {log.debug("t1 释放锁");lock.unlock();}}, "t1").start();new Thread(() -> {try {lock.lock();while (!hasBread) {try {log.debug("等待面包");waitBreadQueue.await(); // 等面包队列} catch (InterruptedException e) {e.printStackTrace();}}log.debug("t2 收到面包");} finally {log.debug("t2 释放锁");lock.unlock();}}, "t2").start();sleep(1000);handleCoffee();sleep(1000);handleBread();}public static void handleCoffee() {lock.lock();try {log.debug("发咖啡");hasCoffee = true;waitCoffeeQueue.signal(); // 唤醒等待咖啡的线程} finally {lock.unlock();}}public static void handleBread() {lock.lock();try {log.debug("发面包");hasBread = true;waitBreadQueue.signal(); // 唤醒等待面包的线程} finally {lock.unlock();}}
    }​


    如有错误,欢迎指正!!! 

    图源来自网络,侵删!

    本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/899134.shtml

    如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

    相关文章

    现阶段高校的人工智能方案培训如何?

    人工智能在未来肯定是核心发展力&#xff0c;核心竞争力&#xff0c;也是国家重点扶持的对象&#xff0c;但我还是不看好高校的人工智能方向&#xff0c;只是怕有些同学对市场前景盲目乐观&#xff0c;就轻易上车了。 你要是985以上的高校&#xff0c;可以考虑选择人工智能&…

    JavaScript中的继承有哪些方式?各有什么优缺点

    在 JavaScript 中&#xff0c;继承主要通过原型链实现&#xff0c;常见的继承方式有以下几种&#xff0c;每种方式都有其优缺点&#xff1a; 1. 原型链继承 1. 实现方式&#xff1a;将子类的原型对象指向父类的实例。 function Parent() {} function Child() {} Child.protot…

    深入理解指针(3)(C语言版)

    文章目录 前言 一、字符指针变量二、数组指针变量2.1 数组指针变量是什么2.2 数组指针变量怎么初始化2.2.1 静态初始化2.2.2 动态初始化 三、二维数组传参的本质四、函数指针变量4.1 函数指针变量的创建4.2 函数指针变量的使用4.3 typedef关键字4.4拓展 五、函数指针数组六、转…

    Linux之 权限提升(Linux Privilege Escalation)

    Linux 之权限提升 系统信息 1.获取操作系统信息 2.检查PATH&#xff0c;是否有任何可写的文件夹&#xff1f; 3.检查环境变量&#xff0c;有任何敏感细节吗&#xff1f; 4.使用脚本&#xff08;DirtyCow&#xff1f;&#xff09;搜索内核漏洞 5.检查sudo 版本是否存在漏洞…

    【leetcode hot 100 215】数组中的第K个最大元素

    解法一&#xff1a;维护最大最小值 -> 堆 -> k个元素的最小值堆 class Solution {public int findKthLargest(int[] nums, int k) {// 维护最大最小值 -> 堆 -> k个元素的最小值堆PriorityQueue<Integer> heap new PriorityQueue<>((n1, n2) -> n…

    csp信奥赛C++常用的数学函数详解

    csp信奥赛C常用的数学函数详解 在信息学奥林匹克竞赛&#xff08;信奥赛&#xff09;中&#xff0c;C 的 <cmath> 头文件提供了丰富的数学函数&#xff0c;用于高效处理数学运算。以下是常用系统数学函数的详细讲解及汇总表格。 绝对值函数 int abs(int x)&#xff1a;返…

    Java IntelliJ IDEA 中配置多个 JDK 版本

    目录 一、添加多个 JDK 版本1. 下载并安装多个 JDK 版本2. 配置 JDK 在 IntelliJ IDEA 中 二、在项目中切换 JDK 版本1. 设置项目使用的 JDK 版本2. 设置模块使用的 JDK 版本 三、在运行配置中指定 JDK 版本四、总结 在实际开发中&#xff0c;我们常常需要在同一个项目中使用不…

    ChatDBA VS DeepSeek:快速诊断 OceanBase 集群新租户数据同步异常

    社区王牌专栏《一问一实验&#xff1a;AI 版》改版以来已发布多期&#xff08;51-60&#xff09;&#xff0c;展现了 ChatDBA 在多种场景下解决问题的效果。 下面让我们正式进入《一问一实验&#xff1a;AI 版》第 62 期&#xff0c;看看 ChatDBA 最新效果以及与热门大模型 De…

    Java条码与二维码生成技术详解

    一、技术选型分析 1.1 条码生成方案 Barbecue是最成熟的Java条码库&#xff0c;支持&#xff1a; Code 128EAN-13/UPC-AUSPS Inteligent Mail等12种工业标准格式 1.2 二维码方案对比 库名称维护状态复杂度功能扩展性ZXing★★★★☆较高强QRGen★★★☆☆简单一般BoofCV★…

    air780eq 阿里云

    硬件&#xff1a;APM32F030C8 Air 780eq 参考文档&#xff1a; 合宙780E-4G模块通过AT指令连接到阿里云平台&#xff0c;实现信息的收发_air780e上传阿里云属性值at命令-CSDN博客 阿里云 - atair780eq - 合宙文档中心 4G模块接入阿里云-实现数据上传和命令下发_4g模块上传…

    oracle数据库(数据库启动关闭/sqlplus登录及基本操作/设置字符集/distinct去重)

    目录 1. Oracle数据库启动 2. Oracle数据库关闭 3. sqlplus登录Oracle数据库 3.1 使用sqlplus登录Oracle数据库 3.2 使用sqlplus登录Oracle数据库 3.3 远程登录 3.4 解锁用户 3.5 修改用户密码 3.6 查看当前语言环境 4. sqlplus基本操作 4.1 显示当前用户 4.2 查看当前用户…

    Java 大视界 -- Java 大数据在智能金融区块链跨境支付与结算中的应用(154)

    &#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…

    大模型词表注入

    大模型词表注入&#xff08;Vocabulary Injection&#xff09; 大模型词表注入&#xff08;Vocabulary Injection&#xff09;是指在预训练语言模型&#xff08;如GPT、LLAMA等&#xff09;的基础上&#xff0c;动态扩展其词表&#xff08;Vocabulary&#xff09;的技术&#…

    在Cesium中使用ThreeJs材质(不是场景融合哦)

    在Cesium中使用ThreeJs材质(不是场景融合哦&#xff09;_哔哩哔哩_bilibili

    初教六双机一飞冲天动作要领

    初教六双机一飞冲天动作要领 初教六双机“一飞冲天”是典型的垂直爬升特技动作&#xff0c;要求双机以近乎垂直的姿态同步高速爬升&#xff0c;展现飞机的动力性能与编队协同能力。以下是该动作的详细技术解析与执行要点&#xff1a; 一、动作定义与特点 基本形态 双机以相同速…

    给Web开发者的HarmonyOS指南02-布局样式

    给Web开发者的HarmonyOS指南02-布局样式 本系列教程适合鸿蒙 HarmonyOS 初学者&#xff0c;为那些熟悉用 HTML 与 CSS 语法的 Web 前端开发者准备的。 本系列教程会将 HTML/CSS 代码片段替换为等价的 HarmonyOS/ArkUI 代码。 布局基础对比 在Web开发中&#xff0c;我们使用CS…

    京东软件测试岗位经典面试题(附答案)

    1、黑盒测试的测试用例常见设计方法都有哪些&#xff1f;请分别以具体的例子来说明这些方法在测试用例设计工作中的应用。 1&#xff09;等价类划分&#xff1a;等价类是指某个输入域的子集合.在该子集合中&#xff0c;各个输入数据对于揭露程序中的错误都是等效的.并合理地假…

    3.26[a]paracompute homework

    5555 负载不平衡指多个线程的计算量差异显著&#xff0c;导致部分线程空转或等待&#xff0c;降低并行效率。其核心矛盾在于任务划分的静态性与计算动态性不匹配&#xff0c;尤其在处理不规则数据或动态任务时尤为突出。以稀疏矩阵的向量乘法为例&#xff0c;假设其非零元素分…

    网站安全专栏-------浅谈CC攻击和DDoS攻击的区别

    CC攻击和DDoS攻击都是网络攻击的类型&#xff0c;但它们在攻击方式、目标和效果上有所不同。以下是它们之间的一些主要区别&#xff1a; ### 1. 定义 - **DDoS攻击&#xff08;分布式拒绝服务攻击&#xff09;**&#xff1a; DDoS攻击是指攻击者通过大量的分布式计算机&#x…

    帕金森患者的生活重塑:从 “嘴” 开启康复之旅

    当提到帕金森病&#xff0c;许多人会联想到震颤、僵硬和行动迟缓等症状。这种神经系统退行性疾病&#xff0c;给患者的生活带来了巨大的挑战。然而&#xff0c;你可知道&#xff0c;帕金森患者恢复正常生活&#xff0c;可以从 “嘴” 开始管理&#xff1f; 帕金森病在全球影响着…