synchronized:使用不规范,老板泪两行!

49b5e79f8fbad7fca4550e869fde0dec.jpeg

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

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

有隐患的代码

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

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)来实现对方法、同步块的同步的。

a4bd7af9cdb9ad48ab8539327c2b8fa6.jpeg

代码在编译时,编译器会在同步方法调用前加入一个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关键字来说,如果加锁的对象是一个可变的对象,那么当这个变量的引用发生了改变,不同的线程可能锁定不同的对象,进而都会成功获得各自的锁。

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

835026a4e9a2130097969364471c9e7c.jpeg

在上图中,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;第二,对于多线程的运用,不仅要全面了解相关的基础知识点,还需要尽可能的进行压测,这样才能让问题事先暴露出来。

f6b78126d4e360e8942ffc6f48a7d249.gif

往期推荐

4afb6a777af8237dd75ae8dd9e7d1b1b.jpeg

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


c8042f17b610d43f4c465d6238630163.jpeg

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


f041edd87d5d6a6fcfc1e88741e4abb9.jpeg

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


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

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

相关文章

SQL --运算符

2019独角兽企业重金招聘Python工程师标准>>> 一、<> (安全等于运算符) mysql中的 、<>或!运算符&#xff0c;相信大家已经很清楚了。今天看到了<>这个运算符&#xff0c;记录下来。 1><>和号的相同点 他们都是两个值比较符&#xff0c;相…

linux 文件浏览器_浏览Linux文件系统

linux 文件浏览器你为什么要学习&#xff1f; (Why would you want to learn?) Linux is probably the most used operating system when it comes to development. For a developer, Linux provides all the required tools. Learning how to navigate the Linux file system…

@Autowired 和 @Resource 的 5 点区别!

作者 | 磊哥来源 | Java面试真题解析&#xff08;ID&#xff1a;aimianshi666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;Autowired 和 Resource 都是 Spring/Spring Boot 项目中&#xff0c;用来进行依赖注入的注解。它们都提供了将依赖对…

rsync同步数据到内网

最近公司要求将IDC的APP日志备份到公司办公网内部&#xff0c;思前想后&#xff0c;结合以前学过的知识&#xff0c;决定用rsync直接推送&#xff0c;即从APP服务器上直接将日志推送到公司内网。这样避免了在生产服务器上额外安装更多软件而且只需要进行简单的配置&#xff0c;…

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

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;在我们日常工作中&#xff0c;时间格式化是一件经常遇到的事儿&#xff0c;所以本文我们就来盘点一下 Spring Boot 中时间格…

SpringBoot 解决跨域问题的 5 种方案!

作者 | 磊哥来源 | Java面试真题解析&#xff08;ID&#xff1a;aimianshi666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;跨域问题指的是不同站点之间&#xff0c;使用 ajax 无法相互调用的问题。跨域问题本质是浏览器的一种保护机制&#…

Java 中的 Lombok 到底能不能用?

一、摘要Java&#xff0c;作为一款非常热门的编程语言&#xff0c;尽管它有着非常丰富的语言特性&#xff0c;完全面向对象编程&#xff0c;编程高度规范化&#xff0c;但是也有一个最受大家诟病的一个缺点&#xff1a;啰嗦&#xff0c;尤其是当你开发了很多年之后&#xff0c;…

旅行商问题

旅行商问题 (Travelling Salesman problem) This problem can be stated as- "Given n number of cities and a travelling salesman has to visit each city. Then we have to find the shortest tour so that the travelling salesman can visit each and every city on…

分页 + 模糊查询竟然有坑?

不知道你有没有使用过Mysql的like语句&#xff0c;进行模糊查询&#xff1f;不知道你有没有将查询结果&#xff0c;进行分页处理&#xff1f;模糊查询&#xff0c;加上分页处理&#xff0c;会有意想不到的坑&#xff0c;不信我们继续往下看。我之前提供过一个品牌查询接口&…

导致事务@Transactional失效的5种场景!

作者 | 磊哥来源 | Java面试真题解析&#xff08;ID&#xff1a;aimianshi666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;一个程序中不可能没有事务&#xff0c;而 Spring 中&#xff0c;事务的实现方式分为两种&#xff1a;编程式事务和声…

操作系统 cpu调度_CPU调度| 操作系统

操作系统 cpu调度调度标准 (Scheduling Criteria) There are many criteria which have been suggested for comparing the CPU scheduling algorithms. The characteristics which are used for comparison and then used to determine the best algorithms, for this some of…

IOS中KVO模式的解析与应用

最近老翁在项目中多处用到了KVO&#xff0c;深感这种模式的好处。现总结如下&#xff1a; 一、概述 KVO,即&#xff1a;Key-Value Observing&#xff0c;它提供一种机制&#xff0c;当指定的对象的属性被修改后&#xff0c;则对象就会接受到通知。简单的说就是每次指定的被观察…

使用 lambda 实现超强的排序功能

我们在系统开发过程中&#xff0c;对数据排序是很常见的场景。一般来说&#xff0c;我们可以采用两种方式&#xff1a;借助存储系统&#xff08;SQL、NoSQL、NewSQL 都支持&#xff09;的排序功能&#xff0c;查询的结果即是排好序的结果查询结果为无序数据&#xff0c;在内存中…

java 的23种设计模式 之单身狗和隔壁老王的故事

2019独角兽企业重金招聘Python工程师标准>>> 觉得代码写的别扭了&#xff0c;回头翻翻java 的23种设计模式。today,额,这么晚了&#xff0c;困了。就弄个最简单的单例模式吧。单例模式&#xff1a;俗称单身狗 package singleton; public class SingleTon { private …

使用python学线性代数_二项式过程| 使用Python的线性代数

使用python学线性代数When we flip a coin, there are two possible outcomes as head or tail. Each outcome has a fixed probability of occurrence. In the case of fair coins, heads and tails each have the same probability of 1/2. In addition, there are cases in …

工作中常见的 6 种设计模式,你用过几种?

前言 哈喽&#xff0c;大家好。平时我们写代码呢&#xff0c;多数情况都是流水线式写代码&#xff0c;基本就可以实现业务逻辑了。如何在写代码中找到乐趣呢&#xff0c;我觉得&#xff0c;最好的方式就是&#xff1a;使用设计模式优化自己的业务代码。今天跟大家聊聊日常工作中…

这12款idea插件,能让你代码飞起来!

前言基本上每个程序员都会写代码&#xff0c;但写代码的速度不尽相同。为什么有些人&#xff0c;一天只能写几百行代码&#xff1f;而有些人&#xff0c;一天可以写几千行代码&#xff1f;有没有办法&#xff0c;可以提升开发效率&#xff0c;在相同的时间内&#xff0c;写出更…

node js 开发网站_使用Node JS开发网站

node js 开发网站You will have your own fully functional website running on "localhost" after going through this article. 阅读完本文后&#xff0c;您将在“ localhost”上运行自己的功能齐全的网站 。 Basic knowledge of JavaScript and HTML is a prereq…

Java:LocalDate / LocalDateTime加减时间

在线API参考&#xff1a;LocalTime (Java Platform SE 8 ) 方法介绍 方法1方法1说明plusYears(long years) minusYears(long years) 返回增加/减少了年数的副本plusMonths(long months) minusMonths(long months)返回增加/减少了月数的副本plusWeeks(long weeks) minusWeeks(…