方法上加上 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 都提供了公平锁的版本,在没有特殊的情况下不要轻易开启公平锁特性,在任务不重的情况下开启公平锁可能会让性能指数级下降
相关文章
|
3月前
|
算法 Java 编译器
Synchronized你又知道多少?
Synchronized 是 JVM 实现的一种互斥同步机制,通过 monitorenter 和 monitorexit 指令控制对象锁的获取与释放。锁的本质是对象头的标记,确保同一时间只有一个线程访问资源。Synchronized 支持可重入性,允许方法内部调用其他同步方法而不阻塞。JVM 对锁进行了优化,引入了自旋锁、偏向锁、轻量级锁和重量级锁,以减少系统开销。Synchronized 属于悲观锁,而乐观锁基于 CAS(Compare and Swap)算法实现非阻塞同步,提高并发性能。
75 6
|
7月前
|
Java
synchronized
synchronized
43 2
|
7月前
|
存储 安全 Java
|
Java
07.synchronized都问啥?
大家好,我是王有志。经过JMM和锁的铺垫,今天我们正式进入synchronized的内容,来看看关于synchronized面试中都会问啥?
68 1
07.synchronized都问啥?
Synchronized
作用:能够保证在同一时刻最多有一个线程执行该段代码,以保证并发的安全性。(当第一个线程去执行该段代码的时候就拿到锁,并独占这把锁,当方法执行结束或者一定条件后它才释放这把锁,在没释放锁之前,所有的线程处于等待状态)
75 0
synchronized的总结
synchronized的总结
101 0
|
存储 缓存 安全
synchronized的简单理解
synchronized的简单理解
106 0
|
Java 编译器
10.关于synchronized的一切,我都写在这里了
大家好,我是王有志。我们已经完成了synchronized的学习,今天我们利用学习到的知识去回答一些关热点问题。
106 0
|
存储 缓存 安全
【Synchronized】
【Synchronized】
145 0
【Synchronized】