讨伐Java多线程与高并发——同步容器和并发容器篇

本文是学习Java多线程与高并发知识时做的笔记。

这部分内容比较多,按照内容分为5个部分:

  1. 多线程基础篇
  2. JUC篇
  3. 同步容器和并发容器篇
  4. 线程池篇
  5. MQ篇

本篇为同步容器和并发容器篇。

目录

1 集合

1.1 List

1.2 Set

1.3 Map

2 阻塞队列

2.1 操作阻塞队列的四组API

2.1.1 抛出异常

2.1.2 返回值

2.1.3 阻塞等待

2.1.4 超时等待

2.2 同步队列

3 AQS


java.util包中的大部分容器都是非线程安全的。

若要在多线程中使用容器,可以使用java.util.Collections提供的包装函数:synchronizedXXX,将普通容器变成线程安全的同步容器。但该方法仅仅是简单地给容器使用同步,效率很低。

java.util.concurrent包提供了高效的并发容器,并且为了保持与普通的容器的接口一致性,仍然使用util包的接口,从而易于使用、易于理解。

1 集合

我们通常说的集合包括List、Set和Map。

List、Set和Map都是java.util包提供的接口。

1.1 List

List接口的所有实现类:

其中比较常用的有:

  • ArrayList
  • LinkedList

ArrayList容器是可调整大小的数组,它允许所有元素,包括null。

LinkedList容器是双向链表,它也允许包括null的所有元素。

ArrayList和LinkedList都是线程不安全的:

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class Test {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        //List<String> list = new LinkedList<>();
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                list.add("element"); //向list中添加元素"element"
                System.out.println(list); //打印list,存在遍历集合的行为
            }).start();
        }
    }
}

不管使用ArrayList还是LinkedList,执行上面的代码都会报错:

Exception in thread "Thread-XX" java.util.ConcurrentModificationException

发生这个异常的原因是:一个线程通常不允许修改 一个正在被其它线程遍历的集合。在这种情况下,迭代的结果是未定义的。

对于ArrayList和LinkedList的线程不安全问题,java.util包和java.util.concurrent包分别提供了解决方案。

java.util包提供的解决方案是:

提供了一个Collenctions类,该类提供了一组包装方法,可以将一个容器对象变为线程安全的。【装饰者模式】

public static <T> Collection<T> synchronizedCollection(Collection<T> c)
public static <T> List<T> synchronizedList(List<T> list)
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
public static <T> Set<T> synchronizedSet(Set<T> s)
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m)
public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s)

看到synchronized就明白了,这些包装方法是利用synchronized锁来实现线程同步的。

代码演示:

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

public class Test {
    public static void main(String[] args) {
        List<String> list = Collections.synchronizedList(new ArrayList<>());
        //List<String> list = Collections.synchronizedList(new LinkedList<>());
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                list.add(Thread.currentThread().getName());
                System.out.println(list);
            }).start();
        }
    }
}

执行代码,不再报java.util.ConcurrentModificationException异常。

java.util.concurrent包提供的解决方案是:

提供了一个CopyOnWriteArrayList类,通过创建CopyOnWriteArrayList类的实例代替ArrayList实例。

CopyOnWriteArrayList的原理:

当有线程向CopyOnWriteArrayList容器中写入数据时,会先copy出一个副本容器,再往这个副本容器里写入数据,最后把副本容器的引用地址赋值给原来的容器地址。在整个写入过程期间,如果有其它线程要读取数据,仍然是读取到原来容器里的数据。

为什么没有CopyOnWriteLinkedList?

原因是copy-on-write LinkedList与传统LinkedList相比没有任何性能优势。

实现CopyOnWriteLinkedList既浪费时间,又浪费了使用它的时间。

代码演示:

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class Test {
    public static void main(String[] args) {
        List<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                list.add(Thread.currentThread().getName());
                System.out.println(list);
            }).start();
        }
    }
}

运行无异常。

1.2 Set

Set接口的所有实现类:

其中比较常用的有:

  • HashSet
  • TreeSet

HashSet容器底层就是一个HashMap容器的key的集合,它的元素是无序、不可重复的,允许有null但只能有一个。

TreeSet容器底层是一个TreeMap容器的key的集合,它的元素是有序、不可重复的,允许有null但只能有一个。

HashSet和TreeSet都是线程不安全的,同样java.util.Collections类提供了包装方法、java.util.concurrent包提供了CopyOnWriteArraySet类 解决线程安全问题:

import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;

public class Test {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>(); //HashSet是线程不安全的
        //Set<String> set = new TreeSet<>(); //TreeSet是线程不安全的
        //解决方案1:java.util包提供了Collections类中的包装方法
        //Set<String> set = Collections.synchronizedSet(new HashSet<>());
        //Set<String> set = Collections.synchronizedSet(new TreeSet<>());
        //解决方案2:java.util.concurrent包提供了CopyOnWriteArraySet类
        //Set<String> set = new CopyOnWriteArraySet<>();
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                set.add(Thread.currentThread().getName());
                System.out.println(set);
            }).start();
        }
    }
}

1.3 Map

Map接口的所有实现类:

其中比较常用的有:

  • HashMap
  • TreeMap

HashMap容器是数组+链表的结构,它的元素是key-value键值对,其中key是无序、不可重复的,允许有null但只能有一个;value是可重复的,允许有null。

TreeMap容器是红黑树的结构,它的元素也是key-value键值对,其中key是有序、不可重复的,允许有null但只能有一个;value是可重复的,允许有null。

HashMap和TreeMap都是线程不安全的,同样java.util.Collections类提供了包装方法、java.util.concurrent包提供了ConcurrentHashMap类 解决线程安全问题:

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class Test {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>(); //HashMap是线程不安全的
        //Map<String, String> map = new TreeMap<>(); //TreeMap是线程不安全的
        //解决方案1:java.util包提供了Collections类中的包装方法
        //Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
        //Map<String, String> map = Collections.synchronizedMap(new TreeMap<>());
        //解决方案2:java.util.concurrent包提供了ConcurrentHashMap类
        //Map<String, String> map = new ConcurrentHashMap<>();
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                map.put(Thread.currentThread().getName(), Thread.currentThread().getName());
                System.out.println(map);
            }).start();
        }
    }
}

2 阻塞队列

队列是线性表的一种,它只允许从表的后端进行插入,从表的前端进行取值(删除)。即FIFO(first in first out),先入先出。

在java.util.concurrent包中,使用阻塞队列作为同步容器,来解决多线程高效安全传输数据的问题。

队列在什么情况下会发生阻塞?

  • 队列已满,发生阻塞
  • 队列为空,发生阻塞,等待生产

阻塞队列:BlockingQueue

BlockingQueue是java.util.concurrent包中的接口,它也实现了Collection接口。

BlockingQueue接口的所有实现类:

其中比较常用的有:

  • ArrayBlockingQueue,基于数组的阻塞队列
  • LinkedBlockingQueue,基于链表的阻塞队列
  • SynchronousQueue,同步队列

2.1 操作阻塞队列的四组API

java.util.concurrent包中提供了四组操作阻塞队列的API:

  • 抛出异常
  • 返回值
  • 阻塞等待
  • 超时等待

它们各有用途,可灵活使用。

操作\方式 抛出异常 返回值 阻塞等待 超时等待
添加 add offer put offer(,,)
移除 remove poll take poll(,)
判断队列首位 element peek - -

2.1.1 抛出异常

  • add(),向阻塞队列队尾插入元素,操作成功返回true,若队列已满发生阻塞,报异常java.lang.IllegalStateException。
  • remove(),从阻塞队列队首取值(删除),操作成功返回取值,若队列为空,报异常java.util.NoSuchElementException。
  • element(),查看阻塞队列队首元素,操作成功返回队首元素值,若队列为空,报异常java.util.NoSuchElementException。

测试代码:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        test1();
    }
    
    public static void test1() {
        BlockingQueue blockingQueue = new ArrayBlockingQueue(2); //设置阻塞队列的大小为2
        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        //System.out.println(blockingQueue.add("c")); //队列已满,发生阻塞,发生异常
        System.out.println(blockingQueue.element());
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        //System.out.println(blockingQueue.remove()); //队列为空,发生异常
        //System.out.println(blockingQueue.element()); //队列为空,发生异常
    }
}

运行结果:

true
true
a
a
b

2.1.2 返回值

  • offer(),向阻塞队列队尾插入元素,操作成功返回true,若队列已满发生阻塞,返回false。
  • poll(),从阻塞队列队首取值(删除),操作成功返回取值,若队列为空,返回null。
  • peek(),查看阻塞队列队首元素,操作成功返回队首元素值,若队列为空,返回null。

测试代码:

​import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        test2();
    }

    public static void test2() {
        BlockingQueue blockingQueue = new ArrayBlockingQueue(2); //设置阻塞队列的大小为2
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));
        System.out.println(blockingQueue.peek());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.peek());
    }
}

运行结果:

true
true
false
a
a
b
null
null

2.1.3 阻塞等待

  • put(),向阻塞队列队尾插入元素,没有返回值,若队列已满发生阻塞,当前线程等待。
  • take(),从阻塞队列队首取值(删除),操作成功返回取值,若队列为空,当前线程等待。

测试代码:

​import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        test3();
    }

    public static void test3() throws InterruptedException {
        BlockingQueue blockingQueue = new ArrayBlockingQueue(2); //设置阻塞队列的大小为2
        blockingQueue.put("a");
        blockingQueue.put("b");
        System.out.println("point1");
        //blockingQueue.put("c"); //队列已满,发生阻塞,线程等待
        System.out.println("point2");
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println("point3");
        //System.out.println(blockingQueue.take()); //队列为空,线程等待
        System.out.println("point4");
    }
}

运行结果:

point1
point2
a
b
point3
point4

2.1.4 超时等待

  • offer(,,):向阻塞队列队尾插入元素,操作成功返回true,若队列已满发生阻塞,当前线程等待,等待超过一定时间后,返回false,继续向下执行。
  • poll(,):从阻塞队列队首取值(删除),操作成功返回取值,若队列为空,当前线程等待,等待超过一定时间后,返回null,继续向下执行。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        test4();
    }

    public static void test4() throws InterruptedException {
        BlockingQueue blockingQueue = new ArrayBlockingQueue(2); //设置阻塞队列的大小为2
        System.out.println(blockingQueue.offer("a", 2, TimeUnit.SECONDS));
        System.out.println(blockingQueue.offer("b", 2, TimeUnit.SECONDS));
        System.out.println(blockingQueue.offer("c", 2, TimeUnit.SECONDS)); //超时等待时间:2秒
        System.out.println(blockingQueue.poll(2, TimeUnit.SECONDS));
        System.out.println(blockingQueue.poll(2, TimeUnit.SECONDS));
        System.out.println(blockingQueue.poll(2, TimeUnit.SECONDS)); //超时等待时间:2秒
        System.out.println("ok");
    }
}

运行结果:

true
true
false
a
b
null
ok

2.2 同步队列

同步队列:SynchronousQueue

SynchronousQueue类是BlockingQueue接口的实现类。

SynchronousQueue容器的特点:容量为1。放入一个元素后,必须等这个元素被取出来,才能再往里面放入元素。

测试代码:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

public class Test {
    public static void main(String[] args) {
        BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + " put 1");
                blockingQueue.put("1");
                System.out.println(Thread.currentThread().getName() + " put 2");
                blockingQueue.put("2");
                System.out.println(Thread.currentThread().getName() + " put 3");
                blockingQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "T1").start();
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + " take " + blockingQueue.take());
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + " take " + blockingQueue.take());
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + " take " + blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "T2").start();
    }
}

运行结果:

T1 put 1
T2 take 1
T1 put 2
T2 take 2
T1 put 3
T2 take 3

3 AQS

AQS:AbstractQueuedSynchronizer,抽象队列同步器。

AQS维护了一个volatile int 类型的变量state 和一个双链表实现的队列,队列中的元素为线程(Thread对象)。

state的初始值为0。

当一个线程A(使用ReentrantLock对象)调用lock()方法时,会调用一个tryAcquire()方法,尝试获取锁资源【CAS】:

若state为0,线程A获取锁资源成功,state+1,设线程A为独占线程;

若锁资源早已被另一个线程B独占,即state不为0,线程A获取锁资源失败,自旋等待;线程A自旋达到一定次数后,进入CLH队列。

线程B在持有锁资源的情况下再次调用lock()方法时,发生锁重入,state+1;线程B每次调用unlock()方法,state-1,直到state减为0时,线程B释放锁资源。

在线程B释放锁资源后,CLH队列中的线程继续抢夺锁资源。

tryAcquire()源码:

学习视频链接:

https://www.bilibili.com/video/BV1B7411L7tE

加油!(ง •_•)ง

猜你喜欢

转载自blog.csdn.net/qq_42082161/article/details/114002333
今日推荐