Skip to content

多线程锁

1. 锁的八个情况

  1. 多线程分别用同一个资源对象调用静态被synchronized修饰的代码和非静态synchronized修饰的代码, 比如标准访问,先打印短信还是邮件
  2. 多线程分别用同一个资源对象调用普通方法和静态被synchronized修饰的代码,比如停4秒在短信方法内,先打印短信还是邮件
  3. 多线程分别用同一个资源对象调用普通方法和非静态被synchronized修饰的代码,比如新增普通的hello方法,是先打短信还是hello
  4. 多线程分别用2个资源对象调用静态被synchronized修饰的代码和非静态synchronized修饰的代码, 比如现在有两部手机,先打印短信还是邮件
  5. 多线程分别用同一个资源对象调用非静态被synchronized修饰的代码和非静态被synchronized修饰的代码, 比如,两个非静态同步方法,1部手机,先打印短信还是邮件
  6. 多线程分别用同一个资源对象调用静态被synchronized修饰的代码和静态被synchronized修饰的代码,比如两个静态同步方法,1部手机,先打印短信还是邮件
  7. 多线程分别用2个资源对象调用普通方法和静态被synchronized修饰的代码,比如1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
  8. 多线程分别用1个资源对象调用普通方法和非静态被synchronized修饰的代码,比如1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
java
public class Lock_8 {

    static class Phone {
        public static synchronized void sendSMS() throws Exception {
            //停留 4 秒
            TimeUnit.SECONDS.sleep(4);
            System.out.println("------sendSMS");
        }

        public synchronized void sendEmail() throws Exception {
            System.out.println("------sendEmail");
        }

        public void getHello() {
            System.out.println("------getHello");
        }
    }
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> {
            try {
                phone.sendEmail();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }, "AA").start();
        Thread.sleep(100)//主线程睡眠,B线程会滞后初始化,也就滞后启动 
        Thread Bthread = new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }, "BB").start();
        
    }
}

结论:
一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法。锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法。
加个普通方法后发现和同步锁无关,不影响普通方法的执行。
换成两个对象后,不是同一把锁了,情况立刻变化。
synchronized实现同步的基础:Java中的每一个对象都可以作为锁。
具体表现为以下3种形式:

  1. 对于普通同步方法,锁是当前实例对象。
  2. 对于静态同步方法,锁是当前类的Class对象。
  3. 对于同步方法块,锁是Synchonized括号里配置的对象。
    当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。所有的静态同步方法用的也是同一把锁——类对象本身。如果两把锁对应的是两个不同的对象,静态同步方法与非静态同步方法之间是不会有竞态条件的。但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象!

2. 公平锁和非公平锁

所谓非公平锁就是按照多个线程一开始启动的顺序直接执行,优先第一个线程再次得到锁,不允许中途被其他线程插队,这容易造成其他线程"被饿死"的现象,效率高; 可以参看代码执行示例: 4. ReentrantLock ;公平锁就是多个线程执行顺序并不固定,而是线程释放锁后按照等待得到锁的队列里面先来后到得到锁去执行,由于队列里面的线程都要等待前面的线程得到锁没有再去拿锁,因而显得公平一些,但是线程的执行效率相对就变低。我们大部分使用的锁都是非公平锁。
比如使用ReentrantLock锁,常用的无参构造源码:

java
public ReentrantLock() {
    sync = new NonfairSync();  // NonfairSync是一个非公平锁的实现内部类
}
// 查看NonfairSync的lock方法
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread()); // 排开除了自己的其他线程
    else
        acquire(1);
}
// 如果需要公平锁,需要传递参数到构造函数中
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
// 发送信号请求得到锁
final void lock() {
    acquire(1);
}

3. 可重入锁

ReentrantLock和Synchronized都是可重入锁,Synchronized为隐式的可重入锁,ReentrantLock为显式的, 可重入锁又被称为递归锁。

java
public static void main(String[] args) {
final Demo_Synchronized test = new Demo_Synchronized();

new Thread(()->{
    synchronized (test){
        System.out.println(Thread.currentThread().getName()+": 外层");
        synchronized (test){
            System.out.println(Thread.currentThread().getName()+": 中层");
            synchronized (test){
                System.out.println(Thread.currentThread().getName()+": 内层");
            }
        }
    }
}, "可重入锁").start();

运行结果:
Alt text 可以发现synchronized修饰的方法可以一层层无障碍的深入执行。

java
public static void main(String[] args) {
    ReentrantLock rl = new ReentrantLock();
    new Thread(() -> {
         // 上锁
        rl.lock();
        try{
            System.out.println(Thread.currentThread().getName()+": 外层");
            // 上锁
            rl.lock();
            try{
                System.out.println(Thread.currentThread().getName()+": 内层");
            }finally {
                // 解锁
                rl.unlock();
            }
        }finally {
            // 解锁
            rl.unlock();
        }
    }, "AA").start();
}

运行结果:
Alt text ReentrantLock在代码中多次使用, 值得注意的是lock()和unlock()必须配合使用,比如没有释放锁,其他线程也要用到该锁,就会被一直阻塞掉。

4. 死锁

两个或者两个以上进程在执行过程中,因为争夺资源而造成一种互相等待的现象, 如果没有外力干涉,他们就无法再执行下去。

4.1 产生死锁的原因

  1. 系统资源不足
  2. 进程运行推进顺序不合适
  3. 资源分配不当
java
public static void main(String[] args) throws InterruptedException {
    ReentrantLock rl1 = new ReentrantLock();
    ReentrantLock rl2= new ReentrantLock();
    new Thread(() -> {
        rl1.lock();
        try{
            System.out.println(Thread.currentThread().getName()+": 尝试获取rl2锁");
            TimeUnit.SECONDS.sleep(1);
            rl2.lock();
            try {

                System.out.println(Thread.currentThread().getName()+": 得到获取rl2锁");
            }finally {
                rl2.unlock();
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            rl1.unlock();
        }
    }, "AA").start();

    new Thread(() -> {
        rl2.lock();
        try{
            System.out.println(Thread.currentThread().getName()+": 尝试获取rl1锁");
            rl1.lock();
            try {
                System.out.println(Thread.currentThread().getName()+": 得到获取rl1锁");
            }finally {
                rl1.unlock();
            }
        }finally {
            rl2.unlock();
        }
    }, "BB").start();
}

运行结果:
Alt text AB线程互相等待。

4.2 排查程序是否死锁

使用jps和jstack可以排查死锁的情况。

cmd
E:\rocket\spark-demo>jps
16964 Launcher
8100 NailgunRunner
12520 
3292 RemoteMavenServer36
6620 Jps
908 DeadLockDemo
E:\rocket\spark-demo>jstack 908
2024-03-27 07:58:00
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.271-b09 mixed mode):

"DestroyJavaVM" #16 prio=5 os_prio=0 tid=0x0000024cb1ea5000 nid=0x24a4 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"BB" #15 prio=5 os_prio=0 tid=0x0000024ccf096000 nid=0x3504 waiting on condition [0x000000a8bc9fe000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x000000076b8f2978> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
        at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
        at org.example.rdd.thread.DeadLockDemo.lambda$main$1(DeadLockDemo.java:34)
        at org.example.rdd.thread.DeadLockDemo$$Lambda$2/1631862159.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

"AA" #14 prio=5 os_prio=0 tid=0x0000024ccf090000 nid=0x47c4 waiting on condition [0x000000a8bc8fe000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x000000076b8f29a8> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
        at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
        at org.example.rdd.thread.DeadLockDemo.lambda$main$0(DeadLockDemo.java:16)
        at org.example.rdd.thread.DeadLockDemo$$Lambda$1/801197928.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

"Service Thread" #13 daemon prio=9 os_prio=0 tid=0x0000024ccecc7800 nid=0x3fc4 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread3" #12 daemon prio=9 os_prio=2 tid=0x0000024ccecae800 nid=0xc80 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread2" #11 daemon prio=9 os_prio=2 tid=0x0000024ccecae000 nid=0x1f04 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #10 daemon prio=9 os_prio=2 tid=0x0000024ccd084800 nid=0x2f1c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #9 daemon prio=9 os_prio=2 tid=0x0000024ccec9f800 nid=0x1a6c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"JDWP Command Reader" #8 daemon prio=10 os_prio=0 tid=0x0000024ccd07a800 nid=0x474c runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"JDWP Event Helper Thread" #7 daemon prio=10 os_prio=0 tid=0x0000024ccd077800 nid=0x12d8 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"JDWP Transport Listener: dt_socket" #6 daemon prio=10 os_prio=0 tid=0x0000024ccd072000 nid=0xb80 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x0000024ccd055800 nid=0x42e8 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x0000024ccd044000 nid=0x4408 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x0000024ccd012000 nid=0xed0 in Object.wait() [0x000000a8bbcff000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076b588ee0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
        - locked <0x000000076b588ee0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)

"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000024ccd009000 nid=0x2394 in Object.wait() [0x000000a8bbbff000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076b586c00> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
        - locked <0x000000076b586c00> (a java.lang.ref.Reference$Lock)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

"VM Thread" os_prio=2 tid=0x0000024cccfdf800 nid=0x2408 runnable

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x0000024cb1ebc000 nid=0x3d54 runnable

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x0000024cb1ebe800 nid=0x2fc0 runnable

"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x0000024cb1ebf800 nid=0x44a4 runnable

"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x0000024cb1ec1000 nid=0x43b8 runnable

"GC task thread#4 (ParallelGC)" os_prio=0 tid=0x0000024cb1ec3000 nid=0x4448 runnable

"GC task thread#5 (ParallelGC)" os_prio=0 tid=0x0000024cb1ec4000 nid=0x252c runnable

"GC task thread#6 (ParallelGC)" os_prio=0 tid=0x0000024cb1ec7000 nid=0x3280 runnable

"GC task thread#7 (ParallelGC)" os_prio=0 tid=0x0000024cb1ec9000 nid=0x2c4 runnable

"VM Periodic Task Thread" os_prio=2 tid=0x0000024cceca0800 nid=0x463c waiting on condition

JNI global references: 2382


Found one Java-level deadlock:
=============================
"BB":
  waiting for ownable synchronizer 0x000000076b8f2978, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
  which is held by "AA"
"AA":
  waiting for ownable synchronizer 0x000000076b8f29a8, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
  which is held by "BB"

Java stack information for the threads listed above:
===================================================
"BB":
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x000000076b8f2978> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
        at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
        at org.example.rdd.thread.DeadLockDemo.lambda$main$1(DeadLockDemo.java:34)
        at org.example.rdd.thread.DeadLockDemo$$Lambda$2/1631862159.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
"AA":
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x000000076b8f29a8> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
        at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
        at org.example.rdd.thread.DeadLockDemo.lambda$main$0(DeadLockDemo.java:16)
        at org.example.rdd.thread.DeadLockDemo$$Lambda$1/801197928.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

可以看出BB线程拥有0x000000076b8f29a8锁, 等待得到0x000000076b8f2978锁, 而AA线程拥有0x000000076b8f2978锁,等待得到0x000000076b8f29a8锁。

5. 乐观锁和悲观锁

5.1 悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁、表锁、读锁,写锁等,都是在做操作之前先上锁。 Alt text 数据库中行锁比表锁更容易发生死锁, 因为表锁同一时间只允许一个线程进入操作。数据库中的每一行具体有读锁和写锁,读锁被称为共享锁,写锁被称为独占锁,数据库中读锁和写锁都可能导致死锁:
比如两个线程AB,A线程拿到读锁读第一行数据后还要修改这行数据,因为读锁不互斥,B线程也得到读锁也去读第一行数据,A线程修改数据要使用写锁就需要等待B线程读完释放读锁,B线程不打算立即释放读锁,因为B线程也要修改这一行数据,修改数据就要需要所有写读锁释放,B线程发现A线程还有读锁,就等待A线程释放读锁,此时发生AB读锁的死锁。 比如两个线程AB,A线程得到第一行写锁进行修改第一行数据后,准备修改第二行需要得到第二行行锁,B线程此时修改完第二行准备修改第一行数据,此时A线程需要等待B线程释放写锁,B线程也等待A线程释放写锁,发生AB写锁的死锁。

5.2 乐观锁

总是假设最好的情况,它获取数据的时候,并不担心数据被修改,每次获取数据的时候也不会加锁,只是在更新数据的时候,通过判断现有的数据是否和原数据一致来判断数据是否被其他线程操作,如果没被其他线程修改则进行数据更新,如果被其他线程修改则不进行数据更新。可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量, Alt text