[synchronized ]关键字详解

news2024/12/24 3:25:32

目录

1.synchronized 特性

1.1互斥性

1.2内存刷新

 1.3可重入

2.Java 标准库中的线程安全类

3.死锁问题

3.1 一个线程,一把锁

3.2 两个线程,两把锁

3.3 多个线程,多把锁

4.死锁的条件


1.synchronized 特性

1.1互斥性

synchronized 关键字会起到互斥效果,当某个线程执行到某个对象的synchronized中时,如果其他线程也执行到了同一个对象的synchronized了,就会阻塞等待

进入 synchronized 修饰的代码块, 相当于 加锁

退出 synchronized 修饰的代码块, 相当于 解锁

有的编程语言加锁和解锁往往是两个分开的操作,例如,加锁lock(),解锁unlock(),这样的方法缺点就是,容易忘记 写unlock(),那后果就比较严重了,别的线程就一直阻塞等待这个对象了.或者没有忘记写unlock(),但是加锁的代码中有条件语句,return等,直接就出方法了,无法执行到unlock().synchronized修饰代码块的方式就很好地解决了这个问题

如何理解阻塞等待呢?

针对每一把锁,操作系统内部都维护了一个等待队列.当这个锁被某个线程占有的时候,其他线程尝试进行加锁,就不会成功,陷入等待这个锁被释放后继续加锁的状态,这个状态就是阻塞等待状态,一直到之前的线程解锁了之后,操作系统唤醒另一个线程来获取到这个锁

1.2内存刷新

synchronized 的工作过程:

1. 获得互斥锁

2. 从主内存拷贝变量的最新副本到工作的内存

3. 执行代码

4. 将更改后的共享变量的值刷新到主内存

5. 释放互斥锁

 1.3可重入

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

锁的对象是this,线程调用add,进入方法时就会加锁,能够加上,然后又到了代码块,开始尝试加锁

问题来了,锁对象已经被加锁了,被一个线程占用了,第二次加锁是否要阻塞等待呢,并且这两个线程还是同一个线程

如果上述场景允许加第二把锁,就是可重入的,反之是不可重入的,不可重入那么就会陷入死锁状态,因为线程一直阻塞等待获取锁

java中的synchronized是可重入的

2.Java 标准库中的线程安全类

Java 标准库中很多都是线程不安全的,这些类可能会涉及到多线程修改共享数据,有没有加锁措施

有一些是线程安全的. 使用了一些锁机制来控制

还有像String类型的,不涉及到修改,就是线程安全的

既然加了锁就会安全,为什么不都加上锁,让线程安全呢,是因为加锁操作也是有额外的时间开销的,有的地方用不到锁,反而浪费时间

3.死锁问题

死锁问题一旦出现,线程就会陷入僵持等待,程序就无法执行,并且死锁非常隐蔽,难以测试

死锁问题有很多种情况,上文中死锁只是其中一种

3.1 一个线程,一把锁

就是上文的情况,一个线程,一把锁,连续加锁两次,如果是不可重入的,就会死锁

3.2 两个线程,两把锁

场景:

t1和t2两个线程先各自针对锁A和锁B加锁,再尝试获取对方的锁

这个场景就像有个人的车钥匙锁在房子里了,房子钥匙锁在车里了

来看这种情况的代码

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

        Thread t1 = new Thread(()->{
            synchronized (locker1){

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2){
                    System.out.println("t1获取到了两把锁");
                }
            }
        });

        Thread t2 = new Thread(()->{
           synchronized (locker2){
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               synchronized (locker1){
                   System.out.println("t2获取到了两把锁");
               }
           }
        });

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

 结果什么都没有打印,就代表着,两个线程都没有执行到获取两把锁这里的代码,也就是一直在僵持不下,互相等待对方释放自己需要的锁,出现了相互阻塞的现象!!!

使用jconsole查看一下线程的情况 

两个线程都出现了BLOCKED状态, t1是阻塞到17行代码,t2阻塞到30行这里,因为相互都在等待释放锁,两个线程会一直处于这个阻塞状态

因此我们也可以使用jconsole工具来定位死锁,查看线程的状态和调用栈,就可以分析出哪里死锁了 !!

解决方法:下文理解锁的条件后就能解决这种死锁情况

3.3 多个线程,多把锁

经典案例就是"哲学家就餐问题"

哲学家就餐问题可以这样表述,假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。哲学家就餐问题有时也用米饭和筷子而不是意大利面和餐叉来描述,因为很明显,吃米饭必须用两根筷子

哲学家从来不交谈,这就很危险,可能产生死锁,每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反)。即使没有死锁,也有可能发生资源耗尽,例如,假设规定当哲学家等待另一只餐叉超过五分钟后就放下自己手里的那一只餐叉,并且再等五分钟后进行下一次尝试.这个策略消除了死锁(系统总会进入到下一个状态),但仍然有可能发生“活锁”。如果五位哲学家在完全相同的时刻进入餐厅,并同时拿起左边的餐叉,那么这些哲学家就会等待五分钟,同时放下手中的餐叉,再等五分钟,又同时拿起这些餐叉

 如果出现了极端的情况,就陷入死锁

同一时刻,所有的哲学家都拿起来左手的筷子,此时所有的哲学家都拿不起右手的筷子,都要等待右边的哲学家放下筷子

4.死锁的条件

1.互斥使用:线程1拿到了锁,线程2如果想要获取锁就必须阻塞等待

2.不可抢占:线程1获取到锁之后,其他线程不能强行获取到

3.请求和保持:线程1拿到锁A之后,再尝试获取锁B时,A这把锁还是保持的,不会因为这个释放了A

4.循环等待:线程1尝试获取锁A和B,线程2在尝试获取锁B和锁A.

线程1尝试获取锁B时等待线程2释放B,线程2在尝试获取锁B时等待线程1释放锁A

四个条件同时具备才会死锁!!前三个都是锁的基本特性,循环等待是最关键的!

循环等待是四个条件中唯一一个和代码结构相关的,也是可以被程序员控制的,为了避免循环等待,突破口就是循环等待!!可以给锁编号,然后指定一个固定的顺序来加锁,任意线程加多把锁的时候,都让线程遵守顺序,循环等待自然破除! 

给筷子编号,规定每次拿较小号的筷子,假设4个哲学家都拿起筷子,那么和5相邻的两个哲学家肯定要阻塞一个,必然还剩一只5号筷子,拿到较小号筷子的哲学家就可以拿5号筷子就餐,另一位哲学家阻塞等待!破除了循环等待避免死锁

再来看上文提到的,如何让t1t2都获取到A锁和B锁?也是对锁进行编号,让他们都先获取A锁,再获取B锁!

Thread t1 = new Thread(()->{
            synchronized (locker1){

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2){
                    System.out.println("t1获取到了两把锁");
                }
            }
        });

        Thread t2 = new Thread(()->{
           synchronized (locker1){
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               synchronized (locker2){
                   System.out.println("t2获取到了两把锁");
               }
           }
        });

 改动t2线程获取锁的顺序,也先获取A锁,后获取B锁

此时两个线程都获取到了两把锁,解决了死锁问题!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/73984.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C语言】字符串函数(一)

目录 一、strlen函数(计算字符串长度) 1、strlen函数的用途 2、strlen函数的使用 3、strlen函数的模拟实现 二、strcpy函数(字符串拷贝) 1、strcpy函数的用途 2、strcpy函数的使用 3、strcpy函数的模拟实现 三、strcat函数(字符串追加) 1、strcat函数的用途 2、strcat函数的使用…

【JavaSE】接口剩余内容

目录 1、接口使用实例 📕逐步分析学生数组排序的写法 ✨思路: ✨代码实现 ✨弊端 📕、改进 改进思路: 代码实现: 2、Cloneable接口和深拷贝 2.1、cloneable接口的作用 2.2、深拷贝和浅拷贝 2.2.1、浅拷贝 …

yum安装openldap2.4.44,并配置增量复制(Delta-syncrepl)环境

本文是在centos7环境下通过yum安装openldap2.4.44,并配置增量复制(Delta-syncrepl)环境 官网对于增量复制介绍:https://www.openldap.org/doc/admin24/replication.html#Delta-syncrepl%20replication Delta-syncrepl 是 syncrep…

ADI Blackfin DSP处理器-BF533的开发详解3:GPIO(含源代码)

我们从最基础的GPIO开始,先讲外设,这玩意不管是单片机,还是ARM,又或是FPGA,甚至SOC的芯片,都有GPIO,有共性,就好理解,让我们看看在ADI的DSP里头,GPIO是怎么一…

MySQL数据库学习(2)

一.MySQL语法书写规范: (1).SQL语句要以分号;结尾 在 RDBMS(关系型数据库)当中,SQL语句是逐条执行的,一条 SQL语句代表着数据库的一个操作。SQL语句是使用英文分号;结尾。 (2).SQL语句不区分大小写 SQL不区分关键字的大小写。例如&#xff0c…

CentOS7下mysql主从复制搭建

mysql安装 CentOS7安装MySql5.7完整教程_长头发的程序猿的博客-CSDN博客_centos7 mysql5.7安装 1、配置主机 1.1、修改my.cnf配置文件 vim /etc/my.cnf 最后一行添加: #主服务器唯一ID server-id1 #启用二进制日志 log-binmysql-bin #设置不要复制的数据库(可…

C++之虚函数

都说面向对象的三大特性是封装、继承、多态。C作为一门面向对象编程语言,肯定也是具备了面向对象的三大特性,那么在C中是如何实现多态的呢? 在C中是通过虚函数动态绑定的方式实现多态的。 虚函数与纯虚函数 首先我们来回顾一下虚函数&…

DAX:GROUPBY函数

DAX 中的 SUMMARIZE 函数功能强大,但同时也很难使用。 它可用于执行表之间的分组和连接,正如我们之前在分组数据一文中描述的那样。 不幸的是,它在计算聚合值时存在一些众所周知的性能问题。除了性能之外,SUMMARIZE 的计算限制是它…

ArrayList中的 subList 强转 ArrayList 导致异常

阿里《Java开发手册》上提过 [强制] ArrayList的sublist结果不可強转成ArrayList,否则会抛出ClassCastException异常,即java.util.RandomAccesSubList cannot be cast to java. util.ArrayList. 说明: subList 返回的是ArrayList 的内部类SubList, 并不是ArrayList …

一块RTX 3090加速训练YOLOv5s,时间减少11个小时,速度提升20%

作者|BBuf 很高兴为大家带来One-YOLOv5的最新进展,在《一个更快的YOLOv5问世,附送全面中文解析教程》发布后收到了很多算法工程师朋友的关注,十分感谢。 不过,可能你也在思考一个问题:虽然OneFlow的兼容性做…

SQL之substrate()函数用法

测试表字段查询如下: 测试在hive中截取前5位字符的第1种写法: 测试在hive中截取前5位字符的第2种写法: 测试在impala中截取前5位字符的第1种写法: 测试在impala中截取前5位字符的第2种写法: 结果: 1、在h…

带分数(蓝桥杯C/C++B组真题详解)

目录 题目 题目思路 题目代码 注解: 1.题目给定的判定条件为 2.关于next_permutation(start,end); 题目: 题目思路: 因为题目要求是满足在1到9中 不重复、不遗漏的所有满足条件的情况 所以我们可以通过全排列 把整数、分子、分母 …

B-树(B-Tree)与二叉搜索树(BST):讲讲数据库和文件系统背后的原理(读写比较大块数据的存储系统数据结构与算法原理)...

人类总喜欢发明创造一些新名词(比如说,简写/缩写/简称什么的),并通过这些名词把人群分成了三六九等。弄到最后,把自己都绕晕了。你看,首先就是,B树,不要与Binary tree或Btree混淆。B…

有奖征文 | 当我们谈操作系统时,我们在谈什么?

OS,Operating System,操作系统,计算机中最基本也是最重要的基础性系统软件。1991 年,大二学生 Linus Torvalds 写出 Linux0.01,经过几十年的发展,以 Linux 为代表的服务器操作系统,成长为一个既…

lambda之Stream流式编程

lambda之Stream流式编程 一、什么是 Stream Stream中文称为 “流”,通过将集合转换为这么一种叫做“流”的元素序列,通过声明性方式,能够对集合中的每个元素进行一系列并行或串行的流水线操作。换句话说,你只需要告诉流你的要求…

led护眼台灯对眼睛好?过来人说说led护眼灯是否真的能护眼

众所周知,现在绝大部分光源都是使用led发光,无论是室内照明灯、室外装饰灯、气氛调节灯、工作学习护眼台灯等等,都是使用led灯珠,那么也就有人会问了:led灯真的对眼睛好吗?Led护眼台灯真的能护眼吗&#xf…

TH7-搜附近

TH7-搜附近说明1、探花1.1、查询推荐列表dubbo服务1.1.1、实体对象1.1.2、定义接口1.1.3、编写实现1.1.4、单元测试1.2、查询推荐列表APP接口实现1.2.1、TanHuaController1.2.2、TanHuaService1.2.3、测试1.3、喜欢的dubbo服务1.3.1、定义接口1.3.2、编写实现1.4、左滑右滑1.4.…

fofa搜索漏洞技巧

fofa搜索漏洞技巧整理,主要有以下十个方面:搜索HTTP响应头中含有"thinkphp"关键词的网站和IP;加上标题带有后台的;加上时间,现在新网站有thinkphp日志泄露的有很多; 搜索html正文中含有"管理后台"关键词的网站和IP body="管理后台"等。 …

Linux内核缓存

【推荐阅读】 轻松学会linux下查看内存频率,内核函数,cpu频率 纯干货,linux内存管理——内存管理架构(建议收藏) 一篇长文叙述Linux内核虚拟地址空间的基本概括 页缓存和块缓存 内核为块设备提供了两种通用的缓存方案: 页缓存&a…

光华股份深交所上市:市值51亿 应收账款余额超5亿

雷递网 雷建平 12月8日浙江光华科技股份有限公司(简称:“光华股份”,证券代码:001333)今日在深交所主板上市。光华股份本次发行3200万股,发行价为27.76元,募资8.88亿元。光华股份开盘价为33.31元…