本章涉及内容:
- 使用非阻塞、线程安全lists集合
- 使用阻塞、线程安全lists集合
- 通过优先级确定顺序来使用阻塞式线程安全lists集合
- 通过延迟元素来使用线程安全lists集合
- 使用线程安全导航maps集合
- 生成一个并发随机数
- 使用原子变量
- 使用原子数组
简介
数据结构是程序最基础的元素。Java API提供java 集合框架来管理数据,当你在选择集合类要特别小心,有些集合类并不支持并发功能,这样会导致数据不一致情况,例如是ArrayList类。
java也提供并发集合类框架,它大致可以分为两种并发类
阻塞集合: 没有取到元素会一直阻塞。
非阻塞集合: 没有获取到元素会立即返回null值或抛出一个异常。线程不会阻塞。
这章会涉及的类:
- 非阻塞list集合,使用ConcurrentLinkedDeque类实现
- 阻塞list集合,使用LinkedBlockingDeque类实现
- 阻塞list集合来实现生产者和消费者模型。使用LinkedTransferQueue类实现
- 通过延迟元素来实现阻塞list集合,使用DelayQueue类
- 非阻塞导航map,使用ConcurrentSkipListMap 类
- 随机数,使用ThreadLocalRandom类
- 原子变量和数组,使用AtomicLong和AtomicIntegerArray类
1、使用非阻塞式线程安全的list集合
ConcurrentLinkedDeque类
package com.jack;
import java.util.concurrent.ConcurrentLinkedDeque;
public class AddTask implements Runnable{
private ConcurrentLinkedDeque<String> list;
public AddTask(ConcurrentLinkedDeque<String> list) {
super();
this.list = list;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i=0; i<10000; i++){
list.add(name+ " : Element " + i);
}
}
}
package com.jack;
import java.util.concurrent.ConcurrentLinkedDeque;
public class PollTask implements Runnable{
private ConcurrentLinkedDeque<String> list;
public PollTask(ConcurrentLinkedDeque<String> list) {
super();
this.list = list;
}
@Override
public void run() {
for (int i=0; i<5000; i++){
list.pollFirst();
list.pollLast();
}
}
}
package com.jack;
import java.util.concurrent.ConcurrentLinkedDeque;
public class Main {
public static void main(String[] args) {
ConcurrentLinkedDeque<String> list = new ConcurrentLinkedDeque<>();
Thread threads[] = new Thread[100];
for (int i=0; i<threads.length ; i++){
AddTask task = new AddTask(list);
threads[i] = new Thread(task);
threads[i].start();
}
System.out.printf("Main : %d AddTask 线程已经启动了\n", threads.length);
for (int i=0; i<threads.length; i++){
try {
threads[i].join();
} catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.printf("Main: List大小 : %d\n", list.size());
for(int i=0; i< threads.length; i++){
PollTask task = new PollTask(list);
threads[i] = new Thread(task);
threads[i].start();
}
System.out.printf("Main: %d PollTask 线程已经启动了\n", threads.length);
for (int i=0; i<threads.length; i++){
try {
threads[i].join();
} catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.printf("Main : 大小集合: %d\n", list.size());
}
}
日志:
Main : 100 AddTask 线程已经启动了
Main: List大小 : 1000000
Main: 100 PollTask 线程已经启动了
Main : 大小集合: 0
- 总结:
- 1、pollFirst移除第一个元素
- 2、pollLast移除最后一个元素
扩展
- 1、getFirst() 和getLast() 获取第一个元素和最后一个元素,并不会把元素移除,如果list集合为空,这个方法将会抛出NoSuchElementException异常。
- 2、peek(),peekFirst() 和peekLast() 返回一个元素和最后一个元素。但是不会移除元素。如果集合为空,返回null值。
- 3、remove(),removeFirst(), removeLast() 返回第一个元素和最后一个元素,会移除元素,如果为空,将会抛出NoSuchElementException异常。
2、使用阻塞线程安全的list集合
使用LinkedBlockingDeque实现阻塞list集合
package com.jack;
import java.util.Date;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
public class Client implements Runnable{
private LinkedBlockingDeque<String> requestList;
public Client(LinkedBlockingDeque<String> requestList) {
super();
this.requestList = requestList;
}
@Override
public void run() {
for (int i=0; i<3; i++){
for (int j=0; j<5; j++){
StringBuilder request = new StringBuilder();
request.append("[" + i);
request.append(",");
request.append(j +"]");
try {
requestList.put(request.toString());
} catch (InterruptedException e){
e.printStackTrace();
}
System.out.printf("客户端==添加的元素为%s 日期: %s.\n", request, new Date());
}
try{
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.printf("客户端: 执行完毕. \n");
}
}
1、创建一个LinkedBlockingDeque集合。
2、往里面添加元素 put() 如果有集合有大小限制,当它满了时候会阻塞
package com.jack;
import java.util.Date;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
public class Main {
public static void main(String[] args) throws Exception{
LinkedBlockingDeque<String> list = new LinkedBlockingDeque<>(3);
Client client = new Client(list);
Thread thread = new Thread(client);
thread.start();
for (int i=0; i<5; i++){
for(int j=0; j<3; j++){
String request = list.take();
System.out.printf("Main: 拿出元素为 : %s 日期为: %s. 当前集合的大小: %d\n", request, new Date(), list.size());
}
TimeUnit.MILLISECONDS.sleep(300);
}
System.out.printf("Main : 执行完毕.\n");
}
}
总结:
- 1、开启一个线程来向list集合添加元素
- 2、主线程循环来获取元素take()方法,如果集合没有元素,那么它会有一直阻塞。
扩展:
- 1、takeFirst() 和takeLast() 返回第一个和最后一个元素,同时会移除元素,如果集合为空,它会一直阻塞
- 2、getFirst() 和getLast() 返回第一个和最后一个元素,不会移除元素,如果集合为空,它将会抛出NoSuchElementException异常。
- 3、peek(), peekFirst(),和peekLast() 返回第一个和最后一个元素,不会移除元素,如果集合为空,它将返回null值
- 4、poll(), pollFirst() 和pollLast() 返回第一个和最后一个元素,同时会移除元素,如果集合为空,它会返回null值
- 5、add(), addFist()和addLast() 向第一个或最后一个位置添加元素,如果集合满了将会抛出IllegalStateException异常。
日志:
3、通过优先级使用阻塞线程安全有序list集合
通过PriorityBlockingQueue进行实现
package com.jack;
public class Event implements Comparable<Event> {
private int thread;
private int priority;
public Event(int thread, int priority) {
super();
this.thread = thread;
this.priority = priority;
}
public int getThread() {
return thread;
}
public int getPriority() {
return priority;
}
@Override
public int compareTo(Event o) {
if(this.priority>o.getPriority()){
return -1;
} else if(this.priority<o.getPriority()){
return 1;
}else{
return 0;
}
}
}
总结:创建一个Event实现Comparable接口,根据Priority进行比较
package com.jack;
import java.util.concurrent.PriorityBlockingQueue;
public class Task implements Runnable {
private int id;
private PriorityBlockingQueue<Event> queue;
public Task(int id, PriorityBlockingQueue<Event> queue) {
super();
this.id = id;
this.queue = queue;
}
@Override
public void run() {
for (int i=0; i<1000; i++){
Event event = new Event(id,i);
queue.add(event);
}
}
}
总结:创建一个PriorityBlockingQueue来添加元素,注意事件需要实现Comparable接口
package com.jack;
import java.util.concurrent.PriorityBlockingQueue;
public class Main {
public static void main(String[] args) throws Exception{
PriorityBlockingQueue<Event> queue = new PriorityBlockingQueue<>();
Thread taskThreads[] = new Thread[5];
for (int i=0; i<taskThreads.length; i++){
Task task = new Task(i, queue);
taskThreads[i] = new Thread(task);
}
for (int i=0; i<taskThreads.length; i++){
taskThreads[i].start();
}
for (int i=0; i<taskThreads.length;i++){
try{
taskThreads[i].join();
}catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.printf("Main: 队列的大小: %d\n", queue.size());
for(int i=0; i<taskThreads.length*1000; i++){
Event event = queue.poll();
System.out.printf("线程 %s: 优先级 %d\n", event.getThread(), event.getPriority(), event);
}
System.out.printf("Main : 队列大小: %d\n", queue.size());
System.out.printf("Main: 执行完毕了");
}
}
总结:
- 1、创建5个线程来执行添加任务
- 2、taskThreads[i].join()表示等待所有线程添加任务完成之后进行后面的内容
- 3、打印结果,会发现按照优先级继续排序。
扩展:
- 1、clear() 移除队列所有元素
- 2、take() 返回和移除第一个元素,如果队列中没有元素会一直阻塞
- 3、put(E e) 插入一个元素
- 4、peek(): 它会返回第一个元素,而不会移除它。
日志:
4、使用延迟来实现线程安全list集合
利用DelayedQueue类来实现延迟执行,但是必须要实现两个方法
compareTo(Delayed o): Delayed接口继承了Comparable接口
getDelay(TimeUnit unit) : 返回激活的延迟的时间
package com.jack;
import java.util.Date;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class Event implements Delayed {
private Date startDate;
public Event(Date startDate) {
super();
this.startDate = startDate;
}
@Override
public int compareTo(Delayed o) {
long result = this.getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
if(result <0){
return -1;
} else if(result>0){
return 1;
}
return 0;
}
@Override
public long getDelay(TimeUnit unit) {
Date now = new Date();
long diff = startDate.getTime() - now.getTime();
return unit.convert(diff, TimeUnit.MILLISECONDS);
}
}
总结:
- 1、这个Event实现接口 Delayed,然后实现两个方法 compareTo方法和getDelay()方法
- 2、以开始时间-当前时间得到延迟时间,最后将延迟时间转成对应的单位covert(diff, TimeUnit.MiLLISECONDS) diff延迟时间的数值,第二个参数是延迟的单位,unit本身单位是转换的目标单位(TimeUnit.NANOSECONDS)。
package com.jack;
import java.util.Date;
import java.util.concurrent.DelayQueue;
public class Task implements Runnable {
private int id;
private DelayQueue<Event> queue;
public Task(int id, DelayQueue<Event> queue) {
super();
this.id = id;
this.queue = queue;
}
@Override
public void run() {
Date now = new Date();
Date delay = new Date();
delay.setTime(now.getTime() + (id*1000));
System.out.printf("线程 %s 延迟了%s\n", id, delay);
for (int i=0; i<100; i++){
Event event = new Event(delay);
queue.add(event);
}
}
}
总结:主要设置延迟的时间为1秒,然后将它加入队列中。
package com.jack;
import java.util.Date;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.TimeUnit;
public class Main {
public static void main(String[] args) throws Exception{
DelayQueue<Event> queue = new DelayQueue<>();
Thread threads[] = new Thread[5];
for(int i=0; i<threads.length; i++){
Task task = new Task(i+1, queue);
threads[i] = new Thread(task);
}
for(int i=0; i<threads.length; i++){
threads[i].start();
}
for(int i=0; i<threads.length; i++){
threads[i].join();
}
do{
int counter =0;
Event event;
do{
event = queue.poll();
if(event != null){
counter++;
}
}while (event !=null);
System.out.printf("日期:%s 已经读取 %d 事件\n", new Date(), counter);
TimeUnit.MILLISECONDS.sleep(500);
}while (queue.size() >0);
}
}
总结:
- 1、创建5个线程代表5个延迟任务。
- 2、开始获取任务,判断获取任务时间是否为延迟之后的时间。
扩展:
- 1、注意Queue.size()方法返回数量包含活动和非活动的元素
- 2、clear() : 移除所有元素
- 3、offer(E e) :插入一个元素
- 4、peek(): 返回一个元素而不移除元素
- 5、take() 返回元素并且移除元素。如果队列为空,线程会阻塞到有元素为止。
日志:
线程 5 延迟了Sat Aug 19 16:25:51 CST 2017
线程 3 延迟了Sat Aug 19 16:25:49 CST 2017
线程 4 延迟了Sat Aug 19 16:25:50 CST 2017
线程 1 延迟了Sat Aug 19 16:25:47 CST 2017
线程 2 延迟了Sat Aug 19 16:25:48 CST 2017
日期:Sat Aug 19 16:25:46 CST 2017 已经读取 0 事件
日期:Sat Aug 19 16:25:47 CST 2017 已经读取 0 事件
日期:Sat Aug 19 16:25:47 CST 2017 已经读取 100 事件
日期:Sat Aug 19 16:25:48 CST 2017 已经读取 0 事件
日期:Sat Aug 19 16:25:48 CST 2017 已经读取 100 事件
日期:Sat Aug 19 16:25:49 CST 2017 已经读取 0 事件
日期:Sat Aug 19 16:25:49 CST 2017 已经读取 100 事件
日期:Sat Aug 19 16:25:50 CST 2017 已经读取 0 事件
日期:Sat Aug 19 16:25:50 CST 2017 已经读取 100 事件
日期:Sat Aug 19 16:25:51 CST 2017 已经读取 0 事件
日期:Sat Aug 19 16:25:51 CST 2017 已经读取 100 事件