0
点赞
收藏
分享

微信扫一扫

(四)PostgreSQL的psql命令

小编 04-14 11:00 阅读 0

目录

Day 7:多线程(5)

回顾synchronized

  • synchronized带有(),填写锁对象,锁对象存在的意义,只是起到“身份标识”效果
  • 两个线程是否是针对同一个对象加锁,如果是,就可能产生阻塞/锁竞争/锁冲突
  • synchronized{},进入代码块,就相当于加锁操作,出了代码块,就相当于解锁操作
  • 修饰普通方法,相当于针对this加锁,修饰静态方法,相当于针对类对象加锁

1. 死锁

package thread;

class Counter2 {
    private int count = 0;

    void add() {
        synchronized (this) {
            count++;
        }
    }

    int get() {
        return count;
    }
}

public class Demo21 {
    public static void main(String[] args) throws InterruptedException {
        Counter2 counter2 = new Counter2();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter2.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (counter2) {
                    counter2.add();
                }
            }
        });
        t2.start();
        t1.start();
        
        t2.join();
        t1.join();
        
        System.out.println("count = " + counter2.get());
    }
}

上述线程t2的代码相当于

Thread t2 = new Thread(() -> {
    for (int i = 0; i < 50000; i++) {
        synchronized (counter2) {
            synchronized (counter2){
                 count++;
            }
        }
    }
});
  • 要想获得到第二层的锁,就要执行完第一层的代码块
  • 要想执行完第一层代码块,就需要先获取到第二层的锁

这种情况下,就叫“死锁”

但是在实际上述过程中,对于synchronized是不适用的,synchronized上述代码是不会出现死锁的,但是如果是C++/Pyhton的锁就会出现死锁

  • synchronized在内部进行了特殊处理(JVM)
  • 每个锁对象里,会记录当前是哪个线程持有了这个锁,当针对这个对象加锁操作时,就会先判定一下,当前尝试加锁的线程,是否是持有同一锁的线程,如果不是,就阻塞,如果是,直接放行
  • 这种机制称为**“可重入锁”**,目的是为了避免程序员粗心大意,搞出死锁

注意:当加了多层锁的时候,代码执行到哪里要真正进行解锁呢

2. 死锁场景

死锁有三种比较典型的场景

(1)场景一:锁是不可重入锁,并且一个线程针对一个锁对象,连续加锁两次,通过引入可重入锁,可以解决上述问题

(2)场景二:两个线程,两把锁

(3)场景三:N个线程,M把锁

3. 场景二:两个线程,两把锁

有线程1和线程2,以及锁A和锁B,现在线程1和2都需要获取到锁A和锁B(拿到锁A之后,不释放A,继续获取锁B),即先让两个线程分别拿到一把锁,然后去尝试获取对方的锁

举个例子:健康码崩了,程序员回到公司修复bug,被保安拦住了

  • 保安:出示健康码,才能进公司
  • 程序员:我得进公司修复bug,才能出示健康码

类似于:家钥匙锁车里了,车钥匙锁家里了

package thread;

public class Demo22 {
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();

        Thread t1 = new Thread(() ->{
            synchronized (locker1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized (locker2){
                    System.out.println("t1 获取了两把锁");
                }
            }
        });

        Thread t2 = new Thread(()->{
            synchronized (locker2){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized (locker1){
                    System.out.println("t2 获取了两把锁");
                }
            }
        });

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

上述代码:

  • t1尝试针对locker2加锁,就会阻塞等待,等待t2释放locker2
  • t2尝试针对locker1加锁,也会阻塞等待,等待t1释放locker1

在这里插入图片描述

当遇到死锁问题,可以通过上述调用栈+状态进行定位

4. 场景三:N个线程,M把锁

随着线程数目/锁的个数增加,此时情况就更复杂了,更容易出现死锁

哲学家就餐问题

如果出现下列极端情况,就相当于死锁了

  • 同一时刻,所有的哲学家都拿起左边的筷子,那么此时所有的哲学家都无法拿起右手的筷子
  • 假如哲学家都是比较固执的人,不能拿起两双筷子,就绝对不会放下手里的筷子

上述就是非常典型的死锁情况

死锁是非常严重的问题:死锁会使线程被卡住,没办法继续工作了,而且死锁这种bug,往往都是概率性出现

5. 避免死锁问题

死锁的四个必要条件

  • 锁具有互斥特性:这个是锁的基本特性,一个线程拿到锁之后,其他线程就得阻塞等待
  • 锁不可抢占(不可被剥夺):锁的基本特点,一个线程拿到锁之后,除非自己主动释放锁,否则别人抢不走
  • 请求和保持:属于代码结构层面,一个线程拿到一把锁之后,不释放这个锁的前提下,再尝试获取其他锁
  • 循环等待:属于代码结构层面,多个线程获取多个锁的过程中,出现了循环等待,A等待B,B又等待A

必要条件缺一不可,任何一个死锁的场景,都必须同时具备上述四点

6. 内存可见性问题

package thread;

import java.util.Scanner;

public class Demo23 {

    private static int count = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while (count==0){

            }
            System.out.println("t1执行结束");
        });

        Thread t2 =new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            count = scanner.nextInt();
        });

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

上述代码,当t2线程读到一个不为0的整数的时候,预期t1就会结束循环,但是结果并非如此

举报

相关推荐

0 条评论