【JavaEE】多线程(四)

多线程(四)

在开始讲之前,我们先来回顾回顾前三篇所讲过的内容~

  1. 线程的概念

    并发编程,多进程,比较重,频繁创建销毁,开销大

  2. Thread的使用

    1. 创建线程
      1. 继承Thread
      2. 实现Runnable
      3. 继承Thread(匿名内部类)
      4. 实现Runnable(匿名内部类)
      5. 使用lambda'
    2. Thread中的重要性
    3. 启动线程start
    4. 终止线程isInterrupted() interrupt()=>本质上是让线程快点执行完入口方法
    5. 等待线程join a.join()让调用这个方法的线程等待a线程的结束
    6. 获取线程引用
    7. 休眠线程
  3. 线程状态(方便快速判定当前程序执行的情况)

    1. NEW
    2. TERMINATED
    3. RUNNABLE
    4. TIMED_WAITING
    5. WAITING
    6. BLOCKED
  4. 线程安全

    1. 演示线程不安全的例子:两个线程自增5w次

    2. 原因:

      • 操作系统对于线程的调度是随机的
      • 多个线程同时修改同一个量
      • 修改操作不是原子性的
      • 内存可见性
      • 指令重排序
    3. 解决:加锁 => synchronized

      synchronized修饰的是一个代码块

      同时指定一个锁对象

      进入代码块的时候,对该对象进行加锁

      出了代码块的时候,对该对象进行解锁


      锁对象

      • 锁对象到底用哪个对象是无所谓的,对象是谁不重要;重要的是两线程加锁的对象是否是同一个对象

      • 这里的意义/规则,有且只有一个

        当两个线程同时尝试对一个对象加锁,此时就会出现“锁冲突”/“锁竞争”,一旦竞争出现,一个线程能够拿到锁,继续执行代码;一个线程拿不到锁,就只能阻塞等待,等待前一个线程释放锁之后,他才有机会拿到锁,继续执行~

      • 这样的规则,本质上就是把“并发执行” => “串行执行”,这样就不会出现“穿插”的情况了。


文章目录

  • 多线程(四)
    • synchronized 关键字
      • 互斥
      • 刷新内存
      • 可重入
    • 死锁
      • 死锁的成因

synchronized 关键字

互斥

续上文最后,synchronized除了修饰代码块之外,还可以修饰一个实例方法,或者一个静态方法

class Counter{
    public int count;

    synchronized public void increase(){
        count++;
    }

    public void increase2(){
        synchronized (this) {
            count++;
        }
    }
    synchronized public static void increase3(){

    }

    public static void increase4(){
        synchronized (Counter.class){

        }
    }
}
// synchtonized 使用方法
public class Demo14 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

【JavaEE】多线程(四)_第1张图片


synchronized用的锁是存在Java对象头里的。

何为对象头呢?

Java的一个对象,对应的内存空间中,除了你自己定义的一些属性之外,还有一些自带的属性

【JavaEE】多线程(四)_第2张图片

在对象头中,其中就会有属性表示当前对象是否已经加锁了


刷新内存

synchronized的工作过程:

  1. 获得互斥锁

  2. 从主内存拷贝变量的最新副本到工作的内存

  3. 执行代码

  4. 将更改后的共享变量的值刷新到主内存

  5. 释放互斥锁

但是目前刷新内存这一块知识各种说法都有,目前也难以通过实例验证,pass~


可重入

synchronized:重要的特性,可重入的

所谓的可重入锁,指的就是,一个线程连续针对一把锁,加锁两次,不会出现死锁。满足这个需求就是“可重入锁”,反之就是“不可重入锁”。

下面见图:

【JavaEE】多线程(四)_第3张图片

上述的现象,很明显就是一个bug,但是我们在日常开发中,又难以避免出现上述的代码~例如下面这样的案例:

public class Demo15 {
    private static Object locker = new Object();

    public static void func1(){
        synchronized (locker){
            func2();
        }
    }

    public  static void func2(){
        func3();
    }

    public static void func3(){
        func4();
    }

    public static void func4(){
        synchronized (locker){

        }
    }

    public static void main(String[] args) {

    }
}

要解决死锁问题,我们可以将synchronized设计成可重入锁,就可以有效解决上述的死锁问题~

就是让锁记录一下,是哪个线程给它锁住的,后续再加锁的时候,如果加锁线程就是持有锁的线程,就直接加锁成功~

用一个例子来理解:

你向一个哥们表白,我爱你,成功了,他接受你了,也就是你对他加锁成功了,同时他也会记得你就是她的男朋友~

过了几天,你又对他说,宝贝我爱你,这时候的那个哥们当然也不会拒绝,反而会更加基情~

不过要是换成别人,结果肯定就是不一样的(排除绿你的情况~)


这里提出个问题:

synchronized(locker){
  synchronized(locker){
  ........................
  }}
  1. 在上述代码中,synchronized是可重入锁,没有因为第二次加锁而死锁,但是当代码执行到 }②,此时锁是否应该释放?

**不能!!!**因为如果释放了锁,很可能就会导致②和①之间的一些代码逻辑无法执行,也就起不到锁保护代码的作用了~

  1. 进一步,如果上述的锁有n层,释放时机该怎么判断?

无论此处有多少层,都是要在最外层才能释放锁~~
引用计数
锁对象中,不光要记录谁拿到了锁,还要记录,锁被加了几次
每加锁一次,计数器就+1.
每解锁一次,计数器就·1.
出了最后一个大括号,恰好就是减成0了,才真正释放锁


死锁

那么上面我们讲解了死锁的一种情况,一个线程针对一把锁,加锁两次。

接下来下面我们继续介绍死锁的情况~

  1. 一个线程针对一把锁,加锁两次,如果是不可重入锁,就会死锁~

    synchronized不会出现,但是隔壁C++的std::mutex就是不可重入锁,就会出现死锁)

  2. 两个线程(t1、t2),两把锁(A、B)(此时无论是不是不可重入锁,都会死锁)

    举个例子:钥匙锁车里,车钥匙锁家里~

    1. t1获取锁A,t2获取锁B
    2. t1尝试获取B,t2尝试获取A

    实例代码

    // 死锁
    public class Demo16 {
        private static Object locker1 = new Object();
        private static Object locker2 = new Object();
    
    //此处的sleep很重要,要确保 t1 和 t2 都分别拿到一把锁之后,再进行后续动作
        public static void main(String[] args) {
         Thread t1 = new Thread(()->{
             synchronized (locker1){
                 try {
                     Thread.sleep(1000);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
    
                 synchronized (locker2){
                     System.out.println("t1 加锁成功");
                 }
             }
    
         });
         Thread t2 = new Thread(()->{
             synchronized (locker2){
                 try {
                     Thread.sleep(1000);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 synchronized (locker1){
                     System.out.println("t2 加锁成功");
                 }
             }
         });
         t1.start();
         t2.start();
        }
    }
    

    【JavaEE】多线程(四)_第4张图片

    死锁现象出现

    我们可以在jconsole.exe中看看线程情况~

    【JavaEE】多线程(四)_第5张图片

    同时也要注意,死锁代码中
    两个synchronized嵌套关系,不是并列关系.
    嵌套关系说明:是在占用一把锁的前提下,获取另一把锁.(则是可能出现死锁)
    并列关系,则是先释放前面的锁,再获取下一把锁.(不会死锁的)

// 死锁 -> 破嵌套
public class Demo17 {
    private static Object locker1 = new Object();
    private static Object locker2 = new Object();

    //此处的sleep很重要,要确保 t1 和 t2 都分别拿到一把锁之后,再进行后续动作
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            synchronized (locker1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
                synchronized (locker2){
                    System.out.println("t1 加锁成功");
                }
        });
        Thread t2 = new Thread(()->{
            synchronized (locker2) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
                synchronized (locker1){
                    System.out.println("t2 加锁成功");
                }

        });
        t1.start();
        t2.start();
    }
}
// 破死锁
public class Demo18 {
    private static Object locker1 = new Object();
    private static Object locker2 = new Object();

    //此处的sleep很重要,要确保 t1 和 t2 都分别拿到一把锁之后,再进行后续动作
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            synchronized (locker1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (locker2){
                System.out.println("t1 加锁成功");
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (locker1) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (locker2){
                System.out.println("t2 加锁成功");
            }

        });
        t1.start();
        t2.start();
    }
}

第一段代码中使用的是同一个对象作为锁,在t1和t2线程中都使用了locker对象作为锁。这样的话,当t1线程获取到锁并休眠时,t2线程就无法获取到锁,导致t2线程一直等待,从而可能引发死锁。

第二段代码中使用了两个不同的对象作为锁,分别是locker1locker2。在t1线程中先获取locker1锁,再获取locker2锁;在t2线程中先获取locker2锁,再获取locker1锁。这样的话,两个线程在互斥的同时也保持了顺序,避免了死锁的发生。

  1. N个线程,M把锁(相当于2的扩充)

    此时这个情况,更加容易出现死锁了。

    下面给出一个经典例子:哲学家就餐问题

    【JavaEE】多线程(四)_第6张图片

    死锁,是属于比较严重的bug,会直接导致线程卡住,也就无法执行后续的工作了~

    那么我们应该怎么避免死锁?

死锁的成因

那么首先我们要了解死锁的成因:

  1. 互斥使用。(锁的基本特性)

    当线程持有一把锁之后,另一个线程也想获取到锁,那么就需要阻塞等待、

  2. 不可抢占。(锁的基本特性)

    当锁已经被 线程 1 拿到之后,线程 2 只能等 线程 1 主动释放,不可以强行抢过来

  3. 请求保持。(代码结构)

    一个线程尝试获取多把锁。(先拿到 锁1 之后,再尝试获取 锁2 ,获取的时候, 锁1 不会被释放)

    这种也就是典型的吃着碗里的,看着锅里的

    public class Demo16 {
        private static Object locker1 = new Object();
        private static Object locker2 = new Object();
    
    //此处的sleep很重要,要确保 t1 和 t2 都分别拿到一把锁之后,再进行后续动作
        public static void main(String[] args) {
         Thread t1 = new Thread(()->{
             synchronized (locker1){
                 try {
                     Thread.sleep(1000);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
    
                 synchronized (locker2){
                     System.out.println("t1 加锁成功");
                 }
             }
    
         });
         Thread t2 = new Thread(()->{
             synchronized (locker2){
                 try {
                     Thread.sleep(1000);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 synchronized (locker1){
                     System.out.println("t2 加锁成功");
                 }
             }
         });
         t1.start();
         t2.start();
        }
    }
    
  4. 循环等待 / 环路等待(代码结构)

    等待的依赖关系,形成环了~

    也即是上面那个例子,钥匙锁车里,车钥匙锁家里

实际上,要想出现死锁,也不是个容易事情
因为得把上面4条都占了.
(不幸的是,1和2是锁本身的特性,只要代码中,把3和4占了,死锁就容易出现了)

所以说,解决死锁,核心就是破坏上述必要条件,死锁就形成不了~

针对上述的四种成因,1 2是破坏不了的,因为synchronized自带特性,我们是无法干预 滴~

对于3来说,就是调整代码结构,避免编写“锁嵌套”逻辑

对于4来说,可以约定加锁的顺序,就可以避免循环等待


所以针对上面的哲学家就餐问题,我们可以采取:针对锁进行编号

比如说约定,加多一把锁的时候,先加编号小的锁,后加编号大的锁(所有线程都要遵守这个规则)

【JavaEE】多线程(四)_第7张图片

这样的话,循环等待就会被解除,死锁也不会出现了~

回到上述我们讲的synchronized关键字
在使用规则上,并不复杂,只要抓住一个原则:两个线程针对同一个对象加锁,就会产生锁竞争.
但是在底层原理上,synchronized还有不少值得讨论的地方.接下来会展开讲讲~


至此,多线程(四)讲解到这,接下来会持续更新,敬请期待~

你可能感兴趣的:(JavaEE,java-ee,java,jvm)