一、synchronized 特性

(1) 互斥

synchronized会起到互斥效果,某个线程执⾏到某个对象的synchronized中时,其他线程如果也执⾏ 到同⼀个对象synchronized就会阻塞等待.

public class Demo {
    private static Object locke = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (locke){
                System.out.println("t1线程启动");
                while (true){

                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (locke){
                System.out.println("t2线程启动");
            }
        });
        t1.start();
        t2.start();
    }
}

t1进入synchronized拿到locke锁 永不释放 t2尝试进入 发现锁被占用 进入阻塞状态

要想解除阻塞,需要往下执行才可以,要想往下执行,就需要等到第一次的锁被释放
这样的问题,就称为“死锁”

1.第一次进行加锁操作,能够成功的(锁没有人使用)

2.第二次进行加锁,此时意味着,锁对象是已经被占用的状态.第二次加锁,就会触发阻塞等待
死锁是一个非常严重的bug.使代码执行到这一块之后,就卡住
为了解决上述的问题,Java的synchronized就引入了
可重入的概念.

(2)可重入

同一个线程可以多次获取同一把锁

synchronized同步块对同⼀条线程来说是可重入的,不会出现自己把自己锁死的问题;

public class Demo {
    private static Object locker = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (locker){
                System.out.println("第一次加锁");
                synchronized (locker){
                    System.out.println("第二次加锁");
                }
            }
        });
        t1.start();
    }
}

例子

不可重入

你追一个妹子第

一次牵手成功

第二次想牵手,

被她说:「你已经牵过了,等别人牵完你再来」
?????????

可重入

你已经牵着她的手你想:
“再牵一次”.“搂一下”“抱一下”
她说:「是你啊,可以」
前提:是同一个人(线程)

游戏

你已经在Boss战中

再放技能、开buff、补血

系统不会说:「你已经在战斗中,不能再战斗」
连招顺畅

可重入锁的实现原理,关键在于让锁对象,内部保存,当前是哪个线程持有的这把锁

站在JVM的视角,看到多个 } 需要执行JVM如何知道哪个 } 是真正解锁的那个??
先引入一个变量,计数器(0)
每次触发 { 的时候,把计数器++
每次触发 } 的时候,把计数器--
当计数器--为0的时候,就是真正需要解锁的时候

二、死锁

1.关于死锁

两个线程,两把锁,每个线程获取到一把锁之后,尝试获取对方的锁

死锁必须满足的4个条件

互斥
请求与保持
不可剥夺
循环等待

2.构造死锁

public class Demo {
    private static Object locker1 = new Object();
    private static Object locker2 = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                System.out.println("t1拿到locker1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker2) {
                    System.out.println("t1拿到locker2");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (locker2){
                System.out.println("t2拿到locker2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker1){
                    System.out.println("t2拿到locker1");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

第一步:t1拿到锁locker1

第二步:t2拿到锁locker2

第三步:t1想拿锁locker2 locker2在t2手上 阻塞等待

               t2想拿锁locker1 locker1在t1手上 阻塞等待

此时 t1持有locker1等待t2

        t2持有locker2等待t1

永久在等待 这就是死锁

如果把代码中sleep去掉会发生什么变化

如果不加sleep,很可能t1一口气就把locker1和locker2都拿到了.这个时候t2还没开始呢自然无法构成死锁.

3.如何避免代码中出现死锁

(1)构成死锁的四个必要条件

1.锁是互斥的.一个线程拿到锁之后,另一个线程再尝试获取锁,必须要阻塞等待.

2.锁是不可抢占的.线程1拿到锁线程2也尝试获取这个锁,线程2必须阻塞等待(不可剥夺)而不是线程2直接把锁抢过来
3.请求和保持.一个线程拿到锁1之后,不释放锁1的前提下,获取锁2 
4.循环等待.多个线程,多把锁之间的等待过程,构成了"循环"A等待B,B也等待A或者A等待B,B等待C,C等待A。

(2)如何避免

打破构成死锁条件的(3) (4)。

1.避免锁的嵌套

2.约定好加锁的顺序,就可以破除循环等待。

N个线程M把锁

1.思考人生(放下筷子,思考)
2.吃面条(拿起筷子)
5个哲学家,随机的触发吃面条和思考人生
5个哲学家,就相当于5个线程5根筷子,就相当于5把锁每个线程只需要拿到其中的两根筷子即可
大部分情况下,上述模型,可以很好的运转在一些极端情况下会造成死锁的。
同一时刻,大家都想吃面条同时拿起左手的筷子此时,任何一个线程都无法拿起右手的筷子任何一个哲学家都吃不成面条

约定,每个线程加锁的时候永远是先获取序号小的锁后获取序号大的锁

三、volatile

1.volatile能够保证内存可见性

什么是内存可见性?

一个线程修改了共享变量,
其他线程能不能立刻看到这个修改。

能看到 → 有可见性
看不到 → 发生了可见性问题

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

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

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

理论上:t2 把 flag 改为 1 1 应该跳出循环

但现实可能:t1 永远不会结束

这就是 内存不可见问题

原因:

编译器优化
咱们写的代码 javac把java=>.class=>jvm
编译器,虽然声称优化操作,是能够保证逻辑不变,尤其是在多线程的程序中,编译器的判断可能出现失误
可能导致编译器的优化,使优化后的逻辑,和优化前的逻辑出现细节上的偏差

稍微调整代码

针对内存可见性问题,也不能指望通过sleep来解决
使用sleep大大影响到程序的效率.
不使用sleep也能解决上述的内存可见性问题呢?

2.volatile

在语法中,引入volatile关键字通过这个关键字来修饰某个变量,此时编译器这对这个变量的读取操作,就不会被优化成都寄存器

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

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

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

注:volatile解决内存可见性问题不是解决原子性问题。

四、synchronized和volatile的区别

1.volatile 解决“可见性” synchronized 解决“原子性 + 可见性”。

2.volatile 不能保证复合操作安全。synchronized 可以保证代码块线程安全。

3.volatile 只能保证可见性和有序性,不能保证原子性,不具备互斥性。
4.synchronized 是悲观锁,可以保证原子性、可见性和有序性,通过互斥访问实现线程安全。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐