使用了synchronized,竟然还有线程安全问题!

线程安全问题一直是系统亘古不变的痛点。这不,最近在项目中发了一个错误使用线程同步的案例。表面上看已经使用了同步机制,一切岁月静好,但实际上线程同步却毫无作用。

关于线程安全的问题,基本上就是在挖坑与填坑之间博弈,这也是为什么面试中线程安全必不可少的原因。下面,就来给大家分析一下这个案例。

有隐患的代码

先看一个脱敏的代码实例。代码要处理的业务逻辑很简单,就是多线程访问一个单例对象的成员变量,对其进行自增处理。

SyncTest类实现了Runnable接口,run方法中处理业务逻辑。在run方法中通过synchronized来保证线程安全问题,在main方法中创建一个SyncTest类的对象,两个线程同时操作这一个对象。

public class SyncTest implements Runnable {private Integer count = 0;@Overridepublic void run() {synchronized (count) {System.out.println(new Date() + " 开始休眠" + Thread.currentThread().getName());count++;try {Thread.sleep(10000);System.out.println(new Date() + " 结束休眠" + Thread.currentThread().getName());} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) throws InterruptedException {SyncTest test = new SyncTest();new Thread(test).start();Thread.sleep(100);new Thread(test).start();}
}

在上述代码中,两个线程访问SyncTest的同一个对象,并对该对象的count属性进行自增操作。由于是多线程,那就要保证count++的线程安全。

代码中使用了synchronized来锁定代码块,进行同步处理。为了演示效果,在处理完业务逻辑对线程进行睡眠。

理想的状况是第一个线程执行完毕,然后第二个线程才能进入并执行。

表面上看,一切都很完美,下面我们来执行一下程序看看结果。

执行验证

执行main方法打印结果如下:

Fri Jul 23 22:10:34 CST 2021 开始休眠Thread-0
Fri Jul 23 22:10:34 CST 2021 开始休眠Thread-1
Fri Jul 23 22:10:44 CST 2021 结束休眠Thread-0
Fri Jul 23 22:10:45 CST 2021 结束休眠Thread-1

正常来说,由于使用了synchronized来进行同步处理,那么第一个线程进入run方法之后,会进行锁定。先执行“开始休眠”,然后再执行“结束休眠”,最后释放锁之后,第二个线程才能够进入。

但分析上面的日志,会发现两个线程同时进入了“开始休眠”状态,也就是说锁并未起效,线程安全依旧存在问题。下面我们就针对synchronized失效原因进行逐步分析。

synchronized知识回顾

在分析原因之前,我们先来回顾一下synchronized关键字的使用。

synchronized关键字解决并发问题时通常有三种使用方式:

  • 同步普通方法,锁的是当前对象;

  • 同步静态方法,锁的是当前Class对象;

  • 同步块,锁的是()中的对象;

很显然,上面的场景中,使用的是第三种方式进行锁定处理。

synchronized实现同步的过程是:JVM通过进入、退出对象监视器(Monitor)来实现对方法、同步块的同步的。

代码在编译时,编译器会在同步方法调用前加入一个monitor.enter指令,在退出方法和异常处插入monitor.exit的指令。其本质就是对一个对象监视器(Monitor)进行获取,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。

原因分析

经过上面基础知识的铺垫,我们就来排查分析一下上述代码的问题。其实,对于这个问题,IDE已经能够给出提示了。

如果你使用的IDE带有代码检查的插件,synchronized (count)的count上会有如下提示:

Synchronization on a non-final field 'xxx' Inspection info: Reports synchronized statements where the lock expression is a reference to a non-final field. Such statements are unlikely to have useful semantics, as different threads may be locking on different objects even when operating on the same object.

很多人可能会忽视掉这个提示,但它已经明确指出此处代码有线程安全问题。提示的核心是“同步处理应用在了非final修饰的变量上”。

对于synchronized关键字来说,如果加锁的对象是一个可变的对象,那么当这个变量的引用发生了改变,不同的线程可能锁定不同的对象,进而都会成功获得各自的锁。

用一个图来回顾一下上述过程:

在上图中,Thread0在①处进行了锁定,但锁定的对象是Integer(0);Thread1中②处也进行锁定,但此时count已经进行自增,导致Thread1锁定的是对象Integer(1);也就是说,两个线程锁定的对象不是同一个,也就无法保证线程安全了。

解决方案

既然找到了问题的原因,我们就可以有针对性的进行解决,这里用的count属性很显然不可能用final进行修饰,不然就无法进行自增处理。这里我们采用对象锁的方式来进行处理,也就锁对象为当前this或者说是当前类的实例对象。修改之后的代码如下:

public class SyncTest implements Runnable {private Integer count = 0;@Overridepublic void run() {synchronized (this) {System.out.println(new Date() + " 开始休眠" + Thread.currentThread().getName());count++;try {Thread.sleep(10000);System.out.println(new Date() + " 结束休眠" + Thread.currentThread().getName());} catch (InterruptedException e) {e.printStackTrace();}}}// ...
}

在上述代码中锁定了当前对象,而当前对象在这个示例中是同一个SyncTest的对象。

再次执行main方法,打印日志如下:

Fri Jul 23 23:13:55 CST 2021 开始休眠Thread-0
Fri Jul 23 23:14:05 CST 2021 结束休眠Thread-0
Fri Jul 23 23:14:05 CST 2021 开始休眠Thread-1
Fri Jul 23 23:14:15 CST 2021 结束休眠Thread-1

可以看到,第一个线程完全执行完毕之后,第二个线程才进行执行,达到预期的同步处理目标。

上面锁定当前对象还是有一个小缺点,大家在使用时需要注意:比如该类有其他方法也使用了synchronized (this),那么由于两个方法锁定的都是当前对象,其他方法也会进行阻塞。所以通常情况下,建议每个方法锁定各自定义的对象。

比如,单独定义一个private的变量,然后进行锁定:

public class SyncTest implements Runnable {private Integer count = 0;private final Object locker = new Object();@Overridepublic void run() {synchronized (locker) {System.out.println(new Date() + " 开始休眠" + Thread.currentThread().getName());count++;try {Thread.sleep(10000);System.out.println(new Date() + " 结束休眠" + Thread.currentThread().getName());} catch (InterruptedException e) {e.printStackTrace();}}}
}

synchronized使用小常识

在使用synchronized时,我们首先要搞清楚它锁定的是哪个对象,这能帮助我们设计更安全的多线程程式。

在使用和设计锁时,我们还要了解一下知识点:

  • 对象建议定义为private的,然后通过getter方法访问。而不是定义为public/protected,否则外界能够绕过同步方法的控制而直接取得对象并改变它。这也是JavaBean的标准实现方式之一。

  • 当锁定对象为数组或ArrayList等类型时,getter方法获得的对象仍可以被改变,这时就需要将get方法也加上synchronized同步,并且只返回这个private对象的clone()。这样,调用端得到的就是对象副本的引用了。

  • 无论synchronized关键字加在方法上还是对象上,取得的锁都是对象,而不是把一段代码或函数当作锁。同步方法很可能还会被其他线程的对象访问;

  • 每个对象只有一个锁(lock)和之相关联;

  • 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制;

小结

通过本文的实践案例主要为大家输出两个关键点:第一,不要忽视IDE对代码的提示信息,某些提示真的很有用,如果深挖还能发现很多性能问题或代码bug;第二,对于多线程的运用,不仅要全面了解相关的基础知识点,还需要尽可能的进行压测,这样才能让问题事先暴露出来。


往期推荐

SpringBoot时间格式化的5种方法!


SpringBoot 如何统一后端返回格式?老鸟们都是这样玩的!


绝,Java 中创建对象的 5 种方法!



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

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

相关文章

序列图| 软件工程

什么是时序图? (What is Sequence Diagram?) Sequence Diagram is a "Connection Diagram" that represents a single structure or storyline executing in a system. It is the second most used UML diagram behind the class diagram. Sequence Diag…

终极解密输入网址按回车到底发生了什么?

详解输入网址点击回车,后台到底发生了什么。透析 HTTP 协议与 TCP 连接之间的千丝万缕的关系。掌握为何是三次握手四次挥手?time_wait 存在的意义是什么?全面图解重点问题,再也不用担心面试问这个问题。大致流程URL 解析&#xff…

unity, 相机空间 与 相机gameObject的局部空间

在unity里 相机空间 与 相机gameObject的局部空间 不重合。 Camera.worldToCameraMatrix的文档中有这样一句话: Note that camera space matches OpenGL convention: cameras forward is the negative Z axis. This is different from Unitys convention, where for…

Winform实现漂亮动画-小火车

一、起因 最近在做一个Winform的项目,其中需要一些加载动画,所以就搜索了一下找些思路,以下链接是本文的参考。 参考:Jeremie Martinez (译文链接) 注:原文中并没有给出图片资源,图…

synchronized 加锁 this 和 class 的区别!

作者 | 王磊来源 | Java中文社群(ID:javacn666)转载请联系授权(微信ID:GG_Stone)synchronized 是 Java 语言中处理并发问题的一种常用手段,它也被我们亲切的称之为“Java 内置锁”,由…

C# WinForm窗体四周阴影效果

一、起因 关于winform窗体无边框的问题很简单,只需要设置winform的窗体属性即可: FormBorderStyle FormBorderStyle.None; 但是这中无边框窗口实现的效果和背景完全没有层次的感觉,所以能加上阴影,突出窗口显示的感觉。 二、…

synchronized 优化手段之锁膨胀机制!

作者 | 王磊来源 | Java中文社群(ID:javacn666)转载请联系授权(微信ID:GG_Stone)synchronized 在 JDK 1.5 之前性能是比较低的,在那时我们通常会选择使用 Lock 来替代 synchronized。然而这个情…

NTFS USN的Create和工具代码汇总

1、 因为之前把相关代码放在了GitHub上,后来突然有人帮忙改了些个BUG,非常感谢 760193107,所以就写了个完整点的例子,希望对别人有所帮助。 GitHub项目地址 2、错误码:ERROR_JOURNAL_NOT_ACTIVE 在测试时&#xff…

在Java中,负数的绝对值不一定是正数!

作者 l Hollis来源 l Hollis(ID:hollischuang)绝对值是指一个数在数轴上所对应点到原点的距离,所以,在数学领域,正数的绝对值是这个数本身,负数的绝对值应该是他的相反数。这几乎是每个人都知道…

自己写着玩(二)

转载于:https://www.cnblogs.com/wangmengmeng/p/4572611.html

实战:隐藏SpringBoot中的私密数据!

这几天公司在排查内部数据账号泄漏,原因是发现某些实习生小可爱居然连带着账号、密码将源码私传到GitHub上,导致核心数据外漏,孩子还是没挨过社会毒打,这种事的后果可大可小。说起这个我是比较有感触的,之前我TM被删库…

JS的条形码和二维码生成

一、前言 最近做项目用到了JS生成条形码和二维码,内容不多,整理一下方便使用。 2018年7月5日更新: 二维码生成时,如果长度太长会有异常: Uncaught Error: code length overflow. (1604>1056) 创建的时候&#…

synchronized 中的 4 个优化,你知道几个?

作者 | 王磊来源 | Java中文社群(ID:javacn666)转载请联系授权(微信ID:GG_Stone)synchronized 在 JDK 1.5 时性能是比较低的,然而在后续的版本中经过各种优化迭代,它的性能也得到了前…

31Exchange Server 2010跨站点部署-搬迁Exchange服务器到分支机构

16.4 将EX07和EX08搬迁到上海分支机构首先我在上海分支机机构站点下创建一个CAS阵列,命令如下:下面获取下当前域中的CAS阵列信息16.4.1搬迁CAS,HT服务器1、从广州总部NLB群集删除EX07主机2、修改EX07的IP地址为分支机构IP地址 192.168.20.27(上海分支机构…

@Autowired的这些骚操作,你都知道吗?

前言最近review别人代码的时候,看到了一些Autowired不一样的用法,觉得有些意思,特定花时间研究了一下,收获了不少东西,现在分享给大家。也许Autowired比你想象中更强大。1. Autowired的默认装配我们都知道在spring中Au…

C# Winform 使用二维码

关于C# Winform 程序中使用二维码的使用记录: 1、使用 Nuget 安装 ZXing.Net 程序包; 2、调用代码: private void button1_Click(object sender, EventArgs e) {BarcodeWriter writer new BarcodeWriter();writer.Format BarcodeFormat…

[Swust OJ 85]--单向公路(BFS)

题目链接:http://acm.swust.edu.cn/problem/0085/ Time limit(ms): 5000      Memory limit(kb): 65535Description某个地区有许多城镇,但并不是每个城镇都跟其他城镇有公路连接,且有公路的并不都能双向行驶。现在我们把这些城镇间的公路分布及允许…

7 种分布式全局 ID 生成策略,你更爱哪种?

上了微服务之后,很多原本很简单的问题现在都变复杂了,例如全局 ID 这事!最近工作中刚好用到这块内容,于是调研了市面上几种常见的全局 ID 生成策略,稍微做了一下对比,供小伙伴们参考。当数据库分库分表之后…

C# 读取照片的EXIF信息

一、使用 MetadataExtractor 读取 EXIF 信息 1、NuGet 中安装 在 NuGet 中搜索并安装 MetadataExtractor; 2、包信息 我安装后会有两个包:MetadataExtractor 2.0.0 和 XmpCore 5.1.3 3、代码实现 我是创建的 WPF 项目: private void B…

ReentrantLock 中的 4 个坑!

作者 | 王磊来源 | Java中文社群(ID:javacn666)转载请联系授权(微信ID:GG_Stone)JDK 1.5 之前 synchronized 的性能是比较低的,但在 JDK 1.5 中,官方推出一个重量级功能 Lock&#x…