【JavaEE】了解synchronized(二)

synchronized也称 加锁,当一个操作为非原子性操作时,可以通过 加锁 成为原子性操作。synchronized可以修饰代码块、实例方法以及静态方法。synchronized的重要特性:可重入锁。

关于死锁

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

2.两个线程针对两把锁,无论是否是可重入锁,都会出现死锁现象。

3.n个线程,m把锁,更容易出现死锁现象(经典问题:哲学家就餐问题)

1.一个线程一把锁

可重入性:一个线程针对一把锁,加锁两次不会出现死锁现象。

死锁:"车里锁了家门钥匙,家里锁了车钥匙"这个可以称为死锁,使得该锁无法解开的锁就称为死锁。如下例子:在还不知道synchronized有可重入性这个概念时,都会默认以下代码是死锁现象

复制代码
public class Demo01 {
    public static void main(String[] args) {
        Object lock = new Object();
        Thread t1 = new Thread(()->{
            synchronized (lock){
                synchronized(lock){
                    System.out.println("nihao");
                }
            }
        });
        t1.start();
        System.out.println("hahaha");
    }
}

synchronized 第一次加锁已经成功了,lock就属于被锁定的状态,第二次再加锁,就应该处于阻塞状态,只有等上一次lock锁被释放后才能加锁成功,但是在进行第二次加锁时就已经阻塞了,程序无法往后继续进行就会出现死锁的现象。这种情况只是在我们不知道synchronized的特性的情况下的想法,正是它有这个可重入性,就不会出现以上的死锁现象。

接着我们进一步分析synchronized是如何做到不会出现死锁的:

当synchronized第一次加锁成功后,又遇到了第二次加锁,此时的第一次加锁并没有释放,可重入性是通过计数器实现的,所以锁对象中不仅需要记录被谁使用还需要记使用次数。正如上面这个例子:当一次加锁成功后,lock这个锁对象中的计数器随其加一,第二次在加锁时计数器也所着加一,直到遇到一个右大括号,相当于解一次锁计数器就减一,这样就是可重入锁。为此锁对象真正释放锁必须是计数器为零时。

2.两个线程两把锁

已知 线程t1,线程t2,锁A,锁B,线程t1已经获得A、线程t2已经获得B,t1还想获得B,t2还想获得A,此时就会出现死锁现象。如下:

复制代码
public static void main(String[] args) {
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(()->{
            synchronized (A){
                System.out.println("t1->A");
                synchronized (B){
                    System.out.println("t1->B");
                }
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (B){
                System.out.println("t2->B");
                synchronized (A){
                    System.out.println("t2->A");
                }
            }
        });
        t1.start();
        t2.start();
    }

以上代码不太严谨,还需要加强,可能会出现一个线程同时获得AB两把锁的情况,所以需要加一个sheep休眠。如下结果,两个线程就出现阻塞现象,两个线程僵持不下就阻塞进程。相反如果是线程t1获得A后又释放,再获得B,这是不会出现死锁现象的。

3.N个线程M把锁

哲学家就餐问题

在圆形餐桌上,每两个哲学家中间就一只筷子,每个哲学家只会作两件事,一件是思考(不拿筷子),另一件是吃面(只能拿左右两边的筷子),每个哲学家思考和吃面都是随机的,当左右边筷子少一个都得等两边哲学家吃完才能拿筷子(等价于阻塞等待),一般情况下是可以正常运行的,如果是极端情况下,就会出现已知等待线程,每个哲学家都拿左手边的筷子,并且都在等右手边的哲学家放下筷子,这时每个人都在等,就等同于出现死锁现象了。

死锁是一个很严重的bug,它会直接导致线程卡住,无法执行后面工作。那我们程序员就需要解决bug,接着就学习如何解决死锁问题。

解决死锁问题

死锁的成因

想要解决死锁问题,需要先了解死锁的成因:

1.互斥使用(锁的基本特性),当一个线程持有一把锁之后,另一个线程也想获得该锁,这个线程就会出现阻塞等待。

2.不可抢占(锁的基本特性),当锁已经被线程t1拿到之后,线程t2只能等待线程t1主动释放锁,而不能强行占有锁。

3.请求保持(代码结构),一个线程尝试获得多把锁(如:上面两个线程两把锁这个例子)。

4.循环等待/环路等待(代码结构),等待关系形成环(如:上面N个线程M把锁、家钥匙锁车里,家里锁车钥匙)。

出现死锁就需要满足以上四个条件,缺一不可,而第一第二都是锁的特性本身就满足条件,只要满足三和四就会出现死锁现象。

解决死锁

既然要解决死锁,那就让以上条件其中有一是不满足即可:

面对1和2,既是锁的特性,那就是不可修改的,我们只需要破坏3和4其中一个即可。

针对3:我们可以通过调整代码避免出现"锁嵌套",比如:将嵌套关系改为并列关系;

针对4:如果一定需要嵌套关系,那可以约定加锁顺序,针对锁进行编号,比如:加多把锁时先加编号小的后加编号大的,针对所有线程都遵循这个约定。

相关推荐
一枚小小程序员哈1 天前
基于Android的车位预售预租APP/基于Android的车位租赁系统APP/基于Android的车位管理系统APP
android·spring boot·后端·struts·spring·java-ee·maven
末央&1 天前
【JavaEE】文件IO操作
java·服务器·java-ee
刘 大 望2 天前
网络编程--TCP/UDP Socket套接字
java·运维·服务器·网络·数据结构·java-ee·intellij-idea
逊嘘4 天前
【JavaEE】多线程(线程安全问题)
java-ee
TT哇9 天前
@[TOC](计算机是如何⼯作的) JavaEE==网站开发
java·redis·java-ee
界面开发小八哥11 天前
「Java EE开发指南」如何使用MyEclipse中的Web Fragment项目?
java·ide·java-ee·eclipse·myeclipse
Brookty11 天前
【Java学习】锁、线程死锁、线程安全2
java·开发语言·学习·java-ee
我命由我1234512 天前
软件开发 - 避免过多的 if-else 语句(使用策略模式、使用映射表、使用枚举、使用函数式编程)
java·开发语言·javascript·设计模式·java-ee·策略模式·js
哈弗小小波12 天前
EasyExcel相关操作
java-ee