简单聊一聊公平锁和非公平锁,Parallel并行流

开发 后端
Synchronized会极大的降低程序的性能,导致整个程序几乎只能支持单线程操作,性能显著降低。那么,如何解决呢?

大家好,我是哪吒。

上一章提到了一个关于 i++ 和 ++i 的面试题打趴了所有人,最终方案是在两个方法上添加synchronized关键字,从而避免i++的线程安全问题,不过,这样真的好吗?在所有有线程安全的方法都添加synchronized?

答案是显而易见的,不行。

synchronized会极大的降低程序的性能,导致整个程序几乎只能支持单线程操作,性能显著降低。

那么,如何解决呢?

一、降低锁的粒度,将synchronized关键字不放在方法上了,改为synchronized代码块。

锁的粒度更小了,也解决了这个问题,确实可以的。

package com.guor.thread;

public class SynchronizedTest2 {
    int a = 1;
    int b = 1;

    public void add() {
        System.out.println("add start");
        synchronized (this) {
            for (int i = 0; i < 10000; i++) {
                a++;
                b++;
            }
        }
        System.out.println("add end");
    }

    public synchronized void compare() {
        System.out.println("compare start");
        synchronized (this) {
            for (int i = 0; i < 10000; i++) {
                boolean flag = a < b;
                if (flag) {
                    System.out.println("a=" + a + ",b=" + b + "flag=" + flag + ",a < b = " + (a < b));
                }
            }
        }
        System.out.println("compare end");
    }

    public static void main(String[] args) {
        SynchronizedTest2 synchronizedTest = new SynchronizedTest2();
        new Thread(() -> synchronizedTest.add()).start();
        new Thread(() -> synchronizedTest.compare()).start();
    }
}

为了更好的优化,有的时候可以将synchronized代码块变为区分读写场景的读写锁,也可以考虑悲观锁和乐观锁的区分。

对于读写场景比较多的情况,可以使用ReentrantReadWriteLock区分读写,再次降低锁的粒度,提高程序的性能。

ReentrantReadWriteLock 还可以选择提供了公平锁,在没有明确必须使用公平锁的情况下,尽量不要使用公平锁,公平锁会使程序性能降低很多很多。

二、先区分一下公平锁和非公平锁

  • 公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一个得到锁。
  • 非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,进入等待队列,如果能获取到,就直接获取到锁。

简单来说,公平锁(谁先排队,谁先执行),非公平锁(不用排队,每个人都有机会)。

1、公平锁

有一天早上,云韵、美杜莎、小医仙结伴去买酱香拿铁,到了咖啡店,先排队,一个一个来。不一会,哪吒来了,也买酱香拿铁,只能在末尾排队。这个就是公平锁。

2、非公平锁

但是呢?第二天早上,哪吒又去买酱香拿铁,上一次去晚了没买到(线程被饿死了),这次急了,要插队买,不讲武德。终于喝上了心心念念的酱香拿铁,这个就是非公平锁。

3、公平锁的优缺点:

  • 优点:所有线程都会获取到锁,只是一个时间的问题,不会出现有线程被饿死的情况;
  • 缺点:吞吐量会下降很多,队列里只有第一个线程能获取到锁,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会增大。

4、非公平锁的优缺点:

  • 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
  • 缺点:如果运气不好,会出现一致获取不到锁的情况,会被活活的饿死。

三、是否对症下药

我们都知道,静态字段属于类,类级别的锁才能保护;非静态字段属于类实例,实例级别的锁才能保护。

先看一下下面的代码:

import lombok.Data;

import java.util.stream.IntStream;

@Data
public class LockTest {
    public static void main(String[] args) {
        IntStream.rangeClosed(1, 100000).parallel().forEach(i -> new LockTest().increase());
        System.out.println(time);
    }

    private static int time = 0;

    public synchronized void increase() {
        time++;
    }
}

在LockTest类中定义一个静态变量time,定义一个非静态方法increase(),实现time++自增。先累加10万次,测试一下。看看是否有线程安全的问题。

这...不对啊,上一节在介绍高并发下i++线程安全问题的时候,synchronized 是好使的啊。

今天这是怎么了?再运行一次,结果依然如此,不等于100000

先来分析一下。

在非静态的方法上加synchronized,只能确保多个线程无法执行同一个实例的increase()方法,却不能保证不同实例的increase()方法。静态的变量time,在多个线程中共享,所以会出现线程安全的问题,synchronized失效了。

那么,将synchronized改为静态方法是不是就可以了,试一下。

有两种写法,一种是直接将方法改为静态方法,一种是使用synchronized代码块。

private static Object obj= new Object();
public void increase() {
    synchronized (obj) {
        time++;
    }
}

四、IntStream.rangeClosed是干嘛的?

很多小伙伴,可能会好奇,这个是干什么的,干了5年后端代码开发了,没见过这玩意儿。

IntStream是一种特殊的stream,用来提供对int相关的stream操作。

IntStream.rangeClosed:生成某个数字范围内的数字集合的stream。

比如上面代码中的IntStream.rangeClosed(1, 100000).parallel().forEach(i -> new LockTest().increase());。

  • range:不包含10000
  • rangeClosed:包含10000

五、parallel是干嘛的?

1、parallel()是什么

Stream.parallel() 方法是 Java 8 中 Stream API 提供的一种并行处理方式。在处理大量数据或者耗时操作时,使用 Stream.parallel() 方法可以充分利用多核 CPU 的优势,提高程序的性能。

Stream.parallel() 方法是将串行流转化为并行流的方法。通过该方法可以将大量数据划分为多个子任务交由多个线程并行处理,最终将各个子任务的计算结果合并得到最终结果。使用 Stream.parallel() 可以简化多线程编程,减少开发难度。

需要注意的是,并行处理可能会引入线程安全等问题,需要根据具体情况进行选择。

2、举一个简单的demo

定义一个list,然后通过parallel() 方法将集合转化为并行流,对每个元素进行i++,最后通过 collect(Collectors.toList()) 方法将结果转化为 List 集合。

使用并行处理可以充分利用多核 CPU 的优势,加快处理速度。

public class StreamTest {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }
        System.out.println(list);
        List<Integer> result = list.stream().parallel().map(i -> i++).collect(Collectors.toList());
        System.out.println(result);
    }
}

我勒个去,什么情况?

这是大部分开发人员都会犯的小错误,在上篇中提到过,i++ 返回原来的值,++i 返回加1后的值。这谁都知道,可是,写的时候,就不一定了,因为你习惯了i++,写顺手了,写的时候也是心不在焉,一蹴而就了。

i++改了++i即可。

3、parallel()的优缺点

优点:

  • 充分利用多核 CPU 的优势,提高程序的性能;
  • 可以简化多线程编程,减少开发难度。

缺点:

  • 并行处理可能会引入线程安全等问题,需要根据具体情况进行选择;
  • 并行处理需要付出额外的开销,例如线程池的创建和销毁、线程切换等,对于小数据量和简单计算而言,串行处理可能更快。

4、何时使用parallel()?

在实际开发中,应该根据数据量、计算复杂度、硬件等因素综合考虑。

比如:

  • 数据量较大,有1万个元素;
  • 计算复杂度过大,需要对每个元素进行复杂的计算;
  • 硬件够硬,比如多核CPU。

责任编辑:姜华 来源: 哪吒编程
相关推荐

2022-12-26 00:00:04

公平锁非公平锁

2022-05-09 07:37:04

Java非公平锁公平锁

2022-07-12 08:56:18

公平锁非公平锁Java

2019-01-04 11:18:35

独享锁共享锁非公平锁

2018-07-31 15:05:51

Java公平锁线程

2020-08-24 08:13:25

非公平锁源码

2022-02-21 15:01:45

MySQL共享锁独占锁

2021-08-20 07:54:20

非公平锁 Java多线编程

2022-12-08 17:15:54

Java并发包

2019-12-12 14:52:10

数据库脚本

2021-07-02 08:51:09

Redisson分布式锁公平锁

2023-05-09 12:46:00

linuxlock

2021-06-30 14:56:12

Redisson分布式公平锁

2020-02-02 13:59:59

MySQL数据库线程

2021-03-11 08:55:47

JavaUser对象

2021-07-01 09:42:08

Redisson分布式

2022-03-08 16:10:38

Redis事务机制

2018-01-10 14:13:04

测试矩阵API测试

2022-06-15 15:14:17

Java公平锁非公平锁

2022-04-14 07:56:30

公平锁Java线程
点赞
收藏

51CTO技术栈公众号