JUC并发编程基础篇第四章之公平锁/重入锁/死锁[常见锁的基本认识]

发布时间 2023-04-07 15:40:47作者: 爱吃糖的靓仔

@

1、公平锁/非公平锁

1.1、概念

  • 公平锁和非公平锁是在多线程编程中使用的两种锁类型,它们的区别在于线程在等待锁时如何被选取获取锁的机会。

  • 公平锁是指多个线程在等待同一个锁时,线程获取锁的顺序是按照它们等待的先后顺序来的。换句话说,公平锁保证线程获取锁的顺序是按照它们等待锁的顺序来的,不会出现“插队”现象。这种锁的实现方式通常是将线程加入到一个FIFO(先进先出)队列中,等待锁释放的时候按照队列中的顺序来选取一个线程获取锁。

  • 非公平锁是指多个线程在等待同一个锁时,线程获取锁的顺序是不定的,也就是说,先等待的线程并不一定先获得锁。这种锁的实现方式是让等待锁的线程通过CAS(比较-交换)操作来尝试获取锁,如果没有竞争者,就成功获取锁,否则就加入到等待队列中等待下一次获取锁的机会。

  • 总的来说,公平锁会按照线程等待的顺序来获取锁,从而避免了线程饥饿的问题,但是它可能会引入一定的线程切换开销。非公平锁则会尽可能快地将锁分配给等待的线程,但是可能会导致某些线程长期等待,从而引起线程饥饿问题。选择哪种锁类型要根据具体的场景和应用需求来决定。

1.2、非公平锁代码案例

class Ticket {
    private int number = 50;
    ReentrantLock lock = new ReentrantLock();
 
    public void sale() {
        lock.lock();
        try {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出了票还剩下第" + --number + " 张票");
            }
        } finally {
            lock.unlock();
        }
    }
}
 
public class SafeLock {
 
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(() -> {
            for (int i = 0; i < 55; i++) {
                ticket.sale();
            }
        }, "a").start();
 
        new Thread(() -> {
            for (int i = 0; i < 55; i++) {
                ticket.sale();
            }
        }, "b").start();
 
        new Thread(() -> {
            for (int i = 0; i < 55; i++) {
                ticket.sale();
            }
        }, "c").start();
    }
}

输出结果

a卖出了票还剩下第49 张票
a卖出了票还剩下第48 张票
a卖出了票还剩下第47 张票
a卖出了票还剩下第46 张票
c卖出了票还剩下第45 张票
c卖出了票还剩下第44 张票
c卖出了票还剩下第43 张票
c卖出了票还剩下第42 张票
c卖出了票还剩下第41 张票
c卖出了票还剩下第40 张票
c卖出了票还剩下第39 张票
c卖出了票还剩下第38 张票
c卖出了票还剩下第37 张票
c卖出了票还剩下第36 张票
c卖出了票还剩下第35 张票
c卖出了票还剩下第34 张票
c卖出了票还剩下第33 张票
c卖出了票还剩下第32 张票
c卖出了票还剩下第31 张票
c卖出了票还剩下第30 张票
c卖出了票还剩下第29 张票
c卖出了票还剩下第28 张票
c卖出了票还剩下第27 张票
c卖出了票还剩下第26 张票
c卖出了票还剩下第25 张票
c卖出了票还剩下第24 张票
c卖出了票还剩下第23 张票
c卖出了票还剩下第22 张票
c卖出了票还剩下第21 张票
c卖出了票还剩下第20 张票
c卖出了票还剩下第19 张票
c卖出了票还剩下第18 张票
c卖出了票还剩下第17 张票
c卖出了票还剩下第16 张票
c卖出了票还剩下第15 张票
c卖出了票还剩下第14 张票
c卖出了票还剩下第13 张票
c卖出了票还剩下第12 张票
c卖出了票还剩下第11 张票
c卖出了票还剩下第10 张票
c卖出了票还剩下第9 张票
c卖出了票还剩下第8 张票
c卖出了票还剩下第7 张票
c卖出了票还剩下第6 张票
c卖出了票还剩下第5 张票
c卖出了票还剩下第4 张票
c卖出了票还剩下第3 张票
c卖出了票还剩下第2 张票
c卖出了票还剩下第1 张票
c卖出了票还剩下第0 张票

通过上面的案例可以知道,整个卖票的过程中,B线程完全没有抢到资源,进行买票,这就是属于非公平锁; 每个线程获取锁的概率是不同的;

1.3、公平锁代码案例

对上面的代码,进行调整

ReentrantLock lock = new ReentrantLock(true);

可以看到输出结果如下

a卖出了票还剩下第49 张票
a卖出了票还剩下第48 张票
a卖出了票还剩下第47 张票
b卖出了票还剩下第46 张票
a卖出了票还剩下第45 张票
c卖出了票还剩下第44 张票
b卖出了票还剩下第43 张票
a卖出了票还剩下第42 张票
c卖出了票还剩下第41 张票
b卖出了票还剩下第40 张票
a卖出了票还剩下第39 张票
c卖出了票还剩下第38 张票
b卖出了票还剩下第37 张票
a卖出了票还剩下第36 张票
c卖出了票还剩下第35 张票
b卖出了票还剩下第34 张票
a卖出了票还剩下第33 张票
c卖出了票还剩下第32 张票
b卖出了票还剩下第31 张票
a卖出了票还剩下第30 张票
c卖出了票还剩下第29 张票
b卖出了票还剩下第28 张票
a卖出了票还剩下第27 张票
c卖出了票还剩下第26 张票
b卖出了票还剩下第25 张票
a卖出了票还剩下第24 张票
c卖出了票还剩下第23 张票
b卖出了票还剩下第22 张票
a卖出了票还剩下第21 张票
c卖出了票还剩下第20 张票
b卖出了票还剩下第19 张票
a卖出了票还剩下第18 张票
c卖出了票还剩下第17 张票
b卖出了票还剩下第16 张票
a卖出了票还剩下第15 张票
c卖出了票还剩下第14 张票
b卖出了票还剩下第13 张票
a卖出了票还剩下第12 张票
c卖出了票还剩下第11 张票
b卖出了票还剩下第10 张票
a卖出了票还剩下第9 张票
c卖出了票还剩下第8 张票
b卖出了票还剩下第7 张票
a卖出了票还剩下第6 张票
c卖出了票还剩下第5 张票
b卖出了票还剩下第4 张票
a卖出了票还剩下第3 张票
c卖出了票还剩下第2 张票
b卖出了票还剩下第1 张票
a卖出了票还剩下第0 张票

此时卖票,每个线程都拿到资源,达到了雨露均沾的效果

1.4、面试题:为什么会有这样的公平锁和非公所这样的设计

  • 公平锁的主要意义在于保证线程获取锁的公平性,避免线程长期等待锁而导致的饥饿问题。公平锁通过将等待锁的线程加入到队列中,按照FIFO的顺序选择获取锁的线程,避免了某些线程长期等待锁的情况,从而保证了公平性和可靠性。
  • 非公平锁的主要意义在于提高系统的吞吐量和性能。非公平锁采用了一种乐观的策略,即先尝试获取锁,如果获取失败则加入到等待队列中。这种策略可以尽可能地减少线程的等待时间,从而提高了系统的吞吐量和性能。但是,非公平锁可能会引起线程饥饿问题,一些线程可能长时间无法获得锁,导致程序的执行效率下降。
  • 因此,在选择公平锁和非公平锁时,需要根据具体的应用场景和需求来决定,选择合适的锁类型来满足性能和可靠性要求。

2、重入锁

2.1、简介

概念: 可重入锁,也称为递归锁,是一种支持线程再次获取自身已经持有的锁的锁机制。换句话说,可重入锁允许一个线程在持有锁的情况下再次获取同一个锁,而不会发生死锁或其他问题。

作用: 在多线程编程中,可重入锁能够有效地防止死锁和其他线程安全问题。当一个线程在持有锁时,如果它需要再次获取同一个锁(例如在递归调用时),如果锁不是可重入的,那么该线程将会被阻塞,导致死锁或其他问题的发生。而可重入锁允许同一个线程多次获取锁,从而避免了这种情况的发生。

实现方法: 实现可重入锁的方法有多种,常见的方式是在锁对象中维护一个计数器,记录当前持有锁的线程个数,每次线程获取锁时,计数器加一,释放锁时计数器减一。只有当计数器归零时,锁才能被其他线程获取。

优缺点: 可重入锁的优点是能够避免死锁和其他线程安全问题,同时允许多个线程同时访问临界区。缺点是相比于非重入锁会增加一些开销,因为需要维护计数器和线程状态等信息。但在大多数情况下,可重入锁的优点大于缺点,因此在多线程编程中被广泛应用。

2.2、没有重入锁,会发生什么问题

如果下面的三个小框,分别都采用了synchronized(o) , 那么当代码跨想要进入第二个synchronized(o)时,第一个synchronized(o)就必须释放锁; 此时就会发生矛盾,冲突;
于是程序规定了: 如果同步代码块中嵌套同步代码块,只要获取的是同一把锁,就可以自动获取该锁;
在这里插入图片描述

2.3、可重入锁的种类

2.3.1、隐式锁

隐式锁(也就是synchronized默认使用的锁) 默认的就是可重入锁


public class ReneeTrentLock {
 
    public static void main(String[] args) {
        Object o = new Object();
        new Thread(() -> {
            synchronized (o) {
                System.out.println(Thread.currentThread().getName()+"----外层调用");
                synchronized (o) {
                    System.out.println(Thread.currentThread().getName() + "----中层调用");
                    synchronized (o) {
                        System.out.println(Thread.currentThread().getName() + "----内层调用");
                    }
                }
            }
        },"a").start();
    }
}

同步方法


class ReneeTrentLockMethodModel{
    public synchronized void m1(){
        System.out.println("我是m1");
        m2();
    }
    public synchronized void m2(){
        System.out.println("我是m2");
        m3();
    }
    public synchronized void m3(){
        System.out.println("我是m3");
    }
}
 
public class ReneeTrentLockMethod {
 
    public static void main(String[] args) {
        ReneeTrentLockMethodModel model = new ReneeTrentLockMethodModel();
        model.m1();
    }
}

2.3.2、显式锁

显式锁(就是lock) 也有ReentrantLock这样的可重入锁
显示锁,不会自动帮我们释放锁,需要我们自己去注意: 锁了几次,就释放几次

public class ReneeTrentLockMethodLock {
 
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        new Thread(()->{
        try {
            lock.lock();
            System.out.println("我进来外层");
            lock.lock();
            System.out.println("我进来中层");
            lock.lock();
            System.out.println("我进来内层");
        } catch (Exception e){
          e.printStackTrace();
        } finally {
            lock.unlock();
            lock.unlock();
        }
       },"a").start();
 
        new Thread(()->{
            lock.lock();
            System.out.println("b线程进来了");
            lock.unlock();
        },"b").start();
    }
}

如果没有锁了几次,就释放几次; 上面你的b线程就会因为拿不到锁,而无法运行;

2.4、面试题: 可重入锁的实现机制

在这个篇章 我们知道了每个java中都会天生携带一个monitor, monitor管程具有计数器等一些列基本属性

  • 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
  • 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
  • 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
  • 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
  • 当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

3、死锁

3.1、概念

死锁指的是两个或多个线程互相等待对方释放资源而陷入了一种无限期的等待状态,导致程序无法继续执行下去,称为死锁。

死锁通常发生在多个线程同时获取多个共享资源时,例如线程A持有资源1,等待获取资源2,而线程B持有资源2,等待获取资源1。这样,两个线程互相等待对方释放资源,导致程序无法继续执行下去。

3.2、死锁案例

public class DeadlockExample {
    private static Object lock1 = new Object();
    private static Object lock2 = new Object();
 
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1 acquired lock1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println("Thread 1 acquired lock2");
                }
            }
        });
 
        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2 acquired lock2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println("Thread 2 acquired lock1");
                }
            }
        });
 
        thread1.start();
        thread2.start();
    }
}
  • 在这个例子中,有两个线程分别尝试获取两个不同的锁lock1和lock2。线程1先获取了lock1,然后尝试获取lock2,而线程2先获取了lock2,然后尝试获取lock1。如果这两个线程在同一时刻都获取了一个锁,然后尝试获取另一个锁时被阻塞,那么就会发生死锁。

  • 例如,线程1获取了lock1,然后尝试获取lock2,但是此时lock2已经被线程2获取了,所以线程1被阻塞。同时,线程2获取了lock2,然后尝试获取lock1,但是此时lock1已经被线程1获取了,所以线程2也被阻塞。这样,两个线程都无法继续执行,就发生了死锁。

3.3、如何证明自己程序是死锁状态,而不是while true导致的

通过jps -l 找到进程号 ; 然后 jstack 进程号 得到下面的结果

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x000000001c4cf1a8 (object 0x000000076bef6c88, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x000000001c4d1ae8 (object 0x000000076bef6c98, a java.lang.Object),
  which is held by "Thread-1"
 
Java stack information for the threads listed above:
===================================================
"Thread-1":
        at com.tvu.deathLock.DeadlockExample.lambda$main$1(DeadlockExample.java:30)
        - waiting to lock <0x000000076bef6c88> (a java.lang.Object)   //互相wait ,互相lock
        - locked <0x000000076bef6c98> (a java.lang.Object)
        at com.tvu.deathLock.DeadlockExample$$Lambda$2/1792845110.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
"Thread-0":
        at com.tvu.deathLock.DeadlockExample.lambda$main$0(DeadlockExample.java:17)
        - waiting to lock <0x000000076bef6c98> (a java.lang.Object)  //互相wait ,互相lock
        - locked <0x000000076bef6c88> (a java.lang.Object)
        at com.tvu.deathLock.DeadlockExample$$Lambda$1/716143810.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
 
Found 1 deadlock.

Found 1 deadlock. 可以我知道我该进程中有死锁;