Java并发编程实战之互斥锁

news2024/11/19 15:27:57

文章目录

    • Java并发编程实战之互斥锁
      • 如何解决原子性问题?
      • 锁模型
      • Java synchronized 关键字
      • Java synchronized 关键字 只能解决原子性问题?
      • 如何正确使用Java synchronized 关键字?
      • 锁和受保护资源的合理关联关系
      • 死锁
      • 预防死锁
        • 破坏占有且等待条件
        • 破坏不可抢占条件
        • 破坏循环等待条件
        • 总结
      • 用 synchronized 实现等待 - 通知机制
        • 尽量使用 notifyAll()
        • wait() 方法和 sleep() 方法
      • 安全性、活跃性以及性能问题
    • 参考

Java并发编程实战之互斥锁

之前在《Java并发编程实战基础概要》中提到了原子性这一个概念,一个或者多个操作在 CPU 执行的过程中不被中断的特性,称为“原子性”。 那么原子性问题到底改如何解决呢?

如何解决原子性问题?

“原子性”的本质是什么?其实不是不可分割,不可分割只是外在表现,其本质是多个资源间有一致性的要求,操作的中间状态对外不可见。所以本质来说,解决原子性问题,是要保证中间状态对外不可见。(这一点需要细细品一品)

原子性问题的源头是线程切换,多个线程同时操作同一个变量。这样就会出现线程冲突的问题。所以需要一种机制保持在多核CPU下,同一个时刻只有一个线程更改某个共享变量。(其实没有共享变量,也不会存在并发问题),所以说如果我们能够保证对共享变量的修改是互斥的,那么就能保证原子性了。

锁模型

一谈到互斥,我们很自然就会想到了锁。首先我们把一段需要互斥执行的代码称为临界区。线程在进入临界区之前,首先尝试加锁 lock(),如果成功,则进入临界区,此时我们称这个线程持有锁;否则呢就等待,直到持有锁的线程解锁;持有锁的线程执行完临界区的代码后,执行解锁 unlock()

这个过程非常像办公室里高峰期抢占坑位,每个人都是进坑锁门(加锁),出坑开门(解锁),如厕这个事就是临界区。

上面的例子虽然挺形象的,但是容易忽略锁的两个很重要的点,分别是我们锁的是什么?和我们想要保护的又是什么?

  • 第一个问题,锁的到底是什么?我们单纯锁的是门吗?这么理解也没有错,但是想一想,实际上我们想要锁的是对这个厕所的使用,因为你不可能锁上这个厕所的门,去上另一个厕所,这样没任何意义。所以锁跟你想保护的东西是有一个对应关系的。所以对应到编程世界中,锁的其实是对共享变量的访问。
  • 第二个问题,我们想要保护的是什么?我们保护的其实就是我们即将要使用的这个厕所。对应到编程世界中,保护的就是共享变量。

所以对应编程世界中,锁和资源是有一个对应关系的,所以锁的模型如下:

在这里插入图片描述

Java synchronized 关键字

锁是一种通用的技术方案,Java 语言提供的 synchronized 关键字,就是锁的一种实现。synchronized 关键字可以用来修饰方法,也可以用来修饰代码块


class X {
  // 修饰非静态方法
  synchronized void foo() {
    // 临界区
  }
  // 修饰静态方法
  synchronized static void bar() {
    // 临界区
  }
  // 修饰代码块
  Object obj = new Object()void baz() {
    synchronized(obj) {
      // 临界区
    }
  }
}  

我们可以通过synchronized关键字对我们的临界区进行加锁和解锁。但是我们从代码中并没有看到这个加锁和解锁的动作,这是因为这些操作是由Java编译器为我们加上的。

我们可以利用javap命令来查看生成的字节码文件,就可以看出来Java编译器会为我们synchronized 修饰的方法或代码块前后自动加上加锁 lock() 和解锁 unlock()

下面通过javap命令查看下面代码生成的字节码文件

public void method() {
     synchronized (this) {
         System.out.println("start");
     }
 }

字节码文件如下:

在这里插入图片描述

图中的monitorenter对应的就是加锁,而monitorexit对应的就是解锁

至于为什么会有两个monitorexit指令呢?

是因为对于synchronized关键字而言,javac在编译时,会生成对应的monitorentermonitorexit指令分别对应synchronized同步块的进入和退出,有两个monitorexit指令的原因是:为了保证抛异常的情况下也能释放锁,所以javac为同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit命令释放锁。

参考: 《Java锁synchronized关键字学习系列之重量级锁》

synchronized锁的是代码块还是锁的是对象?从上面我们可以总结出synchronized锁的其实是对象

synchronized 里的加锁 lock() 和解锁 unlock() 锁定的对象是什么?

下面的代码我们看到只有 synchronized修饰代码块的时候,锁定了一个obj 对象,那 synchronized修饰方法的时候锁定的是什么呢?


class X {
  // 修饰非静态方法
  synchronized void foo() {
    // 临界区
  }
  // 修饰静态方法
  synchronized static void bar() {
    // 临界区
  }
  // 修饰代码块
  Object obj = new Object()void baz() {
    synchronized(obj) {
      // 临界区
    }
  }
}  
  • 当修饰静态方法的时候,锁定的是当前类的 Class 对象,在上面的例子中就是 Class X
class X {
  // 修饰静态方法
  synchronized(X.class) static void bar() {
    // 临界区
  }
}
  • 当修饰非静态方法的时候,锁定的是当前实例对象 this
class X {
  // 修饰非静态方法
  synchronized(this) void foo() {
    // 临界区
  }
}

到这里我们引申出另一个问题,当我们锁住了对象的时候,对象身上发生了什么变化,jvm如何知道这个对象被“锁“住了,关于这个题外话这里不多赘述,可以参考:《Java锁synchronized关键字学习系列之CAS和对象头》

Java synchronized 关键字 只能解决原子性问题?

并发会产生三大问题

  1. 原子性问题
  2. 可见性问题
  3. 有序性问题

前面我们一直在说,锁可以解决原子性问题,Java synchronized 关键字只能解决原子性问题吗?

答案肯定是否定的,前面在《Java并发编程实战基础概要》 说到了Java内存模型规范了Java虚拟机(JVM)如何提供按需禁用缓存和编译优化的方法。这些方法包括:volatile、synchronized和final关键字,以及Java内存模型中的Happens-Before规则

在这里插入图片描述

所以synchronized关键字还可以解决可见性问题(可以参考Happens-Before的锁定规则:对一个锁的解锁操作 Happens-Before于后续对这个锁的加锁操作)。但是以synchronized关键字不能完全解决有序性问题,因为synchronized关键字不能避免指令重排,所以我们在之前《Java并发编程实战基础概要》的双重检验的单例模式中,必须加volatile来避免因为发生指令重排,返回错误实例。

如何正确使用Java synchronized 关键字?

正确使用Java synchronized 关键字主要是关注在synchronized锁定的对象跟受保护资源的关系。如何理解呢?举一个例子:

class SafeCalc {
  static long value = 0L;
  synchronized long get() {
    return value;
  }
  synchronized static void addOne() {
    value += 1;
  }
}

从上面代码我们可以看出来synchronized 关键字锁定的是两个不同的对象,在之前我们讲过,synchronized 关键字修饰非静态方法的时候,锁定的是当前实例对象 this。而当修饰静态方法的时候,锁定的是当前类的 Class 对象。所以我们现在相当于用两个锁保护一个资源(一个共享变量value)。

在这里插入图片描述

从上图可以看出来,由于get() 方法和addOne()方法是两把不同的锁,说明执行addOne()方法的过程中可以执行get() 方法,并发性不能得到保证,所以这两临界区并不是互斥的,临界区 addOne() 对 value 的修改对临界区 get() 也没有可见性保证,这就导致并发问题了。

再举一个例子,下面这个例子是否正确使用synchronized 关键字

class SafeCalc {
  long value = 0L;
  long get() {
    synchronized (new Object()) {
      return value;
    }
  }
  void addOne() {
    synchronized (new Object()) {
      value += 1;
    }
  }
}

答案很明显是错误的使用,synchronized 关键字锁定的new object每次在内存中都是新对象,所以每次锁的都不是同一个对象,怎么做到互斥呢?

所以要真正使用好互斥锁,必须深入分析锁定的对象和受保护资源的关系。

锁和受保护资源的合理关联关系

直接给出结论:受保护资源和锁之间合理的关联关系应该是 N:1 的关系,也就是说可以用一把锁来保护多个资源,但是不能用多把锁来保护一个资源。

我们来举一个用一把锁来保护多个资源的例子。

class Account {
  // 账户余额  
  private Integer balance;
  // 账户密码
  private String password;

  // 取款
  synchronized void withdraw(Integer amt) {
    if (this.balance > amt){
        this.balance -= amt;
      }
  } 
  // 查看余额
  synchronized Integer getBalance() {
     return balance;
  }

  // 更改密码
  synchronized void updatePassword(String pw){
    this.password = pw;
  } 
  // 查看密码
  synchronized String getPassword() {
     return password;
  }
}

从上面代码可以看出来,我们是使用当前实例this来管理Account类中所有的资源。所以会导致取款、查看余额、修改密码、查看密码这四个操作都是串行的,所以不会有并发问题。但是却会产生另一个问题,就是性能太差了。

我们可以稍微修改一下,使用两把锁,让取款和修改密码是可以并行的,因为这两个行为互不干扰。

class Account {
  // 锁:保护账户余额
  private final Object balLock
    = new Object();
  // 账户余额  
  private Integer balance;
  // 锁:保护账户密码
  private final Object pwLock
    = new Object();
  // 账户密码
  private String password;

  // 取款
  void withdraw(Integer amt) {
    synchronized(balLock) {
      if (this.balance > amt){
        this.balance -= amt;
      }
    }
  } 
  // 查看余额
  Integer getBalance() {
    synchronized(balLock) {
      return balance;
    }
  }

  // 更改密码
  void updatePassword(String pw){
    synchronized(pwLock) {
      this.password = pw;
    }
  } 
  // 查看密码
  String getPassword() {
    synchronized(pwLock) {
      return password;
    }
  }
}

用不同的锁对受保护资源进行精细化管理,能够提升性能,这种锁还有个名字,叫细粒度锁。所以我们上锁的时候需要考虑锁的粒度。

所以我们在上锁的时候,应该分析多个资源的关系。如果资源之间没有关系,每个资源一把锁就可以了。如果资源之间有关联关系,就要选择一个粒度更大的锁,这个锁应该能够覆盖所有相关的资源。

死锁

前面说到使用细粒度锁可以提高并行度,是性能优化的一个重要手段。但是使用细粒度锁是有代价的,这个代价就是可能会导致死锁。

死锁的一个比较专业的定义是:一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。

下面举一个死锁的例子:

public class T {
    private Object o1 = new Object();
    private Object o2 = new Object();

    public void m1() {
        synchronized (o1) {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            synchronized (o2) {
                System.out.println("如果出现这句话表示没有死锁");
            }
        }

    }

    public void m2() {
        synchronized(o2) {

            synchronized (o1) {
                System.out.println("如果出现这句话表示没有死锁");
            }

        }

    }
    public static void main(String[] args) {
        T t=new T();
        new Thread(t::m1).start();
        new Thread(t::m2).start();
    }
}

上面这个例子死锁是怎么发生的呢?假设当线程1持有锁对象o1,然后当线程2持有锁对象o2的时候;然后线程1需要对对象o2加锁,但是因为线程2已经对对象o2加锁了,所以线程1需要等待线程2解除锁占用。然后线程2同样需要对对象o1加锁,但是因为线程1已经对对象o1加锁了,所以线程2同样要等待线程1解除锁占用。所以现在就出现了线程1和线程2互相在等待对方解除锁占用,于是就出现了死锁。

预防死锁

那我们如何去预防死锁呢?

那如何避免死锁呢?要避免死锁就需要分析死锁发生的条件,只有以下这四个条件都发生时才会出现死锁:

  • 互斥:一个资源每次只能被一个进程(或者线程)使用。进程(或者线程)对所分配到的资源不允许其他进程(或者线程)进行访问,若其他进程(或者线程)访问该资源,只能等待,直至占有该资源的进程(或者线程)使用完成后释放该资源
  • 占有且等待:进程(或者线程)获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程(或者线程)占有,此时请求阻塞,但又对自己获得的资源保持不放
  • 不可抢占:是指进程(或者线程)已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
  • 循环等待:线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待

所以我们想要避免死锁,其实只要破坏掉上面其中一个条件即可。但是第一个条件互斥是没办法破坏的,因为我们用锁的初衷就是为了互斥,所以我们需要从其他三个条件下手。

破坏占有且等待条件

要破坏这个条件,可以一次性申请所有资源。上面的例子一次性申请所有的资源,就相当于一次性加锁了o1和o2对象,解锁的时候也是一次性解锁了o1和o2对象,所以上面的例子可以改成下面这种方式

public class T {
    private Object o1 = new Object();

    public void m1() {
        synchronized (o1) {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("如果出现这句话表示没有死锁");
        }

    }

    public void m2() {
        synchronized (o1) {
                System.out.println("如果出现这句话表示没有死锁");
            }
    }
    public static void main(String[] args) {
        T t=new T();
        new Thread(t::m1).start();
        new Thread(t::m2).start();
    }
}

上面这种方式直接使用了一个锁,这种肯定是不会有死锁的。

或者我们还是锁定两个不同的对象,我们还可以这么改造

class M {

  private List<Object> list = new ArrayList<>();

  public synchronized boolean lock(Object o1, Object o2) {
    if (list.contains(o1) || list.contains(o2)) {
      return false;
    } else {
      list.add(o1);
      list.add(o2);
    }
    return true;
  }

  public synchronized void unlock(Object o1, Object o2) {
    list.remove(o1);
    list.remove(o2);
  }
}

class T {

  private Object o1 = new Object();
  private Object o2 = new Object();
  private M m = new M();

  public void m1() {
    while (!m.lock(o1, o2)) {

    }
    try {
      synchronized (o1) {
        try {
          Thread.sleep(10000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }

        synchronized (o2) {
          System.out.println("如果出现这句话表示没有死锁");
        }
      }
    } finally {
      m.unlock(o1, o2);
    }

  }

  public void m2() {
    while (!m.lock(o1, o2)) {

    }
    try {
      synchronized (o2) {
        synchronized (o1) {
          System.out.println("如果出现这句话表示没有死锁");
        }

      }
    } finally {
      m.unlock(o1, o2);
    }

  }

  public static void main(String[] args) {
    T t = new T();
    new Thread(t::m1).start();
    new Thread(t::m2).start();
  }
}

从上面代码可以看出来,我们抽取了一个类M来同时申请多个资源,从而破坏了占有且等待条件

破坏不可抢占条件

破坏不可抢占条件看上去很简单,核心是要能够主动释放它占有的资源,这一点 synchronized 是做不到的。Java 在语言层次确实没有解决这个问题,但是java.util.concurrent 这个包下面提供的 Lock类中的tryLock(long, TimeUnit) 方法,可以帮我们在一段时间尝试获取锁,所以可以轻松解决这个问题的

破坏循环等待条件

破坏这个条件,需要对资源进行排序,然后按序申请资源,这样就不会出现两个线程交错加锁的情况。上面的情况就是因为我们申请资源其实不是顺序的,也就是加锁不是顺序的,T1加锁的是o1然后o2,T2加锁的是o2然后o1。 如果T1和T2都是加锁o1然后o2,其实就不会有这种问题。

class T {
  private Object o1 = new Object();
  private Object o2 = new Object();

  public void m1() {
    synchronized (o1) {
      try {
        Thread.sleep(10000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }

      synchronized (o2) {
        System.out.println("如果出现这句话表示没有死锁");
      }
    }

  }

  public void m2() {
    synchronized(o1) {

      synchronized (o2) {
        System.out.println("如果出现这句话表示没有死锁");
      }

    }

  }
  public static void main(String[] args) {
    T t=new T();
    new Thread(t::m1).start();
    new Thread(t::m2).start();
  }
}

总结

但实际上开发过程中的案例肯定不会像我们举例的这么简单,具体问题具体分析,但是我们还是需要从这三个条件出发,去破坏掉我们这三个条件,才能够避免死锁的问题。

用 synchronized 实现等待 - 通知机制

前面我们在讲死锁的破坏占用且等待条件的时候,使用了一个死循环的方式来循环等待

while (!m.lock(o1, o2)) {

}

这种方案,在并发冲突大的场景(也就是可能很久都获取不到锁)不适用,因为这种场景下可能要循环上万次才能获取到锁,太消耗 CPU 了。

那有没有更好的方案呢?那就是使用"等待 - 通知机制"。怎么理解"等待 - 通知机制"呢?你可以类比于医院排队叫号。如果没有排队叫号系统,每个人都需要去问医生是不是轮到我了。而有了排队叫号,病人只需要等着医生把你叫过来,病人和医生是不是都省心省力了。

而在编程世界中,一个完整的“等待 - 通知机制”是这样的:线程首先获取互斥锁,当线程要求的条件不满足时,释放互斥锁,进入等待状态;当要求的条件满足时,通知等待的线程,重新获取互斥锁。

那在Java的世界中如何实现的“等待 - 通知机制”? Java 语言内置的 synchronized 配合 wait()notify()notifyAll() 这三个方法就能轻松实现。

我们看方法名称就可以知道,wait()顾名思义就是让线程等待,而、notify()notifyAll()就是唤醒线程。

那么wait()的实现机制是怎样的?

在这里插入图片描述

在并发程序中,当一个线程进入临界区后,由于某些条件不满足,需要进入等待状态,Java 对象的 wait() 方法就能够满足这种需求。如上图所示,当调用 wait() 方法后,当前线程就会被阻塞,并且进入到右边的等待队列中,这个等待队列也是互斥锁的等待队列。(这个等待队列和互斥锁是一对一的关系,每个互斥锁都有自己独立的等待队列) 线程在进入等待队列的同时,会释放持有的互斥锁,线程释放锁后,其他线程就有机会获得锁,并进入临界区了。

那线程要求的条件满足时,该怎么通知这个等待的线程呢?很简单,就是 Java 对象的 notify()notifyAll() 方法。当条件满足时调用 notify(),会通知等待队列(互斥锁的等待队列)中的线程,告诉它条件曾经满足过(因为notify() 只能保证在通知时间点,条件是满足的)。

在这里插入图片描述

wait()notify()notifyAll() 方法操作的等待队列是互斥锁的等待队列,所以如果 synchronized 锁定的是 this,那么对应的一定是 this.wait()this.notify()this.notifyAll();所以一定是要用锁定的对象去调用这三个方法。

这三个方法能够被调用的前提是已经获取了相应的互斥锁,所以我们会发现 wait()notify()notifyAll() 都是在 synchronized{}内部被调用的。如果在 synchronized{}外部调用,或者锁定的 this,而用 target.wait() 调用的话,JVM 会抛出一个运行时异常:java.lang.IllegalMonitorStateException

尽量使用 notifyAll()

notify() 是会随机地通知等待队列中的一个线程,而 notifyAll() 会通知等待队列中的所有线程。所以实际上使用 notify() 是可能存在风险的,它的风险在于可能导致某些线程永远不会被通知到。

可以参考《Java多线程高并发编程代码笔记(二)》中使用wait和notifyAll方法来实现的例子。

wait() 方法和 sleep() 方法

wait() 方法和 sleep() 方法都能让当前线程挂起一段时间,那它们的区别是什么?

主要的区别在于wait会释放所有锁而sleep不会释放锁资源,而且wait只能在同步方法和同步块中使用,而sleep任何地方都可以。

安全性、活跃性以及性能问题

编写并发程序的初衷是为了提升性能,但在追求性能的同时由于多线程操作共享资源而出现了安全性问题(并发出现原子性问题、可见性问题和有序性问题),当然并不是只要是多线程都会有安全性问题,而是有多线程操作共享资源,也就是共享会发生变化的数据,我们也叫数据竞争,在这种情况下才会有安全性问题。

为了解决安全性问题,我们开始用到了锁技术,一旦用到了锁技术就会出现了死锁,还有两种情况,分别是“活锁”和“饥饿”活跃性问题。

活锁:有时线程虽然没有发生阻塞,但仍然会存在执行不下去的情况,这就是所谓的“活锁。
可以类比现实世界里的例子,路人甲从左手边出门,路人乙从右手边进门,两人为了不相撞,互相谦让,路人甲让路走右手边,路人乙也让路走左手边,结果是两人又相撞了。
解决“活锁”的方案很简单,谦让时,尝试等待一个随机的时间就可以了。例如上面的那个例子,路人甲走左手边发现前面有人,并不是立刻换到右手边,而是等待一个随机的时间后,再换到右手边;同样,路人乙也不是立刻切换路线,也是等待一个随机的时间再切换。由于路人甲和路人乙等待的时间是随机的,所以同时相撞后再次相撞的概率就很低了。“等待一个随机时间”的方案虽然很简单,却非常有效,Raft 这样知名的分布式一致性算法中也用到了它。

参考来源:极客时间 《并发编程实战》—— 安全性、活跃性以及性能问题

饥饿: 指的是线程因无法访问所需资源而无法执行下去的情况。
如果线程优先级“不均”,在 CPU 繁忙的情况下,优先级低的线程得到执行的机会很小,就可能发生线程“饥饿”;持有锁的线程,如果执行的时间过长,也可能导致“饥饿”问题。
解决“饥饿”问题的方案很简单,有三种方案:一是保证资源充足,二是公平地分配资源,三就是避免持有锁的线程长时间执行。这三个方案中,方案一和方案三的适用场景比较有限,因为很多场景下,资源的稀缺性是没办法解决的,持有锁的线程执行的时间也很难缩短。倒是方案二的适用场景相对来说更多一些。
那如何公平地分配资源呢?在并发编程里,主要是使用公平锁。所谓公平锁,是一种先来后到的方案,线程的等待是有顺序的,排在等待队列前面的线程会优先获得资源。
那如何公平地分配资源呢?在并发编程里,主要是使用公平锁。所谓公平锁,是一种先来后到的方案,线程的等待是有顺序的,排在等待队列前面的线程会优先获得资源。

参考来源:极客时间 《并发编程实战》—— 安全性、活跃性以及性能问题

但是如果我们不恰当的使用锁,会导致了串行百分比的增加,由此又产生了性能问题。

那我们如何解决性能问题呢?

  1. 既然使用锁会带来性能问题,那最好的方案自然就是使用无锁的算法和数据结构了。在这方面有很多相关的技术,例如线程本地存储 (Thread Local Storage, TLS)、写入时复制 (Copy-on-write)、乐观锁等;Java 并发包里面的原子类也是一种无锁的数据结构;Disruptor 则是一个无锁的内存队列,性能都非常好
  2. 减少锁持有的时间。互斥锁本质上是将并行的程序串行化,所以要增加并行度,一定要减少持有锁的时间。这个方案具体的实现技术也有很多,例如使用细粒度的锁,一个典型的例子就是 Java 并发包里的 ConcurrentHashMap,它使用了所谓分段锁的技术。还可以使用读写锁,也就是读是无锁的,只有写的时候才会互斥。

参考来源:极客时间 《并发编程实战》—— 安全性、活跃性以及性能问题

性能方面的度量指标有很多,有三个指标非常重要,就是:吞吐量、延迟和并发量。

  • 吞吐量:指的是单位时间内能处理的请求数量。吞吐量越高,说明性能越好。
  • 延迟:指的是从发出请求到收到响应的时间。延迟越小,说明性能越好。
  • 并发量:指的是能同时处理的请求数量,一般来说随着并发量的增加、延迟也会增加。所以延迟这个指标,一般都会是基于并发量来说的。例如并发量是 1000 的时候,延迟是 50 毫秒。

参考来源:极客时间 《并发编程实战》—— 安全性、活跃性以及性能问题

参考

《Java并发编程实战基础概要》

Java多线程高并发编程代码笔记(一)

极客时间 《并发编程实战》—— 互斥锁(上):解决原子性问题

极客时间 《并发编程实战》—— 互斥锁(下):如何用一把锁保护多个资源?

极客时间 《并发编程实战》—— 互斥锁(下):如何用一把锁保护多个资源?

极客时间 《并发编程实战》—— 一不小心就死锁了,怎么办?

极客时间 《并发编程实战》—— 用“等待-通知”机制优化循环等待

极客时间 《并发编程实战》—— 安全性、活跃性以及性能问题

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/605.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

字节一面:TCP 三次握手,问的好细!

大家好&#xff0c;我是小林。 有位读者在面试字节时&#xff0c;被问到这么个问题&#xff1a; 概括起来&#xff0c;是这两个问题&#xff1a; TCP 三次握手中&#xff0c;客户端收到的第二次握手中 ack 确认号不是自己期望的&#xff0c;会发生什么&#xff1f;是直接丢弃…

1024程序员节:从关注自身健康开始

今天是1024程序员节&#xff0c;我们已经历经了尽三年的疫情&#xff0c;健康是我们最应该关注的事情&#xff0c;在这个特别的日子里&#xff0c;希望程序员们都能更加爱惜自己的身体&#xff0c;少加班&#xff0c;多锻炼。 健身不仅是保持健康体魄的关键要素之一&#xff0c…

基于ssm高校科研管理系统-计算机毕业设计源码+LW文档

【摘 要】高校科研管理是一项重要而又繁琐的工作&#xff0c;有效的信息管理平台可以大大缓解科研管理压力&#xff0c;减少工作量。本文以石河子大学信息科学与技术学院为应用背景&#xff0c;开发教师教学信息与论文信息交流平台。该系统能对科研成果和课题进行较为全面的管理…

第十三届蓝桥杯C++B组国赛I题——齿轮 (AC)

目录1.齿轮1.题目描述2.输入格式3.输出格式4.样例输入5.样例输出6.样例说明6.数据范围7.原题链接2.解题思路3.Ac_code1.齿轮 1.题目描述 这天, 小明在组装齿轮。 他一共有 nnn 个齿轮, 第 iii 个齿轮的半径为 rir_{i}ri​, 他需要把这 nnn 个齿轮按一定 顺序从左到右组装起来…

[附源码]Java计算机毕业设计SSM公司办公自动化系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

10个实用的CSS样式之悬浮卡片

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位喜欢写作&#xff0c;计科专业大三菜鸟 &#x1f3e1;个人主页&#xff1a;starry陆离 &#x1f4da;订阅专栏&#xff1a;『10个实用的CSS样式』 10个实用的CSS样式之悬浮卡片1.简介2.布局设计3.样式美化3.1body美化3.2c…

隔离技术之端口隔离

端口隔离技术 端口隔离主要应用在同一个vlan内&#xff0c;不同的用户之间不可互相访问 好处&#xff1a; 可以避免广播风暴&#xff0c;节约了vlan的资源&#xff0c;提高了用户之间的安全性 比如一个用户的电脑中病毒了&#xff0c;不会影响到其他用户 端口隔离是基于端口&…

网络原理——No.2 传输层_TCP的连接管理(画图理解三次握手与四次挥手)

JavaEE传送门JavaEE 网络原理——传输层_UDP 网络原理——No.1 传输层_TCP的确认应答机制与超时重传 目录TCP的连接管理三次握手(建立连接)四次挥手(断开连接)TCP的连接管理 描述的就是 TCP 建立链接和断开链接的过程 TCP 的链接, 只是一个 “逻辑上的” “虚拟的连接” (只要…

qt学习笔记4:QMainWindow 菜单栏、工具栏、状态栏、铆接部件、

在创建基类的时候&#xff0c;有三大选择&#xff0c;一个是QWidge 空窗口&#xff0c; 另一个就是QMainWindow QMainWindow是一个为用户提供主窗口的类&#xff0c;包含一个菜单栏&#xff0c;多个工具栏&#xff0c;多个链接部件&#xff0c; 一个状态栏以及一个中心部件&…

《数据结构》(六)八大排序(上)

生活中大家从小到大处处可见排队&#xff0c;但是排队有哪些快速的方法你了解吗&#xff1f; 八大排序排序的基本概念插入排序直接插入排序基本思想代码直接插入排序总结希尔排序基本思想代码希尔排序总结选择排序直接选择排序基本思想&#xff1a;代码直接选择排序总结堆排序堆…

大数据基础之java常用API一

常用API1. Object类2. String类String案例1. Object类 构造方法空参构造全参构造 Object类: 是所有类的基类,或者说公共父类,每个类都直接或者间接的继承自Object,所以该类中有的方法,其他类中都有 构造方法: public Object(); 所有类的构造方法中都会默认调用super() 会逐级调…

C#里在子窗口与父窗口之间进行数据传送

在C#里经常需要在子窗口与父窗口之间进行数据传送,或者调用,虽然有很多方法可以实现,但是采用委托还是比较简单和直接的方式。 所以这次针对委托来演示一下怎么样实现这种功能。 下面先来创建一个带两窗口的例子,如下图所示: 接着来看一下,创建父窗口的代码: namespace…

【C++笔试强训】第十一天

&#x1f387;C笔试强训 博客主页&#xff1a;一起去看日落吗分享博主的C刷题日常&#xff0c;大家一起学习博主的能力有限&#xff0c;出现错误希望大家不吝赐教分享给大家一句我很喜欢的话&#xff1a;夜色难免微凉&#xff0c;前方必有曙光 &#x1f31e;。 &#x1f4a6; &…

vite基础知识-1

为什么选择vite&#xff1f; 讲vite之前&#xff0c;我们先来了解一下webpack的原理。 webpack支持多种模块化&#xff08;浏览器端和服务端都可以运行&#xff09;。比如&#xff1a; // index.js const lodash require("lodash"); // commonjs规范 import React…

win10 docker desktop 报 docker desktop stopped

win10电脑安装doker deskto遇到一些问题解决过程记录 报 docker desktop stopped 没过多会, docker desktop就自动退出了, 要以理解为闪退 网上查了一下原因, 虚拟化可能没设置 进入bios, 发现笔记本电脑没有这个设置 重启电脑后, 弹出消息 WSL 2 installation is incompl…

通道分离与合并、彩色图转换为灰度图、二值化

文章目录图像基础重要的函数图像基本知识图像基础通道分离与合并彩色图转换为灰度图二值化图像的加减乘除图像基础 矩阵分辨率8位整型图像浮点数图像 现在简单介绍下二值化、灰度图以及真彩色和假彩色 图像的二值化&#xff0c;就是将图像上的像素点的灰度值设置为0或255&am…

安装宝塔面板(详细教程)

目录 安装宝塔面板 &#xff08;一&#xff09;前言 &#xff08;二&#xff09;准备工作 1、官网&#xff1a;宝塔面板下载&#xff0c;免费全能的服务器运维软件 &#xff08;三&#xff09;安装宝塔面板 1、进入官网&#xff0c;选择“安装脚本” 2、选择对应版本的安…

C++游戏开发小笔记

1.入门小语法 1.1 命名空间 当想使用库文件的某个函数时&#xff0c;为了防止由于重名而引起的混乱调用&#xff0c;使用命名空间来区分同名函数。 字符串String也是标准命名空间的一个。如果没有using namespace std; 想用string 类型得 std:: string 1.2浮点数的存储…

计算机视觉--flask部署 目标检测算法,并在局域网内远端访问

1.flask框架 Flask是一个轻量级的基于Python的web框架。static 文件夹来保存静态文件&#xff0c;templates 文件夹存放前端页面 安装&#xff1a; pip install Flask框架代码&#xff1a; from flask import * from flask import Flaskapp Flask(__name__) //获取实例app.…

Web监听器:Listener

Listener简介常用监听接口监听在线用户信息的实现Model层Controller层OnlineUserListener的实现View层测试简介 监听器&#xff08;Listener&#xff09;&#xff0c;是一个实现特定接口的普通Java程序&#xff0c;用于监听Web应用中的对象或信息发生改变时&#xff0c;作出相应…