一、同步容器类
同步容器类实现线程安全的方式是:将它们的状态封装起来,并且对每一个公有的方法都进行同步,使得每次只有一个线程能访问容器的状态。
1.同步容器类的问题
同步容器类是线程安全的,但是在并发的对容器进行复合操作时还需要额外的客户端加锁。容器上常见的复合操作:(1)迭代(2)跳转,即根据指定顺序找到当前元素的下一元素(3)条件运算,比如若没有则添加这类。同步容器类要通过自身的锁来保护它的每个方法,比如,操作容器的每个方法都要包括synchronized(list)
2.迭代器与ConcurrentModificationException
迭代表现的行为是“及时失败”,当它们发现容器在迭代过程中被修改时,就会抛出一个ConcurrentModificationException异常。对所有共享容器进行迭代的地方都需要加锁。
/*
* 迭代器与ConcurrentModificationException异常
* 解释为什么在迭代器中迭代时进行删除会报错,除了倒数第二个
* 首先会报的错误是ConcurrentModificationException异常
* 在List对象里面有一个protected transient int modCount = 0;modCount表示List对象被修改的次数
* 调用迭代器的next方法,每次执行这个方法都会先执行这个方法final void checkForComodification(),
* 它是用来判断modCount和expectedModCount的值是否相等,不相等 则报出那个错误,expectedModCount是迭代器的变量
* expectedModCount的初始值是等于modCount的即为0
* 每次调用List的remove,modCount的值都会加1
* 为什么删除倒数第二个元素的时候不会爆出异常呢?
* hasNext()方法:当返回的下一个元素不等于size时,就返回true: return cursor != size;
* cursor表示的是要返回的下一个元素,当遍历到倒数第二个元素q的时候,cursor=4,删除了以后size也变为了4,cursor和size相等了
* 则不满足while了,就不会执行next了,自然也没有异常了。
* 不仅是删除,添加修改都会另modCount的数值进行加1
*/
public class test2 {
public static void main(String[] args) {
List<String> list=new ArrayList<String>();
Collections.addAll(list, "a","c","e","q","p");
Iterator it=list.iterator();
while(it.hasNext()){
String str=(String) it.next();
if(str.equals("q")){
/*
* 如果容器在迭代中修改对象,则会抛出java.util.ConcurrentModificationException异常
* list.remove(str);
* 如果删除的是倒数第二个元素,则不会报错,但是最后一个元素就不会输出了
* 如果删除的是最后一个元素,则前面的正常输出,最后报错
* 如果删除的是倒数第二个前面的任何一个元素,会报错,而且被删除的后面的元素就不会输出了
*/
//it.remove();//利用迭代器自己的删除就不会报异常
list.remove(str);
}else{
System.out.println(str);
}
}
}
}
next方法里,首先要执行的就是checkForComodification()
checkForComodification()这个方法是判断modConut和expectedModCont是否相等的,如果不相等则会抛出异常
hasNext方法的源码:
3.隐藏迭代器
Set<Integer> set=new hashSet<>();
System.out.println("Dubug:"+set);
在打印的过程中,toString在对容器进行迭代,因此也有可能抛出ConcurrentModification异常
二、并发容器
同步容器将所有对容器状态的访问都进行串行化,以实现它们的线程安全性,但是这样做会降低并发性;并发容器是针对多个线程并发访问设计的。通过并发容器来替代同步容器可以极大的提高伸缩性并降低风险。
1.ConcurrentMap
ConcurrentMap并不是把每个方法都在同一个锁上同步使得每次只能有一个线程访问容器,它采用的是分段锁,在这种机制中,任意数量的读线程可以并发的访问Map,执行读操作和写操作的线程也可以并发的访问Map,一定数量的写线程也可以并发的访问Map。并发容器类提供的迭代器不会抛出ConcurrentModificationException,并非及时失败。由于ConcurrentHashMap不能被加锁来执行独占访问,因此无法使用客户端加锁来创建新的原子操作,但是一些常见的复合操作都已经实现为原子操作并且在ConcurrentMap的接口中声明,如下:
2.CopyOnWriteArrayList、CopyOnWriteArraySet
每次修改时,都会创建并重新发布一个新的容器副本,所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
三、阻塞队列和生产者-消费者模式
阻塞队列提供可阻塞的put和take方法,以及支持定时的offer和poll方法。如果队列已经满了,put方法将阻塞直到有空间可用;如果队列为空,take方法会阻塞直到有元素可用。
阻塞队列支持生产者-消费者模式,该模式把生产过程和消费过程解耦。阻塞队列提供的offer方法,如果数据项不能被添加到队列中,那么就会返回一个失败状态。
四、阻塞方法与中断方法
五、同步工具类
同步工具类封装了一些状态,这些状态将决定执行同步工具类的线程是继续执行还是等待。
1.闭锁
闭锁是一种同步工具类,可以延迟线程的进度直到其达到终止状态。当闭锁到达结束状态后,将不会再改变状态。闭锁可以保证某些活动直到其他活动都完成后才继续执行。
CountDownLatch是闭锁的一种实现,它可以使一个或者多个线程等待一组事件的发生。闭锁状态包括一个计数器,该计数器被初始化一个正数,表示需要等待的事件数量,countDown方法递减计数器,表示有一个事情已经发生了,await方法等到计数器达到0,表示所有需要等待的事情都已经发生了,如果计数器非0,await会一直阻塞直到计数器为0.
/*
* 闭锁:CountDownLatch
*/
public class test3 implements Runnable {
private CountDownLatch start;
private CountDownLatch finish;
private int num;
public test3(int num,CountDownLatch start,CountDownLatch finish){
this.num=num;
this.start=start;
this.finish=finish;
}
@Override
public void run() {
// TODO Auto-generated method stub
try {
System.out.println(num+"ready!");
start.await();
System.out.println(num+"finish!");
finish.countDown();//结束完一个就减1
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch start=new CountDownLatch(1);
CountDownLatch finish=new CountDownLatch(5);
for(int i=0;i<5;i++){
new Thread(new test3(i, start, finish)).start();
}
Thread.sleep(1000);
System.out.println("比赛开始!");
start.countDown();
finish.await();
System.out.println("比赛结束!");
}
}
结果:
2.FutureTask
FutureTask也可以用做闭锁。
(1)关于Runnable接口和Callable接口
Runnble接口:
public interface Runnable {
public abstract void run();
}
Callable接口:
public interface Callable<V> {
V call() throws Exception;
}
Runnable接口不能返回结果也不能抛出异常,Callable接口能返回结果也能抛出异常。
(2)Executor框架
任务:包括Runnable和Callable,其中Runnable表示一个可以异步执行的任务,而Callable表示一个会产生结果的任务
任务的执行:包括Executor框架的核心接口Executor以及其子接口ExecutorService。在Executor框架中有两个关键类ThreadPoolExecutor和ScheduledThreadPoolExecutor实现了ExecutorService接口。
异步计算的结果:包括接口Future和其实现类FutureTask
ExecutorService接口提供的方法:
<T> Future<T> submit(Callable<T> task); //submit提交一个实现Callable接口的任务,并且返回封装了异步计算结果的Future
<T> Future<T> submit(Runnable task, T result);//submit提交一个实现Runnable接口的任务,并且指定了在调用Future的get方法时返回的result对象
Future<?> submit(Runnable task);//submit提交一个实现Runnable接口的任务,并且返回封装了异步计算结果的Future。
(3 )Future<V>接口
Future<V>接口是用来获取异步计算解果的,即对具体的Runnable或者Callable对象任务执行的结果的获取。
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}
Future提供了3种功能:(1)能够中断执行中的任务(2)判断任务是否执行完成(3)获取任务执行完成后额结果。
但是Future只是一个接口,不能创建对象,因此有了FutureTask。
(4)FutureTask
public class FutureTask<V> implements RunnableFuture<V> { }
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
从上面的源码中我们可以看到FutureTask不仅实现了Future接口,还实现了Runnable接口,因此FutureTask也可以直接交给Executor执行。
(1)当FutureTask处于未启动或已启动状态时,如果此时我们执行FutureTask.get()方法将导致调用线程阻塞;当FutureTask处于已完成状态时,执行FutureTask.get()方法将导致调用线程立即返回结果或者抛出异常。
(2)当FutureTask处于未启动状态时,执行FutureTask.cancel()方法将导致此任务永远不会执行。
当FutureTask处于已启动状态时,执行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务,如果任务取消成功,cancel(...)返回true;但如果执行cancel(false)方法将不会对正在执行的任务线程产生影响(让线程正常执行到完成),此时cancel(...)返回false。
当任务已经完成,执行cancel(...)方法将返回false。
FutureTask的两种构造函数:
public FutureTask(Callable<V> callable) {
}
public FutureTask(Runnable runnable, V result) {
}
例子:
/*
* FutureTask提前计算结果,在主线程需要的时候直接通过get方法取得CallableTask的返回值
* FutureTask把计算结果从执行计算的线程传递到获取这个结果的线程
*/
public class CallableFutureTask {
public static void main(String[] args) {
ExecutorService es=Executors.newSingleThreadExecutor();//创建出线程池
CallableDemo callableDemo=new CallableDemo();
FutureTask<Integer> futureTask=new FutureTask<>(callableDemo);//创建FutureTask任务
es.submit(futureTask);//执行任务
es.shutdown();//关闭线程池
System.out.println("主线程在执行其他任务");
try {
if(futureTask.get()!=null){
System.out.println("futureTask.get()="+futureTask.get());
}else{
System.out.println("没有取到futureTask.get()的结果");
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("主线程执行完毕!");
}
}
class CallableDemo implements Callable<Integer>{
private int sum;
@Override
public Integer call() throws Exception {
// TODO Auto-generated method stub
System.out.println("callable子线程开始计算!");
// TODO Auto-generated method stub
for(int i=0;i<100;i++){
sum=sum+i;
}
System.out.println("callable子线程执行结束!");
return sum;
}
}
结果:
3.信号量
计数信号量(Counting Semaphore)用来控制同时访问某个特定资源的操作数量,或者同时执行某个特定操作的数量。Semaphore中管理着一组虚拟的许可,许可的初始数量可以通过构造函数指定。在执行操作时首先获得许可,并在使用以后释放许可,如果没有许可,那么acquire将阻塞直到有许可,release方法将返回一个许可给信号量。
public class testSemaphore {
public static void main(String[] args) {
ExecutorService es=Executors.newCachedThreadPool();
//true表示公平策略,先到先得,若为false表示随机,3表示有3个许可
final Semaphore semaphore=new Semaphore(3,true);
for(int i=0;i<10;i++){
Runnable runnable=new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
/*
* acquire用来申请资源
* acquire()用来申请一个资源
* acquire(int num)用来申请num个资源,如果当前没有这么多资源则会出去阻塞,直到有num个资源为止
*/
semaphore.acquire();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//semaphore.availablePermits()表示有多少个可以被使用
System.out.println(Thread.currentThread().getName()+"进入,当前系统的并发数是:"+(3-semaphore.availablePermits()));
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"即将离开");
semaphore.release();
System.out.println(Thread.currentThread().getName()+"已经离开了!当前系统的并发数:"+(3-semaphore.availablePermits()));
}
};
es.execute(runnable);
}
es.shutdown();
}
}
4.栅栏
栅栏与闭锁的关键区别在于,所有县城必须同时到达栅栏位置才能继续执行,闭锁用于等待事件,栅栏用于等待线程。
CyclieBarrier可以使一定数量的参与方反复的在栅栏位置汇集,当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有的线程都到达栅栏位置。当所有线程都到达了栅栏位置,则栅栏打开,所有线程都被释放。
public class TestCycBarrier {
public static void main(String[] args) {
//Runnable当成功汇合以后才会执行
CyclicBarrier barrier=new CyclicBarrier(5,new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("我们都准备好啦!");
}
});
ExecutorService es=Executors.newCachedThreadPool();
for(int i=0;i<5;i++){
es.execute(new Roommate(barrier));
}
es.shutdown();
}
}
class Roommate implements Runnable{
private CyclicBarrier barrier;
private static int count=1;
private int id;
public Roommate(CyclicBarrier barrier){
this.barrier=barrier;
this.id=count++;
}
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(id+"我到啦!");
try {
barrier.await();//只有都汇合了以后才会打开
System.out.println(id+":点菜吧!");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
结果: