Java多线程与并发库高级应用

传统线程技术回顾

线程就是程序的一条执行线索

创建线程的两种传统方式

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) {
		//5秒之后启动定时器并执行代码
		new Timer().schedule(new TimerTask(){
			@Override
			public void run() {
				System.out.println("bombing!");
			}
			
		}, 5000);
		
		//while循环按打印当前秒数,每过一秒打印一次。
		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) {
		//自定义一个内部类继承TimerTask
		class MyTimerTask extends TimerTask{
			@Override
			public void run() {
				count = (count+1)%2;//让count在0和1之间交替
				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.*;
//自定义一个类继承TimerTask,内部调用另一个自定义的TimerTask对象
class MyTimerTask extends TimerTask{
	@Override
	public void run() {
		System.out.println("bombing!");
		new Timer().schedule(new MyTimerTask2(),2000);//多长时间后执行是一个动态值
	}
}
//自定义一个类继承TimerTask,内部调用另一个自定义的TimerTask对象
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方法分析静态方法所使用的同步监视器对象是什么?是所属类的字节码对象
不论是同步代码块还是同步方法(包括静态同步方法),只要他们使用的锁是同一个对象就可以实现互斥,即同步
Java多线程与并发库高级应用_第1张图片

传统线程同步通信技术

wait与notify实现线程间的通信
面试题:子线程循环10次,接着主线程循环100,接着又回到子线程循环10次,接着再回到主线程又循环100,如此循环50次,请写出程序。
最初写出来的代码如下:
package ThreadDemo;
public class ThreadTest {
	private static boolean bShouldMain = false;//这里相当于定义了控制该谁执行的一个信号灯
	public static void main(String[] args) {
		new Thread(
				new Runnable(){
					public void run() {
						for(int i=1;i<=50;i++){
							//这里使用类的字节码对象作为锁进行同步,但是当需要对同步进行分组时就不科学了
							synchronized(ThreadTest.class){
								if(bShouldMain){
									try {
										ThreadTest.class.wait();
									} catch (InterruptedException e) {
										e.printStackTrace();
									}
								}
								for(int j=1;j<=10;j++){
									System.out.println("sub thread sequence of "+j+", loop of "+i);
								}
								bShouldMain = true;
								ThreadTest.class.notify();
							}
						}
					}
				}
		).start();

		//main方法本身是一个线程,这里是主线程的运行代码
		for(int i=1;i<=50;i++){
			synchronized(ThreadTest.class){
				if(!bShouldMain){
					try {
						ThreadTest.class.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				for(int j=1;j<=100;j++){
					System.out.println("main thread sequence of "+j+", loop of "+i);
				}
				bShouldMain = false;
				ThreadTest.class.notify();
			}
		}
	}
}
问题在于两个线程的代码要参照同一个变量,即bShouldMain,即这两个线程的代码要共享数据,所以,把这两个线程的执行代码搬到同一个类中去(注意循环50次不属于各自的业务执行代码):
package ThreadDemo;
public class ThreadTest {
	private static boolean bShouldMain = false;//这里相当于定义了控制该谁执行的一个信号灯
	public static void main(String[] args) {
		//因为用到了匿名内部类,内部类访问的局部变量需要用final修饰
		final Business business = new Business();
		new Thread(
				new Runnable(){
					public void run() {
						for(int i=1;i<=50;i++){
							//这里使用类的字节码对象作为锁进行同步,但是当需要对同步进行分组时就不科学了
							business.sub(i);
						}
					}
				}
		).start();

		//main方法本身是一个线程,这里是主线程的运行代码
		for(int i=1;i<=50;i++){
			business.main(i);
		}
	}
}

class Business {
	private boolean bShouldSub = true;//最开始该子线程走
	public synchronized void sub(int i){
		while(!bShouldSub){//用while而不是if线程醒来还会再次进行判断,防止代码被伪唤醒,代码更健壮。还可以防止生产者消费者问题
			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){
			try {
				this.wait();//这里的锁是this
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		for(int j=1;j<=100;j++){
			System.out.println("main thread sequence of "+j+", loop of "+i);
		}
		bShouldSub = true;
		this.notify();
	}
}
经验:
  1. 要用到共同数据(包括同步锁)或共同算法的若干方法应该归在同一个类身上,这种设计正好体现了高类聚和程序的健壮性
  2. 锁是上在要操作的资源的类的内部方法中,而不是线程代码中!好处是以后该类交给任何线程自然就同步了,而不需要考虑互斥同步的问题。
  3. Eclipse中将运行结果保存至文件的操作:Run as-->Run Configuration-->Common-->File处打钩然后选择一个文件

线程范围内的共享数据

线程范围内共享变量的概念与作用

线程范围内共享数据的示意图

Java多线程与并发库高级应用_第2张图片
全局变量会被所有的线程都共享,现在需要实现同一个线程内不同模块间变量的共享
关于线程范围内的变量共享的举例,直接用程序代码进行时说明,创建两个线程,它们都访问了两个模块,两个模块都取值,同一个线程设置的值,只能被相同的线程获取。
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();//定义一个Map,为每一个线程存放独立数据
	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();//A模块取出数据
					new B().get();//B模块取出数据
				}
			}).start();
		}
	}

	//模块A
	static class A{
		public void get(){
			int data = threadData.get(Thread.currentThread());//取出当前线程中变量的值
			System.out.println("A from "+Thread.currentThread().getName()+" has put data: "+data);
		}
	}

	//模块B
	static class B{
		public void get(){
			int data = threadData.get(Thread.currentThread());
			System.out.println("B from "+Thread.currentThread().getName()+" has put data: "+data);
		}
	}

}
运行结果:
Java多线程与并发库高级应用_第3张图片
应用:账户的转入转出。在同一个线程中有一个转入模块和一个转出模块,如果刚刚把钱转入时程序崩溃,转出模块还没有执行,就需要撤销之前的转入操作。并且只能提交己方线程的转出请求,而不能提交其他线程的转出请求。

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中存储这一个对象。比如定义一个学生实体,存放姓名,年龄等变量。
如何设计线程范围内的共享对象?
  • 第一种不优雅的实现方式:
    package cn.itcast.heima;
    import java.util.Random;
    public class ThreadLocalTest {
    	
    	//定义一个ThreadLocal变量存储线程内共享变量
    	private static ThreadLocal x = new ThreadLocal();
    	//定义一个ThreadLocal变量存储线程内的实体对象
    	private static ThreadLocal myThreadScopeData = new ThreadLocal();
    	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 myData = new MyThreadScopeData();
    					myData.setName("name"+data);
    					myData.setAge(data);
    					//将实体存入ThreadLocal变量中
    					myThreadScopeData.set(myData);
    					
    					new A().get();
    					new B().get();
    				}
    			}).start();
    		}
    	}
    
    	//模块A
    	static class A{
    		public void get(){
    			int data = x.get();//不用指定线程号就取出当前线程中变量的值
    			System.out.println("A from "+Thread.currentThread().getName()+" has put data: "+data);
    			
    			//获取线程内存入的实体对象并获取对象中存储的数据
    			MyThreadScopeData myData = myThreadScopeData.get();
    			System.out.println("A from "+Thread.currentThread().getName()+" getMyData: "+myData.getName()+","+myData.getAge());
    		}
    	}
    
    	//模块B
    	static class B{
    		public void get(){
    			int data = x.get();
    			System.out.println("B from "+Thread.currentThread().getName()+" has put data: "+data);
    		}
    	}
    
    }
    
    //定义一个实体
    class 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;
    	}
    }
  • 第二种优雅的实现方式:
    package cn.itcast.heima;
    import java.util.Random;
    public class ThreadLocalTest {
    	
    	//定义一个ThreadLocal变量存储线程内共享变量
    	private static ThreadLocal x = new ThreadLocal();
    	
    	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 myData = new MyThreadScopeData();
    					myData.setName("name"+data);
    					myData.setAge(data);
    					//将实体存入ThreadLocal变量中
    					myThreadScopeData.set(myData);*/
    					
    					//拿到与本线程相关的实例对象,然后向实体对象中存入数据
    					MyThreadScopeData.getThreadInstance().setName("name"+data);;
    					MyThreadScopeData.getThreadInstance().setAge(data);;
    					new A().get();
    					new B().get();
    				}
    			}).start();
    		}
    	}
    
    	//模块A
    	static class A{
    		public void get(){
    			int data = x.get();//不用指定线程号就取出当前线程中变量的值
    			System.out.println("A from "+Thread.currentThread().getName()+" has put data: "+data);
    			
    			/*//获取线程内存入的实体对象并获取对象中存储的数据
    			MyThreadScopeData myData = myThreadScopeData.get();
    			System.out.println("A from "+Thread.currentThread().getName()+" getMyData: "+myData.getName()+","+myData.getAge());
    		*/
    			//获取本线程相关的实体对象并获取对象中存储的数据
    			MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
    			System.out.println("A from "+Thread.currentThread().getName()+" getMyData: "+myData.getName()+","+myData.getAge());
    
    		}
    	}
    
    	//模块B
    	static class B{
    		public void get(){
    			int data = x.get();
    			System.out.println("B from "+Thread.currentThread().getName()+" has put data: "+data);
    			 
    			MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
    			System.out.println("B from "+Thread.currentThread().getName()+" getMyData: "+myData.getName()+","+myData.getAge());
    
    		}
    	}
    
    }
    
    //定义一个实体
    class MyThreadScopeData{
    	
    	//将该类设计成类似单例模式
    	private MyThreadScopeData(){}
    	private static ThreadLocal map = new ThreadLocal();
    	public static MyThreadScopeData getThreadInstance(){//这里不需要同步,因为ThreadLocal变量与当前线程相关,各个线程会获取各自的实例
    		MyThreadScopeData instance = map.get();
    		if(instance==null){//如果实例没有,就创建实例并保存
    			instance = new MyThreadScopeData();//保证返回的变量不为null
    			map.set(instance);//存入的实体与当前线程相关,所以该方法不需要加同步
    		}
    		return instance;
    	}
    	
    	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;
    	}
    }
    • 优点:
      • 与前一种实现方式相比,这种方式向调用者隐藏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类的应用
  • 创建固定大小的线程池
  • 创建缓存线程池//线程数可随需求变化
  • 创建单一线程池(实现线程死掉之后重新启动)
关闭线程池
  • shutdown与shutdownNow的比较
用线程池启动定时器
  • 调用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) {
		//创建一个固定大小的线程池,里面有3个线程
		ExecutorService threadPool = Executors.newFixedThreadPool(3);
		//创建一个缓存的线程池,池中线程不够,会自动创建新的线程
		//ExecutorService threadPool = Executors.newCachedThreadPool();
		//创建单一线程池,好处是始终保证池中有一个线程存在,如果线程死了,会再创建一个
		//ExecutorService threadPool = Executors.newSingleThreadExecutor();
		
		//通过循环向池中添加10个任务,但是同时只有3个任务被池中的3个线程执行,只有这3个任务都执行结束了才会执行其余任务 
		for(int i=1;i<=10;i++){
			final int task = i;
			threadPool.execute(new Runnable(){
				public void run() {
					//任务循环10遍,线程池中的某一个线程就会执行该任务
					for(int j=1;j<=10;j++){
						try {
							Thread.sleep(20);
						} catch (InterruptedException e) {
							// TODO Auto-generated catch block
							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();//关闭线程池(当所有的任务执行完毕,所有的线程都空闲下来时,结束池中的所有线程)
		//threadPool.shutdownNow();//关闭线程池(立即结束池中的所有线程,即使还有任务没有执行完毕)
		
		//创建一个调度线程池
		Executors.newScheduledThreadPool(3).schedule(
				new Runnable(){
					public void run() {
						System.out.println("bombing!");
					}
				}, 
				3,//delay 
				TimeUnit.SECONDS);
	}
}

Callable&Future

使用Callable接口编写的线程中的任务会在线程运行结束会返回一个结果,该结果类型为Future
Future通过get()方法取得返回结果,线程没有结束get方法会一直等待。
返回的结果类型和Callable返回的结果类型必须一致,这是通过泛型来实现的。
Callable要采用ExecutorSevice的submit方法提交,而不是execute方法,因为execute方法没有返回值,返回的future对象可以取消任务。
CompletionService接口用于提交一组Callable任务,其take方法返回已完成的一个Callable任务对应的Future对象。
  • 好比我同时种了几块地的麦子,然后就等待收割。收割时,则是哪块先成熟了,则先去收割哪块麦子。
应用:既然苦苦等待线程运行完毕返回的结果,还不如直接调用一个方法,运行完成就返回一个结果。
package cn.itcast.heima;

import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CallableAndFuture {
	public static void main(String[] args) {
		ExecutorService threadPool1 = Executors.newSingleThreadExecutor();
		//使用Callable编写任务,Future接受返回结果
		Future future = threadPool1.submit(
				new Callable(){
					public String call() throws Exception {
						Thread.sleep(2000);
						return "hello";
					}
				});
		System.out.println("等待结果");
		try {
			System.out.println("拿到结果: "+future.get());
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (ExecutionException e) {
			e.printStackTrace();
		}
		
		ExecutorService threadPool2 = Executors.newFixedThreadPool(10);
		
		CompletionService completionService = new ExecutorCompletionService(threadPool2);
		//向CompletionService中提交10个任务
		for(int i=1;i<=10;i++){
			final int sequence = i;//记录任务序号
			completionService.submit(
					new Callable(){
						public Integer call() throws Exception {
							Thread.sleep(new Random().nextInt(5000));
							return sequence;//返回的是当前任务的序号
						}
					});
		}
		//获取结果
		for (int i = 0; i < 10; i++) {
			try {
				System.out.println(completionService.take().get());
			} catch (InterruptedException e) {
				e.printStackTrace();
			} catch (ExecutionException e) {
				e.printStackTrace();
			}
		}
	}
}

java5的线程锁技术Lock&Condition实现线程同步通信

Lock

Lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象。两个线程执行的代码片段要实现 同步互斥的效果,它们必须用同一个Lock对象。锁是上在要操作的资源的类的内部方法中,而不是线程代码中!
package cn.itcast.heima;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockTest {
	public static void main(String[] args) {
		new LockTest().init();
	}
	
	private void init(){
		final Outputer outputer = new Outputer();
		new Thread(new Runnable(){
			public void run() {
				while(true){
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					outputer.output("zhangxiaoxiang");//线程如果不上锁,输出就会被打乱
				}
			}
		}).start();
		
		new Thread(new Runnable(){
			public void run() {
				while(true){
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					outputer.output("lihuoming");
				}
			}
		}).start();
	}

	static class Outputer{
		Lock lock = new ReentrantLock();//创建锁对象
		public void output(String name){
			int len = name.length();
			lock.lock();//上锁
			try{
				for(int i=0;i

读写锁ReadWriteLock

分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,写锁与写锁互斥,这是由jvm自己控制的,你只要上好相应的锁即可。如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。读与读不互斥,读与写互斥,写与写也互斥。总之,读的时候上读锁,写的时候上写锁!
读写锁技能提高性能,又能实现互斥。如果只是使用Lock,那么读和读的线程之前也会互斥
代码示例:创建3个读线程,3个写线程:
package cn.itcast.heima;

import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockTest {
	public static void main(String[] args) {
		final Queue3 q3 = new Queue3();
		//同时创建3个读取线程,3个写入线程
		for(int i=0;i<3;i++)
		{
			new Thread(){
				public void run(){
					while(true){
						q3.get();//读取数据						
					}
				}
			}.start();

			new Thread(){
				public void run(){
					while(true){
						q3.put(new Random().nextInt(10000));
					}
				}			

			}.start();
		}
	}
}

class Queue3{
	private Object data = null;//共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。
	ReadWriteLock rwl = new ReentrantReadWriteLock();//创建读写锁对象
	//读的方法
	public void get(){
		rwl.readLock().lock();//读的时候上读锁
		try {
			System.out.println(Thread.currentThread().getName() + " be ready to read data!");
			Thread.sleep((long)(Math.random()*1000));
			System.out.println(Thread.currentThread().getName() + "have read data :" + data);			
		} catch (InterruptedException e) {
			e.printStackTrace();
		}finally{
			rwl.readLock().unlock();
		}
	}
	//写的方法
	public void put(Object data){
		rwl.writeLock().lock();//写的时候上写锁
		try {
			System.out.println(Thread.currentThread().getName() + " be ready to write data!");					
			Thread.sleep((long)(Math.random()*1000));
			this.data = data;		
			System.out.println(Thread.currentThread().getName() + " have write data: " + data);					
		} catch (InterruptedException e) {
			e.printStackTrace();
		}finally{
			rwl.writeLock().unlock();
		}
	}
}

缓存对象和缓存系统

在Hibernate中,获取数据库数据实体有两种方式:
User user = session.get(id,User.class);//如果数据对象不存在,返回null
User user = session.load(id,User.class);//如果数据对象不存在,则加载的是一个缓存代理对象
User$Proxy extends User{
	private Integer id = id;
	User realUser = null;
	getName(){
		if(realUser==null){
			realUser = session.get(id);
			if(realUser==null)
				throw new Exception();
		}
		return realUser.getName();
	}
}
API文档中对ReentrantReadWriteLock示例代码:
/*
 * 示例用法。下面的代码展示了如何利用重入来执行升级缓存后的锁降级(为简单起见,省略了异常处理):
 * 当有许多读的线程访问一个数据实体时,如果有一个线程发现该数据实体为空,该线程就需要将读锁释放,上写锁,写入数据
 * 然后在释放写锁前重新上读锁
 */
class CachedData {
	Object data;
	volatile boolean cacheValid;//判断实体中是否有值
	ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

	void processCachedData() {
		rwl.readLock().lock();
		if (!cacheValid) {
			// Must release read lock before acquiring write lock
			rwl.readLock().unlock();
			rwl.writeLock().lock();
			// Recheck state because another thread might have acquired
			//   write lock and changed state before we did.
			if (!cacheValid) {
				data = ...//加载数据
						cacheValid = true;
			}
			// Downgrade by acquiring read lock before releasing write lock
			rwl.readLock().lock();
			rwl.writeLock().unlock(); // Unlock write, still hold read
		}
		use(data);
		rwl.readLock().unlock();
	}
}
面试题:请设计一个 缓存系统
以上是对单个对象进行缓存,缓存系统就是可以装很多个对象。
要拿对象别直接找数据库,而是访问缓存系统。当缓存系统的数据被访问时,就应该检查内部是否有该数据,如果有,就直接返回,如果没有就查询数据库,然后把数据存入内存中,下次就直接返回该数据不需要再查数据库了。
package cn.itcast.heima;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class CacheDemo {
	//定义一个Map,存储多个对象
	private Map cache = new HashMap();
	public static void main(String[] args) {}
	private ReadWriteLock rwl = new ReentrantReadWriteLock();
	//获取对象的方法,当多个线程来读(即获取对象时)不需要互斥,只有写的时候才需要互斥,因此需要使用读写锁
	public  Object getData(String key){
		rwl.readLock().lock();//先上读锁,这样多个线程都可以来读
		Object value = null;
		try{
			value = cache.get(key);
			//如果线程读的时候发现数据为空,那么就把读锁释放,禁止数据的读取,然后上写锁
			if(value == null){
				rwl.readLock().unlock();
				rwl.writeLock().lock();
				try{
					//再次判断数据是否存在,因为当一个线程获取写锁完成了数据填充并释放了写锁,另外一个线程也可能获取了写锁,会重复填充数据。
					if(value==null){
						value = "aaaa";//实际是去queryDB();即查询数据库
					}
				}finally{
					rwl.writeLock().unlock();//数据填充完,释放写锁
				}
				rwl.readLock().lock();//恢复为读锁
			}
		}finally{
			rwl.readLock().unlock();
		}
		return value;
	}
}

Condition

Lock和Condition的关系:Lock只能实现 互斥(一个线程持有锁,另外的线程不能访问该锁),但是不能实现 通信(即使获取了CPU的执行权,但是也可以让出执行权通知另外的线程执行),一个是哥们你不能干,一个是哥们你可以干。
Condition的功能类似于在传统线程计数中的Object.wait和Object.notify的功能。在等待 Condition 时,允许发生“虚假唤醒”,这通常作为对基础平台语义的让步。对于大多数应用程序,这带来的实际影响很小,因为 Condition 应该总是在一个循环中被等待,并测试正被等待的状态声明。某个实现可以随意移除可能的虚假唤醒,但建议应用程序程序员总是假定这些虚假唤醒可能发生,因此总是在一个循环中等待。
package cn.itcast.heima;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionCommunication {
	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 {//解决两个java文件中都有该类的问题:这里的完整类名是ConditionCommunicaiton.Business,另一个是Business
		Lock lock = new ReentrantLock();
		Condition condition = lock.newCondition();//获取该锁的condition对象
		private boolean bShouldSub = true;
		public void sub(int i){
			lock.lock();
			try{
				while(!bShouldSub){//while循环防止虚假唤醒(线程被唤醒但是运行条件并不满足)
					try {
						condition.await();//调用condition对象特有的await方法,调用wait方法不报错,因为condition也是一个object,但是不是预期的方法。
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
				for(int j=1;j<=10;j++){
					System.out.println("sub thread sequence of " + j + ",loop of " + i);
				}
				bShouldSub = false;
				condition.signal();//调用condition对象特有的signal方法
			}finally{
				lock.unlock();
			}
		}

		public void main(int i){
			lock.lock();
			try{
				while(bShouldSub){
					try {
						condition.await();
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
				for(int j=1;j<=100;j++){
					System.out.println("main thread sequence of " + j + ",loop of " + i);
				}
				bShouldSub = true;
				condition.signal();
			}finally{
				lock.unlock();
			}
		}
	}
}
一个锁内部可以有多个Condition,即有 多路等待和通知,可以参看jdk1.5提供的Lock与Condition实现的 可阻塞队列的应用案例(见下面代码),从中除了要体味算法,还要体味面向对象的封装。在传统的线程机制中一个监视器对象上只能有一路等待和通知,要想实现多路等待和通知,必须嵌套使用多个同步监视器对象。(如果只用一个Condition,两个存放的线程都在等,一旦一个放的线程进去了,那么它通知 可能会导致另一个放的线程接着往下走。)
可阻塞队列起到缓冲的效果,应用:使用该队列实现寻呼信息的存放和取出,当队列满时,存放线程等待,当队列空时,取出线程等待。
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轮流交替运行:
package cn.itcast.heima;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ThreeConditionCommunication {
	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.sub2(i);
						}					
					}
				}
				).start();

		new Thread(
				new Runnable() {
					public void run() {				
						for(int i=1;i<=50;i++){
							business.sub3(i);
						}					
					}
				}
				).start();		

		for(int i=1;i<=50;i++){
			business.main(i);
		}	
	}

	static class Business {
		Lock lock = new ReentrantLock();
		//几个线程轮流执行就定义几个condition
		Condition condition1 = lock.newCondition();
		Condition condition2 = lock.newCondition();
		Condition condition3 = lock.newCondition();
		private int shouldSub = 1;//条件,该第几个线程运行
		public  void sub2(int i){
			lock.lock();
			try{
				while(shouldSub != 2){
					try {
						condition2.await();
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
				for(int j=1;j<=10;j++){
					System.out.println("sub2 thread sequence of " + j + ",loop of " + i);
				}
				shouldSub = 3;//该第三个线程运行了
				condition3.signal();
			}finally{
				lock.unlock();
			}
		}

		public  void sub3(int i){
			lock.lock();
			try{
				while(shouldSub != 3){
					try {
						condition3.await();
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
				for(int j=1;j<=20;j++){
					System.out.println("sub3 thread sequence of " + j + ",loop of " + i);
				}
				shouldSub = 1;
				condition1.signal();
			}finally{
				lock.unlock();
			}
		}		  

		public  void main(int i){
			lock.lock();
			try{
				while(shouldSub != 1){
					try {
						condition1.await();
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
				for(int j=1;j<=100;j++){
					System.out.println("main thread sequence of " + j + ",loop of " + i);
				}
				shouldSub = 2;
				condition2.signal();
			}finally{
				lock.unlock();
			}
		}
	}
}

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);
		//循环10遍相当于创建了10个线程
		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);//约定需要3个同时到达的线程
		for(int i=0;i<3;i++){
			Runnable runnable = new Runnable(){
					public void run(){
					try {
						Thread.sleep((long)(Math.random()*10000));	
						//getNumberWaiting返回当前在屏障处等待的参与者数目。
						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);//计数器的初始值是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();//让当前线程等待该计数器到0
						System.out.println("线程" + Thread.currentThread().getName() + 
						"已接受命令");								
						Thread.sleep((long)(Math.random()*10000));	
						System.out.println("线程" + Thread.currentThread().getName() + 
								"回应命令处理结果");						
						cdAnswer.countDown();//在当前线程中让计数器减1					
					} catch (Exception e) {
						e.printStackTrace();
					}				
				}
			};
			service.execute(runnable);
		}		
		try {
			Thread.sleep((long)(Math.random()*10000));
			System.out.println("线程" + Thread.currentThread().getName() + 
					"即将发布命令");						
			cdOrder.countDown();//在主线程中将计数器的计数减1,类似于裁判比赛倒计时
			System.out.println("线程" + Thread.currentThread().getName() + 
			"已发送命令,正在等待结果");	
			cdAnswer.await();//让主线程等待该计数器为0。当上面的3个线程都运行完计数器就为0了。类似于裁判等待运动员到达公布成绩
			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);//创建一个可阻塞数组队列,允许放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 {
						//将此处的睡眠时间分别改为100和1000,观察运行结果
						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);//一开始主线程不运行,就通过构造代码块在程序运行之前在队列2中放入数据
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}

		public  void sub(int i){//这里如果加synchronized关键字那么会出现死锁
			try {
				queue1.put(1);//程序一执行让子线程先运行,就先在队列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();//子线程运行完毕取走队列2的元素让主线程运行
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}

		public  void main(int i){
			try {
				queue2.put(1);//由于程序开始执行时队列2是满的所以会阻塞在这里
			} 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

传统集合类在并发访问(多个线程访问)时会出现问题。比如死循环。
  • 此时就需要在读的时候不能写,写的时候不能读,但可以并发地读。
  • 如果不被多个线程并发访问,就使用传统集合
  • 		/*
    		 * 在进行元素读取的时候不要进行其他操作,会出现死循环。
    		 * 比如hasNext正好读到最后一个元素时,其他线程进来执行remove操作,hasNext内部cursor!=count,始终返回true,成为死循环
    		 */
    		count=4;
    		while(hasNext()){
    			next(){cursor++}//next方法内部原理
    		}
    		//hasNext方法内部原理
    		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异常的原因。
使用同步集合实现迭代时对集合进行修改:
package cn.itcast.heima;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
public class CollectionModifyExceptionTest {
	public static void main(String[] args) {
		Collection users = new CopyOnWriteArrayList();//在写的时候有一分拷贝
		users.add(new User("张三",28));	
		users.add(new User("李四",25));			
		users.add(new User("王五",31));	
		Iterator itrUsers = users.iterator();
		while(itrUsers.hasNext()){
			System.out.println("aaaa");
			User user = (User)itrUsers.next();
			if("李四".equals(user.getName())){
				users.remove(user);
				//itrUsers.remove();
			} else {
				System.out.println(user);				
			}
		}
	}
}	 

空中网挑选实习生的面试题

第一题:

现有的程序代码模拟产生了16个日志对象,并且需要运行16秒才能打印完这些日志,请在程序中增加4个线程去调用parseLog()方法来分头打印这16个日志对象,程序只需要运行4秒即可打印完这些日志对象。原始代码如下:
package cn.itcast.heima;
public class Test {
	public static void main(String[] args){
		System.out.println("begin:"+(System.currentTimeMillis()/1000));
		/*模拟处理16行日志,下面的代码产生了16个日志对象,当前代码需要运行16秒才能打印完这些日志。
			修改程序代码,开四个线程让这16个对象在4秒钟打完。
		 */
		for(int i=0;i<16;i++){  //这行代码不能改动
			final String log = ""+(i+1);//这行代码不能改动
			{
				Test.parseLog(log);
			}
		}
	}

	//parseLog方法内部的代码不能改动
	public static void parseLog(String log){
		System.out.println(log+":"+(System.currentTimeMillis()/1000));
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}		
	}
}
分析:线程如何得到产生的数据?将数据存放在队列中,然后从队列中取
步骤:创建四个线程,将产生的数据存入集合,线程在集合中取数据
package cn.itcast.heima;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class Test {
	public static void main(String[] args){
        final BlockingQueue queue = new ArrayBlockingQueue(1);//1个数据也可以,
        //final BlockingQueue queue = new ArrayBlockingQueue(16);//可以装16个数据
        //创建四个线程
		for(int i=0;i<4;i++){
			new Thread(new Runnable(){
				@Override
				public void run() {
					while(true){
						try {
							String log = queue.take();//从阻塞队列中拿数据
							parseLog(log);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
			}).start();
		}
		
		System.out.println("begin:"+(System.currentTimeMillis()/1000));
		/*模拟处理16行日志,下面的代码产生了16个日志对象,当前代码需要运行16秒才能打印完这些日志。
		修改程序代码,开四个线程让这16个对象在4秒钟打完。
		*/
		for(int i=0;i<16;i++){  //这行代码不能改动
			final String log = ""+(i+1);//这行代码不能改动
			{
					try {
						queue.put(log);//将数据放入队列中
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
			}
		}
	}
	
	//parseLog方法内部的代码不能改动
	public static void parseLog(String log){
		System.out.println(log+":"+(System.currentTimeMillis()/1000));
		
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}		
	}
}
考察了阻塞队列的使用

第二题:

现成程序中的Test类中的代码在不断地产生数据,然后交给TestDo.doSome()方法去处理,就好像生产者在不断地产生数据,消费者在不断消费数据。请将程序改造成有10个线程来消费生成者产生的数据,这些消费者都调用TestDo.doSome()方法去进行处理,故每个消费者都需要一秒才能处理完,程序应保证这些消费者线程依次有序地消费数据,只有上一个消费者消费完后,下一个消费者才能消费数据,下一个消费者是谁都可以,但要保证这些消费者线程拿到的数据是有顺序的。原始代码如下:
package queue;
public class Test {
	public static void main(String[] args) {
		System.out.println("begin:"+(System.currentTimeMillis()/1000));
		for(int i=0;i<10;i++){  //这行不能改动
			String input = i+"";  //这行不能改动
			String output = TestDo.doSome(input);
			System.out.println(Thread.currentThread().getName()+ ":" + output);
		}
	}
}

//不能改动此TestDo类
class TestDo {
	public static String doSome(String input){
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		String output = input + ":"+ (System.currentTimeMillis() / 1000);
		return output;
	}
}
分析:
线程如何得到产生的数据?通过队列
这里使用同步队列SynchronousQueue,它是一个阻塞队列,每一个插入操作对应一个取出操作,只有线程一旦来取了,才会插入数据。所以它不指定大小
使用lock或者semaphore实现线程互斥,一个一个执行
package queue;

import java.util.concurrent.Semaphore;
import java.util.concurrent.SynchronousQueue;

public class Test {
	public static void main(String[] args) {
		final Semaphore semaphore = new Semaphore(1);
		final SynchronousQueue queue = new SynchronousQueue();
		for(int i=0;i<10;i++){
			new Thread(new Runnable(){
				public void run() {	
					try {
						semaphore.acquire();//当前线程获取一个信号灯,保证线程一个一个取,而不是10个线程同时取数据(这里用锁也可以)
						String input = queue.take();
						String output = TestDo.doSome(input);
						System.out.println(Thread.currentThread().getName()+ ":" + output);
						semaphore.release();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}	
				}
			}).start();
		}
		
		System.out.println("begin:"+(System.currentTimeMillis()/1000));
		for(int i=0;i<10;i++){  //这行不能改动
			String input = i+"";  //这行不能改动
			try {
				queue.put(input);//将数据放在队列中
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

//不能改动此TestDo类
class TestDo {
	public static String doSome(String input){
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		String output = input + ":"+ (System.currentTimeMillis() / 1000);
		return output;
	}
}

第三题:

现有程序同时启动了4个线程去调用TestDo.doSome(key, value)方法,由于TestDo.doSome(key, value)方法内的代码是先暂停1秒,然后再输出以秒为单位的当前时间值,所以,会打印出4个相同的时间值,如下所示:
4:4:1258199615
1:1:1258199615
3:3:1258199615
1:2:1258199615
请修改代码,如果有几个线程调用TestDo.doSome(key, value)方法时,传递进去的key相等(equals比较为true),则这几个线程应互斥排队输出结果,即当有两个线程的key都是"1"时,它们中的一个要比另外其他线程晚1秒输出结果,如下所示:
4:4:1258199615
1:1:1258199615
3:3:1258199615
1:2:1258199616
总之,当每个线程中指定的key相等时,这些相等key的线程应每隔一秒依次输出时间值(要用互斥),如果key不同,则并行执行(相互之间不互斥)。原始代码如下:
	package syn;

	//不能改动此Test类	
	public class Test extends Thread{
		
		private TestDo testDo;
		private String key;
		private String value;
		
		public Test(String key,String key2,String value){
			this.testDo = TestDo.getInstance();
			/*常量"1"和"1"是同一个对象,下面这行代码就是要用"1"+""的方式产生新的对象,
			以实现内容没有改变,仍然相等(都还为"1"),但对象却不再是同一个的效果*/
			this.key = key+key2; 
			this.value = value;
		}

		public static void main(String[] args) throws InterruptedException{
			Test a = new Test("1","","1");
			Test b = new Test("1","","2");
			Test c = new Test("3","","3");
			Test d = new Test("4","","4");
			System.out.println("begin:"+(System.currentTimeMillis()/1000));
			a.start();
			b.start();
			c.start();
			d.start();

		}
		
		public void run(){
			testDo.doSome(key, value);
		}
	}

	class TestDo {

		private TestDo() {}
		private static TestDo _instance = new TestDo();	
		public static TestDo getInstance() {
			return _instance;
		}

		public void doSome(Object key, String value) {
	
			// 以大括号内的是需要局部同步的代码,不能改动!
			{
				try {
					Thread.sleep(1000);
					System.out.println(key+":"+value + ":"
							+ (System.currentTimeMillis() / 1000));
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
分析:
需要同步,同步的锁就是传入的key,但是锁字符串内容相同,但不一定是同一对象,所以需要进行判断。
需要用到同步集合。
package syn;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;

//不能改动此Test类	
public class Test extends Thread{
	
	private TestDo testDo;
	private String key;
	private String value;
	
	public Test(String key,String key2,String value){
		this.testDo = TestDo.getInstance();
		/*常量"1"和"1"是同一个对象,下面这行代码就是要用"1"+""的方式产生新的对象,
		以实现内容没有改变,仍然相等(都还为"1"),但对象却不再是同一个的效果*/
		this.key = key+key2; 
/*		a = "1"+"";
		b = "1"+"";//这里a和b都是常量相加,编译器会自动优化为同一对象
*/
		this.value = value;
	}

	public static void main(String[] args) throws InterruptedException{
		Test a = new Test("1","","1");
		Test b = new Test("1","","2");
		Test c = new Test("3","","3");
		Test d = new Test("4","","4");
		System.out.println("begin:"+(System.currentTimeMillis()/1000));
		a.start();
		b.start();
		c.start();
		d.start();

	}
	
	public void run(){
		testDo.doSome(key, value);
	}
}

class TestDo {

	private TestDo() {}
	private static TestDo _instance = new TestDo();	
	public static TestDo getInstance() {
		return _instance;
	}

//	private ArrayList keys = new ArrayList();//这里不能使用ArrayList集合。因为会发生ConcurrentModificationException的问题,此例中会出现一个线程迭代集合的同时另一个线程在往集合中添加元素。比如此例中a线程进入往集合中添加了一个元素,b线程进入因为元素相同就进行迭代操作,这时c线程也是同时执行的,进入后会执行添加操作。出现该错误必须是这种情况出现,否则程序运行不会报错!!
	private CopyOnWriteArrayList keys = new CopyOnWriteArrayList();
	public void doSome(Object key, String value) {
		Object o = key;
		if(!keys.contains(o)){
			keys.add(o);//如果集合中没有该元素(实际上判断有无元素与之相等),就存入该对象
		}else{
			//如果有该元素,就直接遍历集合,找到该元素,并用该元素作为同步的锁
			for(Iterator iter=keys.iterator();iter.hasNext();){
				try {
					Thread.sleep(20);//减慢迭代的速度(等着别的线程进行加入元素操作),增加出现并发修改问题的概率
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				Object oo = iter.next();
				if(oo.equals(o)){
					o = oo;
					break;
				}
			}
		}
		synchronized(o)
		// 以大括号内的是需要局部同步的代码,不能改动!
		{
			try {
				Thread.sleep(1000);
				System.out.println(key+":"+value + ":"
						+ (System.currentTimeMillis()/1000));//这里将毫秒数除以1000,所以看到的时间会一样
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

你可能感兴趣的:(JavaSE)