多线程系列(三) -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.xdnf.cn/news/1412597.html

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

相关文章

牛客NC383 主持人调度(一)【简单 排序 Java/Go/C++】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/e160b104354649b69600803184094adb 思路 直接看代码&#xff0c;不难Java代码 import java.util.*;public class Solution {/*** 代码中的类名、方法名、参数名已经指定&#xff0c;请勿修改&#xff0c;直接返…

学习如何使用PyQt5实现notebook功能

百度搜索“pyqt5中notebook控件”&#xff0c;AI自动生成相应例子的代码。在 PyQt5 中&#xff0c;QTabWidget 类被用作 Notebook 控件。以下是一个简单的示例&#xff0c;展示如何创建一个带有两个标签的 Notebook 控件&#xff0c;并在每个标签中放置一些文本。 import sys f…

伺服电机初识

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

45. UE5 RPG 增加角色受击反馈

在前面的文章中&#xff0c;我们实现了对敌人的属性的初始化&#xff0c;现在敌人也拥有的自己的属性值&#xff0c;技能击中敌人后&#xff0c;也能够实现血量的减少。 现在还需要的就是在技能击中敌人后&#xff0c;需要敌人进行一些击中反馈&#xff0c;比如敌人被技能击中后…

40.乐理基础-拍号-什么是一拍

拍&#xff1a; 首先 以Y分音符的时长为一拍 这一句话&#xff0c;然后拍是音乐中的时长单位&#xff0c;但这个时长单位有点特殊&#xff0c;它并不是完全绝对的某一个时间&#xff0c;而正是因为如此&#xff0c;所以不能用 秒 之类的&#xff0c;已经很确定很绝对的时间单位…

C++手写协程项目(协程实现线程结构体、线程调度器定义,线程挂起、切换、恢复函数,模块测试)

协程结构体定义 之前我们使用linux下协程函数实现了线程切换&#xff0c;使用的是ucontext_t结构体&#xff0c;和基于这个结构体的四个函数。现在我们要用这些工具来实现我们自己的一个线程结构体&#xff0c;并实现线程调度和线程切换、挂起。 首先我们来实现以下线程结构体…

C语言自定义类型枚举、枚举类型的定义、枚举的特点、以及自定义类型联合体、联合类型的定义、联合的特点、联合大小的计算、联合判断大小端 的介绍

文章目录 前言一、枚举1. 枚举类型的定义2. 枚举的特点 二、联合&#xff08;共用体&#xff09;1. 联合类型的定义2. 联合的特点3. 联合大小的计算4. 联合体判断大小端1. 不适用联合体判断大小端2. 使用联合体判断大小端 总结 前言 C语言自定义类型枚举、枚举类型的定义、枚举…

520表白代码

一、以下代码用html及css编写 代码用记事本打开可直接使用 二、效果如下 代码如下&#xff0c;以下代码复制记事本里面&#xff0c;文件名称后缀改成.html格式&#xff0c;即可运行 名称可在记事本里进行更改 文件名称更改后&#xff0c;文件会变成如下图所示的样式 <ht…

STM32 PWM波定时溢出中断

打开定时器和中断 主函数初始化开启PWM和中断 HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_1); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, Pwm_data); HAL_TIM_Base_Start_IT(&htim2); 回调函数中判断是否为tim2 void HAL_TIM_PeriodElapsedCallback(TIM_Han…

Docker目录迁移

我们在生产环境中安装Docker时&#xff0c;默认的安装目录是 /var/lib/docker&#xff0c;而通常情况下&#xff0c;规划给系统盘的目录一般为50G&#xff0c;该目录是比较小的&#xff0c;一旦容器过多或容器日志过多&#xff0c;就可能出现Docker无法运行的情况&#xff0c;所…

视频下载器 UC网盘

老王导航 - 复杂问题找老王&#xff0c;简单问题百度搜 神器啊

Mysql 8.0.33 迁移至 Postgresql 16.2

小伙伴们&#xff0c;你们好&#xff0c;我是老寇&#xff0c;我又回来&#xff0c;几个月不见&#xff0c;甚是想念啊&#xff01;&#xff01;&#xff01;&#xff01; 这不&#xff0c;云平台需要改造&#xff0c;将Mysql替换成Postgresql&#xff0c;话说回来&#xff0c…

nginx--反向代理

反向代理 指的是代理外网用户的请求到内部的指定web服务器器&#xff0c;并将数据返回给用户的一种方式&#xff0c;这是用的比较多的一种方式 模块和功能 ngx_http_proxy_module&#xff1a; 将客户端的请求以http协议转发至指定服务器进行处理。ngx_stream_proxy_module&…

【Python项目】基于时间序列的【大气污染预测系统】

技术简介&#xff1a;使用Python技术、B/S架构、MYSQL数据库等实现。 系统简介&#xff1a;本系统的主要使用角色为普通用户和管理员用户&#xff0c;两者的功能几乎是一致的&#xff0c;但管理员用户比普通用户多了用户管理的功能&#xff0c;可以对系统内的用户进行管理。普通…

实时聊天系统设计

设计一个聊天系统最主要是保证消息能够及时可靠的从一端传入到另外一端&#xff0c;同时要支持对历史消息的查看。按照同时聊天人数聊天系统可以分&#xff0c;一对一&#xff08;one on one&#xff09;和群聊&#xff08;group chat&#xff09;&#xff0c;按照消息传递的及…

JavaEE >> Spring Boot 日志

日志的作用以及什么是日志 日志就是为了当程序出错的时候&#xff0c;程序员们可以通过日志看到是哪部分出现错误了&#xff0c;为了发现和定位问题。当然&#xff0c;我们还可以通过日志实现一些功能&#xff0c;如下&#xff1a; 记录系统的操作⽇志&#xff0c;⽅便数据恢…

Java常用命令总结 持续更新中!!!

蓝桥杯JAVA组 推荐输入输出示例 // 基础输入 import java.util.*;public class Main{public static void main(String[] args){} }// 非静态方法调用 new Main.Solution();//static函数里面调用非static函数 类.函数// 更快的输入方式 BufferedReader // 更快的输出方式 Print…

如何将jsp项目转成springboot项目

昨天说过&#xff0c;springboot推荐使用Thymeleaf作为前后端渲染的模板引擎&#xff0c;为什么推荐用Thymeleaf呢&#xff0c;有以下几个原因&#xff1a; 动静结合&#xff1a;Thymeleaf支持HTML原型&#xff0c;允许在HTML标签中增加额外的属性来实现模板与数据的结合。这样…

docker-compose编排集成工具,consul服务更新与发现

一、引言 我们知道使用一个 Dockerfile 模板文件可以定义一个单独的应用容器&#xff0c;如果需要定义多个容器就需要服务编排。服务编排有很多种技术方案&#xff0c;今天给大家介绍 Docker 官方产品 Docker-Compose Dockerfile 可以定义一个单独的应用容器&#xff1…

虾皮(Shopee)商品详情API接口:轻松获取商品深度信息

API接口概述 虾皮的商品详情API接口是专为商家和开发者提供的服务接口&#xff0c;通过该接口&#xff0c;您可以快速、准确地获取指定商品的详细信息。这些信息包括但不限于商品标题、价格、库存、描述、图片、规格参数等&#xff0c;为您的商品展示、比价、推荐等场景提供有…