方法上加上 synchronized 就可以了么

简介: 方法上加上 synchronized 就可以了么

方法上加上 synchronized 就可以了么

我们先来看一段代码

public static void main(String[] args) {
    
    
       DemoController demoController = new DemoController();
       new Thread(() -> demoController.sum()).start();
       new Thread(() -> demoController.isEqual()).start();
   }

   private volatile int counter1 = 0;
   private volatile int counter2 = 0;


   public void sum () {
    
    
       IntStream.rangeClosed(0, 1000).boxed().forEach(i -> {
    
    
           counter1 += i;
           counter2 += i;
       });
   }

   public void isEqual () {
    
    
       IntStream.rangeClosed(0, 1000).boxed().forEach(i -> {
    
    
           if (counter1 != counter2) {
    
    
               System.out.println("counter1 != counter2 counter1 = " + counter1 + " counter2 = " + counter2);
           }
       });
   }

代码中定义两个变量 counter1 和 counter2 , 方法 sum 对两个变量循环 1000 次进行求和;

方法 isEqual 对这两个变量进行测试看是否一直相等。如果一直相等,程序结束后是不会有输出的。从代码上来看, 我们在 sum 方法中同时对 counter1 和 counter2 进行加和, 它们应该说是一直相等的。 但是运行之后,我们会发现程序结束后有很多次都出现了 counter1 和 counter2 不相等的情况。

image-20230726151847569

对多线程敏感的朋友其实一眼就看到了,说,你这是多线程操作,需要保证多个操作换原子性,这样才能保证 counter1 和 counter2 时刻相等。

那这个代码要怎么改造呢?

既然 sum 方法不是原子性操作,那我们给 sum 方法无脑加上 synchronized 不就好了么?

public synchronized void sum () {
    
    
       IntStream.rangeClosed(0, 1000).boxed().forEach(i -> {
    
    
           counter1 += i;
           counter2 += i;
       });
   }

这样可以解决问题么? 我们运行看一下:

image-20230726152624733

没有内容输出,好像真的可以!别急,我们多运行几次程序

image-20230726152723329

image-20230726152741513

是的,高兴早了,并没有解决问题。

那为什么呢,我们在 sum 方法前面加上了 synchronized 关键字,保证了 sum 方法的操作原子性, 为啥不能解决这个问题呢?回过头来,我们再看看代码,会发现 sum 和 isEqual 方法运行在两个不同的线程中,thread1 运行的 sum 方法, thread2 运行的 isEqual 方法, 我们只保证 sum 方法的原子性是行的,因为 sum 方法至始至终都只运行在 thread1 中,并没有产生多线程的并发问题。产生并发问题的是 sum 方法和 isEqual 方法之间。所以要想解决这个问题应该是在 sum 和 isEqual 方法都加上 synchronized 关键字

public synchronized void sum () {
    
    
       IntStream.rangeClosed(0, 1000).boxed().forEach(i -> {
    
    
           counter1 += i;
           counter2 += i;
       });
   }

   public synchronized void isEqual () {
    
    
       IntStream.rangeClosed(0, 1000).boxed().forEach(i -> {
    
    
           if (counter1 != counter2) {
    
    
               System.out.println("counter1 != counter2 counter1 = " + counter1 + " counter2 = " + counter2);
           }
       });
   }

运行一下看看结果

image-20230726153909997

image-20230726153931025

image-20230726153946012

这样看起来才是真正把这个问题给解决了。


我们回过头来看看为什么会出现这种误区:在没有保证原子性的地方加上 synchronized 关键字保证原子性即可。

  1. 需要明确谁是共享变量
  2. 共享变量在哪里出现了资源的竞争
  3. 在哪里出现资源的竞争才需要在哪里加锁

上面的例子中, 共享变量就是 counter1 和 counter2 ;counter1 和 counter2 在 执行 sum 和 isEqual 时才出现了资源的竞争;所以我们要在 sum 和 isEqual 中加上 synchronized

为什么锁不住

再来看一个例子

public static void main(String[] args) {
    
    
       IntStream.rangeClosed(1, 10000).boxed().parallel().forEach(i -> new DemoController().add());
       System.out.println(DemoController.getA());
   }

   private static int a;

   public synchronized void add() {
    
    
       a++;
   }

   public static int getA() {
    
    
       return a;
   }

这个例子中, 一个共享变量 a, 一个方法 add 每次 +1 ,现在10000个并发对 a 变量进行加1 操作,最后输出 a 的值。add 方法前我们加了 synchronized ,按照预期来讲,我们 getA() 的时候应该得到的是 10000, 但真的是这样么?我们运行一下:

image-20230726165339881

很遗憾,结果并不是我们想要的。为什么会这样呢?

我们也严格按照前面讲的来做了呀:明确了 a 是共享变量;资源在 add 方法中出现了竞争;在 add 方法上加了 synchronized。

这是因为我们还要搞清楚一件事:我们加锁,锁的是谁

在这个例子中,变量 a 是静态的,它的锁级别应该是类级别的, 而 add 方法不静态方法, 在前面加上 synchronized ,锁的只是DemoController 类的某个实例,所以会出现明明加了 synchronized 还是出现并发问题。

既然知道了问题的所在,那就很好解决了。我们只需要把锁加在类级别上就可以了。

private static int a;

   private static Object locker = new Object();

   public void add() {
    
    
       synchronized (locker) {
    
    
           a++;
       }
   }

定义一个静态的 Object 对象用来当作锁的标志量,synchronized 锁的是 locker 这个标志量,那么这样这个锁就是在类级别了。我们再来验证一下

image-20230726173827299

哪哪都是 synchronized

方法上加 synchronized 关键字实现加锁确实很爽,那假如涉及到共享变量的方法都加了 synchronized,这个系统会变成什么样呢?想想都可怕。 这种不管三七二十一的 加上 synchronized 的做法有以下几个坏处:

  1. 真的没必要!通常情况我们的系统都是MVC三层架构,数据经过无状态的 Controller、Service、DAO 流转到DB,没必要使用 synchronized 来保护什么共享资源数据。
  2. 极大地降低性能!使用 Spring 框架时,默认情况下 Controller、Service、Repository 是单例的,加上 synchronized 会导致整个程序几乎就只能支持单线程,造成极大的性能问题,并发量大大降低。

那如果我们确实有一些共享数据需要保护,怎么办呢?

那我们需要要尽可能降低锁的粒度,仅对必要的代码块甚至是需要保护的资源本身加锁。

比如前段时间,有一个业务场景:有一个线程共享的 List, 里面存的是当前活跃的连接信息。

List 又有一段比较耗时的操作,但是不涉及到线程安全问题,那么我们应该如何加锁呢?

private List<Integer> list = new ArrayList<>();

   //不涉及共享资源的慢方法
   private void compute() {
    
    
       try {
    
     TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) {
    
     }
   }

   @GetMapping("long")
   public void lon9() {
    
    
       long start = System.currentTimeMillis();
       IntStream.rangeClosed(0, 1000).boxed().parallel().forEach(i -> {
    
    
           synchronized (this) {
    
    
               compute();
               list.add(i);
           }
       });
       log.info("花费时间为: {}", (System.currentTimeMillis() - start) / 1000.0);
   }

   @GetMapping("short")
   public void sh0rt() {
    
    
       long start = System.currentTimeMillis();
       IntStream.rangeClosed(0, 1000).boxed().parallel().forEach(i -> {
    
    
           compute();
           synchronized (this) {
    
    
               list.add(i);
           }
       });
       log.info("花费时间为: {}", (System.currentTimeMillis() - start) / 1000.0);
   }

image-20230726185116463

上面代码中,有一个共享资源 list, 对 list 会进行比较耗时的计算操作,另一个是模拟多个请求,long 接口 synchronized 的范围比较大, 运行结果可以看到, 耗时有 11s+ , short 接口synchronized 只进行了必要地方的进行括起来,耗时只有 1s+ 。

从这个例子中我们可以看到,使用 synchronized 对系统的性能影响是很大的。业务中真的要用到的地方一定要减小粒度。

那么问题来了,锁范围已经不能再缩小了,性能还无法满足需求的话,我们就要考虑另一个的粒度问题了。

即:区分读写场景以及资源的访问冲突,考虑使用悲观方式的锁还是乐观方式的锁。

常见的业务代码中,很少需要进一步考虑这两种更细粒度的锁,所以我就只说几个大概的结论。然后根据自己的需求来考虑是否有必要进一步优化:

  1. 对于读写比例差异明显的场景,考虑使用 ReentrantReadWriteLock 细化区分读写锁,来提高性能。
  2. JDK 里 ReentrantLock 和 ReentrantReadWriteLock 都提供了公平锁的版本,在没有特殊的情况下不要轻易开启公平锁特性,在任务不重的情况下开启公平锁可能会让性能指数级下降
相关文章
|
数据格式
Layui中table数据表格使用方法渲染 返回的数据不符合规范,正确的成功状态码应为:“code“: 0异常处理
Layui中table数据表格使用方法渲染 返回的数据不符合规范,正确的成功状态码应为:“code“: 0异常处理
1411 0
|
7月前
|
存储 缓存 安全
Java HashMap详解及实现原理
Java HashMap是Java集合框架中常用的Map接口实现,基于哈希表结构,允许null键和值,提供高效的存取操作。它通过哈希函数将键映射到数组索引,并使用链表或红黑树解决哈希冲突。HashMap非线程安全,多线程环境下需注意并发问题,常用解决方案包括ConcurrentHashMap和Collections.synchronizedMap()。此外,合理设置初始化容量和加载因子、重写hashCode()和equals()方法有助于提高性能和避免哈希冲突。
400 17
Java HashMap详解及实现原理
|
8月前
|
存储 Java API
SpringBoot整合Flowable【02】- 整合初体验
本文介绍了如何基于Flowable 6.8.1版本搭建工作流项目。首先,根据JDK和Spring Boot版本选择合适的Flowable版本(7.0以下)。接着,通过创建Spring Boot项目并配置依赖,包括Flowable核心依赖、数据库连接等。然后,建立数据库并配置数据源,确保Flowable能自动生成所需的表结构。最后,启动项目测试,确认Flowable成功创建了79张表。文中还简要介绍了这些表的分类和常用表的作用,帮助初学者理解Flowable的工作原理。
1570 0
SpringBoot整合Flowable【02】- 整合初体验
|
7月前
|
存储 小程序 前端开发
微信小程序与Java后端实现微信授权登录功能
微信小程序极大地简化了登录注册流程。对于用户而言,仅仅需要点击授权按钮,便能够完成登录操作,无需经历繁琐的注册步骤以及输入账号密码等一系列复杂操作,这种便捷的登录方式极大地提升了用户的使用体验
2347 12
|
11月前
|
机器学习/深度学习 人工智能 自然语言处理
软件测试中的人工智能:改变游戏规则的革新
在这篇技术性文章中,我们将深入探讨人工智能(AI)如何彻底改变了软件测试领域。从自动化测试到智能缺陷检测,AI不仅提高了测试的效率和准确性,还为软件开发团队提供了前所未有的洞察力。通过具体案例,本文揭示了AI在软件测试中应用的现状、挑战及未来趋势,强调了技术创新在提升软件质量与开发效率中的关键作用。
|
11月前
|
缓存 IDE Java
idea的maven项目打包时没有source下的文件
【10月更文挑战第21天】idea的maven项目打包时没有source下的文件
712 1
|
机器学习/深度学习 编解码 文件存储
深度学习中的模型压缩技术:从理论到实践
本文旨在探讨深度学习领域中的模型压缩技术,包括其背后的理论基础、常见方法以及在实际场景中的应用。我们将从基本的量化和剪枝技术开始,逐步深入到更高级的知识蒸馏和模型架构搜索。通过具体案例分析,本文将展示这些技术如何有效减少模型的大小与计算量,同时保持甚至提升模型的性能。最后,我们将讨论模型压缩技术未来的发展方向及其潜在影响。
|
小程序 Java 开发工具
【Java】@Transactional事务套着ReentrantLock锁,锁竟然失效超卖了
本文通过一个生动的例子,探讨了Java中加锁仍可能出现超卖问题的原因及解决方案。作者“JavaDog程序狗”通过模拟空调租赁场景,详细解析了超卖现象及其背后的多线程并发问题。文章介绍了四种解决超卖的方法:乐观锁、悲观锁、分布式锁以及代码级锁,并重点讨论了ReentrantLock的使用。此外,还分析了事务套锁失效的原因及解决办法,强调了事务边界的重要性。
308 2
【Java】@Transactional事务套着ReentrantLock锁,锁竟然失效超卖了
|
存储 Java
|
前端开发 JavaScript
VUE——Uncaught (in promise) TypeError: Cannot read property '__esModule' of undefined
VUE——Uncaught (in promise) TypeError: Cannot read property '__esModule' of undefined
335 0