多线程系列(三) -synchronized 关键字使用详解

一、简介

在之前的线程系列文章中,我们介绍了线程创建的几种方式以及常用的方法介绍。

今天我们接着聊聊多线程线程安全的问题,以及解决办法。

实际上,在多线程环境中,难免会出现多个线程对一个对象的实例变量进行同时访问和操作,如果编程处理不当,会产生脏读现象。

二、线程安全问题介绍

我们先来看一个简单的线程安全问题的例子!

public class DataEntity {private int count = 0;public void addCount(){count++;}public int getCount(){return count;}
}
public class MyThread extends Thread {private DataEntity entity;public MyThread(DataEntity entity) {this.entity = entity;}@Overridepublic void run() {for (int j = 0; j < 1000000; j++) {entity.addCount();}}
}
public class MyThreadTest {public static void main(String[] args) {// 初始化数据实体DataEntity entity = new DataEntity();//使用多线程编程对数据进行计算for (int i = 0; i < 10; i++) {MyThread thread = new MyThread(entity);thread.start();}try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("result: " + entity.getCount());}
}

多次运行结果如下:

第一次运行:result: 9788554
第二次运行:result: 9861461
第三次运行:result: 6412249
...

上面的代码中,总共开启了 10 个线程,每个线程都累加了 1000000 次,如果结果正确的话,自然而然总数就应该是 10 * 1000000 = 10000000。

但是多次运行结果都不是这个数,而且每次运行结果都不一样,为什么会出现这个结果呢

简单的说,这是主内存和线程的工作内存数据不一致,以及多线程执行时无序,共同造成的结果

我们先简单的了解一下 Java 的内存模型,后期我们在介绍里面的原理!

如上图所示,线程 A 和线程 B 之间,如果要完成数据通信的话,需要经历以下几个步骤:

  • 1.线程 A 从主内存中将共享变量读入线程 A 的工作内存后并进行操作,之后将数据重新写回到主内存中;
  • 2.线程 B 从主存中读取最新的共享变量,然后存入自己的工作内存中,再进行操作,数据操作完之后再重新写入到主内存中;

如果线程 A 更新后数据并没有及时写回到主存,而此时线程 B 从主内存中读到的数据,可能就是过期的数据,于是就会出现“脏读”现象。

因此在多线程环境下,如果不进行一定干预处理,可能就会出现像上文介绍的那样,采用多线程编程时,程序的实际运行结果与预期会不一致,就会产生非常严重的问题。

针对多线程编程中,程序运行不安全的问题,Java 提供了synchronized关键字来解决这个问题,当多个线程同时访问共享资源时,会保证线程依次排队操作共享变量,从而保证程序的实际运行结果与预期一致。

我们对上面示例中的DataEntity.addCount()方法进行改造,再看看效果如下。

public class DataEntity {private int count = 0;/*** 在方法上加上 synchronized 关键字*/public synchronized void addCount(){count++;}public int getCount(){return count;}
}

多次运行结果如下:

第一次运行:result: 10000000
第二次运行:result: 10000000
第三次运行:result: 10000000
...

运行结果与预期一致!

三、synchronized 使用详解

synchronized作为 Java 中的关键字,在多线程编程中,有着非常重要的地位,也是新手了解并发编程的基础,从功能角度看,它有以下几个比较重要的特性:

  • 原子性:即一个或多个操作要么全部执行成功,要么全部执行失败。synchronized关键字可以保证只有一个线程拿到锁,访问共享资源
  • 可见性:即一个线程对共享变量进行修改后,其他线程可以立刻看到。执行synchronized时,线程获取锁之后,一定从主内存中读取数据,释放锁之前,一定会将数据写回主内存,从而保证内存数据可见性
  • 有序性:即保证程序的执行顺序会按照代码的先后顺序执行。synchronized关键字,可以保证每个线程依次排队操作共享变量

synchronized也被称为同步锁,它可以把任意一个非 NULL 的对象当成锁,只有拿到锁的线程能进入方法体,并且只有一个线程能进入,其他的线程必须等待锁释放了才能进入,它属于独占式的悲观锁,同时也属于可重入锁。

关于锁的知识,我们后面在介绍,大家先了解一下就行。

从实际的使用角度来看,synchronized修饰的对象有以下几种:

  • 修饰一个方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
  • 修饰一个静态的方法:其作用的范围是整个静态方法,作用的对象是这个类的所有对象
  • 修饰一个代码块:被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象,使用上比较灵活

下面我们一起来看看它们的具体用法。

3.1、修饰一个方法

synchronized修饰一个方法时,多个线程访问同一个对象,哪个线程持有该方法所属对象的锁,就拥有执行权限,否则就只能等待。

如果多线程访问的不是同一个对象,不会起到保证线程同步的作用

示例如下:

public class DataEntity {private int count;/*** 在方法上加上 synchronized 关键字*/public synchronized void addCount(){for (int i = 0; i < 3; i++) {try {System.out.println(Thread.currentThread().getName() + ":" + (count++));Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}public int getCount() {return count;}
}
public class MyThreadA extends Thread {private DataEntity entity;public MyThreadA(DataEntity entity) {this.entity = entity;}@Overridepublic void run() {entity.addCount();}
}
public class MyThreadB extends Thread {private DataEntity entity;public MyThreadB(DataEntity entity) {this.entity = entity;}@Overridepublic void run() {entity.addCount();}
}
public class MyThreadTest {public static void main(String[] args) {// 初始化数据实体DataEntity entity = new DataEntity();MyThreadA threadA = new MyThreadA(entity);threadA.start();MyThreadB threadB = new MyThreadB(entity);threadB.start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("result: " + entity.getCount());}
}

运行结果如下:

Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6

当两个线程共同操作一个对象时,此时每个线程都会依次排队执行。

假如两个线程操作的不是一个对象,此时没有任何效果,示例如下:

public class MyThreadTest {public static void main(String[] args) {DataEntity entity1 = new DataEntity();MyThreadA threadA = new MyThreadA(entity1);threadA.start();DataEntity entity2 = new DataEntity();MyThreadA threadB = new MyThreadA(entity2);threadB.start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("result: " + entity1.getCount());System.out.println("result: " + entity2.getCount());}
}

运行结果如下:

Thread-0:0
Thread-1:0
Thread-0:1
Thread-1:1
Thread-0:2
Thread-1:2
result: 3
result: 3

从结果上可以看出,当synchronized修饰一个方法,当多个线程访问同一个对象的方法,每个线程会依次排队;如果访问的不是一个对象,线程不会进行排队,像正常执行一样。

3.2、修饰一个静态的方法

synchronized修改一个静态的方法时,代表的是对当前.java文件对应的 Class 类加锁,不区分对象实例。

示例如下:

public class DataEntity {private static int count;/*** 在静态方法上加上 synchronized 关键字*/public synchronized static void addCount(){for (int i = 0; i < 3; i++) {try {System.out.println(Thread.currentThread().getName() + ":" + (count++));Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}public static int getCount() {return count;}
}
public class MyThreadA extends Thread {@Overridepublic void run() {DataEntity.addCount();}
}
public class MyThreadB extends Thread {@Overridepublic void run() {DataEntity.addCount();}
}
public class MyThreadTest {public static void main(String[] args) {MyThreadA threadA = new MyThreadA();threadA.start();MyThreadB threadB = new MyThreadB();threadB.start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("result: " + DataEntity.getCount());}
}

运行结果如下:

Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6

静态同步方法和非静态同步方法持有的是不同的锁,前者是类锁,后者是对象锁,类锁可以理解为这个类的所有对象。

3.3、修饰一个代码块

synchronized用于修饰一个代码块时,只会控制代码块内的执行顺序,其他试图访问该对象的线程将被阻塞,编程比较灵活,在实际开发中用的应用比较广泛。

示例如下

public class DataEntity {private int count;/*** 在方法上加上 synchronized 关键字*/public void addCount(){synchronized (this){for (int i = 0; i < 3; i++) {try {System.out.println(Thread.currentThread().getName() + ":" + (count++));Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}}public int getCount() {return count;}
}
public class MyThreadTest {public static void main(String[] args) {// 初始化数据实体DataEntity entity = new DataEntity();MyThreadA threadA = new MyThreadA(entity);threadA.start();MyThreadB threadB = new MyThreadB(entity);threadB.start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("result: " + entity.getCount());}
}

运行结果如下:

Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6

其中synchronized (this)中的this,表示的是当前类实例的对象,效果等同于public synchronized void addCount()

除此之外,synchronized()还可以修饰任意实例对象,作用的范围就是具体的实例对象。

比如,修饰个自定义的类实例对象,作用的范围是拥有lock对象,其实也等价于synchronized (this)

public class DataEntity {private Object lock = new Object();/*** synchronized 可以修饰任意实例对象*/public void addCount(){synchronized (lock){// todo...}}
}

当然也可以用于修饰类,表示类锁,效果等同于public synchronized static void addCount()

public class DataEntity {/*** synchronized 可以修饰类,表示类锁*/public void addCount(){synchronized (DataEntity.class){// todo...}}
}

synchronized修饰代码块,比较经典的应用案例,就是单例设计模式中的双重校验锁实现。

public class Singleton {  private volatile static Singleton singleton;  private Singleton (){}  public static Singleton getSingleton() {  if (singleton == null) {  synchronized (Singleton.class) {  if (singleton == null) {  singleton = new Singleton();  }  }  }  return singleton;  }  
}

采用代码块的实现方式,编程会更加灵活,可以显著的提升并发查询的效率。

四、synchronized 锁重入介绍

synchronized关键字拥有锁重入的功能,所谓锁重入的意思就是:当一个线程得到一个对象锁后,再次请求此对象锁时可以再次得到该对象的锁,而无需等待。

我们看个例子就能明白。

public class DataEntity {private int count = 0;public synchronized void addCount1(){System.out.println(Thread.currentThread().getName() + ":" + (count++));addCount2();}public synchronized void addCount2(){System.out.println(Thread.currentThread().getName() + ":" + (count++));addCount3();}public synchronized void addCount3(){System.out.println(Thread.currentThread().getName() + ":" + (count++));}public int getCount() {return count;}
}
public class MyThreadA extends Thread {private DataEntity entity;public MyThreadA(DataEntity entity) {this.entity = entity;}@Overridepublic void run() {entity.addCount1();}
}
public class MyThreadB extends Thread {private DataEntity entity;public MyThreadB(DataEntity entity) {this.entity = entity;}@Overridepublic void run() {entity.addCount1();}
}
public class MyThreadTest {public static void main(String[] args) {// 初始化数据实体DataEntity entity = new DataEntity();MyThreadA threadA = new MyThreadA(entity);threadA.start();MyThreadB threadB = new MyThreadB(entity);threadB.start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("result: " + entity.getCount());}
}

运行结果如下:

Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6

从结果上看线程没有交替执行,线程Thread-0获取到锁之后,再次调用其它带有synchronized关键字的方法时,可以快速进入,而Thread-1线程需等待对象锁完全释放之后再获取,这就是锁重入。

五、小结

从上文中我们可以得知,在多线程环境下,恰当的使用synchronized关键字可以保证线程同步,使程序的运行结果与预期一致。

  • 1.当synchronized修饰一个方法时,作用的范围是整个方法,作用的对象是调用这个方法的对象;
  • 2..当synchronized修饰一个静态方法时,作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  • 3.当synchronized修饰一个代码块时,作用的范围是代码块,作用的对象是修饰的内容,如果是类,则这个类的所有对象都会受到控制;如果是任意对象实例子,则控制的是具体的对象实例,谁拥有这个对象锁,就能进入方法体

synchronized是一种同步锁,属于独占式,使用它进行线程同步,JVM 性能开销很大,大量的使用未必会带来好处。

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

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

相关文章

3-qt综合实例-贪吃蛇的游戏程序

引言&#xff1a; 如题&#xff0c;本次实践课程主要讲解贪吃蛇游戏程序。 qt贪吃蛇项目内容&#xff1a; 一、功能需求 二、界面设计 各组件使用&#xff1a; 对象名 类 说明 Widget QWidge 主窗体 btnRank QPushButton 排行榜-按钮 groupBox QGroupBox 难…

【数据结构与算法】力扣 347. 前 K 个高频元素

题目描述 给你一个整数数组 nums 和一个整数 k &#xff0c;请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。 示例 1: 输入: nums [1,1,1,2,2,3], k 2 输出: [1,2]示例 2: 输入: nums [1], k 1 输出: [1]提示&#xff1a; 1 < nums.length < …

python数据分析——数据分析的统计推断

数据分析的统计推断 前言一、提出问题二、统计归纳方法三、统计推断四、统计推断步骤4.1.点估计4.2.区间估计4.2.1. 总体方差已知4.2.2总体方差未知 4.3. 假设检验4.4. 假设检验的假设4.5.显著性水平 五、检验统计量六、检验方法七、拒绝域八、假设检验步骤九、重要假设检验方法…

伺服电机初识

目录 一、伺服电机的介绍二、伺服电机的基本原理三、伺服电机的技术特点四、伺服电机的分类五、实际产品介绍1、基本技术规格&#xff1a;2、MD42电机硬件接口3、通讯协议介绍3.1 通讯控制速度运行3.2 通讯控制位置运行3.3 通讯控制转矩运行 4、状态灯与报警信息 一、伺服电机的…

金融行业AI大模型百项应用案例综述【大模型系列】

逐浪金融大模型的玩家&#xff0c;除了BAT、华为等高科技巨头&#xff0c;试图以技术优势充当产业链的“卖铲人”&#xff0c;更多的还是金融和类金融企业&#xff0c;包括银行、保险、互金、券商等&#xff0c;既不想被喧宾夺主&#xff0c;又不想肥水外流&#xff0c;都在押注…

确保WEB应用安全:深入分析与有效防范

目录 前言 一. 概述 二. WEB应用体系结构脆弱性分析 1.前端用户界面 2.后端服务器 3.数据库 4.风险和后果 5.缓解措施 三. SQL注入攻击及防范 1.使用参数化查询或预定义语句 2.输入验证和转义 3.最小化数据库权限 4.使用存储过程和预定义语句 5.保持软件更新 6.实…

【Linux系统化学习】网络套接字(编写简单的TCP服务端和客户端)

目录 TCP服务端 创建套接字 解决绑定失败问题 填充网络信息 绑定 设置监听状态 接受请求 收取和反馈消息 完整的服务端封装代码 TCP客户端 创建套接字 填充网络信息 发起连接 发送和收取消息 客户端完整代码 一些补充 TCP服务端 初始化服务端 创建套接字 和U…

【JAVA项目】基于个人需求和地域特色的【外卖推荐系统】

技术简介&#xff1a;采用B/S架构、ssm 框架、Java技术、MySQL等技术实现。 系统简介&#xff1a;统权限按管理员&#xff0c;商家和用户这三类涉及用户划分。(a) 管理员&#xff1b;管理员使用本系统涉到的功能主要有&#xff1a;首页&#xff0c;个人中心&#xff0c;用户管理…

C++ | Leetcode C++题解之第61题旋转链表

题目&#xff1a; 题解&#xff1a; class Solution { public:ListNode* rotateRight(ListNode* head, int k) {if (k 0 || head nullptr || head->next nullptr) {return head;}int n 1;ListNode* iter head;while (iter->next ! nullptr) {iter iter->next;n…

ctfshow web入门 sql注入 web201--web208

web201 先扫描先 python .\sqlmap.py -u "http://4863661d-2371-4812-ae62-128fadbdc0a4.challenge.ctf.show/api/?id" --user-agentsqlmap 加头 python .\sqlmap.py -u "http://4863661d-2371-4812-ae62-128fadbdc0a4.challenge.ctf.show/api/?id" --u…

每日算法之从前序与中序遍历序列构造二叉树

题目描述 给定两个整数数组 preorder 和 inorder &#xff0c;其中 preorder 是二叉树的先序遍历&#xff0c; inorder 是同一棵树的中序遍历&#xff0c;请构造二叉树并返回其根节点。 示例 1: &#xfffc; 输入: preorder [3,9,20,15,7], inorder [9,3,15,20,7] 输出: […

央视影音 视频下载 2

浏览器猫抓插件&#xff0c;拿到视频地址&#xff0c;这个地址的播放不正常&#xff0c;花屏。https://dh5.cntv.qcloudcdn.com/asp/h5e/hls/2000/0303000a/3/default/6edd15a0ebb3467993bec51a95be0e22/2000.m3u8 改一下地址&#xff0c;把代码中的h5e去掉。网址改为https://…

Wezterm的安装和简单配置

配置文件 进入 C:\Users\xxx 目录&#xff0c; 创建 .wezterm.lua 配置内容 wezterm 默认用 cmd.exe, 我是 win11&#xff0c;希望默认用 powershell。 希望 wezterm 的主题配色&#xff0c;保持和 powershell 默认配色一致&#xff0c;也就是 Compell 主题。 目的是在频繁…

基于springboot+vue+Mysql的自习室预订系统

开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;…

【centos】vmware安装airflow流程

文章目录 1.下载系统https://mirrors.aliyun.com/centos/7/isos/x86_64/ 选择DVD20092.在VMware中&#xff0c;除了修改安装位置&#xff0c;其它选择默认安装centos73.用户名为root,登录4.网络适配器为桥接模式。5.使用命令消除显示器警告。6.安装anaconda6.1下载包&#xff1…

企业计算机服务器中了halo勒索病毒怎么处理,halo勒索病毒解密流程

随着网络技术的不断发展&#xff0c;网络在企业生产运营过程中发挥着重大作用&#xff0c;很多企业利用网络开展各项工作业务&#xff0c;网络也大大提高了企业的生产效率&#xff0c;但随之而来的网络数据安全问题成为众多企业关心的主要话题。近日&#xff0c;云天数据恢复中…

50个前端实战项目之04:隐藏的搜索小组件

大家好&#xff0c;我是宝哥。 今天讲50个前端实战项目之04&#xff1a;隐藏的搜索小组件。 源码下载地址 https://github.com/bradtraversy/50projects50days/tree/master/hidden-search 前端实战项目系列正在更新&#xff1a;04/50 01&#xff1a;可展开卡片02&#xff1a;进…

按键精灵纯本地离线文字识别插件

目的 按键精灵是一款可以模拟鼠标和键盘操作的自动化工具。它可以帮助用户自动完成一些重复的、繁琐的任务&#xff0c;节省大量人工操作的时间。但按键精灵是不包含图色功能&#xff0c;无法识别屏幕上的图像&#xff0c;根据图像的变化自动执行相应的操作。本篇文章主要讲解下…

Docker in Docker:深入解析与实战应用

Docker in Docker&#xff1a;深入解析与实战应用 一、引言 随着容器化技术的日益普及&#xff0c;Docker已经成为开发、测试、部署应用的标配工具。而在某些特定的场景下&#xff0c;如持续集成/持续部署&#xff08;CI/CD&#xff09;流水线中&#xff0c;我们可能需要在Do…

400 Bad Request问题

总结&#xff1a;请求路径写错了 400 问题 原地址&#xff0c;deleteSetmeal的参数应该改为param 更改请求地址正确后即可