传统线程技术回顾
线程就是程序的一条执行线索
创建线程的两种传统方式
1. 在Thread子类覆盖的run方法中编写运行代码
涉及一个以往知识点:能否在run方法声明上抛出InterruptedException异常,以便省略run方法内部对Thread.sleep()语句的try…catch处理?
不行,子类不能抛出比父类更多的异常
2. 在传递给Thread对象的Runnable对象的run方法中编写代码
总结:查看Thread类的run()方法的源代码,可以看到其实这两种方式都是在调用Thread对象的run方法,如果Thread类的run方法没有被覆盖,并且为该Thread对象设置了一个Runnable对象,该run方法会调用Runnable对象的run方法。
问题:如果在Thread子类覆盖的run方法中编写了运行代码,也为Thread子类对象传递了一个Runnable对象,那么,线程运行时的执行代码是子类的run方法的代码?还是Runnable对象的run方法的代码?
子类的,因为子类已经把父类的run方法覆盖了,会以子类的run方法为准。为什么没有执行runnable对象?因为执行该对象的代码在父类中,现在父类Thread的代码被子类覆盖已经没有了,所以不可能执行父类中找runnable对象的方法
涉及到的一个以往知识点:匿名内部类对象的构造方法如何调用父类的非默认构造方法。
多线程机制会提高程序的运行效率吗?
不会,会更慢,因为CPU资源有限
为什么会有多线程下载呢?
是为了抢夺服务器带宽
传统定时器技术回顾
Timer类与TimerTask类
void schedule(TimerTask task, long delay, long period); //使用相对时间,delay表示多长时间后执行任务
void schedule(TimerTask task, Date firstTime, long period); //使用绝对时间
启动定时器的代码,过10秒钟后启动定时器,然后每过1秒定时器执行一次:
- new Timer().schedule(new TimerTask(){
- public void run() {
- System.out.println(Thread.currentThread().getName());
- }
- },
- 10000,
- 1000);
代码实例:
- package ThreadDemo;
- import java.util.*;
- public class TimerDemo {
- public static void main(String[] args) {
-
- new Timer().schedule(new TimerTask(){
- @Override
- public void run() {
- System.out.println("bombing!");
- }
-
- }, 5000);
-
-
- while(true){
- System.out.println(new Date().getSeconds());
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
定时器嵌套应用
需求:两秒启动一个炸弹,然后四秒启动一个炸弹,交替运行
方法一:静态全局变量
- package ThreadDemo;
- import java.util.*;
- public class TimerDemo {
-
- private static int count = 0;
- public static void main(String[] args) {
-
- class MyTimerTask extends TimerTask{
- @Override
- public void run() {
- count = (count+1)%2;
- System.out.println("bombing!");
- new Timer().schedule(new MyTimerTask(),2000+2000*count);
- }
- }
- new Timer().schedule(new MyTimerTask(),2000);
-
- while(true){
- System.out.println(new Date().getSeconds());
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
方法二:两个自定义定时器交替运行
- package ThreadDemo;
- import java.util.*;
-
- class MyTimerTask extends TimerTask{
- @Override
- public void run() {
- System.out.println("bombing!");
- new Timer().schedule(new MyTimerTask2(),2000);
- }
- }
-
- class MyTimerTask2 extends TimerTask{
- @Override
- public void run() {
- System.out.println("bombing!");
- new Timer().schedule(new MyTimerTask(),4000);
- }
- }
- public class TimerDemo {
- public static void main(String[] args) {
- new Timer().schedule(new MyTimerTask(),4000);
- while(true){
- System.out.println(new Date().getSeconds());
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
需求:工作日周一至周五发邮件,周末不发(使用开源工具Quartz)
传统线程互斥技术
静态方法中不能直接创建内部类的实例对象,因为内部类可以访问外部类的成员变量,而静态方法存在时还没有外部类的对象存在。
线程安全问题可以用银行转账来解释使用synchronized代码块及其原理使用synchronized方法分析静态方法所使用的同步监视器对象是什么?是所属类的字节码对象
不论是同步代码块还是同步方法(包括静态同步方法),只要他们使用的锁是同一个对象就可以实现互斥,即同步
传统线程同步通信技术
wait与notify实现线程间的通信
面试题:子线程循环10次,接着主线程循环100,接着又回到子线程循环10次,接着再回到主线程又循环100,如此循环50次,请写出程序。
最初写出来的代码如下:
经验:
- 要用到共同数据(包括同步锁)或共同算法的若干方法应该归在同一个类身上,这种设计正好体现了高类聚和程序的健壮性
- 锁是上在要操作的资源的类的内部方法中,而不是线程代码中!好处是以后该类交给任何线程自然就同步了,而不需要考虑互斥同步的问题。
- Eclipse中将运行结果保存至文件的操作:Run as-->Run Configuration-->Common-->File处打钩然后选择一个文件
线程范围内的共享数据
线程范围内共享变量的概念与作用
线程范围内共享数据的示意图
全局变量会被所有的线程都共享,现在需要实现同一个线程内不同模块间变量的共享
关于线程范围内的变量共享的举例,直接用程序代码进行时说明,创建两个线程,它们都访问了两个模块,两个模块都取值,同一个线程设置的值,只能被相同的线程获取。
- package cn.itcast.heima;
-
- import java.util.HashMap;
- import java.util.Map;
- import java.util.Random;
-
- public class ThreadScopeShareData {
- private static Map threadData = new HashMap();
- public static void main(String[] args) {
-
- for(int i=0;i<2;i++){
- new Thread(new Runnable(){
- public void run() {
- int data = new Random().nextInt();
- System.out.println(Thread.currentThread().getName()+" has put data: "+data);
- threadData.put(Thread.currentThread(),data);
- new A().get();
- new B().get();
- }
- }).start();
- }
- }
-
-
- static class A{
- public void get(){
- int data = threadData.get(Thread.currentThread());
- System.out.println("A from "+Thread.currentThread().getName()+" has put data: "+data);
- }
- }
-
-
- static class B{
- public void get(){
- int data = threadData.get(Thread.currentThread());
- System.out.println("B from "+Thread.currentThread().getName()+" has put data: "+data);
- }
- }
-
- }
运行结果:
应用:账户的转入转出。在同一个线程中有一个转入模块和一个转出模块,如果刚刚把钱转入时程序崩溃,转出模块还没有执行,就需要撤销之前的转入操作。并且只能提交己方线程的转出请求,而不能提交其他线程的转出请求。
ThreadLocal实现线程范围的共享变量
ThreadLocal类就相当于一个Map,用于实现线程内的数据共享,即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
每个线程调用全局ThreadLocal对象的set方法,就相当于往其内部的map中增加一条记录,key分别是各自的线程,value是各自的set方法传进去的值。在线程结束时可以调用ThreadLocal.clear()方法,这样会更快释放内存,不调用也可以,因为线程结束后也可以自动释放相关的ThreadLocal变量。
怎样得到线程结束的通知呢,或者是监听线程死亡的事件?比如监听虚拟机退出,Runtime类代表虚拟机,其addShutdownHook(Thread hook)方法会在虚拟机停止前运行传入线程的代码。那么要得到线程结束的通知,也会用到同样的思想。
注:api中包名以com.sun打头的是属于底层不被程序员调用的
ThreadLocal的应用场景:
- 订单处理包含一系列操作:减少库存量、增加一条流水台账、修改总账,这几个操作要在同一个事务中完成,通常也即同一个线程中进行处理,如果累加公司应收款的操作失败了,则应该把前面的操作回滚,否则,提交所有操作,这要求这些操作使用相同的数据库连接对象,而这些操作的代码分别位于不同的模块类中。
- 银行转账包含一系列操作: 把转出帐户的余额减少,把转入帐户的余额增加,这两个操作要在同一个事务中完成,它们必须使用相同的数据库连接对象,转入和转出操作的代码分别是两个不同的帐户对象的方法。
- 例如Strut2的ActionContext,同一段代码被不同的线程调用运行时,该代码操作的数据是每个线程各自的状态和数据,对于不同的线程来说,getContext方法拿到的对象都不相同,对同一个线程来说,不管调用getContext方法多少次和在哪个模块中getContext方法,拿到的都是同一个。
实验案例:定义一个全局共享的ThreadLocal变量,然后启动多个线程向该ThreadLocal变量中存储一个随机值,接着各个线程调用另外其他多个类的方法,这多个类的方法中读取这个ThreadLocal变量的值,就可以看到多个类在同一个线程中共享同一份数据。
实现对ThreadLocal变量的封装,让外界不要直接操作ThreadLocal变量。
- 对基本类型的数据的封装,这种应用相对很少见。
- 对对象类型的数据的封装,比较常见,即让某个类针对不同线程分别创建一个独立的实例对象。
总结:一个ThreadLocal代表一个变量,故其中里只能放一个数据,你有两个变量都要线程范围内共享,则要定义两个ThreadLocal对象。如果有一个百个变量要线程共享呢?那请先定义一个实体对象来装这一百个变量,然后在ThreadLocal中存储这一个对象。比如定义一个学生实体,存放姓名,年龄等变量。
如何设计线程范围内的共享对象?
如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,买票系统就可以这么做。
如果每个线程执行的代码不同,这时候需要用不同的Runnable对象,有如下两种方式来实现这些Runnable对象之间的数据共享:
- 将共享数据传递给Runnable实现类:将共享数据封装在另外一个对象中,然后将这个对象通过Runnable接口实现类的构造方法逐一传递给各个Runnable对象。每个线程对共享数据的操作方法也分配到那个对象身上去完成,这样容易实现针对该数据进行的各个操作的互斥和通信。
- 让Runnable实现类去访问共享数据:将这些Runnable对象作为某一个类中的内部类,共享数据作为这个外部类中的成员变量,每个线程对共享数据的操作方法也分配给外部类,以便实现对共享数据进行的各个操作的互斥和通信,作为内部类的各个Runnable对象调用外部类的这些方法。也可以将成员变量作为局部变量加上final。
- 上面两种方式的组合:将共享数据封装在另外一个对象中,每个线程对共享数据的操作方法也分配到那个对象身上去完成,对象作为这个外部类中的成员变量或方法中的局部变量,每个线程的Runnable对象作为外部类中的成员内部类或局部内部类。
- 总之,要同步互斥的几段代码最好是分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现它们之间的同步互斥和通信。
极端且简单的方式,即在任意一个类中定义一个static的变量,这将被所有线程共享。
面试题示例:
设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1。写出程序。
方式一(上述第三种方式):
- public class MultiThreadShareData {
- private static ShareData1 data1 = new ShareData1();
- public static void main(String[] args) {
- new Thread(new Runnable(){
- public void run() {
- while(true){
- data1.decrement();
- }
- }
-
- }).start();
- new Thread(new Runnable(){
- public void run() {
- while(true){
- data1.increment();
- }
- }
-
- }).start();
- }
- }
-
-
- class ShareData1 implements Runnable{
- private int j=0;
- public synchronized void increment(){
- j++;
- }
- public synchronized void decrement(){
- j--;
- }
- public void run() {
-
- }
- }
方式二(上述第二种方式):
- public class ThreadTest1
- {
- private int j;
- public static void main(String args[]){
- ThreadTest1 tt=new ThreadTest1();
- Inc inc=tt.new Inc();
- Dec dec=tt.new Dec();
- for(int i=0;i<2;i++){
- Thread t=new Thread(inc);
- t.start();
- t=new Thread(dec);
- t.start();
- }
- }
-
- private synchronized void inc(){
- j++;
- System.out.println(Thread.currentThread().getName()+"-inc:"+j);
- }
- private synchronized void dec(){
- j--;
- System.out.println(Thread.currentThread().getName()+"-dec:"+j);
- }
-
- class Inc implements Runnable{
- public void run(){
- for(int i=0;i<100;i++){
- inc();
- }
- }
- }
-
- class Dec implements Runnable{
- public void run(){
- for(int i=0;i<100;i++){
- dec();
- }
- }
- }
- }
Java5中的线程并发库
看java.util.concurrent包及子包的API帮助文档
- 看concurrent包的帮助文档页面,对并发库中涉及的内容有一个总体上的介绍:在并发编程中很常用的实用工具类。
- 如何看包的API帮助文档:可以先找到该包下的某个类的帮助页面,然后在该页面的顶部单击package超链接。
java.util.concurrent.atomic包
- 查看atomic包文档页下面的介绍:类的小工具包,支持在单个变量上解除锁的线程安全编程。
- 可以对基本数据,对数组中的基本数据,对类中的基本数据等进行操作
- 通过如下两个方法快速理解atomic包的意义:
- AtomicInteger类的boolean compareAndSet(expectedValue, updateValue);
- AtomicIntegerArray类的int addAndGet(int i, int delta);
- 顺带解释volatile类型的作用,需要查看java语言规范。Volatile的意思是说:在jvm中,一个线程更新了共享变量i,另外一个线程立即去读取共享区中的i时,读到的可能不是刚才另外那个线程更新过的结果,这就类似数据库中的事务隔离级别中的read uncommited,volatile就是解决这个问题的。
了解java.util.concurrent.lock包
原子性操作类的应用
AtomicInteger类
java.util.concurrent.atomic包下的AtomicInteger类可以解决
多线程访问同一个整数的问题
首先通过构造函数AtomicInteger(int initialValue)创建一个给定值的原子整数
然后比如调用对象上的addAndGet(int delta)方法返回对象中的数和delta的和(传入负数就是相减),该方法的调用过程中别的线程无法参与进来,因此自动实现了线程同步。还有其他的方法比如decrementAndGet()和incrementAndGet()
一般用于定义被多线程访问的成员变量而不是局部变量,因为局部变量会在每个线程中都有一份
类似的有AtomicBoolean、AtomicLong类操作不同的数据
AtomicIntegerArray类
java.util.concurrent.atomic包下的AtomicIntegerArray类用于解决
多线程操作数组中的一个整数的问题
addAndGet(int i, int delta) 以原子方式将给定值与索引 i 的元素相加。
AtomicIntegerFieldUpdater类
解决
多线程操作类的对象中存储的整数的问题
static AtomicIntegerFieldUpdater
newUpdater(Class tclass, String fieldName) 使用给定字段为对象创建和返回一个更新器
int
addAndGet(T obj, int delta) 以原子方式将给定值添加到此更新器管理的给定对象的字段当前值
线程池
线程池的概念
- 首先介绍在Tcp服务器编程模型的原理,每一个客户端连接用一个单独的线程为之服务,当与客户端的会话结束时,线程也就结束了,即每来一个客户端连接,服务器端就要创建一个新线程。
- 线程池首先创建一些线程,它们的集合称为线程池,当服务器接受到一个客户请求后,就从线程池中取出一个空闲的线程为之服务,服务完后不关闭该线程,而是将该线程还回到线程池中。
- 任务是提交给整个线程池,一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务,池中的线程会各自同时执行一个任务,其他任务会排队等待
- 所有任务执行完后线程还存在,程序不会结束,需要手动关闭线程池
- 不需要和传统方式一样把任务交给特定的线程执行,而是把任务交给线程池,如果池中有空闲线程,就执行该任务,否则任务就等待被执行
Executors类的应用
- 创建固定大小的线程池
- 创建缓存线程池//线程数可随需求变化
- 创建单一线程池(实现线程死掉之后重新启动)
关闭线程池
用线程池启动定时器
- 调用ScheduledExecutorService的schedule方法,返回的ScheduleFuture对象可以取消任务。
- 支持间隔重复任务的定时方式(scheduleAtFixedRate)。
- 所有的 schedule 方法都接受相对(相对现在) 延迟和周期作为参数,而不是绝对的时间或日期。将以
Date
所表示的绝对时间转换成要求的形式很容易。例如,要安排在某个以后的 Date 运行,可以使用:schedule(task, date.getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS)。
- package cn.itcast.heima;
-
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.TimeUnit;
-
- public class ThreadPool {
- public static void main(String[] args) {
-
- ExecutorService threadPool = Executors.newFixedThreadPool(3);
-
-
-
-
-
-
- for(int i=1;i<=10;i++){
- final int task = i;
- threadPool.execute(new Runnable(){
- public void run() {
-
- for(int j=1;j<=10;j++){
- try {
- Thread.sleep(20);
- } catch (InterruptedException e) {
-
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread().getName()+" is looping of "+j+" for task "+task);
- }
- }
- });
- }
- System.out.println("all of 10 tasks has committed!");
- threadPool.shutdown();
-
-
-
- Executors.newScheduledThreadPool(3).schedule(
- new Runnable(){
- public void run() {
- System.out.println("bombing!");
- }
- },
- 3,
- TimeUnit.SECONDS);
- }
- }
Callable&Future
使用Callable接口编写的线程中的任务会在线程运行结束会返回一个结果,该结果类型为Future
Future通过get()方法取得返回结果,线程没有结束get方法会一直等待。
返回的结果类型和Callable返回的结果类型必须一致,这是通过泛型来实现的。
Callable要采用ExecutorSevice的submit方法提交,而不是execute方法,因为execute方法没有返回值,返回的future对象可以取消任务。
CompletionService接口用于提交一组Callable任务,其take方法返回已完成的一个Callable任务对应的Future对象。
- 好比我同时种了几块地的麦子,然后就等待收割。收割时,则是哪块先成熟了,则先去收割哪块麦子。
应用:既然苦苦等待线程运行完毕返回的结果,还不如直接调用一个方法,运行完成就返回一个结果。
- class BoundedBuffer {
- final Lock lock = new ReentrantLock();
- final Condition notFull = lock.newCondition();
- final Condition notEmpty = lock.newCondition();
-
- final Object[] items = new Object[100];
- int putptr, takeptr, count;
-
- public void put(Object x) throws InterruptedException {
- lock.lock();
- try {
- while (count == items.length)
- notFull.await();
- items[putptr] = x;
- if (++putptr == items.length) putptr = 0;
- ++count;
- notEmpty.signal();
- } finally {
- lock.unlock();
- }
- }
-
- public Object take() throws InterruptedException {
- lock.lock();
- try {
- while (count == 0)
- notEmpty.await();
- Object x = items[takeptr];
- if (++takeptr == items.length) takeptr = 0;
- --count;
- notFull.signal();
- return x;
- } finally {
- lock.unlock();
- }
- }
- }
并法库包中的ArrayBlockingQueue 类提供了这项功能,因此没有理由去实现这个示例类
可以实现多个线程按顺序执行。下面代码实现主线程,子线程1,子线程2轮流交替运行:
Semaphore实现信号灯
Semaphore(信号)可以控制当前访问资源的线程个数,并提供了同步机制。例如,实现一个文件允许的并发访问数。
- Semaphore实现的功能就类似厕所有5个坑,假如有十个人要上厕所,那么同时能有多少个人去上厕所呢?同时只能有5个人能够占用,当5个人中的任何一个人让开后,其中在等待的另外5个人中又有一个可以占用了。
- 同步锁是一个坑,现在是多个坑一起参与资源的管理
- 另外等待的5个人中可以是随机获得优先机会,也可以是按照先来后到的顺序获得机会,这取决于构造Semaphore对象时传入的参数选项:public Semaphore(int permits, boolean fair),fair为 true则保证此信号量在争用时按先进先出的顺序授予许可
单个信号量的Semaphore对象可以实现互斥锁的功能,这时这个信号量就类似一个锁,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合(锁只能被本线程释放,不能被其他线程释放)。
- package cn.itcast.heima;
-
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.Semaphore;
-
- public class SemaphoreTest {
- public static void main(String[] args) {
- ExecutorService service = Executors.newCachedThreadPool();
- final Semaphore sp = new Semaphore(3);
-
- for(int i=0;i<10;i++){
- Runnable runnable = new Runnable(){
- public void run(){
- try {
- sp.acquire();
- } catch (InterruptedException e1) {
- e1.printStackTrace();
- }
- System.out.println("线程" + Thread.currentThread().getName() +
- "进入,当前已有" + (3-sp.availablePermits()) + "个并发");
- try {
- Thread.sleep((long)(Math.random()*10000));
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("线程" + Thread.currentThread().getName() +
- "即将离开");
- sp.release();
-
- System.out.println("线程" + Thread.currentThread().getName() +
- "已离开,当前已有" + (3-sp.availablePermits()) + "个并发");
- }
- };
- service.execute(runnable);
- }
- }
- }
管理停车位,一个小的电子设备,实时性强就要semaphore。
其他同步工具类
CyclicBarrier
一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。
表示大家彼此等待,大家集合好后才开始出发,分散活动后又在指定地点集合碰面,这就好比整个公司的人员利用周末时间集体郊游一样,先各自从家出发到公司集合后,再同时出发到公园游玩,在指定地点集合后再同时开始就餐,…。
Cyclic:循环的,有周期性的,Barrier:障碍物,屏障。多个线程干完各自的任务,在不同的时刻到达集合点后,就可以接着忙各自的工作去了,再到达新的集合点,再去忙各自的工作,到达集合点了用CyclicBarrier对象的await方法表示。想在什么地方集合,就在什么地方调用await方法
- package cn.itcast.heima;
- import java.util.concurrent.CyclicBarrier;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
-
- public class CyclicBarrierTest {
-
- public static void main(String[] args) {
- ExecutorService service = Executors.newCachedThreadPool();
- final CyclicBarrier cb = new CyclicBarrier(3);
- for(int i=0;i<3;i++){
- Runnable runnable = new Runnable(){
- public void run(){
- try {
- Thread.sleep((long)(Math.random()*10000));
-
- System.out.println("线程" + Thread.currentThread().getName() +
- "即将到达集合地点1,当前已有" + (cb.getNumberWaiting()+1) + "个已经到达," + (cb.getNumberWaiting()==2?"都到齐了,继续走啊":"正在等候"));
- cb.await();
-
- Thread.sleep((long)(Math.random()*10000));
- System.out.println("线程" + Thread.currentThread().getName() +
- "即将到达集合地点2,当前已有" + (cb.getNumberWaiting()+1) + "个已经到达," + (cb.getNumberWaiting()==2?"都到齐了,继续走啊":"正在等候"));
- cb.await();
- Thread.sleep((long)(Math.random()*10000));
- System.out.println("线程" + Thread.currentThread().getName() +
- "即将到达集合地点3,当前已有" + (cb.getNumberWaiting() + 1) + "个已经到达," + (cb.getNumberWaiting()==2?"都到齐了,继续走啊":"正在等候"));
- cb.await();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- };
- service.execute(runnable);
- }
- service.shutdown();
- }
- }
CountDownLatch
Latch:门闩,闩锁
犹如倒计时计数器,调用CountDownLatch对象的await方法让当前线程等待计数器到0,调用countDown方法就将计数器减1,当计数到达0时,则所有等待者或单个等待者开始执行。
可以实现一个人(也可以是多个人)等待其他所有人都来通知他,可以实现一个人通知多个人的效果,类似裁判一声口令,运动员同时开始奔跑,或者所有运动员都跑到终点后裁判才可以公布结果,用这个功能做百米赛跑的游戏程序不错哦!还可以实现一个计划需要多个领导都签字后才能继续向下实施的情况。
- package cn.itcast.heima;
- import java.util.concurrent.CountDownLatch;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- public class CountdownLatchTest {
- public static void main(String[] args) {
- ExecutorService service = Executors.newCachedThreadPool();
- final CountDownLatch cdOrder = new CountDownLatch(1);
- final CountDownLatch cdAnswer = new CountDownLatch(3);
- for(int i=0;i<3;i++){
- Runnable runnable = new Runnable(){
- public void run(){
- try {
- System.out.println("线程" + Thread.currentThread().getName() +
- "正准备接受命令");
- cdOrder.await();
- System.out.println("线程" + Thread.currentThread().getName() +
- "已接受命令");
- Thread.sleep((long)(Math.random()*10000));
- System.out.println("线程" + Thread.currentThread().getName() +
- "回应命令处理结果");
- cdAnswer.countDown();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- };
- service.execute(runnable);
- }
- try {
- Thread.sleep((long)(Math.random()*10000));
- System.out.println("线程" + Thread.currentThread().getName() +
- "即将发布命令");
- cdOrder.countDown();
- System.out.println("线程" + Thread.currentThread().getName() +
- "已发送命令,正在等待结果");
- cdAnswer.await();
- System.out.println("线程" + Thread.currentThread().getName() +
- "已收到所有响应结果");
- } catch (Exception e) {
- e.printStackTrace();
- }
- service.shutdown();
- }
- }
Exchanger
用于实现两个人之间的数据交换,每个人在完成一定的事务后想与对方交换数据,第一个先拿出数据的人将一直等待第二个人拿着数据到来时,才能彼此交换数据。
好比两个毒贩要进行交易,一手交钱、一手交货,不管谁先来到接头地点后,就处于等待状态了,当另外一方也到达了接头地点(所谓到达接头地点,也就是到到达了准备接头的状态)时,两者的数据就立即交换了,然后就又可以各忙各的了。
exchange方法就相当于两手高高举着待交换物,等待人家前来交换,一旦人家到来(即人家也执行到exchange方法),则两者立马完成数据的交换
- package cn.itcast.heima;
- import java.util.concurrent.Exchanger;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
-
- public class ExchangerTest {
-
- public static void main(String[] args) {
- ExecutorService service = Executors.newCachedThreadPool();
- final Exchanger exchanger = new Exchanger();
- service.execute(new Runnable(){
- public void run() {
- try {
- String data1 = "zxx";
- System.out.println("线程" + Thread.currentThread().getName() +
- "正在把数据" + data1 +"换出去");
- Thread.sleep((long)(Math.random()*10000));
- String data2 = (String)exchanger.exchange(data1);
- System.out.println("线程" + Thread.currentThread().getName() +
- "换回的数据为" + data2);
- }catch(Exception e){}
- }
- });
- service.execute(new Runnable(){
- public void run() {
- try {
- String data1 = "lhm";
- System.out.println("线程" + Thread.currentThread().getName() +
- "正在把数据" + data1 +"换出去");
- Thread.sleep((long)(Math.random()*10000));
- String data2 = (String)exchanger.exchange(data1);
- System.out.println("线程" + Thread.currentThread().getName() +
- "换回的数据为" + data2);
- }catch(Exception e){ }
- }
- });
- }
- }
可阻塞的队列BlockingQueue
队列:先进先出。包含固定长度队列和不固定长度队列
什么是可阻塞队列:队列满了添加线程会阻塞等待,队列空了获取线程也会阻塞等待。非阻塞队列则会直接报错
阻塞队列的作用与实际应用,阻塞队列的实现原理。
阻塞队列与Semaphore有些相似,但也不同,阻塞队列是一方存放数据,另一方释放数据,Semaphore通常则是由同一方设置和释放信号量。
BlockingQueue接口的子类提供了可阻塞队列功能,ArrayBlockingQueue类是固定大小的,LinkedBlockingQueue类可以是不固定大小的(数组是连续的一片内存,链表是不连续的内存)
在其接口类BlockingQueue中提供了3个插入方法。add抛异常,offer返回真假值,put方法阻塞。3个取的方法:remove抛出异常,poll返回null,take可阻塞
只有put方法和take方法才具有阻塞功能
用3个空间的队列来演示阻塞队列的功能和效果。
- package cn.itcast.heima;
- import java.util.concurrent.ArrayBlockingQueue;
- import java.util.concurrent.BlockingQueue;
- public class BlockingQueueTest {
- public static void main(String[] args) {
- final BlockingQueue queue = new ArrayBlockingQueue(3);
- for(int i=0;i<2;i++){
- new Thread(){
- public void run(){
- while(true){
- try {
- Thread.sleep((long)(Math.random()*1000));
- System.out.println(Thread.currentThread().getName() + "准备放数据!");
- queue.put(1);
- System.out.println(Thread.currentThread().getName() + "已经放了数据," +
- "队列目前有" + queue.size() + "个数据");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }.start();
- }
-
- new Thread(){
- public void run(){
- while(true){
- try {
-
- Thread.sleep(1000);
- System.out.println(Thread.currentThread().getName() + "准备取数据!");
-
- queue.take();
- System.out.println(Thread.currentThread().getName() + "已经取走数据," +
- "队列目前有" + queue.size() + "个数据");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }.start();
- }
- }
用两个具有1个空间的队列来实现同步通知的功能(你一下,我一下,轮流执行)。
在前面用Condition实现的同步通知的例子的基础上,改为用阻塞队列来实现。
第一个线程:A.take()……..B.put()
第二个线程:B.take()……..A.put()
- package cn.itcast.heima;
-
- import java.util.Collections;
- import java.util.concurrent.ArrayBlockingQueue;
- import java.util.concurrent.BlockingQueue;
- import java.util.concurrent.atomic.AtomicInteger;
-
- public class BlockingQueueCommunication {
- public static void main(String[] args) {
- final Business business = new Business();
- new Thread(
- new Runnable() {
- public void run() {
- for(int i=1;i<=50;i++){
- business.sub(i);
- }
- }
- }
- ).start();
-
- for(int i=1;i<=50;i++){
- business.main(i);
- }
- }
-
- static class Business {
-
- BlockingQueue queue1 = new ArrayBlockingQueue(1);
- BlockingQueue queue2 = new ArrayBlockingQueue(1);
-
-
- {
- Collections.synchronizedMap(null);
- try {
- System.out.println("xxxxxdfsdsafdsa");
- queue2.put(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
-
- public void sub(int i){
- try {
- queue1.put(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- for(int j=1;j<=10;j++){
- System.out.println("sub thread sequece of " + j + ",loop of " + i);
- }
- try {
- queue2.take();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
-
- public void main(int i){
- try {
- queue2.put(1);
- } catch (InterruptedException e1) {
- e1.printStackTrace();
- }
- for(int j=1;j<=100;j++){
- System.out.println("main thread sequece of " + j + ",loop of " + i);
- }
- try {
- queue1.take();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
同步集合Concurrent Collections
传统集合类在并发访问(多个线程访问)时会出现问题。比如死循环。
- 此时就需要在读的时候不能写,写的时候不能读,但可以并发地读。
- 如果不被多个线程并发访问,就使用传统集合
-
-
-
-
-
- count=4;
- while(hasNext()){
- next(){cursor++}
- }
-
- hasNext(){
- if(cursor==count)
- return false;
- return true;
- }
- remove(){
- count--;count=3;
- }
传统方式下用Collections工具类提供的synchronizedCollection方法来获得同步集合
- 比如Collections.synchronizedMap(new HashMap()),分析该方法的实现源码,可以到使用了代理类返回了一个同步集合,在该代理类中对被代理集合的方法都加上了synchronized关键字实现方法间的互斥
- 面试题---HashSet和HashMap的关系:HashSet的内部实现使用HashMap,只使用了HashMap的key部分,value部分不考虑(随便填写但是不使用),因为key不能重复,所以是一个Set
Java5中提供了如下一些同步集合类(通过看java.util.concurrent包下的介绍可以知道有哪些并发集合):
- ConcurrentHashMap
- ConcurrentSkipListMap/Set:可以排序的同步集合,需要指定比较器
- CopyOnWriteArrayList
- CopyOnWriteArraySet
传统方式下的Collection在迭代集合时,不允许对集合进行修改。
- 用空中网面试的同步级线程题进行演示(见下)
- 根据AbstractList的checkForComodification方法的源码,分析产生ConcurrentModificationException异常的原因。
使用同步集合实现迭代时对集合进行修改: