JUC进阶-NO.3 说说Java锁

文章目录

⭐NO.3 说说Java锁

一. 乐观锁 & 悲观锁

1.悲观锁

适合写操作多的场景,先加锁可以保证写操作时数据正确

显示的锁定之后再操作同步资源

认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改.

synchronized关键子和Lock的实现类都是悲观锁

2.伪代码

.\JUC.assets\image-20220426113556066

3.乐观锁

乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据.

如果这个数据没有被更新,当前线程将自己修改的数据成功写入,如果数据已经被其他线程更新了,则根据不同的实现方法执行不同的操作

乐观锁在Java中是通过使用无锁编程来实现的,最常用的CAS算法,Java原子类中的递增操作就通过CAS自旋实现的

  • 适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升.

  • 乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸失之我命,再抢

  • 乐观锁一般有两种实现方式

    1. 采用版本号机制
    2. CAS(compare-and-Swap,即比较并替换)算法实现

    CAS 方式为乐观锁,synchronized 为悲观锁。因此使用 CAS 解决并发问题通常情况下性能更优。

    但使用 CAS 方式也会有几个问题:

    • ABA问题

      因为CAS需要在操作值的时候,检查值有没有发生变化,比如没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时则会发现它的值没有发生变化,但是实际上却变化了。

      ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A->B->A就会变成1A->2B->3A。

      从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

    • 循环时间长开销大

      自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:第一,它可以延迟流水线执行命令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而提高CPU的执行效率。

    • 只能保证一个共享变量的原子操作

      当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。

      还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i = 2,j = a,合并一下ij = 2a,然后用CAS来操作ij。

      从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

二. 通过8种情况演示锁运行案例,看看我们到底锁的是什么

1.8种锁案例
(1). 标准访问有ab两个线程,请问先打印邮件还是短信
public class Lock8Demo {
    //一切程序的入口,主线程
    public static void main(String[] args) {
        //资源类1
        Phone phone = new Phone();
        new Thread(() -> phone.sendEmail(), "a").start();

        //暂停毫秒
        try {
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> phone.sendSMS(), "b").start();
    }
}

class Phone //资源类
{
    public synchronized void sendEmail() {
        //暂停几秒钟线程
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-------sendEmail");
    }

    public synchronized void sendSMS() {
        System.out.println("-------sendSMS");
    }
}


// 控制台输出
-------sendEmail
-------sendSMS

一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,

其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法

锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法

(2). sendEmail方法暂停3秒钟,请问先打印邮件还是短信

同理情况1

(3).新增一个普通的hello方法,请问先打印邮件还是hello
public class Lock8Demo {
    //一切程序的入口,主线程
    public static void main(String[] args) {
        //资源类1
        Phone phone = new Phone();
        new Thread(() -> phone.sendEmail(), "a").start();

        //暂停毫秒
        try {
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> phone.hello(), "b").start();
    }
}

class Phone //资源类
{
    public synchronized void sendEmail() {
        //暂停几秒钟线程
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-------sendEmail");
    }

    public synchronized void sendSMS() {
        System.out.println("-------sendSMS");
    }

    public void hello() {
        System.out.println("-------hello");
    }
}

// 控制台输出
-------hello
-------sendEmail

加个普通方法后发现和同步锁无关
普通方法和同步锁方法是没有关系的不发生争抢

(4).有两部手机,请问先打印邮件还是短信
public class Lock8Demo {
    //一切程序的入口,主线程
    public static void main(String[] args) {
        //资源类1
        Phone phone1 = new Phone();
        //资源类2
        Phone phone2 = new Phone();
        new Thread(() -> phone1.sendEmail(), "a").start();

        //暂停毫秒
        try {
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> phone2.sendSMS(), "b").start();
    }
}

class Phone //资源类
{
    public synchronized void sendEmail() {
        //暂停几秒钟线程
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-------sendEmail");
    }

    public synchronized void sendSMS() {
        System.out.println("-------sendSMS");
    }
}

// 控制台
-------sendSMS
-------sendEmail

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

(5).两个静态同步方法,同1部手机,请问先打印邮件还是短信
public class Lock8Demo {
    //一切程序的入口,主线程
    public static void main(String[] args) {
        //资源类1
        Phone phone = new Phone();
      
        new Thread(() -> phone.sendEmail(), "a").start();

        //暂停毫秒
        try {
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> phone.sendSMS(), "b").start();
    }
}

//资源类
class Phone {
    public static synchronized void sendEmail() {
        //暂停几秒钟线程
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-------sendEmail");
    }

    public static synchronized void sendSMS() {
        System.out.println("-------sendSMS");
    }
}

// 控制台
-------sendEmail
-------sendSMS
  • 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部手机,所有的普通同步方法用的都是同一把锁——实例对象本身
  • 对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板
  • 对于同步方法块,锁的是 synchronized 括号内的对象
  • -> 阿里规范,能用对象锁就不要用类锁
  • 静态同步方法加的就是类锁
(6).两个静态同步方法, 2部手机,请问先打印邮件还是短信
public class Lock8Demo {
    //一切程序的入口,主线程
    public static void main(String[] args) {
        //资源类1
        Phone phone1 = new Phone();
        //资源类2
        Phone phone2 = new Phone();
        new Thread(() -> phone1.sendEmail(), "a").start();

        //暂停毫秒
        try {
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> phone2.sendSMS(), "b").start();
    }
}

//资源类
class Phone {
    public static synchronized void sendEmail() {
        //暂停几秒钟线程
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-------sendEmail");
    }

    public static synchronized void sendSMS() {
        System.out.println("-------sendSMS");
    }
}

// 控制台
-------sendEmail
-------sendSMS

同理情况5

(7).1个静态同步方法,1个普通同步方法,同1部手机,请问先打印邮件还是短信
public class Lock8Demo {
    //一切程序的入口,主线程
    public static void main(String[] args) {
        //资源类1
        Phone phone = new Phone();

        new Thread(() -> phone.sendEmail(), "a").start();

        //暂停毫秒
        try {
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> phone.sendSMS(), "b").start();
    }
}

//资源类
class Phone {
    public static synchronized void sendEmail() {
        //暂停几秒钟线程
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-------sendEmail");
    }

    public synchronized void sendSMS() {
        System.out.println("-------sendSMS");
    }
}

// 控制台
-------sendSMS
-------sendEmail
  • 当一个线程试图访问同步代码时它首先必须得到锁,退出或抛出异常时必须释放锁。
  • 所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
  • 所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class.具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
  • 但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
(8).1个静态同步方法,1个普通同步方法,2部手机,请问先打印邮件还是短信
public class Lock8Demo {
    //一切程序的入口,主线程
    public static void main(String[] args) {
        //资源类1
        Phone phone1 = new Phone();
        //资源类2
        Phone phone2 = new Phone();

        new Thread(() -> phone1.sendEmail(), "a").start();

        //暂停毫秒
        try {
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> phone2.sendSMS(), "b").start();
    }
}

//资源类
class Phone {
    public static synchronized void sendEmail() {
        //暂停几秒钟线程
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-------sendEmail");
    }

    public synchronized void sendSMS() {
        System.out.println("-------sendSMS");
    }
}

// 控制台
-------sendSMS
-------sendEmail

同理7

.\JUC.assets\image-20220426141236152

2.synchronizde有三种应用方式
  • JDK源码(notify方法)说明

    .\JUC.assets\image-20220426163116815

  • 8种锁的案例实际体现在三个地方

    1. 作用于实例方法,当前实例加锁,进入同步代码前腰获得当前实例的锁
    2. 作用于代码块,对括号里配置的对象加锁
    3. 作用于静态方法,当前类加锁,进入(静态)+同步代码前要获得当前类对象的锁
3.从字节码角度分析synchronized实现
  • javap -c ***.class文件反汇编

  • javap -v ***.class文件反编译

  • javap -v -verbose ***.class文件反编译输出附加信息(包括行号,本地变量表,反汇编等详细信息)

  • synchronized同步代码块:

    实现使用的是monitorenter和monitorexit指令

  • 每一个synchronized一定是一个monitorenter和两个monitorexit么?

    一般来说会是1:2的情况是第一个是程序正常完成第二个是为了保证当出现异常时可以over程序

    但是如果你在程序的结尾throw new RuntimeException的情况下,比例将会是1:1但同时字节码指令中会多出一个 athrow

  • 普通同步方法

    调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置.如果设置了,执行线程会将现持有monitor然后在执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放monitor

  • 静态同步方法

    比普通同步方法多了一个ACC_STATIC标识从而区分是否为静态同步方法

  • 反编译synchronized锁的究竟是什么? -> 什么是monitor?

    1. 为什么任何一个对象都可以成为一个锁?

      -> 所有的对象都继承了Object

      -> 在HotSpot虚拟机中,omitor采用ObjectMonitor实现

      -> \hotspot\agent\src\share\classes\sun\jvm\hotspot\runtimeObjectMonitor.java -> \hotspot\src\share\vm\runtime\ObjectMonitor.cpp -> \hotspot\src\share\vm\runtime\ObjectMonitor.hpp

       ObjectMonitor() {
          _header       = NULL;
          _count        = 0; // 用来记录该线程获取锁的次数
          _waiters      = 0,
          _recursions   = 0; // 锁的重入次数
          _object       = NULL;
          _owner        = NULL; // 指向持有ObjectMonitor对象的线程
          _WaitSet      = NULL; // 存放处于wait状态的线程队列
          _WaitSetLock  = 0 ;
          _Responsible  = NULL ;
          _succ         = NULL ;
          _cxq          = NULL ;
          FreeNext      = NULL ;
          _EntryList    = NULL ; // 存放处于等待锁block状态的线程队列
          _SpinFreq     = 0 ;
          _SpinClock    = 0 ;
          OwnerIsThread = 0 ;
          _previous_owner_tid = 0;
        }
      

      -> 每一个对象都天生带一个对象监视器

4.对于synchronized关键字,我们在<synchronized与锁升级>章节还会在深度讲解

sychronized必须作用于某个对象中,所以java在对象的头文件储存了锁的相关信息.锁升级功能主要依赖于MarkWord中的锁标志位和释放偏向锁标志位,后续笔记锁升级的时候再加深,没有钱了解一下,下图

Hotspot的实现

.\JUC.assets\image-20220427104150939

三. 公平锁 & 非公平锁

1.从ReentrantLock卖票编码演示公平和非公平
  • codeDemo(公平锁)

    public class SaleTicketDemo {
        public static void main(String[] args) {
            Ticket ticket = new Ticket();
    
            new Thread(() -> {
                for (int i = 1; i <= 55; i++) ticket.sale();
            }, "a").start();
            new Thread(() -> {
                for (int i = 1; i <= 55; i++) ticket.sale();
            }, "b").start();
            new Thread(() -> {
                for (int i = 1; i <= 55; i++) ticket.sale();
            }, "c").start();
            new Thread(() -> {
                for (int i = 1; i <= 55; i++) ticket.sale();
            }, "d").start();
            new Thread(() -> {
                for (int i = 1; i <= 55; i++) ticket.sale();
            }, "e").start();
        }
    }
    
    class Ticket {
        private int number = 50;
    
        //默认用的是非公平锁
        private Lock lock = new ReentrantLock();
    
        public void sale() {
            lock.lock();
            try {
                if (number > 0) {
                    System.out.println(Thread.currentThread().getName() + "\t 卖出第: " + (number--) + "\t 还剩下: " + number);
                }
            } finally {
                lock.unlock();
            }
        }
    }
    
    
    // 控制台
    a	 卖出第: 50	 还剩下: 49
    a	 卖出第: 49	 还剩下: 48
    a	 卖出第: 48	 还剩下: 47
    a	 卖出第: 47	 还剩下: 46
    a	 卖出第: 46	 还剩下: 45
    a	 卖出第: 45	 还剩下: 44
    a	 卖出第: 44	 还剩下: 43
    a	 卖出第: 43	 还剩下: 42
    a	 卖出第: 42	 还剩下: 41
    a	 卖出第: 41	 还剩下: 40
    a	 卖出第: 40	 还剩下: 39
    a	 卖出第: 39	 还剩下: 38
    a	 卖出第: 38	 还剩下: 37
    a	 卖出第: 37	 还剩下: 36
    a	 卖出第: 36	 还剩下: 35
    a	 卖出第: 35	 还剩下: 34
    a	 卖出第: 34	 还剩下: 33
    a	 卖出第: 33	 还剩下: 32
    a	 卖出第: 32	 还剩下: 31
    a	 卖出第: 31	 还剩下: 30
    a	 卖出第: 30	 还剩下: 29
    a	 卖出第: 29	 还剩下: 28
    a	 卖出第: 28	 还剩下: 27
    a	 卖出第: 27	 还剩下: 26
    c	 卖出第: 26	 还剩下: 25
    c	 卖出第: 25	 还剩下: 24
    b	 卖出第: 24	 还剩下: 23
    b	 卖出第: 23	 还剩下: 22
    b	 卖出第: 22	 还剩下: 21
    b	 卖出第: 21	 还剩下: 20
    b	 卖出第: 20	 还剩下: 19
    b	 卖出第: 19	 还剩下: 18
    b	 卖出第: 18	 还剩下: 17
    b	 卖出第: 17	 还剩下: 16
    b	 卖出第: 16	 还剩下: 15
    b	 卖出第: 15	 还剩下: 14
    b	 卖出第: 14	 还剩下: 13
    b	 卖出第: 13	 还剩下: 12
    b	 卖出第: 12	 还剩下: 11
    b	 卖出第: 11	 还剩下: 10
    b	 卖出第: 10	 还剩下: 9
    b	 卖出第: 9	 还剩下: 8
    b	 卖出第: 8	 还剩下: 7
    b	 卖出第: 7	 还剩下: 6
    b	 卖出第: 6	 还剩下: 5
    b	 卖出第: 5	 还剩下: 4
    b	 卖出第: 4	 还剩下: 3
    b	 卖出第: 3	 还剩下: 2
    b	 卖出第: 2	 还剩下: 1
    b	 卖出第: 1	 还剩下: 0
    

    非公平锁容易出现锁饥饿现象,有的线程不一定会抢到锁

  • codeDemo(公平锁)

    public class SaleTicketDemo {
        public static void main(String[] args) {
            Ticket ticket = new Ticket();
    
            new Thread(() -> {
                for (int i = 1; i <= 55; i++) ticket.sale();
            }, "a").start();
            new Thread(() -> {
                for (int i = 1; i <= 55; i++) ticket.sale();
            }, "b").start();
            new Thread(() -> {
                for (int i = 1; i <= 55; i++) ticket.sale();
            }, "c").start();
            new Thread(() -> {
                for (int i = 1; i <= 55; i++) ticket.sale();
            }, "d").start();
            new Thread(() -> {
                for (int i = 1; i <= 55; i++) ticket.sale();
            }, "e").start();
        }
    }
    
    class Ticket {
        private int number = 50;
    
        //默认用的是非公平锁,分配的平均一点,=--》公平一点
        private Lock lock = new ReentrantLock(true);
    
        public void sale() {
            lock.lock();
            try {
                if (number > 0) {
                    System.out.println(Thread.currentThread().getName() + "\t 卖出第: " + (number--) + "\t 还剩下: " + number);
                }
            } finally {
                lock.unlock();
            }
        }
    
    }
    
    // 控制台
    a	 卖出第: 50	 还剩下: 49
    a	 卖出第: 49	 还剩下: 48
    a	 卖出第: 48	 还剩下: 47
    a	 卖出第: 47	 还剩下: 46
    a	 卖出第: 46	 还剩下: 45
    a	 卖出第: 45	 还剩下: 44
    a	 卖出第: 44	 还剩下: 43
    a	 卖出第: 43	 还剩下: 42
    a	 卖出第: 42	 还剩下: 41
    a	 卖出第: 41	 还剩下: 40
    a	 卖出第: 40	 还剩下: 39
    a	 卖出第: 39	 还剩下: 38
    a	 卖出第: 38	 还剩下: 37
    a	 卖出第: 37	 还剩下: 36
    a	 卖出第: 36	 还剩下: 35
    b	 卖出第: 35	 还剩下: 34
    a	 卖出第: 34	 还剩下: 33
    b	 卖出第: 33	 还剩下: 32
    a	 卖出第: 32	 还剩下: 31
    b	 卖出第: 31	 还剩下: 30
    a	 卖出第: 30	 还剩下: 29
    b	 卖出第: 29	 还剩下: 28
    a	 卖出第: 28	 还剩下: 27
    b	 卖出第: 27	 还剩下: 26
    c	 卖出第: 26	 还剩下: 25
    a	 卖出第: 25	 还剩下: 24
    b	 卖出第: 24	 还剩下: 23
    c	 卖出第: 23	 还剩下: 22
    a	 卖出第: 22	 还剩下: 21
    b	 卖出第: 21	 还剩下: 20
    c	 卖出第: 20	 还剩下: 19
    a	 卖出第: 19	 还剩下: 18
    d	 卖出第: 18	 还剩下: 17
    b	 卖出第: 17	 还剩下: 16
    c	 卖出第: 16	 还剩下: 15
    a	 卖出第: 15	 还剩下: 14
    d	 卖出第: 14	 还剩下: 13
    b	 卖出第: 13	 还剩下: 12
    c	 卖出第: 12	 还剩下: 11
    a	 卖出第: 11	 还剩下: 10
    d	 卖出第: 10	 还剩下: 9
    e	 卖出第: 9	 还剩下: 8
    b	 卖出第: 8	 还剩下: 7
    c	 卖出第: 7	 还剩下: 6
    a	 卖出第: 6	 还剩下: 5
    d	 卖出第: 5	 还剩下: 4
    e	 卖出第: 4	 还剩下: 3
    b	 卖出第: 3	 还剩下: 2
    c	 卖出第: 2	 还剩下: 1
    a	 卖出第: 1	 还剩下: 0
    

    公平锁相对于非公平锁雨露均沾

2.何为公平锁 | 非公平锁?
  • 生活中,排队讲究先来后到视为公平.程序中的公平性也是符合请求锁的绝对时间的,其实就是FIFO(先入先出队列(First Input First Output, FIFO)这是一种传统的按序执行方法,先进入的指令先完成并引退,跟着才执行第二条指令.),否则视为不公平

  • 源码解读

    按序排队公平锁,就是判断同步队列是否还有先驱节点的存在(我前面还有人么?),如果没有先驱节点才能获取锁;先占先得非公平锁,是不管这个事的,只要能抢获到同步状态就可以
    .\JUC.assets\image-20220427150155166

  • 为什么会有公平锁 | 非公平锁的设计为什么默认非公平锁?

    1. 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的.所以非公平锁能充分的利用CPU的时间片,尽量减少CPU空闲状态时间
    2. 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程再次获取同步状态的概率就变得非常大,所以就减少了线程的开销
  • 使用公平锁会有什么问题?

    公平锁保证了排队的公平性,等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大,非公平锁霸气的忽略了这个规则,所以就有可能导致排队的长时间在排队,也没有机会获取到锁,这就是传说中的"锁饥饿"

  • 什么时候用公平?什么时候用非公平?

    如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;否则就用公平锁,大家公平使用

3.预埋伏AQS
  • code

    public class ReentrantLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = 7373984872572414699L;
        /** Synchronizer providing all implementation mechanics */
        private final Sync sync;
    
        /**
         * Base of synchronization control for this lock. Subclassed
         * into fair and nonfair versions below. Uses AQS state to
         * represent the number of holds on the lock.
         */
        abstract static class Sync extends AbstractQueuedSynchronizer {...}
    
        /**
         * Sync object for non-fair locks
         */
        static final class NonfairSync extends Sync {...}
    
        /**
         * Sync object for fair locks
         */
        static final class FairSync extends Sync {}
    .......
    

    .\JUC.assets\image-20220427154434744

四. 可重入锁(又名递归锁)

1.说明
  • 可重入锁又名递归锁
  • 是指在同一个线程在外面方法获取锁的时候,在进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞
  • 如果是1个有synchronized修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚
  • 所以Java中Reentrantlock和sychronized都是可重入锁,可重入锁的一个有点是可一定程度避免死锁
2."可重入锁"这四个字分开来解释:
  • 可: 可以

  • 重: 再次

  • 入: 进入

    进入什么?

    进入同步域(即同步代码块/方法或显式锁锁定的代码)

  • 锁: 同步锁

  • 一句话: 一个线程中的多个流程可以获取同一把锁,只有这把同步锁可以再次进入,自己可以获取自己的内部锁

3.可重入锁种类
(1).隐式锁(即synchronized关键字使用的锁)默认是可重入锁
  1. 指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫可重入锁

  2. 简单的来说就是: 在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的

    • 同步代码块

       static Object objectLock = new Object();
          public static void main(String[] args)
          {
              new Thread(() -> {
                  synchronized (objectLock) {// lock
                      System.out.println("-----外层");
                      synchronized (objectLock)
                      {
                          System.out.println("-----中层");
                          synchronized (objectLock)
                          {
                              System.out.println("-----内层");
                          }
                      }
                  }//unlock
              },"t1").start();
          }
      
      // 控制台
      -----外层
      -----中层
      -----内层
      

      以上这种递归写法一般来说没有人会这么写东西,只展示语法规定

  3. 与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁

(2).synchronize的重入的实现机理
  • 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程指针
  • 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明他没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1
  • 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么JVM可以将其计数器加1,否则需要等待,直至持有线程释放该锁
  • 当执行monitorexit时,JVM则需将锁对象的计数器减1.计数器为零代表锁已经被释放了
(3).显示锁(Lock)也有ReentrantLock这样的可重入锁
  • lock()和unlock() 1:1的情况下.不会有什么问题

     public static void main(String[] args) {
            Lock lock = new ReentrantLock();
    
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "\t" + "-----外层");
                    lock.lock();
                    try {
                        System.out.println(Thread.currentThread().getName() + "\t" + "-----内层");
                    } finally {
                        lock.unlock();
                    }
                } finally {
                    lock.unlock();
                }
            }, "t1").start();
    
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println("------22222");
                } finally {
                    lock.unlock();
                }
            }, "t2").start();
        }
    
    // 控制台
    -----外层
    -----中层
    -----内层
    
  • 当lock()多于unlock()的情况下如果只有一个线程程序(可能)会正常运行

     public static void main(String[] args) {
            Lock lock = new ReentrantLock();
    
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "\t" + "-----外层");
                    lock.lock();
                    try {
                        System.out.println(Thread.currentThread().getName() + "\t" + "-----内层");
                    } finally {
                    }
                } finally {
                    lock.unlock();
                }
            }, "t1").start();
        }
    
    // 控制台
    t1	-----外层
    t1	-----内层
    
  • 出大问题的情况,当lock()多于unlock()的情况下多个线程: 注意这种情况只是会导致t2一直没有办法获取锁但是不属于死锁的情况

     public static void main(String[] args) {
            Lock lock = new ReentrantLock();
    
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "\t" + "-----外层");
                    lock.lock();
                    try {
                        System.out.println(Thread.currentThread().getName() + "\t" + "-----内层");
                    } finally {
                    }
                } finally {
                    lock.unlock();
                }
            }, "t1").start();
    
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println("------22222");
                } finally {
                    lock.unlock();
                }
            }, "t2").start();
        }
    
    // 控制台
    t1	-----外层
    t1	-----内层
    

    .\JUC.assets\image-20220428173014485

    问题: 程序没有结束,因为t2蚌住了他没有办法获取到锁,因为t1没有释放完全

五. 死锁及排查

1.死锁是什么?(验证死锁是死锁方法)

死锁是指两个或两个以上的线程在执行过程中,引争夺资源而造成的一种相互等待的现象,若无外力干涉那他们都无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能就很低,否则就会因争夺有限的资源而陷入死锁

  • code ->死锁Demo

    public static void main(String[] args) {
    
            Thread a = new Thread(() -> {
                synchronized (lockA) {
                    System.out.println(Thread.currentThread().getName() + "\t" + " 自己持有A锁,期待获得B锁");
    
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                    synchronized (lockB) {
                        System.out.println(Thread.currentThread().getName() + "\t 获得B锁成功");
                    }
                }
            }, "a");
            a.start();
    
            new Thread(() -> {
                synchronized (lockB) {
                    System.out.println(Thread.currentThread().getName() + "\t" + " 自己持有B锁,期待获得A锁");
    
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                    synchronized (lockA) {
                        System.out.println(Thread.currentThread().getName() + "\t 获得A锁成功");
                    }
                }
            }, "b").start();
    
    
        }
    
    // 控制台
    a	 自己持有A锁,期待获得B锁
    b	 自己持有B锁,期待获得A......同时程序没有结束
    
  • 论证是死锁方法一: terminal查询

    ## jps -> Java的ps
    ## 查看所有的正在运行的进程
    [azang]$ jps -l
    21152 com.atguigu.juc.locks.DeadLockDemo
    6592
    2532 org.jetbrains.idea.maven.server.RemoteMavenServer36
    6836 org.jetbrains.jps.cmdline.Launcher
    14728 org.jetbrains.idea.maven.server.RemoteMavenServer36
    556 sun.tools.jps.Jps
    
    ## 打印出对应进程ID的堆栈信息
    [azang]$ jstack 21152
    2022-04-28 17:48:03
    Full thread dump OpenJDK 64-Bit Server VM (17.0.2+8-LTS mixed mode, sharing):
    
    Threads class SMR info:
    _java_thread_list=0x000001cc44be2f50, length=15, elements={
    0x000001cc435de300, 0x000001cc44800080, 0x000001cc44823cc0, 0x000001cc44824670,
    0x000001cc44827030, 0x000001cc448279e0, 0x000001cc44828770, 0x000001cc44829210,
    0x000001cc44832950, 0x000001cc435cb4b0, 0x000001cc44a0b5c0, 0x000001cc44b63d50,
    0x000001cc44c316a0, 0x000001cc44c31ff0, 0x000001cc171e8720
    }
    
    "Reference Handler" #2 daemon prio=10 os_prio=2 cpu=0.00ms elapsed=47.87s tid=0x000001cc
    435de300 nid=0x3af8 waiting on condition  [0x000000851e0ff000]
       java.lang.Thread.State: RUNNABLE
            at java.lang.ref.Reference.waitForReferencePendingList(java.base@17.0.2/Native M
    ethod)
            at java.lang.ref.Reference.processPendingReferences(java.base@17.0.2/Reference.j
    ava:253)
            at java.lang.ref.Reference$ReferenceHandler.run(java.base@17.0.2/Reference.java:
    215)
    
    "Finalizer" #3 daemon prio=8 os_prio=1 cpu=0.00ms elapsed=47.87s tid=0x000001cc44800080
    nid=0x1c80 in Object.wait()  [0x000000851e1fe000]
       java.lang.Thread.State: WAITING (on object monitor)
            at java.lang.Object.wait(java.base@17.0.2/Native Method)
            - waiting on <0x0000000623c0d5c8> (a java.lang.ref.ReferenceQueue$Lock)
            at java.lang.ref.ReferenceQueue.remove(java.base@17.0.2/ReferenceQueue.java:155)
    
            - locked <0x0000000623c0d5c8> (a java.lang.ref.ReferenceQueue$Lock)
            at java.lang.ref.ReferenceQueue.remove(java.base@17.0.2/ReferenceQueue.java:176)
    
            at java.lang.ref.Finalizer$FinalizerThread.run(java.base@17.0.2/Finalizer.java:1
    72)
    
    "Signal Dispatcher" #4 daemon prio=9 os_prio=2 cpu=0.00ms elapsed=47.86s tid=0x000001cc4
    4823cc0 nid=0x2924 waiting on condition  [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "Attach Listener" #5 daemon prio=5 os_prio=2 cpu=15.62ms elapsed=47.86s tid=0x000001cc44
    824670 nid=0x6384 waiting on condition  [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "Service Thread" #6 daemon prio=9 os_prio=0 cpu=0.00ms elapsed=47.86s tid=0x000001cc4482
    7030 nid=0x5708 runnable  [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "Monitor Deflation Thread" #7 daemon prio=9 os_prio=0 cpu=0.00ms elapsed=47.86s tid=0x00
    0001cc448279e0 nid=0x5748 runnable  [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "C2 CompilerThread0" #8 daemon prio=9 os_prio=2 cpu=46.88ms elapsed=47.86s tid=0x000001c
    c44828770 nid=0x49dc waiting on condition  [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
       No compile task
    
    "C1 CompilerThread0" #11 daemon prio=9 os_prio=2 cpu=31.25ms elapsed=47.86s tid=0x000001
    cc44829210 nid=0x4214 waiting on condition  [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
       No compile task
    
    "Sweeper thread" #12 daemon prio=9 os_prio=2 cpu=0.00ms elapsed=47.86s tid=0x000001cc448
    32950 nid=0x5fc4 runnable  [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "Common-Cleaner" #13 daemon prio=8 os_prio=1 cpu=0.00ms elapsed=47.84s tid=0x000001cc435
    cb4b0 nid=0x610c in Object.wait()  [0x000000851e9fe000]
       java.lang.Thread.State: TIMED_WAITING (on object monitor)
            at java.lang.Object.wait(java.base@17.0.2/Native Method)
            - waiting on <0x0000000623d2b3f0> (a java.lang.ref.ReferenceQueue$Lock)
            at java.lang.ref.ReferenceQueue.remove(java.base@17.0.2/ReferenceQueue.java:155)
    
            - locked <0x0000000623d2b3f0> (a java.lang.ref.ReferenceQueue$Lock)
            at jdk.internal.ref.CleanerImpl.run(java.base@17.0.2/CleanerImpl.java:140)
            at java.lang.Thread.run(java.base@17.0.2/Thread.java:833)
            at jdk.internal.misc.InnocuousThread.run(java.base@17.0.2/InnocuousThread.java:1
    62)
    
    "Monitor Ctrl-Break" #14 daemon prio=5 os_prio=0 cpu=0.00ms elapsed=47.80s tid=0x000001c
    c44a0b5c0 nid=0x62a0 runnable  [0x000000851ebfe000]
       java.lang.Thread.State: RUNNABLE
            at sun.nio.ch.SocketDispatcher.read0(java.base@17.0.2/Native Method)
            at sun.nio.ch.SocketDispatcher.read(java.base@17.0.2/SocketDispatcher.java:46)
            at sun.nio.ch.NioSocketImpl.tryRead(java.base@17.0.2/NioSocketImpl.java:261)
            at sun.nio.ch.NioSocketImpl.implRead(java.base@17.0.2/NioSocketImpl.java:312)
            at sun.nio.ch.NioSocketImpl.read(java.base@17.0.2/NioSocketImpl.java:350)
            at sun.nio.ch.NioSocketImpl$1.read(java.base@17.0.2/NioSocketImpl.java:803)
            at java.net.Socket$SocketInputStream.read(java.base@17.0.2/Socket.java:966)
            at sun.nio.cs.StreamDecoder.readBytes(java.base@17.0.2/StreamDecoder.java:270)
            at sun.nio.cs.StreamDecoder.implRead(java.base@17.0.2/StreamDecoder.java:313)
            at sun.nio.cs.StreamDecoder.read(java.base@17.0.2/StreamDecoder.java:188)
            - locked <0x00000006238abbf8> (a java.io.InputStreamReader)
            at java.io.InputStreamReader.read(java.base@17.0.2/InputStreamReader.java:177)
            at java.io.BufferedReader.fill(java.base@17.0.2/BufferedReader.java:162)
            at java.io.BufferedReader.readLine(java.base@17.0.2/BufferedReader.java:329)
            - locked <0x00000006238abbf8> (a java.io.InputStreamReader)
            at java.io.BufferedReader.readLine(java.base@17.0.2/BufferedReader.java:396)
            at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:49)
    
    "Notification Thread" #15 daemon prio=9 os_prio=0 cpu=0.00ms elapsed=47.80s tid=0x000001
    cc44b63d50 nid=0x730 runnable  [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "a" #16 prio=5 os_prio=0 cpu=0.00ms elapsed=47.80s tid=0x000001cc44c316a0 nid=0x34ec wai
    ting for monitor entry  [0x000000851eeff000]
       java.lang.Thread.State: BLOCKED (on object monitor)
            at com.atguigu.juc.locks.DeadLockDemo.lambda$main$0(DeadLockDemo.java:27)
            - waiting to lock <0x000000062384f1f0> (a java.lang.Object)
            - locked <0x000000062384f1e0> (a java.lang.Object)
            at com.atguigu.juc.locks.DeadLockDemo$$Lambda$14/0x0000000800c01200.run(Unknown
    Source)
            at java.lang.Thread.run(java.base@17.0.2/Thread.java:833)
    
    "b" #17 prio=5 os_prio=0 cpu=0.00ms elapsed=47.80s tid=0x000001cc44c31ff0 nid=0x33d0 wai
    ting for monitor entry  [0x000000851efff000]
       java.lang.Thread.State: BLOCKED (on object monitor)
            at com.atguigu.juc.locks.DeadLockDemo.lambda$main$1(DeadLockDemo.java:44)
            - waiting to lock <0x000000062384f1e0> (a java.lang.Object)
            - locked <0x000000062384f1f0> (a java.lang.Object)
            at com.atguigu.juc.locks.DeadLockDemo$$Lambda$15/0x0000000800c01418.run(Unknown
    Source)
            at java.lang.Thread.run(java.base@17.0.2/Thread.java:833)
    
    "DestroyJavaVM" #18 prio=5 os_prio=0 cpu=93.75ms elapsed=47.80s tid=0x000001cc171e8720 n
    id=0x37ec waiting on condition  [0x0000000000000000]
       java.lang.Thread.State: RUNNABLE
    
    "VM Thread" os_prio=2 cpu=0.00ms elapsed=47.87s tid=0x000001cc435da870 nid=0x1ffc runnab
    le
    
    "GC Thread#0" os_prio=2 cpu=0.00ms elapsed=47.88s tid=0x000001cc1508a850 nid=0x24d0 runn
    able
    
    "G1 Main Marker" os_prio=2 cpu=0.00ms elapsed=47.88s tid=0x000001cc1508b570 nid=0xa28 ru
    nnable
    
    "G1 Conc#0" os_prio=2 cpu=0.00ms elapsed=47.88s tid=0x000001cc1508bd90 nid=0x2dd8 runnab
    le
    
    "G1 Refine#0" os_prio=2 cpu=0.00ms elapsed=47.88s tid=0x000001cc434a6d90 nid=0x59e0 runn
    able
    
    "G1 Service" os_prio=2 cpu=0.00ms elapsed=47.88s tid=0x000001cc434a77b0 nid=0x5254 runna
    ble
    
    "VM Periodic Task Thread" os_prio=2 cpu=0.00ms elapsed=47.80s tid=0x000001cc1727e450 nid
    =0x33f0 waiting on condition
    
    JNI global refs: 15, weak refs: 0
    
    ####################################################################################################################
    ## 发现死锁
    ####################################################################################################################
    Found one Java-level deadlock:
    =============================
    "a":
      waiting to lock monitor 0x000001cc44823740 (object 0x000000062384f1f0, a java.lang.Obj
    ect),
      which is held by "b"
    
    "b":
      waiting to lock monitor 0x000001cc44823040 (object 0x000000062384f1e0, a java.lang.Obj
    ect),
      which is held by "a"
    ####################################################################################################################
    ## 问题所在
    ####################################################################################################################
    Java stack information for the threads listed above:
    ===================================================
    "a":
            at com.atguigu.juc.locks.DeadLockDemo.lambda$main$0(DeadLockDemo.java:27)
            - waiting to lock <0x000000062384f1f0> (a java.lang.Object)
            - locked <0x000000062384f1e0> (a java.lang.Object)
            at com.atguigu.juc.locks.DeadLockDemo$$Lambda$14/0x0000000800c01200.run(Unknown
    Source)
            at java.lang.Thread.run(java.base@17.0.2/Thread.java:833)
    "b":
            at com.atguigu.juc.locks.DeadLockDemo.lambda$main$1(DeadLockDemo.java:44)
            - waiting to lock <0x000000062384f1e0> (a java.lang.Object)
            - locked <0x000000062384f1f0> (a java.lang.Object)
            at com.atguigu.juc.locks.DeadLockDemo$$Lambda$15/0x0000000800c01418.run(Unknown
    Source)
            at java.lang.Thread.run(java.base@17.0.2/Thread.java:833)
    
    Found 1 deadlock.
    
  • 论证是死锁方法二: win+r 或者mac 终端打开jconsole

    连接对应想查看的进程

    .\JUC.assets\image-20220428175738079

    等待一会儿

    .\JUC.assets\image-20220428175817219

    进入控制台 -> 点击线程 -> 检查死锁

    .\JUC.assets\image-20220428175905473

    出现死锁

    .\JUC.assets\image-20220428175932843

2.产生死锁的主要原因
  • 系统资源不足
  • 进程运行推进的顺序不合适
  • 资源分配不当

六. 写锁(独占锁) | 读锁(共享锁)

七. 自旋锁SpinLock

八. 无锁 -> 独占锁 -> 读写锁 -> 邮戳锁

1.有无比读写锁更快的锁?

StampedLock(邮戳锁,票据锁)

九. 无锁 -> 偏向锁 -> 轻量锁 -> 重量锁

十. 其他细节

不可以String同一把锁 -> 严禁这么搞事情
因为String有一个不可变的常量池,下方代码定义的不是两把锁 ->享元对象

String lockA = "A";
String lockB = "A";
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

社畜阿藏405

挣点钱不丢人吧?

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值