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

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

时间:2023-04-07 15:45:34浏览次数:64  
标签:重入 JUC Thread lock 张票 剩下 死锁 线程 卖出

@

目录

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. 可以我知道我该进程中有死锁;

标签:重入,JUC,Thread,lock,张票,剩下,死锁,线程,卖出
From: https://www.cnblogs.com/itcastwzp/p/17296378.html

相关文章

  • JUC并发编程基础篇第六章之LockSupport[notify,signal之外的另一种唤醒方式]
    目录1、LockSupport有什么用2、使用wait和notify唤醒一个线程2.1、正常情况2.2、异常情况2,这里去掉了synchronized(o){}代码块2.3、异常情况3先notify再wait3、使用await和signal唤醒一个线程3.1、正常情况3.2、异常情况:如果去除锁块3.3、异常情况:先执行signal......
  • JUC并发编程基础篇第五章之线程中断状态[你理解的线程中断,只会Thread.interrupted()
    目录1、什么是线程的中断机制2、isterruptinterruptedisInterrupted区别3、面试题3.1、如何停止中断运行中的线程3.2、当前线程的中断标识符为true,是不是线程就立马停止了3.3、如果线程处于被阻塞的状态(列入sleep,wait,join等状态),在别的线程调用当前线程的interrupt()方法,会发生......
  • 【Java 并发】【九】【AQS】【一】什么是AQS?为什么说它是JUC基础框架?
    1 前言这节我们来开始看AQS,这个东西可以说是搞Java的都知道的,本节会介绍一下AQS以及它提供的基本机制,后面再对AQS提供的每一个机制一个个深入的剖析。2  什么是AQS?(同步器基础框架)AQS叫做抽象队列同步器(AbstractQueuedSynchronizer),它是一个实现了同步器功能的基础框架,其......
  • 使用jstack排查JVM进程死锁
    前言在Linux系统使用JDK自带的jstack指令分析输出的线程信息排查死锁的详细步骤。例子程序下面是一个模拟线程死锁的例子程序,编译(javacDeadLockSample.java)后执行(javaDeadLockSample)这个程序来启动一个JVM进程。其中一个线程会成功获取到DeadLockSample的Class对象锁持续打......
  • freeRTOS任务死锁
    一、freeRTOS任务死锁FreeRTOS任务死锁是一种常见的问题,通常发生在多个任务相互等待对方释放资源的情况下。以下是一个简单的例子,用于说明FreeRTOS任务死锁的情况:假设有两个任务Task1和Task2,它们需要共享两个资源ResourceA和ResourceB。每个任务都需要同时访问这两个资源才能完成它......
  • 并发编程——JUC并发大厂面试问题
    摘要现如今,不管是应届毕业生还是工作了三五年之内的工程师,在面试招聘的时候JUC并发编程的是必须掌握的一个技能,否者你将会被面试官玩弄。本博文将整理有关于的JUC的大厂面试问题和答案。帮助大家在面试过程中能够回答面试官问题的一二。同时本人也总结相关的面试问题的在相关文档中......
  • 【Java 并发】【八】【Atomic】【一】JUC下的Atomic原子类体系概览
    1 前言这节我们就开始看看Atomic原子类系列,JUC包下提供的原子类底层的实现原理基本都是差不多的,都是基于volatile和CAS操作来保证线程安全的,我们后续会着重分析几个类。2  概览我们看下JUC下边都有哪些原子类:看上面的图形,我们使用红色圈中的那些,就是我们要着重讨论的,一共......
  • 【打怪升级】【juc】关于LockSupport
    通过juc下LockSupport,可以达到阻塞和唤醒线程的操作 LockSupportLockSupport是juc下一个线程阻塞唤醒的工具类。它的主要方法有:每个使用LockSupport的线程,会有一个许可;调用park会立即返回,否则会被阻塞。如果许可不可用,则可以调用unpark供其可使用,......
  • SQLSERVER 语句交错引发的死锁研究
    一:背景1.讲故事相信大家在使用SQLSERVER的过程中经常会遇到阻塞和死锁,尤其是死锁,比如下面的输出:(1rowaffected)Msg1205,Level13,State51,Line5Transaction(ProcessID62)wasdeadlockedonlockresourceswithanotherprocessandhasbeenchosenasthed......
  • 死锁(deadlock)
    线程死锁的必备要素互斥条件:进程要求对所分配的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待;不可剥夺条......