现在的位置: 首页 > 综合 > 正文

黑马程序员 – java高级特性 – 多线程及java5的线程并发库

2014年08月31日 ⁄ 综合 ⁄ 共 18142字 ⁄ 字号 评论关闭

-------android培训java培训、java基础学习技术博客、期待与您交流!
----------

8.1 传统的线程

8.1.1 启动传统线程

1. 创建和启动线程的两种传统方式
(1)继承Thread类,重写run方法;通过Thread类的start方法启动线程
(2)在传递给Thread对象的Runnable对象的run方法中编写代码
2. 示例代码

Thread thread = new Thread(){
	public void run() {
		while(true){
			try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}
			System.out.println("1:" + Thread.currentThread().getName());
		}
	}
};
thread.start();		
		
Thread thread2 = new Thread(new Runnable(){
	public void run() {
		while(true){
			try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}
			System.out.println("1:" + Thread.currentThread().getName());
		}
	}
});
thread2.start();

3.备注:
(1)查看Thread类的run()方法的源代码,可以看到其实这两种方式都是在调用Thread对象的run方法,如果Thread类的run方法没有被覆盖,并且为该Thread对象设置了一个Runnable对象,该run方法会调用Runnable对象的run方法。
(2)如果在Thread子类覆盖的run方法中编写了运行代码,也为Thread子类对象传递了一个Runnable对象,那么,线程运行时的执行代码是子类的run方法的代码。匿名内部类对象的构造方法如何调用父类的非默认构造方法。

8.1.2 传统线程定时器

传统的线程定时器类是Timer类
1.Timer的常用方法

(1)void schedule(TimerTask task,Date time);//在指定的时间指定任务。
(2)void schedule(TimerTask task,Date firstTime,long period);//在firstTime执行任务,然后每隔period在执行一次。
(3)void schedule(TimerTask task,long delay);//在指定的延迟时间之后指定任务。
(4)void schedule(TimerTask task,long delay,long period);//在指定的延迟时间之后执行任务,之后每隔一段时间在执行一次。
(5)void scheduleAtFixedRate(TimerTask task,Date firstTime,long period);//在firstTime执行一次,然后每隔period循环执行。
(6)void scheduleAtFixedRate(TimerTask task,long delay,long period);//在delay之后执行一次,然后每隔period循环执行。
task:所要安排的任务。
time:执行任务的时间。
firstTime:首次执行任务的时间。
period:执行各后续任务之间的时间间隔,单位是毫秒。
delay:执行任务前的延迟时间,单位是毫秒。
2.TimkerTask的常用方法
abstract void run();//此计时器任务要执行的操作。
3.示例代码

(1)过10秒钟后启动定时器,然后每过1秒定时器执行一次。

new Timer().schedule(new TimerTask() {
			public void run() {
				try {Thread.sleep(100);
					System.out.println(new Date().getSeconds());
					System.out.println(Thread.currentThread().getName());
				} catch (InterruptedException e) {e.printStackTrace();}
			}
		}, 10000, 1000);

(2)启动定时器,2秒后执行任务,然后4秒后执行任务,接着交替执行。

public class TraditionalTimerTest {
	private static int count = 0;//count初始值为0
	public static void main(String[] args) {
		class MyTimerTask extends TimerTask{
			public void run() {
				count = (count+1)%2;//0和1交替
				System.out.println("bombing!");
				new Timer().schedule(
					new MyTimerTask(),2000+2000*count);//定时时间切换
			}
		}
		
		new Timer().schedule(new MyTimerTask(), 2000);//在主线程中2s以后,启动定时器
		
		while(true){
			System.out.println(new Date().getSeconds());//打印当前的时间值(秒)
			try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}
		}	
	}
}

8.1.3 传统线程同步互斥

1.互斥原理
通过监视器来实现同步互斥,实现同步方式有两种:一种是synchronized代码块,另一种是synchronized函数。
2.同步函数synchronized的监视器对象
(1)如果方法是非静态的,则监视器对象是this。
(2)如果方法是静态的,则监视器对象是函数所在类在内存中的字节码文件。

8.1.4 传统的线程间通信(一)

1.线程间通信的原理
线程间的通信主要是通过读写标记flag以及wait和notifyAll来实现的。假定读取线程时,flag标记为false。
(1)bShouldSub为true,则sub线程进行操作,操作完成之后,将bShouldSub置为false,sub线程wait,notifyAll线程池中的线程,如果是main线程,则执行。
(2)bShouldSub为false,则main线程进行操作,操作完成之后,将bShouldSub置为true,main线程wait,notifyAll线程池中的线程,如果是sub线程,则执行。
2. 示例代码

public class TraditionalThreadCommunication {
	public static void main(String[] args) {
		final Business business = new Business();
		new Thread(new Runnable() {
			public void run() {
				for (int i = 1; i <= 20; i++) {
					business.sub(i);//调用子线程
				}
			}
		}).start();

		for (int i = 1; i <= 20; i++) {
			business.main(i);//调用主线程
		}
	}
}

class Business {
	private boolean bShouldSub = true;
	public synchronized void sub(int i) {
		while (!bShouldSub) {//子线程执行(bShouldSub为true)
			try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}
		}
		for (int j = 1; j <= 10; j++) {
			System.out.println("sub thread sequence of " + j + ",loop of " + i);
		}
		bShouldSub = false;
		this.notify();
	}

	public synchronized void main(int i) {
		while (bShouldSub) {//主线程执行(bShouldSub为false)
			try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}
		}
		for (int j = 1; j <= 10; j++) {
			System.out.println("main thread sequence of " + j + ",loop of " + i);
		}
		bShouldSub = true;
		this.notify();
	}
}

8.1.4 传统的线程间通信(二)

1.重入锁 ReentrantLock
(1)概念
Lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象。两个线程执行的代码片段要实现同步互斥的效果,它们必须用同一个Lock对象。
(2)使用Lock和Condition实现线程间的通信
第一,使用Lock替换synchronized,使用Condition替换Object;
第二,使用Condition中的await替换Object中的wait,使用Condition中的signal替换Object中的notifyAll即可。可以显示唤醒指定的Condition,因此可以使用singnal唤醒需要唤醒的Condition。
(3)示例代码

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;//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();
     }
   } 
 }

2.读写锁 ReentrantReadWriteLock
(1)概念
分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,你只要上好相应的锁即可。
第一,如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;
第二,如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。
总之,读的时候上读锁,写的时候上写锁!
(2)示例代码

class CachedData {
   Object data;
   volatile boolean cacheValid;
   ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

   void processCachedData() {
     rwl.readLock().lock();
     if (!cacheValid) {
        // 获取写锁之前,必须释放读锁。
        rwl.readLock().unlock();
        rwl.writeLock().lock();
        // 重新检查锁的状态,因此有可能此时其他所在获取请求时,该状态发生了变化
        if (!cacheValid) {
          data = ...
          cacheValid = true;
        }
        // 在释放写锁前,写锁降级为读锁
        rwl.readLock().lock();
        rwl.writeLock().unlock(); // 释放写锁,此时保持读状态。
     }

     use(data);
     rwl.readLock().unlock();
   }
 }

8.1.5 多线程访问共享数据总结

多个线程访问共享对象和数据的方式
1. 如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,买票系统就可以这么做。
2. 如果每个线程执行的代码不同,这时候需要用不同的Runnable对象,有如下两种方式来实现这些Runnable对象之间的数据共享:
(1)将共享数据封装在另外一个对象中,然后将这个对象逐一传递给各个Runnable对象。每个线程对共享数据的操作方法也分配到那个对象身上去完成,这样容易实现针对该数据进行的各个操作的互斥和通信。
(2)将这些Runnable对象作为某一个类中的内部类,共享数据作为这个外部类中的成员变量,每个线程对共享数据的操作方法也分配给外部类,以便实现对共享数据进行的各个操作的互斥和通信,作为内部类的各个Runnable对象调用外部类的这些方法。
(3)上面两种方式的组合:将共享数据封装在另外一个对象中,每个线程对共享数据的操作方法也分配到那个对象身上去完成,对象作为这个外部类中的成员变量或方法中的局部变量,每个线程的Runnable对象作为外部类中的成员内部类或局部内部类。
(4)总之,要同步互斥的几段代码最好是分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现它们之间的同步互斥和通信。
4.极端且简单的方式,即在任意一个类中定义一个static的变量,这将被所有线程共享。

8.2 并发库

8.2.1 ThreadLocal与线程级变量共享

1. ThreadLocal的作用和目的:
用于实现线程内的数据共享,即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
2. 每个线程调用全局ThreadLocal对象的set方法,就相当于往其内部的map中增加一条记录,key分别是各自的线程,value是各自的set方法传进去的值。在线程结束时可以调用ThreadLocal.clear()方法,这样会更快释放内存,不调用也可以,因为线程结束后也可以自动释放相关的ThreadLocal变量。
3. ThreadLocal的应用场景:
(1)订单处理包含一系列操作:减少库存量、增加一条流水台账、修改总账,这几个操作要在同一个事务中完成,通常也即同一个线程中进行处理,如果累加公司应收款的操作失败了,则应该把前面的操作回滚,否则,提交所有操作,这要求这些操作使用相同的数据库连接对象,而这些操作的代码分别位于不同的模块类中。
(2)银行转账包含一系列操作: 把转出帐户的余额减少,把转入帐户的余额增加,这两个操作要在同一个事务中完成,它们必须使用相同的数据库连接对象,转入和转出操作的代码分别是两个不同的帐户对象的方法。
(3)例如Strut2的ActionContext,同一段代码被不同的线程调用运行时,该代码操作的数据是每个线程各自的状态和数据,对于不同的线程来说,getContext方法拿到的对象都不相同,对同一个线程来说,不管调用getContext方法多少次和在哪个模块中getContext方法,拿到的都是同一个。
4. 实现对ThreadLocal变量的封装,让外界不要直接操作ThreadLocal变量。
(1)对基本类型的数据的封装,这种应用相对很少见。
(2)对对象类型的数据的封装,比较常见,即让某个类针对不同线程分别创建一个独立的实例对象。
5. 实验案例:
定义一个全局共享的ThreadLocal变量,然后启动多个线程向该ThreadLocal变量中存储一个随机值,接着各个线程调用另外其他多个类的方法,这多个类的方法中读取这个ThreadLocal变量的值,就可以看到多个类在同一个线程中共享同一份数据。

public class ThreadLocalTest {
	private static ThreadLocal<Integer> x = new ThreadLocal<Integer>();
	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);
					x.set(data);
					MyThreadScopeData.getThreadInstance().setName("name" + data);
					MyThreadScopeData.getThreadInstance().setAge(data);
					new A().get();
					new B().get();
				}
			}).start();
		}
	}
	
	static class A{
		public void get(){
			int data = x.get();
			System.out.println("A from " + Thread.currentThread().getName() + " get data :" + data);
			MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
			System.out.println("A from " + Thread.currentThread().getName() + " getMyData: " + myData.getName() + "," +	myData.getAge());
		}
	}
	
	static class B{
		public void get(){
			int data = x.get();			
			System.out.println("B from " + Thread.currentThread().getName() + " get data :" + data);
			MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
			System.out.println("B from " + Thread.currentThread().getName() + " getMyData: " + myData.getName() + "," + myData.getAge());			
		}		
	}
}

class MyThreadScopeData{
	private MyThreadScopeData(){}
	public static MyThreadScopeData getThreadInstance(){
		MyThreadScopeData instance = map.get();
		if(instance == null){
			instance = new MyThreadScopeData();
			map.set(instance);
		}
		return instance;
	}
	private static ThreadLocal<MyThreadScopeData> map = new ThreadLocal<MyThreadScopeData>();
	
	private String name;
	private int age;
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
}

8.2.2 Java5中的线程池

1. 常用的方法:
(1)创建线程池
方式1:创建固定大小的线程池
Executors.newFixedThreadPool(3).execute(Runnable command);创建固定大小的线程池,每次只能运行固定数量的线程,当这些线程运行完之后才能运行其他的线程。
方式2:创建缓存线程池
Executors.newCachedThreadPool().execute(Runnable command);创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。
方式3:创建单一线程池
Executors.newSingleThreadExecutor().execute(Runnable command);创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。
(2)关闭线程池(ExecutorService类)
方式1:
shutdown
启动一次顺序关闭,执行以前提交的任务,但不接受新任务。shutdown会执行完线程池中提交的所有任务,然后停止。
方式2:
shutdownNow
试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。shutdownNow会执行完固定线程池中固定的任务,然后就停止。不会执行线程池中的其他任务。
(3)线程池启动定时器
方式1:
调用ScheduledExecutorService的schedule方法,返回的ScheduleFuture对象可以取消任务。
方式2:
支持间隔重复任务的定时方式,不直接支持绝对定时方式,需要转换成相对时间方式。

2.应用场景(Tcp服务器编程)
每一个客户端连接用一个单独的线程为之服务,当与客户端的会话结束时,线程也就结束了,每来一个客户端连接,服务器端就要创建一个新线程。由于这样的模式下,需要频繁的创建线程和销毁线程,因此效率并不高。通常的做法是先建立一些连接,存放在线程池中,当服务器接受到一个客户请求后,就从线程池中取出一个空闲的线程为之服务,服务完后不关闭该线程,而是将该线程还回到线程池中。需要注意的是一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。

3. 示例代码

/*1. 用3个大小的固定线程池去执行10个内部循环10次就结束的任务,每次只有3个线程在执行,
并看到任务前仆后继的效果。*/
ExecutorService threadPool = Executors.newFixedThreadPool(3);
//2. 缓存线程池,可以看到当前有多少个任务,就会分配多少个线程为之服务。
ExecutorService threadPool = Executors.newCachedThreadPool();
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++) {
				System.out.println(Thread.currentThread().getName()
						+ " is looping of " + j + " for  task of "
						+ task);
			}
		}
	});
}

//3.以下是一个带方法的类,它设置了 ScheduledExecutorService ,在 1 小时内每 10 秒钟蜂鸣一次:  
class BeeperControl {
	private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
	public void beepForAnHour() {
		final Runnable beeper = new Runnable() {
			public void run() {
				System.out.println("beep");
			}
		};
		final ScheduledFuture<?> beeperHandle = scheduler.scheduleAtFixedRate(
				beeper, 10, 10, TimeUnit.SECONDS);
		scheduler.schedule(
					new Runnable() {
						public void run() {
							beeperHandle.cancel(true);
						}
					}, 
					60 * 60, 
					TimeUnit.SECONDS);
	}
}

8.2.3 Callable&Future

1. 概念
java5提供的可返回结果的线程模式
(1)Future取得的结果类型和Callable返回的结果类型必须一致,这是通过泛型来实现的。
(2)Callable要采用ExecutorSevice的submit方法提交,返回的future对象可以取消任务。
(3)CompletionService用于提交一组Callable任务,其take方法返回已完成的一个Callable任务对应的Future对象。
2. 应用场景
好比我同时种了几块地的麦子,然后就等待收割。收割时,则是那块先成熟了,则先去收割哪块麦子。
3. 示例代码

public class CallableAndFuture {
	public static void main(String[] args) {
		//1.ExecutorSevice提交一个任务
		ExecutorService threadPool =  Executors.newSingleThreadExecutor();
		Future<String> future =
			threadPool.submit(
				new Callable<String>() {
					public String call() throws Exception {
						Thread.sleep(2000);
						return "hello";
					};
				}
		);
		System.out.println("等待结果");
		try {
			System.out.println("拿到结果:" + future.get());
		} catch (Exception e) {e.printStackTrace();}
		
		//2.CompletionService提交一组任务
		ExecutorService threadPool2 =  Executors.newFixedThreadPool(10);
		CompletionService<Integer> completionService = new ExecutorCompletionService<Integer>(threadPool2);
		for(int i=1;i<=10;i++){
			final int seq = i;
			completionService.submit(new Callable<Integer>() {
				public Integer call() throws Exception {
					Thread.sleep(new Random().nextInt(1000));
					return seq;
				}
			});
		}
		for(int i=0;i<10;i++){
			try {
				System.out.println(completionService.take().get());				
			} catch (Exception e) {	e.printStackTrace();}
		}
	}
}

8.3 并发库中的工具

8.3.1 Semaphore同步工具

1. 概念
Semaphore可以维护当前访问自身的线程个数,并提供了同步机制,使用Semaphore可以控制同时访问资源的线程个数。
(1)构造Semaphore对象时传入的参数选项fair的取值,用来设置等待线程获取机会的顺序。如果为真,则是按照先来后到的顺序,否则,随机获取优先顺序。
(2)单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合。

2. 应用场景
(1)当需要控制资源访问进程数,要求线程按照FIFO的顺序进行访问时。
(2)死锁恢复(由于Semaphore对象可以在一个线程中获取锁,另外的线程中恢复锁)。

3. 示例代码

public class SemaphoreTest {
	public static void main(String[] args) {
		ExecutorService service = Executors.newCachedThreadPool();
		final  Semaphore sp = new Semaphore(3);//1. 获取Semaphore同步对象
		for(int i=0;i<10;i++){
			Runnable runnable = new Runnable(){
					public void run(){
					try {
						sp.acquire();//2. 获取Semaphore许可
					} catch (InterruptedException e1) {
						e1.printStackTrace();
					}
					System.out.println("线程" + Thread.currentThread().getName() + 
							"进入,当前已有" + (3-sp.availablePermits()) + "个并发");//3. 返回当前的许可数。
					try {Thread.sleep((long)(Math.random()*10000));} 
					catch (InterruptedException e) {e.printStackTrace();}
					System.out.println("线程" + Thread.currentThread().getName() + "即将离开");					
					sp.release();//4. 释放Semaphore许可
					System.out.println("线程" + Thread.currentThread().getName() + 
							"已离开,当前已有" + (3-sp.availablePermits()) + "个并发");					
				}
			};
			service.execute(runnable);			
		}
	}
}

8.3.2 CyclicBarrier 同步工具

1. 概念
一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。

2. 应用场景
当多个线程需要在某一个点汇集之后,然后在去做其他事情的时候。

3. 示例代码

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();
	}
}

8.3.3 CountDownLatch 同步工具

1. 概念
一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。 调用CountDownLatch对象的countDown方法就将计数器减1,当计数到达0时,则所有等待者或单个等待者开始执行。

2. 应用场景
(1)将计数 1 初始化的 CountDownLatch 用作一个简单的开/关锁存器。在通过调用 countDown() 的线程打开入口前,所有调用 await 的线程都一直在入口处等待。
(2)用 N 初始化的 CountDownLatch 可以使一个线程在 N 个线程完成某项操作之前一直等待,或者使其在某项操作完成 N 次之前一直等待。

3. 使用方式
(1)构造一个用给定计数初始化的 CountDownLatch。
CountDownLatch cdOrder = new CountDownLatch(3);
(2)使当前线程在锁存器倒计数至零之前一直等待。
cdOrder.await();
(3)递减锁存器的计数,如果计数到达零,则释放所有等待的线程。
cdOrder.countDown(); 

8.3.4 Exchanger 同步与数据交换工具

1. 概念
可以在对中对元素进行配对和交换的线程的同步点。

2. 应用场景
用于实现两个人之间的数据交换,每个人在完成一定的事务后想与对方交换数据,第一个先拿出数据的人将一直等待第二个人拿着数据到来时,才能彼此交换数据。

3. 使用方式
(1)同步工具 Exchanger
Exchanger exchanger = new Exchanger();
(2)线程组1
String data1 = "zxx";
String data2 = (String)exchanger.exchange(data1);
(3)线程组2
String data1 = "lhm";
String data2 = (String)exchanger.exchange(data1);

8.3.5 可阻塞队列 ArrayBlockingQueue

1. 概念
一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。队列的头部 是在队列中存在时间最长的元素。队列的尾部 是在队列中存在时间最短的元素。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。 这是一个典型的“有界缓存区”,固定大小的数组在其中保持生产者插入的元素和使用者提取的元素。一旦创建了这样的缓存区,就不能再增加其容量。试图向已满队列中放入元素会导致操作受阻塞;试图从空队列中提取元素将导致类似阻塞。

2. 特点
(1)阻塞队列与Semaphore有些相似,但也不同,阻塞队列是一方存放数据,另一方释放数据,Semaphore通常则是由同一方设置和释放信号量。
(2)ArrayBlockingQueue只有put方法和take方法才具有阻塞功能。
(3)适用于多线程中需要固定长度缓冲区。

3. 示例代码
(1)创建一个带有给定的(固定)容量。
BlockingQueue queue = new ArrayBlockingQueue(3);
(2)指定的元素插入此队列的尾部,如果该队列已满,则等待可用的空间。
queue.put(1);
(3)获取并移除此队列的头部,在元素变得可用之前一直等待(如果有必要)。
queue.take();

8.3.6 传统与java5中的同步集合

1. 概念
传统方式下的Collection在迭代集合时,不允许对集合进行修改。当多线程对结合进行并发访问时,就会发生安全问题,jdk1.5之前可以通过Collections工具类提供的synchronizedCollection方法来获得同步集合。Java5中提供了并发集合来解决多线程访问产生的并发问题。
2. 并发库中的集合
java.util.concurrent包含除队列外,此包还提供了设计用于多线程上下文中的 Collection 实现:
(1)ConcurrentHashMap
(2)ConcurrentSkipListMap
(3)ConcurrentSkipListSet
(4)CopyOnWriteArrayList
(5)CopyOnWriteArraySet
当期望许多线程访问一个给定 collection 时,ConcurrentHashMap 通常优于同步的 HashMap,ConcurrentSkipListMap 通常优于同步的 TreeMap。当期望的读数和遍历远远大于列表的更新数时,CopyOnWriteArrayList 优于同步的 ArrayList。因此,实际开发中,当集合被并发访问时,可以使用java5并发库中提供的集合。

总结:
1. 传统的多线程
(1)启动线程 Thread
继承Thread类,重写run方法;通过Thread类的start方法启动线程;在传递给Thread对象的Runnable对象的run方法中编写代码。
(2)定时器 Timer
void scheduleAtFixedRate(TimerTask task,long delay,long period);通过Timer的scheduleAtFixedRate方法,可以实现在delay延迟之后执行一个task任务,每隔period执行一次。其中task是TimerTask,通常采用匿名内部类的方式来指定需要实现的任务。
(3)同步互斥
传统线程同步互斥通过synchronized代码块或者synchronized函数来实现互斥,同时使用的监视器是同一个监视器。同步函数是非静态的,监视器对象是this;方法是静态的,则监视器对象是该类在内存中的字节码对象。
(4)线程间通信1
线程间的通信主要是通过读写标记flag以及wait和notifyAll来实现的。通过判断读写标记,标记为真,则读取线程执行,写入线程堵塞,执行完成之后,标记置为假,读取线程堵塞,叫醒写入线程;标记为假,则写入线程执行,读取线程堵塞,执行完成之后,标记置为真,写入线程堵塞,叫醒读取线程。
(5)线程间通信2
Java5中提供了更加面向对象的同步工具,就是Lock和Condition,通过将synchronized替换为Lock,将监视器Object替换为Condition,就可以实现线程间的通信。同时,提供了更加强大的功能。ReentrantLock锁中可以直接定义显示的Condition,可以直接叫醒指定的线程。同时,如果想实现读写锁,可以直接使用
ReentrantReadWriteLock,就可以直接获取读锁和写锁。
(6)多线程访问的数据
通常,要同步互斥的几段代码最好是分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现它们之间的同步互斥和通信。
2. 并发库
在并发编程中很常用的实用工具类。
(1)线程级变量共享 ThreadLocal
ThreadLocal用于实现线程内的数据共享,对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
每个线程调用全局ThreadLocal对象的set方法,就相当于往其内部的map中增加一条记录,key分别是各自的线程,value是各自的set方法传进去的值。ThreadLocal对象的get方法可以获取到存储到该线程对应的值。
(2)线程池
通过Executors类的静态方法可以创建固定大小的线程池,缓存线程池以及单一线程池;然后通过返回值ExecutorService的execute,指定线程池中执行的线程代码。通过ExecutorService的shutdown可以执行完线程池中所有的线程然后,停止;而shutdownNow会执行初始线程池中的线程,然后停止。通过Executors类的newScheduledThreadPool方法,创建一个在给定延迟后运行命令或者定期地执行任务的线程池。
(3)可返回结果的线程模式
方式1:提交一个Callable任务
ExecutorSevice类的submit方法提交,返回的future对象可以取消任务。
方式2:提交一组Callable任务
CompletionService用于提交一组Callable任务,其take方法返回已完成的一个Callable任务对应的Future对象。
(4)同步工具 Semaphore
Semaphore可以维护当前访问自身的线程个数,并提供了同步机制,使用Semaphore可以控制同时访问资源的线程
个数。
第一,通过指定构造函数中初始化线程数来获取Semaphore同步对象;
第二,通过Semaphore中的方法acquire获取Semaphore许可;
第三,多线程中需要执行的任务代码;
第四,通过释放release方法释放Semaphore许可。
(5)同步工具 CyclicBarrier
CyclicBarrier工具允许一组线程互相等待,直到到达某个公共屏障点。通过CyclicBarrier中的await方法可以
指定公共屏障点。
第一,通过指定构造函数中初始化线程数来获取CyclicBarrier同步对象;
第二,在公共屏障点之前,填写多线程中需要执行的任务代码,通过CyclicBarrier类的getNumberWaiting方法
可以获取等待的线程数量。
第三,通过CyclicBarrier中的await方法设置公共屏障点。
(6)同步工具 CountDownLatch
CountDownLatch工具,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。调用
CountDownLatch对象的countDown方法就将计数器减1,当计数到达0时,则所有等待者或单个等待者开始执行。
第一,构造一个用给定计数初始化的 CountDownLatch。
CountDownLatch cdOrder = new CountDownLatch(3);
第二,使当前线程在锁存器倒计数至零之前一直等待。
cdOrder.await();
第三,递减锁存器的计数,如果计数到达零,则释放所有等待的线程。
cdOrder.countDown(); 
(7)同步与数据交换工具 Exchanger
Exchanger 可以在对中对元素进行配对和交换的线程的同步点。
(8)可阻塞队列 ArrayBlockingQueue
ArrayBlockingQueue类是一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。队列的头部是在队列中存在时间最长的元素。队列的尾部 是在队列中存在时间最短的元素。通过put方法将指定元素插入到队列的结尾,通过take方法,移除此队列的头部。
(9)并发 Collection
Java5还提供了用于并发访问的集合类 Collection 实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList 和 CopyOnWriteArraySet。
第一,当期望许多线程访问一个给定 collection 时,ConcurrentHashMap 通常优于同步的 HashMap,
ConcurrentSkipListMap 通常优于同步的 TreeMap。
第二,当期望的读数和遍历远远大于列表的更新数时,CopyOnWriteArrayList 优于同步的 ArrayList。

-------android培训java培训、java基础学习技术博客、期待与您交流!
----------

抱歉!评论已关闭.