当前位置:首页 » 《随便一记》 » 正文

synchronized 关键字

20 人参与  2023年01月03日 12:20  分类 : 《随便一记》  评论

点击全文阅读


在这里插入图片描述

文章目录

一、synchronized 的特性互斥可重入 二、 synchronized 使用示例三、 java标准库的线程安全类四、 死锁可重入死锁相互争夺锁哲学家就餐问题死锁的四个必要条件

一、synchronized 的特性

互斥

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.
在这里插入图片描述
进入该方法相当于针对该对象"加锁" ( lock )
执行完该方法相当于对该对象"解锁" ( unlock )
在这里插入图片描述
当有一个线程加锁之后,其他线程只能阻塞等待直到释放锁
注意:

上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这也就是操作系统线程调度的一部分工作.假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.

可重入

一个线程对同一个对象是否可以连续加锁两次,如果可以就是可重入的

public synchronized void add() {        synchronized (this) {            this.num++;        }    }

在这里插入图片描述
在这里插入图片描述
站在this锁对象,它认为自己已经被另外的线程占用的,这里的第二次加锁是否要阻塞等待?
但此处第二个线程和第一个线程是同一个线程,是否允许加锁,如果允许,这个操作就算可重入的,否则会导致死锁。
在这里插入图片描述
线程进入一个层锁,第二次加锁的时候会组设等待,直到第一次锁释放,才能获取第二个锁,但是我们会在锁对象里记录一下,当前的锁是那个对象是那个线程持有的,如果加锁线程和持有线程是同一个,就放过,否则阻塞。

上面的代码是完全没问题的. 因为 synchronized 是可重入锁.

二、 synchronized 使用示例

synchronized的3种使用方式:

修饰实例方法:作用于当前实例加锁
public synchronized void add() {           }
修饰静态方法:作用于当前类对象加锁
public synchronized static void add() {           }
修饰代码块:指定加锁对象,对给定对象加锁
锁当前对象
public void add() {        synchronized (this) {                    }    }

锁类对象

public class SynchronizedDemo {    public void method() {        synchronized (SynchronizedDemo.class) {       }   }}

我们需要注意的是,synchronized锁的是什么?
只有两个线程竞争获取同一把锁,才会阻塞等待。
在这里插入图片描述
两个线程分别获取不同的锁不会产生竞争。

三、 java标准库的线程安全类

之前我们一直是单线程操作线程,所以也不用太注重线程安全问题,但当我们多线程操作集合的时候,我们就需要注意线程安全问题了。
线程不安全的集合:
1.ArrayList
2.LinkedList
3.HashMap
4.TreeMap
5.HashSet
6.TreeSet
7.StringBuilder

线程安全的集合:
1.ConcurrentHashMap
2.StringBuffer
在这里插入图片描述
我们可以看到StringBuffer的方法大多数都加了synchronized.
在这里插入图片描述
但是concurrentHashMap的方法没有加synchronized,但是同样是线程安全的,大家下去可以研究一下。
String我们没有加任何的锁,但它是不可修改的,仍然是线程安全的。
为什么不给每个集合都加上锁呢?
因为在加锁的同时,会带来额外的时间开销,有些情况下,不使用多线程,不会面临线程安全问题,所以我们的一些集合并没有加锁,因此,我们在多线程情况下使用这些集合时,可以手动加锁。

四、 死锁

可重入死锁

public synchronized void add() {        synchronized (this) {            this.num++;        }    }

一个线程,一把锁,连续加入两次,如果锁不是可重入锁,就会造成死锁问题。
但是Java的synchronized和ReentrantLock都是可重入锁,这里我们无法演示,大家可以参考我上面加可重入问题所讲的。

相互争夺锁

两个线程两把锁,t1,t2现针对锁A,锁B加锁,再去获取对方的锁,双方都不愿意让步,就会造成死锁问题。
在这里插入图片描述

这里我们举一个生活中类似的例子,某一天一码通给崩了,我们的手机健康吗打不开了,于是我们的程序员就需要赶到公司去修复这个问题,但是在公司楼下被保安拦住了,要求出示健康码,程序员说: 我上楼修复了bug才能出示健康码。保安: 你出示了健康码才能上楼修复bug. 这个情景和我们此处的死锁非常类似。

public static void main(String[] args) {        Object A = new Object();        Object B = new Object();        Thread t1 = new Thread(() -> {            synchronized (A) {                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    throw new RuntimeException(e);                }                synchronized (B) {                    System.out.println("t1获取到了锁A和锁B");                }            }        });        Thread t2 = new Thread(() -> {            synchronized (B) {                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    throw new RuntimeException(e);                }                synchronized (A) {                    System.out.println("t2获取到了锁A和锁B");                }            }        });        t1.start();        t2.start();    }

在这里插入图片描述
我们可以发现程序什么都没有输出,证明t1,t2都没有获取到两把锁,相互堵塞状态。
我们打开jconsole看一下线程详细状态。
在这里插入图片描述
我们可以看到我们创建的t1,t2线程
在这里插入图片描述
我们可以看到t1处于BLOCKED阻塞状态,看到堆栈信息20行,证明我们t1线程获取不到锁B。
在这里插入图片描述
我们可以看到t2也处于BLOCKED阻塞状态,看到堆栈信息32行,证明我们t1线程获取不到锁A。
针对这样的死锁问题,我们需要借助jconsole这样的工具查看状态和堆栈信息去分析原因并进行修改。

哲学家就餐问题

在这里插入图片描述
我们有六个哲学家和六根筷子,每个哲学家要想吃饭,就必须拿起左手和右手的两根筷子。
每个哲学家只有两种情况:
1.发呆状态(线程阻塞状态)
2.吃饭状态(线程获取到锁并执行)
由于操作系统的随机调度,这六个哲学家,随时都可能吃面条,和发呆。但有这么一种情况。
在这里插入图片描述
我们六个哲学将同时处于吃饭状态,拿起了右手的筷子,当准备拿左手的筷子时,发现没有筷子可拿,都在等左边的哲学家放下筷子,可是没有哲学家放,于是都陷入了阻塞状态,这就是一种典型的死锁问题。

死锁的四个必要条件

互斥使用:线程1拿到了锁,线程2就得进行阻塞状态。不可抢占:线程1拿到锁之后,必须是线程1主动释放,线程2不可能强行获取到请求和保持:线程1拿到锁A后,再去获取锁B的时候,A这把锁仍然保持,不会释放循环等待:我们第二种死锁的典型情况,线程1和线程2尝试获取锁A,B,线程1在获取锁B的时候等待线程2释放B,同时线程2在获取锁A的时候等待线程1释放A

由于synchronized的特性,前三点我们是无法改变的,想要打破死锁,我们只能从循环等待这里改变。
给锁编号,按照一个固定的顺序来加锁,我们针对银行家问题按从小到大加锁。
在这里插入图片描述
我们可以发现当该哲学家去拿左手小的1筷子的时候,发现已经被拿了,于是它进入阻塞状态。
在这里插入图片描述
另一个哲学家拿起5.6,然后放下,然后旁边的哲学家在拿起4.5放下,这样死锁问题就解决了。

我们再看一下t1,t2获取A,B的那个死锁问题,我们规定按照锁A,B的顺序进行获取。

public static void main(String[] args) {        Object A = new Object();        Object B = new Object();        Thread t1 = new Thread(() -> {            synchronized (A) {                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    throw new RuntimeException(e);                }                synchronized (B) {                    System.out.println("t1获取到了锁A和锁B");                }            }        });        Thread t2 = new Thread(() -> {            synchronized (A) {                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    throw new RuntimeException(e);                }                synchronized (B) {                    System.out.println("t2获取到了锁A和锁B");                }            }        });        t1.start();        t2.start();    }

在这里插入图片描述
我们可以看到不会再出现死锁问题,t1,t2都获取到了A,B锁。


点击全文阅读


本文链接:http://zhangshiyu.com/post/50915.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1