我工作三年了,该懂并发了(干货)(一)

简介: 本文的组织形式如下,主要会介绍到同步容器类,操作系统的并发工具,Java 开发工具包(只是简单介绍一下,后面会有源码分析)。同步工具类有哪些。

本文的组织形式如下,主要会介绍到同步容器类,操作系统的并发工具,Java 开发工具包(只是简单介绍一下,后面会有源码分析)。同步工具类有哪些。

微信图片_20220414220824.png

下面我们就来介绍一下 Java 并发中都涉及哪些模块,这些并发模块都是 Java 并发类库所提供的。

同步容器类

同步容器主要包括两类,一种是本来就是线程安全实现的容器,这类容器有 Vector、Hashtable、Stack,这类容器的方法上都加了 synchronized 锁,是线程安全的实现。

Vector、Hashtable、Stack 这些容器我们现在几乎都不在使用,因为这些容器在多线程环境下的效率不高。

还有一类是由 Collections.synchronizedxxx 实现的非线程安全的容器,使用 Collections.synchronized 会把它们封装起来编程线程安全的容器,举出两个例子

  • Collections.synchronizedList
  • Collections.synchronizedMap

我们可以通过 Collections 源码可以看出这些线程安全的实现

微信图片_20220414221000.png

要不为啥要称 Collections 为集合工具类呢?Collections 会把这些容器类的状态封装起来,并对每个同步方法进行同步,使得每次只有一个线程能够访问容器的状态。

其中每个 synchronized xxx都是相当于创建了一个静态内部类。

微信图片_20220414221003.png

虽然同步容器类都是线程安全的,但是在某些情况下需要额外的客户端加锁来保证一些复合操作的安全性,复合操作就是有两个及以上的方法组成的操作,比如最典型的就是 若没有则添加,用伪代码表示则是

if(a == null){
  a = get();
}

比如可以用来判断 Map 中是否有某个 key,如果没有则添加进 Map 中。这些复合操作在没有客户端加锁的情况下是线程安全的,但是当多个线程并发修改容器时,可能会表现出意料之外的行为。例如下面这段代码

public class TestVector implements Runnable{
    static Vector vector = new Vector();
    static void addVector(){
        for(int i = 0;i < 10000;i++){
            vector.add(i);
        }
    }
    static Object getVector(){
        int index = vector.size() - 1;
        return vector.get(index);
    }
    static void removeVector(){
        int index = vector.size() - 1;
        vector.remove(index);
    }
    @Override
    public void run() {
        getVector();
    }
    public static void main(String[] args) {
        TestVector testVector = new TestVector();
        testVector.addVector();
        Thread t1 = new Thread(() -> {
            for(int i = 0;i < vector.size();i++){
                getVector();
            }
        });
        Thread t2 = new Thread(() -> {
            for(int i = 0;i < vector.size();i++){
                removeVector();
            }
        });
        t1.start();
        t2.start();
    }
}

这些方法看似没有问题,因为 Vector 能够保证线程安全性,无论多少个线程访问 Vector 也不会造成 Vector 的内部产生破坏,但是从整个系统来说,是存在线程安全性的,事实上你运行一下,也会发现报错。

会出现

微信图片_20220414221008.png

如果线程 A 在包含这么多元素的基础上调用 getVector 方法,会得到一个数值,getVector 只是取得该元素,而并不是从 vector 中移除,removeVector 方法是得到一个元素进行移除,这段代码的不安全因素就是,因为线程的时间片是乱序的,而且 getVector 和 removeVector 并不会保证互斥,所以在 removeVector 方法把某个值比如 6666 移除后,vector 中就不存在这个 6666 的元素,此时 getVector 方法取得 6666 ,就会抛出数组越界异常。为什么是数组越界异常呢?可以看一下 vector 的源码

微信图片_20220414221011.png

如果用图表示的话,则会是下面这样。

微信图片_20220414221014.png

所以,从系统的层面来看,上面这段代码也要保证线程安全性才可以,也就是在客户端加锁 实现,只要我们让复合操作使用一把锁,那么这些操作就和其他单独的操作一样都是原子性的。如下面例子所示

static Object getVector(){
  synchronized (vector){
    int index = vector.size() - 1;
    return vector.get(index);
  }
}
static void removeVector(){
  synchronized (vector) {
    int index = vector.size() - 1;
    vector.remove(index);
  }
}

也可以通过锁住 .class 来保证原子性操作,也能达到同样的效果。

static Object getVector(){
  synchronized (TestVector.class){
    int index = vector.size() - 1;
    return vector.get(index);
  }
}
static void removeVector(){
  synchronized (TestVector.class) {
    int index = vector.size() - 1;
    vector.remove(index);
  }
}

在调用 size 和 get 之间,Vector 的长度可能会发生变化,这种变化在对 Vector 进行排序时出现,如下所示

for(int i = 0;i< vector.size();i++){
  doSomething(vector.get(i));
}

这种迭代的操作正确性取决于运气,即在调用 size 和 get 之间会修改 Vector,在单线程环境中,这种假设完全成立,但是再有其他线程并发修改 Vector 时,则可能会导致麻烦。

我们仍旧可以通过客户端加锁的方式来避免这种情况

synchronized(vector){
  for(int i = 0;i< vector.size();i++){
    doSomething(vector.get(i));
  }  
}

这种方式为客户端的可靠性提供了保证,但是牺牲了伸缩性,而且这种在遍历过程中进行加锁,也不是我们所希望看到的。

fail-fast

针对上面这种情况,很多集合类都提供了一种 fail-fast 机制,因为大部分集合内部都是使用 Iterator 进行遍历,在循环中使用同步锁的开销会很大,而 Iterator 的创建是轻量级的,所以在集合内部如果有并发修改的操作,集合会进行快速失败,也就是 fail-fast。当他们发现容器在迭代过程中被修改时,会抛出 ConcurrentModificationException 异常,这种快速失败不是一种完备的处理机制,而只是 善意的捕获并发错误。

如果查看过 ConcurrentModificationException 的注解,你会发现,ConcurrentModificationException 抛出的原则由两种,如下

微信图片_20220414221020.png

造成这种异常的原因是由于多个线程在遍历集合的同时对集合类内部进行了修改,这也就是 fail-fast 机制。

该注解还声明了另外一种方式

微信图片_20220414221024.png

这个问题也是很经典的一个问题,我们使用 ArrayList 来举例子。如下代码所示

public static void main(String[] args) {
  List<String> list = new ArrayList<>();
  for (int i = 0 ; i < 10 ; i++ ) {
    list.add(i + "");
  }
  Iterator<String> iterator = list.iterator();
  int i = 0 ;
  while(iterator.hasNext()) {
    if (i == 3) {
      list.remove(3);
    }
    System.out.println(iterator.next());
    i ++;
  }
}

该段代码会发生异常,因为在 ArrayList 内部,有两个属性,一个是 modCount ,一个是 expectedModCount ,ArrayList 在 remove 等对集合结构的元素造成数量上的操作会有 checkForComodification 的判断,如下所示,这也是这段代码的错误原因。

微信图片_20220414221028.png

fail-safe

fail-safe 是 Java 中的一种 安全失败 机制,它表示的是在遍历时不是直接在原集合上进行访问,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 ConcurrentModificationException。java.util.concurrent 包下的容器都是安全失败的,可以在多线程条件下使用,并发修改。

比如 CopyOnWriteArrayList, 它就是一种 fail-safe 机制的集合,它就不会出现异常,例如如下操作

List<Integer> integers = new CopyOnWriteArrayList<>();
integers.add(1);
integers.add(2);
integers.add(3);
Iterator<Integer> itr = integers.iterator();
while (itr.hasNext()) {
    Integer a = itr.next();
    integers.remove(a);
}

CopyOnWriteArrayList 就是 ArrayList 的一种线程安全的变体,CopyOnWriteArrayList 中的所有可变操作比如 add 和 set 等等都是通过对数组进行全新复制来实现的。

相关文章
|
8月前
|
算法 前端开发 Java
面试机会增加100倍,建议收藏!
面试机会增加100倍,建议收藏!
88 0
|
9月前
|
Java
项目实战典型案例20——内存长期占用导致系统慢
项目实战典型案例20——内存长期占用导致系统慢
55 0
|
9月前
|
Java
【项目实战典型案例】20.内存长期占用导致系统慢
【项目实战典型案例】20.内存长期占用导致系统慢
|
缓存 监控 前端开发
项目维护几年了,为啥还这么卡?
前段时间有个客户问我,为啥你们项目都搞了好几年了,为啥线上还会经常反馈卡顿,呃呃呃。。
基本时间单位 | 带你读《5G 空口设计与实践进阶 》之十五
为提供精确、一致的时间度量,NR 定义了最小时间单位 Tc。
基本时间单位 | 带你读《5G 空口设计与实践进阶 》之十五
|
Java 编译器 程序员
我工作三年了,该懂并发了(干货)(二)
本文的组织形式如下,主要会介绍到同步容器类,操作系统的并发工具,Java 开发工具包(只是简单介绍一下,后面会有源码分析)。同步工具类有哪些。
我工作三年了,该懂并发了(干货)(二)
|
Java Linux 数据库
我工作三年了,该懂并发了(干货)(四)
本文的组织形式如下,主要会介绍到同步容器类,操作系统的并发工具,Java 开发工具包(只是简单介绍一下,后面会有源码分析)。同步工具类有哪些。
我工作三年了,该懂并发了(干货)(四)
|
存储 安全 Java
我工作三年了,该懂并发了(干货)(三)
本文的组织形式如下,主要会介绍到同步容器类,操作系统的并发工具,Java 开发工具包(只是简单介绍一下,后面会有源码分析)。同步工具类有哪些。
我工作三年了,该懂并发了(干货)(三)
|
算法 大数据 程序员
自从掌握了软件开发的 5 条核心原则,我每天工作时至少可以多摸鱼 4 个小时
自从掌握了软件开发的 5 条核心原则,我每天工作时至少可以多摸鱼 4 个小时
161 0
自从掌握了软件开发的 5 条核心原则,我每天工作时至少可以多摸鱼 4 个小时
再谈研发那些事——两项核心工作的区别与联系
导读:不久前,技术工程师zhuoqun发表了两篇博客在《开发与研发:区别很大》和《开发与研发:领会编程魅力所在》引发了技术人士的热议。在那两篇文章里,zhuoqun谈到了程序开发两大类别:开发和研发的区别以及两类工程师的职业规划。
1238 0