当前位置: 首页 > news >正文

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: dup
     4: astore_1
     5: monitorenter                     // 获取锁
     6: getstatic     #3                  // Field cnt:I
     9: iconst_1
    10: iadd
    11: putstatic     #3                  // Field cnt:I
    14: aload_1
    15: monitorexit                      // 释放锁
    16: goto          24
    19: astore_2
    20: aload_1
    21: monitorexit                      // 确保在异常情况下也释放锁
    22: aload_2
    23: athrow
    24: return
  Exception table:
     from    to  target type
         6    16    19   any
        19    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;
       }
    
       @Override
       public 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());
        }
    
        @Override
        public 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(); // 打断t1
                log.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;
        }
        @Override
        public 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();
            }
        }
        @Override
        public 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();
            }
        }
    }
    
    ​


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

    图源来自网络,侵删!

    相关文章:

  • Linux: 静态库和动态库
  • 在 Linux(Ubuntu / CentOS 7)上快速搭建我的世界 MineCraft 服务器,并实现远程联机,详细教程
  • 【C语言】深入理解指针(1)
  • Docker应用部署之mysql篇(day5)
  • 用Python掌握算法:初学者指南
  • springboot+mybatisplus
  • MySQL安装与卸载
  • 开源webmail邮箱客户端rainloop的分支版本SnappyMail 设置发件人允许多重身份
  • Linux一步部署主DNS服务器
  • 《DepMamba : Progressive Fusion Mamba for Mutilmodal Depression Detection》论文精读笔记
  • (九)Spring Webflux
  • 深度学习 Note.1
  • 海康HTTP监听报警事件数据
  • 从 MySQL 到时序数据库 TDengine:Zendure 如何实现高效储能数据管理?
  • 破局离散制造:主数据管理驱动数字化转型的实践与启示
  • Rabbitmq消息被消费时抛异常,进入Unacked 状态,进而导致消费者不断尝试消费(上)
  • BC93 公务员面试
  • 16-CSS3新增选择器
  • 从子查询到连接:提升数据库查询性能的 7 种方法
  • 生成式AI课程 比较好
  • 4月份全国93个国家气象站日最高气温达到或突破极值
  • 商超展销延长、专区专柜亮相……上海“外贸拓内销”商品与市民见面
  • 下任美联储主席热门人选沃什:美联储犯下“系统性错误”,未能控制一代人以来最严重的通胀
  • 5月动漫|“爱死机”即将回归,《明末》或是下一个大IP?
  • QFII一季度现身超300家公司:持有南京银行市值最高,5家青睐立航科技
  • 人民日报:光荣属于每一个挺膺担当的奋斗者