第四章Java锁—基础

乐观锁和悲观锁

悲观锁

  • 悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
  • 悲观锁:每次去读取数据都会发生冲突(与其他线程读数据),每次在进行数据读写都会上锁(互斥),保证同一时间段只有一个线程只有一个线程在读写数据

悲观锁的实现方式

  • synchronized关键字

  • Lock的实现类都是悲观锁

  • 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁

适合写操作多的场景,先加锁可以保证写操作时数据正确。显示的锁定之后再操作同步资源。

  • 当线程冲突严重的时,就需要加锁,来避免线程频繁访问共享数据失败带来的CPU空转问题
public class OptimismLock {
    
    
    // 保证多个线程使用的是同一个lock对象的前提下
    ReentrantLock lock = new ReentrantLock();
    //悲观锁加锁方式 synchronized
    public synchronized void test(){
    
    

    }
    //Lock的实现类都是悲观锁
    public void test2(){
    
    
        lock.lock();
        try {
    
    
            // 操作同步资源
        }finally {
    
    
            lock.unlock();
        }

    }
}

乐观锁

  • 乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作
  • 乐观锁:每次读写数据都认为不会发生冲突,线程不会阻塞,一般来说。只有进行数据更新时才会检查是否发生冲突,若没有冲突,直接更新,只有多个线程都在更新数据,才会解决冲突问题(若线程冲突不严重的时候,可以采用乐观锁策略来避免多次的加锁解锁操作)

乐观锁的实现方式

  • 版本号机制Version。(只要有人提交了就会修改版本号,可以解决ABA问题)

    • ABA问题:再CAS中想读取一个值A,想把值A变为C,不能保证读取时的A就是赋值时的A,中间可能有个线程将A变为B再变为A。

      • 解决方法:Juc包提供了一个AtomicStampedReference,原子更新带有版本号的引用类型,通过控制版本值的变化来解决ABA问题。
  • 最常采用的是CAS算法

    • 在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
  • 数据库提供的类似于 write_condition 机制

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

//=============乐观锁的调用方式
// 保证多个线程使用的是同一个AtomicInteger
private AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();

版本号实现的大致流程

image-20230609104344480

image-20230609104727720

  • 乐观锁并不是真正把线程阻塞了,乐观锁的实现一般都是采用版本进制来实现
  • 核心是线程是否能够成功刷新主内存的值,当工作内存的版本号==主存的版本号才能更新成功,同步刷新自己的版本号和主内存的版本号,表示此时更新成功
  • 一般锁都是实现乐观锁和悲观锁并用的策略,synchronized最开始就是乐观锁,当竞争激烈再升级为悲观锁

如果写入失败的处理方法

  • 就从主存中读取最新的版本号到工作内存,然后尝试再最新的数据上进行操作,若最后写入成功,那么主存和工作内存的版本号都+1(CAS策略,不断重试写回,直到成功为止)
  • 直接报错,线程2退出,不写回

img

8锁案例弄清synchronized锁了什么

synchronized定义:就是监视器锁,monitor lock(对象锁),锁的是资源

  • 阿里巴巴代码规范
    • 【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
    • 说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。

1 标准访问有ab两个线程,请问先打印邮件还是短信?

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");
    }
}
public class LockSynchronized {
    
    
    public static void main(String[] args) {
    
    
        Phone phone = new Phone();
        new Thread(()->{
    
    
            phone.sendEmail();
        },"a").start();

        //暂停毫秒,保证a线程先启动
        try {
    
    TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {
    
    e.printStackTrace();}

        new Thread(()->{
    
    
            phone.sendSMS();
        },"b").start();
    }
}
----------sendEmail
-------------sendSMS
  • 先输出邮件,因为线程a先进入了,进入后,上锁了,只有线程a执行完毕,线程b才能进行执行

2 a里面故意停3秒?先输出什么

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 class LockSynchronized {
    
    
    public static void main(String[] args) {
    
    
        Phone phone = new Phone();
        new Thread(()->{
    
    
            phone.sendEmail();
        },"a").start();

        //暂停毫秒,保证a线程先启动
        try {
    
    TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {
    
    e.printStackTrace();}

        new Thread(()->{
    
    
            phone.sendSMS();
        },"b").start();
    }
}
//过了三秒输出
----------sendEmail
-------------sendSMS
  • 一个对象里面如果有多个synchronized方法,某一时刻内,只要一个线程去调用其中的一个synchronized方法了,其他的线程都只能是等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这个对象中的这些synchronized方法,**锁的是当前对象this,**被锁定后,其它的线程都不能 进入到当前对象的其他synchronized方法

3 添加一个普通的hello方法,请问先打印邮件还是hello?

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");
    }
}
public class LockSynchronized {
    
    
    public static void main(String[] args) {
    
    
        Phone phone = new Phone();
        new Thread(()->{
    
    
            phone.sendEmail();
        },"a").start();

        //暂停毫秒,保证a线程先启动
        try {
    
    TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {
    
    e.printStackTrace();}

        new Thread(()->{
    
    
            phone.hello();
        },"b").start();
    }
}
-------------hello
----------sendEmail
  • hello并未和其他synchronized修饰的方法产生争抢

4 有两部手机,请问先打印邮件(这里有个3秒延迟)还是短信?

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");
    }
}
public class LockSynchronized {
    
    
    public static void main(String[] args) {
    
    
        Phone phone = new Phone();
        Phone phone1 = new Phone();
        new Thread(()->{
    
    
            phone.sendEmail();
        },"a").start();

        //暂停毫秒,保证a线程先启动
        try {
    
    TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {
    
    e.printStackTrace();}

        new Thread(()->{
    
    
            phone1.sendSMS();
        },"b").start();
    }
}
-------------sendSMS
----------sendEmail
  • 锁在两个不同的对象/两个不同的资源上,不产生竞争条件

5.有两个静态同步方法(synchroized前加static,3秒延迟也在),有1部手机,先打印邮件还是短信?

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");
    }
    public void hello(){
    
    
        System.out.println("-------------hello");
    }
}
public class LockSynchronized {
    
    
    public static void main(String[] args) {
    
    
        Phone phone = new Phone();
        Phone phone1 = new Phone();
        new Thread(()->{
    
    
            phone.sendEmail();
        },"a").start();

        //暂停毫秒,保证a线程先启动
        try {
    
    TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {
    
    e.printStackTrace();}

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

6.两个手机,有两个静态同步方法(synchroized前加static,3秒延迟也在),先打印邮件还是短信?

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");
    }
    public void hello(){
    
    
        System.out.println("-------------hello");
    }
}
public class LockSynchronized {
    
    
    public static void main(String[] args) {
    
    
        Phone phone = new Phone();
        Phone phone1 = new Phone();
        new Thread(()->{
    
    
            phone.sendEmail();
        },"a").start();

        //暂停毫秒,保证a线程先启动
        try {
    
    TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {
    
    e.printStackTrace();}

        new Thread(()->{
    
    
            phone1.sendSMS();
        },"b").start();
    }
}
----------sendEmail
-------------sendSMS
  • 5.6中 static+synchronized 对应的锁加到了类对象中

    • phone = new Phone();中 加到了左边的Phone上
  • 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁→实例对象本身。

  • 对于静态同步方法,锁的是当前类的Class对象,如Phone,class唯一的一个模板。

  • 对于同步方法块,锁的是synchronized括号内的对象。synchronized(o)

7 一个静态同步方法,一个普通同步方法,请问先打印邮件还是手机?

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");
    }
    public void hello(){
    
    
        System.out.println("-------------hello");
    }
}
public class LockSynchronized {
    
    
    public static void main(String[] args) {
    
    
        Phone phone = new Phone();
        new Thread(()->{
    
    
            phone.sendEmail();
        },"a").start();

        //暂停毫秒,保证a线程先启动
        try {
    
    TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {
    
    e.printStackTrace();}

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

8 两个手机,一个静态同步方法,一个普通同步方法,请问先打印邮件还是手机?

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");
    }
    public void hello(){
    
    
        System.out.println("-------------hello");
    }
}
public class LockSynchronized {
    
    
    public static void main(String[] args) {
    
    
        Phone phone = new Phone();
        Phone phone1 = new Phone();
        new Thread(()->{
    
    
            phone.sendEmail();
        },"a").start();

        //暂停毫秒,保证a线程先启动
        try {
    
    TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {
    
    e.printStackTrace();}

        new Thread(()->{
    
    
            phone1.sendSMS();
        },"b").start();
    }
}
-------------sendSMS
----------sendEmail
  • 7.8中一个加了对象锁,一个加了类锁,不产生竞争条件

3个体现

  • 8种锁的案例实际体现在3个地方-相当于总结
    • 作用域实例方法,当前实例加锁,进入同步代码块前要获得当前实例的锁。
    • 作用于代码块,对括号里配置的对象加锁。
    • 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁

img

同步方法和同步块,哪个是更好的选择

  • 据阿里巴巴代码规范知道同步块是更好的选择
  • 同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。
  • 同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁

字节码角度分析synchronized实现

文件反编译技巧

  • 文件反编译javap -c ***.class文件反编译,-c表示对代码进行反汇编
  • 假如需要更多信息 javap -v ***.class ,-v即-verbose输出附加信息(包括行号、本地变量表、反汇编等详细信息)

synchronized同步代码块

public class SynchronizedTest {
    Object object = new Object();

    public void m1(){
        synchronized (object){
            System.out.println("-----hello synchronized code block");
        }
    }

    public static void main(String[] args) {

    }
}
Compiled from "SynchronizedTest.java"
public class com.lsc.day03.SynchronizedTest {
    
    
  java.lang.Object object;

  public com.lsc.day03.SynchronizedTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: new           #2                  // class java/lang/Object
       8: dup
       9: invokespecial #1                  // Method java/lang/Object."<init>":()V
      12: putfield      #3                  // Field object:Ljava/lang/Object;
      15: return

  public void m1();
    Code:
       0: aload_0
       1: getfield      #3                  // Field object:Ljava/lang/Object;
       4: dup
       5: astore_1
       6: monitorenter
       7: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      10: ldc           #5                  // String -----hello synchronized code block
      12: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      15: aload_1
      16: monitorexit
      17: goto          25
      20: astore_2
      21: aload_1
      22: monitorexit
      23: aload_2
      24: athrow
      25: return
    Exception table:
       from    to  target type
           7    17    20   any
          20    23    20   any

  public static void main(java.lang.String[]);
    Code:
       0: return
}

  • synchronized同步代码块,实现使用的是moniterenter和moniterexit指令(moniterexit可能有两个)

  • 那一定是一个enter两个exit吗?(不一样,如果主动throw一个RuntimeException,发现一个enter,一个exit,还有两个athrow)

synchronized普通同步方法

/**
 * 锁普通的同步方法
 */
public class LockSyncDemo {
    
    

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

    public static void main(String[] args) {
    
    

    }
}

.....
public synchronized void m2();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED //请注意该标志
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String ------hello synchronized m2
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 11: 0
        line 12: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/zhang/admin/controller/LockSyncDemo;
......

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

synchronized静态同步方法

/**
 * 锁静态同步方法
 */
public class LockSyncDemo {

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

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


    public static void main(String[] args) {

    }
}


 ......
 public static synchronized void m3();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED //访问标志 区分该方法是否是静态同步方法
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #5                  // String ------hello synchronized m3---static
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 15: 0
        line 16: 8
......

  • 访问标志区分该方法是否是静态同步方法

反编译synchronized锁的是什么——Monitor

我们这里的说synchronized锁的是monitor,是不考虑其锁升级流程,只考虑重量级锁这种情况

Monitor 被翻译为监视器管程

  • 管程:Monitor(监视器),也就是我们平时说的锁。监视器锁

  • 信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。 管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。

  • 执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管理。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。

  • Java虚拟机给每个对象和class字节码都设置了一个监听器Monitor,用于检测并发代码的重入,同时在Object类中还提供了notify和wait方法来对线程进行控制。

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

  • Java Object 类是所有类的父类,也就是说 Java 的所有类都继承了 Object,子类可以使用 Object 的所有方法。

  • ObjectMonitor.java→ObjectMonitor.cpp→objectMonitor.hpp

ObjectMonitor.cpp中引入了头文件(include)objectMonitor.hpp

//140行
  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;
  }
  • 追溯底层可以发现每个对象天生都带着一个对象监视器。

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针

image-20230620221331489

  • 刚开始 Monitor 中 Owner 为 null
  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner
  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲wait-notify 时会分析

注意:

synchronized 必须是进入同一个对象的 monitor 才有上述的效果
不加 synchronized 的对象不会关联监视器,不遵从以上规则

Monitor是JVM底层实现,底层代码是c++,本质是依赖于底层操作系统的Mutex Lock实现

提前熟悉锁升级

synchronized必须作用于某个对象中,所以Java在对象的头文件存储了锁的相关信息。锁升级功能主要依赖于 MarkWord 中的锁标志位和释放偏向锁标志位

Mark Word

  • 用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。

  • Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit,因为对象头中要存储的数据已经超过了64bit的限制,考虑到了了虚拟机的空间效率,所以Mark Word被设计成动态定义的数据结构,以便在极小的内存空间存储尽量多的数据,根据对象的状态来复用我们的存储空间

在32位JVM中是这么存储的

img

在64位JVM中是这么存的

img

公平锁和非公平锁

ReentrantLock抢票案例

class Ticket{
    
    
    private int num=30;
    ReentrantLock lock =new ReentrantLock();
   //ReentrantLock lock = new ReentrantLock(true);
    public void sale(){
    
    
        lock.lock();
        try {
    
    
            if(num>0){
    
    
                System.out.println(Thread.currentThread().getName()+"卖出第:\t"+(num--)+"\t 还剩下:"+num);
            }
        }catch (Exception e){
    
    
            e.printStackTrace();
        }finally {
    
    
            lock.unlock();
        }
    }
}
public class FairnessLock {
    
    
    public static void main(String[] args) {
    
    
        Ticket ticket = new Ticket();
        new Thread(()->{
    
     for (int i = 0; i < 35; i++) {
    
     ticket.sale(); } },"t1").start();
        new Thread(()->{
    
     for (int i = 0; i < 35; i++) {
    
     ticket.sale(); } },"t2").start();
        new Thread(()->{
    
     for (int i = 0; i < 35; i++) {
    
     ticket.sale(); } },"t3").start();
    }
}

非公平锁

  • 非公平锁:获取失败的线程进入阻塞队列,当锁被释放,阻塞队列的所有线程都有可能获得到锁(不一定是等待时间最长的线程获得)

  • synchronized,ReentrantLock默认是非公平锁

  • 对应到买票案例——非公平锁可以插队,买卖票不均匀。

    • 是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或饥饿的状态(某个线程一直得不到锁)

公平锁

  • 公平锁:获取失败的线程进入阻塞队列,当锁被释放,第一个进入阻塞队列的线程首先获得到锁(等待时间最长的线程获得锁)

  • ReentrantLock lock = new ReentrantLock(true);对应公平锁

  • 对应到买票案例——买卖票一开始t1占优,后面a b c a b c a b c均匀分布

    • 是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的。

为什么会有公平锁/非公平锁的设计?为什么默认是非公平?

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

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

  • 公平锁和非公平锁没有好坏之分, 关键还是看适用场景.
  • 如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;否则那就用公平锁,大家公平使用。

操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是非公平锁. 如果要想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序.

可重入锁 vs 不可重入锁

可重入锁说明

  • 可重入锁又名递归锁

    • 可重入的意思就是获取的对象锁的线程可以再次加锁
  • 是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

  • 如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。

  • Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括 synchronized关键字锁都是可重入的。

    • 而 Linux 系统提供的 mutex 是不可重入锁
  • 可重入锁的一个优点是可一定程度避免死锁。

  • 可重入锁又称为递归锁

    • 比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)

可重入锁 详细解释

  • 可:可以

  • 重:再次

  • 入:进入

  • 锁:同步锁

image-20230609173543421

进入什么:进入同步域(即同步代码块/方法或显示锁锁定的代码)

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

可重入锁种类

隐式锁Synchronized

  • synchronized是Java中的关键字,默认可重入锁,即隐式锁

在同步块中

public class ReentrantLockTest {
    
    
    public static void main(String[] args) {
    
    
        final Object objectLockA=new Object();
        new Thread(()->{
    
    
            synchronized (objectLockA){
    
    
                System.out.println("-----外层调用");
                synchronized (objectLockA){
    
    
                    System.out.println("-------中层调用");
                    synchronized (objectLockA){
    
    
                        System.out.println("------内层调用");
                    }
                }
            }
        },"a").start();
    }
}
//-----外层调用
//-----中层调用
//-----内层调用

在同步方法中

public class ReentrantLockTest {
    
    
    public static void main(String[] args) {
    
    
        ReentrantLockTest lockTest=new ReentrantLockTest();
        lockTest.m1();
    }
    public synchronized void m1() {
    
    
        //指的是可重复可递归调用的锁,在外层使用之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁
        System.out.println(Thread.currentThread().getName()+"\t"+"-----come in m1");
        m2();
        System.out.println(Thread.currentThread().getName()+"\t-----end m1");
    }

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

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

}

Synchronized的重入实现机理

  • 回看上方的ObjectMoitor.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;
  }

  • ObjectMoitor.hpp底层:每个锁对象(Monitor)拥有一个锁计数器和一个指向持有该锁的线程的指针。分别是_count 和 _owner

  • 首次加锁:当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。

  • 重入:在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。

  • 释放锁:当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

显式锁Lock

  • 显式锁(即Lock)也有ReentrantLock这样的可重入锁

感觉所谓的显式隐式即是指显示/隐式的调用锁

我们synchronized就不需要我们自己来加锁和解锁,进入对应方法体就加锁,执行完就解锁了

public class ReentrantLockTest {
    static Lock lock=new ReentrantLock();
    public static void main(String[] args) {
       new Thread(() -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t----come in 外层调用");
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "\t------come in 内层调用");
                } finally {
                    lock.unlock();
                }
            } finally {
                lock.unlock();
            }
        }, "t1").start();
    }
}
t1	----come in 外层调用
t1	------come in 内层调用
  • 注意:lock unlock成对
    • 假如lock unlock不成对,单线程情况下问题不大,但多线程下出问题
public class ReentrantLockTest {
    
    
    static Lock lock=new ReentrantLock();
    public static void main(String[] args) {
    
    
       new Thread(() -> {
    
    
            lock.lock();
            try {
    
    
                System.out.println(Thread.currentThread().getName() + "\t----come in 外层调用");
                lock.lock();
                try {
    
    
                    System.out.println(Thread.currentThread().getName() + "\t------come in 内层调用");
                } finally {
    
    
                    lock.unlock();
                }
            } finally {
    
    
//                lock.unlock();
                //不成对
            }
        }, "t1").start();
        new Thread(() -> {
    
    
            lock.lock();
            try
            {
    
    
                System.out.println("t2 ----外层调用lock");
            }finally {
    
    
                lock.unlock();
            }
        },"t2").start();

    }
}
t1	----come in 外层调用
t1	------come in 内层调用   
  • 由于没有成对,也就是加锁了,但是没有对应的解锁,我们发现我们的t2线程并不能进入临界区

死锁及排查

死锁是什么

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

死锁产生的原因

  • 系统资源不足

  • 进程运行推进的顺序不合适

  • 资源分配不当

死锁代码case

public class DeadLock {
    
    
    public static void main(String[] args) {
    
    
        Object object1 = new Object();
        Object object2 = new Object();
        new Thread(()->{
    
    
            synchronized (object1){
    
    
                System.out.println(Thread.currentThread().getName()+"我现在占用了object1资源,我还想要object2资源");
                try {
    
     TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {
    
    e.printStackTrace();}//使得线程b也启动
                synchronized (object2){
    
    
                    System.out.println(Thread.currentThread().getName()+"我现在占用了object1和object2资源");
                }
            }
        },"t1").start();
        new Thread(()->{
    
    
            synchronized (object2){
    
    
                System.out.println(Thread.currentThread().getName()+"我现在占用了object2资源,我还想要object1资源");
                try {
    
     TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {
    
    e.printStackTrace();}//使得线程b也启动
                synchronized (object1 ){
    
    
                    System.out.println(Thread.currentThread().getName()+"我现在占用了object1和object2资源");
                }
            }
        },"t2").start();
    }
}
t1我现在占用了object1资源,我还想要object2资源
t2我现在占用了object2资源,我还想要object1资源
  • 因为第t1占用着object1资源,然后想要object2资源,t2占着object2资源,想要object1资源,陷入死循环,这就是死锁

哲学家进餐问题

img

class Chopstick {
    
    
    String name;
    public Chopstick(String name) {
    
    
        this.name = name;
    }
    @Override
    public String toString() {
    
    
        return "筷子{" + name + '}';
    }
}
class Philosopher extends Thread {
    
    
    Chopstick left;
    Chopstick right;
    public Philosopher(String name, Chopstick left, Chopstick right) {
    
    
        super(name);
        this.left = left;
        this.right = right;
    }
    private void eat()  {
    
    
        System.out.println(Thread.currentThread().getName()+"eating");
        try {
    
    
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
    
    
        while (true){
    
    
            synchronized (left){
    
    
                //先获得左手筷子
                synchronized (right){
    
    
                    //获得右手筷子
                    eat();
                }
                //放下右手筷子
            }
            //放下左手筷子
        }
    }
}
public class DiningPhilosophers {
    
    
    public static void main(String[] args) {
    
    
        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");
        new Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里士多德", c3, c4).start();
        new Philosopher("赫拉克利特", c4, c5).start();
        new Philosopher("阿基米德", c5, c1).start();
    }
}
苏格拉底eating
亚里士多德eating
苏格拉底eating
苏格拉底eating
苏格拉底eating
发生死锁程序没有停止

理论产生死锁的四个必要条件

  1. 互斥条件,线程程对所分配到的资源是排他性使用的,在一段时间内,某资源只能被一个线程占用
  2. 请求和保持条件 进程已经占用了一个资源,但又提出了新的资源请求,但是被请求的资源已经被其他线程占用,此时请求线程被阻塞,但是又不会去释放自己已经拥有的资源(吃着自己碗里的,还看着别人的)
  3. 不可抢占条件 线程已经获得的资源在未使用完之前不能被其他进程所抢占,只能自己完成任务后释放
  4. 循环等待条件 就如上面的哲学家进餐问题一样,发生死锁时,必定会存在一个“进程-资源循环链” 但是存在死锁必定存在循环等待链,但是存在循环等待链不一定存在死锁,可能同类的资源不止一个

只要破坏掉其中一个条件就可以解决死锁,最容易破坏的条件就是循环等待

破坏循环等待

  • 最常用的一种死锁阻止技术就是锁排序. 假设有 N 个线程尝试获取 M 把锁, 就可以针对 M 把锁进行编号(1, 2, 3…M).N 个线程尝试获取锁的时候, 都按照固定的按编号由小到大顺序来获取锁…它必须按照确定的顺序获取它们。它不能获取序列后面的锁,除非它获得了前面的锁。这样就可以避免循环等待

    • 例如,t2线程不能在获得object1之前,就去获得object2` 1
public class DiningPhilosophers {
    
    
    public static void main(String[] args) {
    
    
        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");
        new Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里士多德", c3, c4).start();
        new Philosopher("赫拉克利特", c4, c5).start();
        new Philosopher("阿基米德", c1, c5).start();
    }
}
  • 这种写法就不会发生死锁问题,阿基米德获取锁从5,1变成1,5,这样哲学家获取锁的顺序都是必须获取编号小的,才能获取编号大的锁

如何排查死锁

纯命令

  • jps -l 查看当前进程运行状况
  • jstack 进程编号 查看该进程信息

图形化

  • win + r 输入jconsole ,打开图形化工具,打开线程 ,点击 检测死锁

小总结-重要

  • 指针指向monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个monitor与之关联,当一个monitor被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp,C++实现的)

image-20230609211755869

  • 我们这里将synchronized直接认为是重量级锁,在以前的JDK是这样的,但是后面JDK对synchronized进行优化,引入了对应的锁优化,而对于synchronized我们在后面进行讲解

以下相当于一些前置知识,为后面的章节做铺垫

  • 写锁(独占锁)/读锁(共享锁)

  • 自旋锁SpinLock

  • 无锁-独占锁-读写锁-邮戳锁

  • 无锁-偏向锁-轻量锁-重量锁

猜你喜欢

转载自blog.csdn.net/qq_50985215/article/details/131178868