本文是学习Java多线程与高并发知识时做的笔记。
这部分内容比较多,按照内容分为5个部分:
- 多线程基础篇
- JUC篇
- 同步容器和并发容器篇
- 线程池篇
- MQ篇
本篇为同步容器和并发容器篇。
目录
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
加油!(ง •_•)ง