本文最后更新于 2023-09-19,文章内容可能已经过时。

5 多线程锁

5.1 锁的八个问题演示

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");
    }
}

1 标准访问,先打印短信还是邮件

------sendSMS

------sendEmail

这里把睡4秒注释,因为两个线程创建之间主线程睡了100ms,导致必定第一个线程先创建,故必然执行顺序固定为上

    public static void main(String[] args) throws Exception {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();

        new Thread(()->{

            try {
                phone1.sendSMS();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }

        }, "AA").start();

        Thread.sleep(100);

        new Thread(()->{

            try {
                phone1.sendEmail();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }

        }, "BB").start();
    }

2 停 4 秒在短信方法内,先打印短信还是邮件

------sendSMS

------sendEmail

如果把睡4秒取消注释,还是这个顺序,因为我们的锁是加在非静态方法,即给对象加锁,而调用同一个对象,故当第一个线程执行senSMS,持有的是类锁,第二个线程可以获取对象锁,执行其他的同步方法。

3 新增普通的 hello 方法,是先打短信还是 hello

------getHello

------sendSMS

显然,肯定是非同步方法执行,它不是同步方法,不需要拿锁,不会被阻塞

4 现在有两部手机,先打印短信还是邮件

------sendEmail

------sendSMS

必然没有睡的sendEmail先执行完,目前是对象锁,换了对象,拿到的不是同一个锁,那加了线程休眠的肯定执行的慢

5 两个静态同步方法,1 部手机,先打印短信还是邮件

------sendSMS

------sendEmail

6 两个静态同步方法,2 部手机,先打印短信还是邮件

------sendSMS

------sendEmail

既然是静态方法,那就是单例的Class类锁,那显然执行的方法哪怕对象不同,仍会阻塞别的Class锁的线程。

7 1 个静态同步方法,1 个普通同步方法,1 部手机,先打印短信还是邮件

------sendEmail

------sendSMS

8 1 个静态同步方法,1 个普通同步方法,2 部手机,先打印短信还是邮件

------sendEmail

------sendSMS

一个是Class锁,一个是对象锁,互不影响,所以不睡的方法,显然先执行。

**结论 : **

一个对象里面如果有多个 synchronized 方法,某一个时刻内,只要一个线程去调用其中的一个 synchronized 方法【前提是监视器相同】了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些 synchronized 方法锁的是当前对象 this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized 方法

加个普通方法后发现和同步锁无关

换成两个对象后,不是同一把锁了,情况立刻变化。

synchronized 实现同步的基础:Java 中的每一个对象都可以作为锁。

具体表现为以下 3 种形式。

对于普通同步方法,锁是当前实例对象。

对于静态同步方法,锁是当前类的 Class 对象。

对于同步方法块,锁是 Synchonized 括号里配置的对象

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。

所有的静态同步方法用的也是同一把锁——类对象本身,这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞争条件的。

但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象,就会产生竞争关系!

5.2 公平锁和非公平锁

6.png

这里以之前的卖票代码为案例,我们发现 ReentrantLock 默认构造就是非公平锁,同时有一个带布尔的有参构造是可以构建公平锁。

之前卖票案例,可以看到在AA线程没有结束for循环之前,其他线程没有机会调用方法进行卖票。

非公平锁 : 线程饿死 效率高

公平锁 : 均衡调用 效率低

非公平锁的话就是抢的,就不管你来的早还是晚,同时竞争,公平锁的话就是有一个队列,咱们依次按队列排。

`

5.3. 可重入锁

synchronized(隐式)和Lock(显式)都是可重入锁

重复获取锁有一个加锁计数器,同一个线程获取同一个锁时+1,离开时-1,到0就完全释放锁

可重入锁指的是同一个线程可无限次地进入同一把锁的不同代码,又因该锁通过线程独占共享资源的方式确保并发安全,又称为独占锁可重入指的是已经获得该锁了,但在代码块里还能接着获得该锁,只是后面也要释放两次该锁。

举个例子:同一个类中的synchronize关键字修饰了不同的方法。synchronize是内置的隐式的可重入锁,例子中的两个方法使用的是同一把锁,只要能执行testB()也就说明线程拿到了锁,所以执行testA()方法就不用被阻塞等待获取锁了;如果不是同一把锁或非可重入锁,就会在执行testA()时被阻塞等待。

class tryReentrant{

    public synchronized void testA() throws InterruptedException {
        System.out.println( Thread.currentThread().getName() + " :: TestA");
        Thread.sleep(1000);
        testB();
    }

    public synchronized void testB(){
        System.out.println(Thread.currentThread().getName() + " :: TestB");
    }
}

public class ReentrantLockTest {

    public static void main(String[] args) throws InterruptedException {
        tryReentrant tryReentrant = new tryReentrant();

        new Thread(()->{
            try {
                tryReentrant.testA();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "AA").start();

        Thread.sleep(100);

        new Thread(tryReentrant::testB, "BB").start();
    }
}
AA :: TestA
AA :: TestB
BB :: TestB

5.4. 死锁

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。 一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

4.png

诱发死锁的原因:

  • 互斥条件
  • 占用且等待
  • 不可抢夺(或不可抢占)
  • 循环等待

以上4个条件,同时出现就会触发死锁。

解决死锁:

死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。

针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。

针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。

针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。

针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。

public class DeadLock {

    public static void main(String[] args) {
        Object a = new Object();
        Object b = new Object();

        new Thread(()-> {
            synchronized (a){
                System.out.println(Thread.currentThread().getName() + 
                                   " 持有锁 a 想要持有锁 b");
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (b){
                    System.out.println(Thread.currentThread().getName() +
                                       " 持有锁 a b 线程死亡");
                }
            }
        }, "A").start();

        new Thread(()-> {
            synchronized (b){
                System.out.println(Thread.currentThread().getName() + 
                                   " 持有锁 b 想要持有锁 a");
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (a){
                    System.out.println(Thread.currentThread().getName() + 
                                       " 持有锁 a b 线程死亡");
                }
            }
        }, "B").start();
    }
}

验证是否死锁

1 jps

如果没有给 jps.exe 配置到环境变量需要自行前往jdk目录使用命令

jps -l

7.png

2 jstack

jstack 23604

8.png