多线程(七)

一、synchornized

1. 可重入性

public static void main(String[] args) {
        Object locker = new Object();
        Thread t = new Thread(()->{
            synchronized (locker){
                synchronized (locker){
                    System.out.println("hello");
                }
            }
        });
        t.start();
    }

以上代码中,我们可以看到,我们在synchronized中又使用了一次.

在之前多线程的学习中,我们可以知道,使用synchronized进行加锁时," { "  代表加锁  ->   " } "代表解锁.

那么,本次代码中,第一个synchronized进行加锁,但是并没有解锁,第二个synchronized为什么可以加锁并执行其中的内容呢?

这是因为,这两次加锁是在同一个线程中的.

此时锁的对象,就知道了第二次加锁的线程,就是持有锁的线程,第二次操作就可以直接通过,而不会阻塞.  这个特性,就称为 " 可重入 ".

这个过程中,进入第一个synchronized,是真正的加锁,同时把计数器+1,(计数器初始为0,之后成了1,就代表这个对象已经被加锁了.),同时记录线程是谁.

第二个synochronized,发现要加锁的线程已经被加锁了,并且加锁线程和持有线程是同一个线程,第二次就可以加锁成功,不过第二次加锁,就只有计数器++,而没有其他的操作了.

二、死锁

" 死锁 " 是多线程的一种典型问题,加锁可以解决线程安全问题,但是如果加锁使用不当,就有可能造成死锁.

死锁的三种典型场景:

2.1 一个线程,一把锁

如果锁是不可重入的,并且一个线程对这把锁加锁两次,就会出现死锁.

2.2 两个线程,两把锁.

线程1 获取到 锁A

线程2 获取到 锁B

接下来,线程1 尝试获取 锁B,线程2 尝试获取 锁A

这同样会出现死锁.

这就好像车钥匙锁家里了,家里要是锁车里了.谁都无法拿到谁.

2.3 N个线程M把锁

哲学家就餐问题.

三、产生死锁的 四个 必要条件

3.1 互斥使用.

获取锁的过程是互斥的,一个线程拿到了这把锁,另一个线程也想获取,就需要阻塞等待.

3.2 不可抢占.

一个线程拿到锁之后,只能主动解锁,不能让别的线程强行把锁抢走.

3.3 请求保持.

一个线程拿到了锁A之后,在持有锁A的前提下,尝试获取锁B.

3.4 循环等待/环路 等等

解决死锁问题的核心思路,就是把四个必要条件其中一个破坏就可以了.

四、内存可见性引起的 线程安全问题

        如果一个线程写,一个线程读,也是可能存在线程安全问题的.

private static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while(flag==0){

            }
            System.out.println("线程结束");
        });

        Thread t2 = new Thread(()->{
            System.out.println("请输入flag的值 : ");
            Scanner scanner = new Scanner(System.in);
            flag=scanner.nextInt();
        });

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

        
    }

在上面一段代码中,我们是想,在我们输入一个非0的数后,t1就会打印 " 线程结束 ",但是,在我们输入一个非0的数后,他的结果却和我们想的有所偏差.

那这又是为什么呢?

这是因为,在while循环,判断flag==0  时,他的核心指令有两条

一是 load读取内存中flag的值到cpu寄存器里.( 条件转跳 指令 )

二是拿着寄存器的值和0比较

由于这个循环执行的非常快,反复的执行一二.

在这个执行的过程中呢,有两个关键的要点:

1. load操作执行的结果,每次都是一样的,

2. load操作开销远超过 条件转跳.访问寄存器的速度,远远超过访问内存.

load开销大,load的值又没有变化,JVM就会怀疑这个操作是否有必要,就会把这个load的过程给优化掉了.

这就导致了,后面输入内容时,flag的值也没有被修改.

所以,当我们对whil循环体内就行修改,加入一个sleep,这个进程就可以顺利的执行了.

while(flag==0){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }

五、volatile关键字

为了让我们的代码能够确保,无论当前这个代码怎么写,都不会出现这种内存可见性问题.

Java提供了 volatile ( 强制读取内存 )  ,就可以使上述的优化被强制关闭,可以确保每次循环都会重新从内存中读取了.

volatile其中一个核心功能,就是保证内存可见性. ( 另一个功能 : 禁止指令重排序 )

  • 22
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值