1.使用CopyOnwrite实现并发操作
进行并发读写
package juc;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
/*
* CopyOnWriteArrayList/CopyOnWriteArraySet : “写入并复制”
* 注意:添加操作多时,效率低,因为每次添加时都会进行复制,开销非常的大。并发迭代操作多时可以选择。
*/
public class TestCopyOnWriteArrayList {
public static void main(String[] args) {
/*
java.util.ArrayList
*/
ArrayList<String> names = new ArrayList<String>();
// ArrayList<String> names = new ArrayList<>();
// Vector<String> names = new Vector<>() ;//1.5
names.add("zs") ;
names.add("ls") ;
names.add("ww") ;
Iterator<String> iter = names.iterator();
while(iter.hasNext()) {
System.out.println(iter.next());//仅仅对集合进行 读操作,不会有异常
names.add("x");//仅仅对集合进行 写操作:因为ArrayList会动态扩容(1.5倍),因此names会无限扩大,因此会包
}
}
}
程序出现异常
zs
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at juc.TestCopyOnWriteArrayList.main(TestCopyOnWriteArrayList.java:24)
ConcurrentModifcationException产生原因
- ArrayList存在全局变量modCount(继承自AbstractList)并在ArrayList的内部类ITR中有一个exceptedModCount变量
- 当ArrayList进行迭代是,迭代器会先确保modCount和expectedModCount的一致性,如果不一致就抛出异常(与CAS算法较为相似的确认了数据的一致性)
- 上述例子中在迭代的同时向集合中添加元素(modCount++)导致modCount != expectedModCount,最终抛出异常。
java.util.ArrayList 907-910
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
modCount != expectedModCount这种确定代码一致性的操作被称为“fail-fast”策略,
修改为
CopyOnWriteArrayList<String> names = new CopyOnWriteArrayList<String>();
zs
ls
ww
解决办法为将同步类容器修改为并发类容器
同步类容器 | 并发类容器 |
---|---|
HashTable | ConcurrentHashMap |
vecttor | CopyOnWriteArrayList |
CopyOnWrite容器
容器包括了
- CopyOnWriteArrayList
- CopyOnWriteArraySet
操作原理为在遇到写操作时:
- 将当前容器复制一份,想新的容器中添加元素。
- 添加玩元素后更改原容器的引用指向新元素,原容器等待GC收集。
思考
利用冗余实现了读写分离,使得在读操作的时候依然可以进行写操作,不影响读操作时的数据。但是这种读写分离的方式需要使用容器的拷贝,写入操作过多的话就会非常影响性能。
2.使用ReadWriteLock实现读写锁
JUC提供的专门的读写锁,可以分贝用于对读操作或写操作进行加锁。
java.util.concurrent.locks.ReadWriteLock;
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
假设两线程:线程t1线程t2
- readLock():
添加了读锁的资源可以被多个线程同时读- 如果t1已经获取了读锁,此时t2想要进行写操作
- t2需要等待t1释放读锁
- t1与t2可以同时对资源添加读锁,同时进行读操作
- 如果其他线程需要进行写操作,要等待资源上说有的读锁消失
- writeLock():
写锁,独占锁。添加写锁之后不能在被其他线程读或写
示例
public class TestReadWriteLock {
// 读写锁
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
TestReadWriteLock test = new TestReadWriteLock();
//t1线程
new Thread(() -> {
//读操作
test.myRead(Thread.currentThread());
//写操作
test.myWrite(Thread.currentThread());
},"t1").start();
//t2线程
new Thread(() -> {
//读操作
test.myRead(Thread.currentThread());
//写操作
test.myWrite(Thread.currentThread());
},"t2").start();
}
//用读锁来锁定读操作
public void myRead(Thread thread) {
rwl.readLock().lock();
try {
for (int i = 0; i < 10000; i++) {
System.out.println(thread.getName() + "正在进行读操作");
}
System.out.println(thread.getName() + "===读操作完毕===");
} finally {
rwl.readLock().unlock();
}
}
//用写锁来锁定写操作
public void myWrite(Thread thread) {
rwl.writeLock().lock();
try {
for (int i = 0; i < 10000; i++) {
System.out.println(thread.getName() + "正在进行写操作");
}
System.out.println(thread.getName() + "===写操作完毕===");
} finally {
rwl.writeLock().unlock();
}
}
}
3.ConcurrentHashMap底层结果与演变过程
JDK7中的ConcurrentHashMap
采用数组+链表来实现,总体上来说是HashMap是一个数组,每一个数组元素是一张链表,向HashMap中写入数据时,会将元素的Key的hash值计算出元素在数组中的存储位置,具有相同hash值的元素被存放在相同的序列号的链表中。
JDK8之前,ConcurrentHashMap简介实现了Map<K,V>,将每一个元素成为segment,每个segment都是一个HashEntry<K,V>,然后对存入的链表数据进行重新的粒度划分,相同的在此存储为一个链表
默认情况下ConcurrentHashMap会生成16个segment.
在进行多线程访问时,对被访问的segment设置锁,其他segment可以被正常访问,减少访问锁带来的线程冲突。
JDK8中ConcurrentHashMap
JDK8中的HashMap进行了优化,当链表中的元素超过8个时链表会转换为红黑数,其余不变。只有超过了8个节点的链表会转换为红黑树
同时
ConcurrentHashMap也进行了同样的结构优化。
废弃了segment的加锁操作,对每一条数据使用volatile避免冲突,进行每一个元素的同步,更加细分了粒度。
4.BlockingQueue 实现排序和定时任务
BlockingQueue是JUC提供的一个用于控制线程同步的队列,可以给队列中的元素添加定时任务等功能。
方法 | 简介 |
---|---|
boolean add() | 向队列中添加元素,有剩余空间返回True否则抛出异常 |
boolean offer() | 向队列中添加元素,有剩余空间返回True没有返回False |
void put(E e) | 向队列中添加元素,吐过还有声韵空间则直接入队,否则当前线程一直等待知道有空闲位置时加入 |
E poll() | 取出队列对首的元素,若队列为空泽惠等待一段时间,若在等待时间队列依然为空返回NULL |
E take() | 取出排在队列队首的元素,若队列为空则一直等待 |
boolean remove(Obeject o) | 删除队列的o元素 |
int remainingCapacity() | 返回队列社会观念与容量,线程不安全,数据不真实 |
BlockingQueue的实现类分为有界队列和误解队列两种,有界队列是指队列中更多元素个数是有限的,而无界队列是指队列中元素个数可以是无穷多个的。
实现类
- ArrayBlockingQueue:由数组构成的有界阻塞队列,队列的大小由构造方法的参数决定。
- LinkedBlockingQueue:由链表结构组成的有界阻塞队列。可以通过构造方法的参数指定队列的大小,无参构造方法使用默认大小Integer.MAX_VALUE。
- PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列,排序规则可以通过构造方法中的Comparator对象指定
- DelayQueue:一个支持延迟存取的无界队列,如队列中的某个元素必须在第一时间后才能被取出
延迟阻塞队列示例 DelayQueue
假设三人进入游泳馆,各自游泳的时间为30分钟,45分钟,60分钟,时间到了自动离开
游泳者类
public class Swimmer implements Delayed {
private String name;
private long endTime;
public Swimmer() {
}
public Swimmer(String name, long endTime) {
this.name = name;
this.endTime = endTime;
}
public String getName() {
return this.name;
}
/*
是否还有剩余时间。
如果返回正数,表明还有剩余时间;
如果返回0或者负数,说明已超时;超时时,才会让DelayQueue的take()方法真正取出元素。
*/
@Override
public long getDelay(TimeUnit unit) {
return endTime - System.currentTimeMillis();
}
//线程(游泳者)之间,根据剩余时间的大小进行排序
@Override
public int compareTo(Delayed delayed) {
Swimmer swimmer = (Swimmer) delayed;
return this.getDelay(TimeUnit.SECONDS) - swimmer.getDelay(TimeUnit.SECONDS) > 0 ? 1 : 0;
}
}
游泳馆类
public class Natatorium implements Runnable {
// 用延迟队列模拟多个Swimmer,每个Swimmer的getDelay()方法就表示自己剩余的游泳时间
private DelayQueue<Swimmer> queue = new DelayQueue<Swimmer>();
// 标识游泳馆是否开业
private volatile boolean isOpen = true;
// 向DelayQueue中增加游泳者
public void addSwimmer(String name, int playTime) {
// 规定游泳的结束时间
long endTime = System.currentTimeMillis() + playTime * 1000 * 60;
Swimmer swimmer = new Swimmer(name, endTime);
System.out.println(swimmer.getName() + "进入游泳馆,可供游泳时间:" + playTime + "分");
this.queue.add(swimmer);
}
@Override
public void run() {
while (isOpen) {
try {
/*
* 注意:在DelayQueue中,take()并不会立刻取出元素。
* 只有当元素(Swimmer)所重写的getDelay()返回0或者负数时,才会真正取出该元素。
*/
Swimmer swimmer = queue.take();
System.out.println(swimmer.getName() + "游泳时间结束");
// 如果DelayQueue中的元素已被取完,则停止线程
if (queue.size() == 0) {
isOpen = false;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试类
public class TestNatatorium {
public static void main(String args[]) {
try {
Natatorium natatorium = new Natatorium();
Thread nataThread = new Thread(natatorium);
nataThread.start();
natatorium.addSwimmer("zs", 30);
natatorium.addSwimmer("ls", 45);
natatorium.addSwimmer("ww", 60);
} catch (Exception e) {
e.printStackTrace();
}
}
}
结果
zs进入游泳馆,可供游泳时间:30分
ls进入游泳馆,可供游泳时间:45分
ww进入游泳馆,可供游泳时间:60分
zs游泳时间结束
ls游泳时间结束
ww游泳时间结束
5.通过CountDownLatch 实现多线程闭锁
CountDownLatch是个线程同步工具,可以用来协调多个线程的执行时间。可以让A线程在其他线程执行结束后再执行。
CountDownLatch可以指定等待的线程数量,在其他线程执行结束后调用,在每一个线程结束后调用countDow()方法来使计数器递减。同时A线程调用await()方法用来等待计数器为0。当计数器为0时唤醒线程执行内容。
示例
public class TestCountDownLatch {
public static void main(String[] args) {
//计数器为10
CountDownLatch countDownLatch = new CountDownLatch(10);
//将CountDownLatch对象传递到线程的run()方法中,当每个线程执行完毕run()后就将计数器减1
MyThread myThread = new MyThread(countDownLatch);
long start = System.currentTimeMillis();
//创建10个线程,并执行
for (int i = 0; i < 10; i++) {
new Thread(myThread).start();
}
try {
//主线程(main)等待:等待的计数器为0;即当CountDownLatch中的计数器为0时,Main线程才会继续执行。
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start));
}
}
class MyThread implements Runnable {
private CountDownLatch latch;
public MyThread(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
try {
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
finally {
latch.countDown();//每个子线程执行完毕后,触发一次countDown(),即计数器减1
}
}
}
结果(根据不同的计算机效果不同)
耗时:3009
思考
- 如果注释掉countDownLatch.await()方法:
结果:线程取消等待,直接执行完成 - 如果注释掉countDown()方法:
线程一直等待,无法被唤醒继续执行。
6.CyclicBarrier在多线程中设置屏障
用于解决多个线程之间相互等待的问题。
线程执行时会使先到达的线程执行await()方法进行等待,之后当所有需要的线程到位后一起跨越屏障(执行)
示例
public class TestCyclicBarrier {
static class MyThread implements Runnable {
//用于控制会议开始的屏障
private CyclicBarrier barrier;
//参会人员
private String person;
public MyThread(CyclicBarrier barrier, String name) {
this.barrier = barrier;
this.person = name;
}
@Override
public void run() {
try {
Thread.sleep((int) (10000 * Math.random()));
System.out.println(person + " 已经到会...");
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(person + " 开始会议...");
}
}
public static void main(String[] args) throws IOException, InterruptedException {
//将屏障设置为3,即当有3个线程执行到await()时,再同时释放
CyclicBarrier barrier = new CyclicBarrier(3);
ExecutorService executor = Executors.newFixedThreadPool(3);
//三个人去开会
executor.submit(new MyThread(barrier, "zs"));
executor.submit(new MyThread(barrier, "ls"));
executor.submit(new MyThread(barrier, "ww"));
executor.shutdown();
}
}
结果
注:有时间滞后性,运行后体会。
ww 已经到会...
zs 已经到会...
ls 已经到会...
ls 开始会议...
ww 开始会议...
zs 开始会议...
7.使用FutureTask 和Callable 实现多线程
FutureTask
Future是JDK提供的一种用于多线程环境下异步问题的一种模式。
对话:
老板:“小王,吧会议记录整理好给我”
小王:“好的”
随后,小王理科开始整理,几分钟,将文件送给老板。
Future模式 就是以上情景的体现。客户端向服务端发起请求,服务端会立即向客户端发出回复,客户端拿到假的返回结果,当服务端处理完成后将结果返回给客户端。
优点:客户端在发出请求后可以马上拿到结果,不需要等待。
使用FutureTask时,使用get()获取最终的返回结果。get()是一个阻塞方法,会一直等待返回值出现。
Callable
类似于Runnalbe方法,需要重写一个 Call() 方法。拥有一个泛型返回值。通过start(()调用。
组合使用
public class TestCallable {
public static void main(String[] args) {
//创建一个Callable类型的线程对象
MyCallableThread myThread = new MyCallableThread();
//将线程对象包装成FutureTask对象,用于接收线程的返回值
FutureTask<Integer> result = new FutureTask<>(myThread);
//运行线程
new Thread(result).start();
//通过FutureTask的get()接收myThread的返回值
try {
Integer sum = result.get();//以闭锁的方式,获取线程的返回值
System.out.println(sum);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
class MyCallableThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("线程运行中...计算1-100之和");
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
Thread.sleep(3000);
return sum;
}
}
结果
线程运行中...计算1-100之和
5050