篇外:推荐使用的IDE是基于Eclipse开源工程的进行Spring Boot, Spring Cloud 开发的STS(Spring Tools Suite)。
1 第一章 并发编程基础
1.1 熟悉并发容器类
同步类容器都是线程安全的。但在某些情况下需要加锁来保护复合操作。
1.1.1 同步类容器的常见问题
下面两种写法(增强For循环及迭代器While循环)对容器内容进行修改的时候会触发ConcurrentModificationException异常。
// 增强For循环中,移除元素导致异常Exception发生:ConcurrentModificationException
public Collection<String> m1(Vector<String> list){
for ( String temp : list) {
if("3".equals(temp)) {
list.remove(temp);
}
}
return list;
}
// 迭代器While循环中,移除元素导致异常Exception发生:ConcurrentModificationException
public Collection<String> m2(Vector<String> list){
Iterator<String> iterator = list.iterator();
while ( iterator.hasNext() ) {
String temp = iterator.next();
if("3".equals(temp)) {
list.remove(temp);
}
}
return list;
}
从下面的图上可以看到异常时怎么抛出的。
使用普通For循环可以避免上述异常,
// 普通For循环:Success
public Collection<String> m3(Vector<String> list){
for ( int i = 0; i<list.size(); i++ ) {
if("3".equals(list.get(i))) {
list.remove("3");
}
}
return list;
}
1.1.2 同步类容器的使用
同步类容器包括Vector, HashTable等。
这些容器的同步功能其实都是有JDK的Collections.synchronized等工厂方法去创建实现的。
其底层的机制无非就是用synchronized*关键字对每个公用的方法都进行同步,或者使用Object mutex对象锁的机制使得每次只能有一个线程访问容器的状态。
通过Collections.synchronizedCollection()方法,可以将普通的list变成为线程安全的list。
List<String> list= new ArrayList<>();
Collections.synchronizedCollection(list);
并发类容器概念
-
jdk5.0以后提供了多种并发类容器来替代同步类容器从而改善性能。
-
同步类容器的状态都是串行化的。
-
synchronized关键字虽然实现了线程安全,但是严重降低了并发性,在多线程环境时,严重降低了应用程序的吞吐量。
常见的并发类容器包括ConcurrentMap、CopyOnWrite***等。 -
ConcurrentMap接口下有俩个重要的实现:
ConcurrentHashMap
ConcurrentSkipListMap(支持并发排序功能) -
ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的HashTable,它们有自己的锁。
-
只要多个修改操作发生在不同的段上,它们就可以并发进行。把一个整体分成了16个段(Segment),也就是最高支持16个线程的并发修改操作。
-
这也是在多线程场景时减小锁的粒度从而降低锁竞争的一种方案。并且代码中大多共享变量使用volatile关键字声明,目的是第一时间获取修改的内容,性能非常好。
Copy On Write
Copy On Write 简称COW,是一种用于程序设计中的优化策略,可以在非常多的并发场景中使用到。
JDK里的COW容器有两种:CopyOnWriteArrayList、CopyOnWriteArraySet
什么是Copy On Write, 通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。这种容器适合于读多写少而且容器不太大的场景。
1.1.3 并发队列
在并发队列上JDK提供了两套实现,一个是以ConcurrentLinkedQueue为代表的高性能队列,一个是以BlockingQueue接口为代表的阻塞队列,无论哪种都继承自Queue接口!
ConcurrentLinkedQueue
ConcurrentLinkedQueue是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue性能好于BlockingQueue。它是一个基于链接节点的无界线程安全队列。
ConcurrentLinkedQueue重要方法:
- add() 和 offer() 都是加入元素的方法 (在ConcurrentLinkedQueue中,这俩个方法没有任何区别)
- poll() 和 peek() 都是取头元素节点,区别在于前者会删除元素,后者不会。
BlockingQueue
- offer(anObject): 表示如果可能的话, 将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳, 则返回true, 否则返回false.(本方法不阻塞当前执行方法的线程)
- offer(E o, long timeout, TimeUnit unit), 可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。
- put(anObject): 把anObject加到BlockingQueue里, 如果BlockQueue没有空间, 则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续.
- poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则知道时间超时还没有数据可取,返回失败。
- take(): 取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入;
- drainTo(): 一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。
模拟自己的阻塞队列
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class MyQueue {
// 整个队列的容器
private final LinkedList<Object> list = new LinkedList<>();
// 计数器
private final AtomicInteger count = new AtomicInteger(0);
private int maxSize = 0; //最大容量限制
private final int minSize = 0; //最小容量限制
private final Object lock = new Object(); //锁
public MyQueue(int maxSize) {
this.maxSize = maxSize;
}
public void put(Object obj) {
// 如果容器已满,等待空余出现
synchronized (lock) { //阻塞操作
while(count.get() == maxSize) {
try {
lock.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//添加新元素到容器里
list.add(obj);
count.getAndIncrement();
System.err.println("元素:" + obj + " 已经添加到容器中");
// 进行唤醒可能正在等待的take方法操作中的线程
lock.notify();
}
}
public Object take() {
Object temp = null;
synchronized (lock) { //阻塞操作
while (count.get() == minSize) {
try {
lock.wait();
}catch(InterruptedException e) {
e.printStackTrace();
}
}
temp = list.removeFirst();
count.getAndDecrement(); // i--
System.err.println("元素:" + temp + " 已经从容器移除");
// 进行唤醒可能正在等待的take方法操作中的线程
lock.notify();
}
return temp;
}
public int size() {
return count.get();
}
public List<Object> getQueueList() {
return list;
}
}
DelayQueue的使用
下面的例子讲的是在一个大型购物中心中设置的充气城堡的场景。 通过为时间付费,孩子们可以在相应的时间内在充气城堡中愉快滴玩耍^_^, 时间到了之后就会提醒孩子游玩结束,请父母来接孩子走。按照时间到达的顺序,对象依次离开队列。
下面是源代码。
import java.util.concurrent.DelayQueue;
public class BouncyCastle implements Runnable {
private DelayQueue<Child> delayQueue = new DelayQueue<>();
public boolean start = true;
/**
* 在充气城堡游玩。 到时间后通知大人来接孩子。 本方法启动游玩。
*
* @param name
* @param parentPhoneNumber
* @param money
*/
public void startPlaying(String name, String parentPhoneNumber, int money) {
Child child = new Child(name, parentPhoneNumber, System.currentTimeMillis() + money * 2000);
System.out.println("孩子姓名:" + name + ",家长电话:" + parentPhoneNumber + ",缴费: " + money + "元,现在开始游玩,可以玩"
+ String.valueOf(money * 2) + "秒。");
delayQueue.add(child);
}
/**
* 结束游玩
*
* @param child
*/
public void stopPlaying(Child child) {
System.out.println("时间到!请接走" + child.getName() + "小朋友,父母电话:" + child.getParentPhoneNumber());
}
public void run() {
while (start) {
try {
Child child = delayQueue.take();
stopPlaying(child);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
System.out.println("充气城堡开始营业!");
BouncyCastle bouncyCastle = new BouncyCastle();
Thread yingye = new Thread(bouncyCastle);
yingye.start();
bouncyCastle.startPlaying("宸宸", "13912345678", 1);
bouncyCastle.startPlaying("Elisa", "13811112222", 3);
bouncyCastle.startPlaying("Augus", "17355556666", 2);
}
}
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class Child implements Delayed{
// 孩子名字
private String name;
// 孩子家长电话
private String parentPhoneNumber;
// 游乐结束时间
private long endTime;
private final TimeUnit timeUnit = TimeUnit.MILLISECONDS;
/**
*
*/
public Child() {
}
/**
* @param name
* @param parentPhoneNumber
* @param endTime
*/
public Child(String name, String parentPhoneNumber, long endTime) {
super();
this.name = name;
this.parentPhoneNumber = parentPhoneNumber;
this.endTime = endTime;
}
@Override
public int compareTo(Delayed o) {
Child child = (Child)o;
long diff = this.getDelay(timeUnit) - child.getDelay(timeUnit);
return diff == 0 ? 0 : (diff > 0 ? 1 : -1 );
}
@Override
public long getDelay(TimeUnit unit) {
return endTime - System.currentTimeMillis() ;
}
public void setName(String name) {
this.name = name;
}
public void setEndTime(long endTime) {
this.endTime = endTime;
}
public void setParentPhoneNumber(String parentPhoneNumber) {
this.parentPhoneNumber = parentPhoneNumber;
}
public long getEndTime() {
return endTime;
}
public String getName() {
return name;
}
public String getParentPhoneNumber() {
return parentPhoneNumber;
}
}
这里是输出结果: