在学习使用多线程之前,首先要先明白为什么需要使用它。
使用多线程只有一个目的,那就是更好的利用CPU资源,因为大多数的多线程代码都可以用单线程来实现。
让我们想象一个简单的例子,火车站的售票厅,我们用单线程来想一下:
public class SignleSale {
	static int num = 10;
	static public void sale() {
		System.out.println("剩余票数: " + num--);
	}
	public static void main(String[] args) {
		int n = 5;
		for(int i=0;i<5;i++) {
			sale();
		}
	}
}

java多线程的艺术_java多线程的艺术


我们也做到了卖出火车票的任务,但是请我们结合实际的想一下,一个火车站,只有一个售票窗口,所有的消费者都来这一个窗口排队,是不是太耗时了。我们应该不难想到,开设多个售票窗口不就好了吗?在开设新的售票窗口前,先看一下如何创建线程(开设多个窗口)。

创建线程:

1.继承Thread类

Thread类中常用的两个构造方法如下:
public Thread(String threadName);
public Thread();

通过继承Thread类创建新的线程的语法如下:

public class ThreadTest extends Thread{
	//.....
}

完成线程功能的代码放在run()方法中,当一个类继承Thread类后,就可以在该类中覆盖run()方法,将实现该线程功能的代码写入run()方法中,然后调用Thread类中的start()方法,也就是调用run()方法。实例如下:

public class ThreadTest extends Thread{
	private int count = 10;
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(true) {
			System.out.println(count + " ");
			if (--count == 0) {
				return;
			}
		}
	}
	public static void main(String[] args) {
		new ThreadTest().start();
		new ThreadTest().start();
	}
}

java多线程的艺术_java多线程的艺术_02


在上例中,该类继承了Thread类,然后覆盖了run()方法。通常在run()方法中使用无限循环的形式,使得线程一直运行下去,所以要指定一个跳出循环的条件,如本例中使用变量count递减为零作为跳出循环的条件。

在main方法中,使线程执行需要调用Thread类中的start()方法,,start()方法调用被覆盖的run()方法,如果不调用start()方法,线程永远不会启动,在主方法没有调用start()方法之前,Thread对象只是一个实例,而不是一个真正的线程。

2.实现Runnable接口

既然可以用继承Thread的方式创建线程,那么为什么还要存在Runnable接口?因为Thread在一个方面非常的尴尬,由于Java是单继承语言,所以如果你需要继承其他类(非Thread类)并使该程序可以使用线程,就需要用到Runnable接口了。
实现Runnable接口的语法如下:

public class Thread extends Object implements Runnable

如果你有兴趣去看一下Thread类的官方API,从中就可以惊奇的发现,Thread类实际上就是实现了Runnable接口,其中的run()方法正式对Runnable接口中的run()方法的具体实现。
实现Runnable接口的程序会创建一个Thread对象,并将Runnable对象与Thread对象相关联。Thread类有两个构造方法:

public Thread(Runnable r)
public Thread(Runnable r, String name)
使用Runnable接口启动新线程的步骤如下:
1.首先编写一个实现Runnable接口的类,然后实例化该类对象,建立Runnable对象。
2.接下来使用相应的构造方法创建Thread实例。
3.使用该实例调用Thread类中的start()方法启动线程
实例如下:
public class ThreadDemo implements Runnable{
	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i=0 ; i<10;i++) {
			System.out.println("TestThread 线程在运行");
		}
	}
	public static void main(String[] args) {
		ThreadDemo demo = new ThreadDemo();
		new Thread(demo).start();
		for(int i= 0;i<10;i++) {
			System.out.println("main 线程在运行");
		}
	}
}

java多线程的艺术_就绪状态_03

3.线程的生命周期

首先上一张典型的图:

java多线程的艺术_就绪状态_04

java线程具有五中基本状态:
**新建状态(new)**:当线程对象被创建后,即进入了新建状态,如:Thread t = new MyThread();
**就绪状态(Runnable)**:当调用线程对象的start()方法,线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行t.start()此线程立即就会执行;
**运行状态(Running)**:当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程想要进入运行状态执行,首先必须处于就绪状态中;
**阻塞状态(Blocked)**:处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种。
	a)等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
	b)同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
	c)其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,该线程重新转入就绪状态;
**死亡状态(Dead)**:线程执行完了或者因异常退出了run()方法,该线程结束生命周期;

4.多线程的创建及启动

回到我们的火车站售票窗口,我们希望开设多个窗口,于是我们利用实现Runnable接口的方法得到了下面的代码,如例:

public class ThreadSafeTest implements Runnable{
	int num=10;
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(true) {
			if (num > 0) {
				try {
					Thread.sleep(300);
				} catch (Exception e) {
					// TODO: handle exception
					e.printStackTrace();
				}
				System.out.println("剩余票数: "+num--);
			}
		}
	}
	public static void main(String[] args) {
		ThreadSafeTest test = new ThreadSafeTest();
		Thread tAThread = new Thread(test);
		Thread tBThread = new Thread(test);
		Thread tCThread = new Thread(test);
		Thread tDThread = new Thread(test);
		tAThread.start();
		tBThread.start();
		tCThread.start();
		tDThread.start();
	}
}

java多线程的艺术_System_05


我们成果的开设了四个新的售票窗口,但是我们也发现了新的问题。在代码中我们判断当前未售出票数是否大于0,则执行将票出售给乘客的功能,但当两个线程同时访问这段代码时(假如这时只剩下一张票),第一个线程将票售出,但与此同时,线程二刚好执行完判断的语句,并且得出结论是票数大于0,于是第二个线程也执行了售出操作,这样就产生了负数。于是我们引出了新的问题-----》线程安全。

5.线程同步机制

为了解决上述问题,我们提出了锁这个概念,在给定时间内只允许一个线程访问共享资源,这就需要给共享资源上一道锁。就好像是上洗手间,当一个人进去的时候,就要给门上锁,当出来的时候把锁打开,其它人才可以进。【线程同步机制中,对于锁此处只是简单的使用,今后会单独写关于synchronized的文章】
**a)同步快**
public class CopyOfThreadSafeTest2 implements Runnable{
	int num = 10;
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(true) {
			synchronized (this) {
				if (num > 0) {
					try {
						Thread.sleep(1000);
					} catch (Exception e) {
						// TODO: handle exception
						e.printStackTrace();
					}
					System.out.println("剩余票数为: "+ --num);
				}
			}
		}
	}
	public static void main(String[] args) {
		CopyOfThreadSafeTest2 test2 = new CopyOfThreadSafeTest2();
		Thread tA = new Thread(test2);
		Thread tB = new Thread(test2);
		Thread tC = new Thread(test2);
		Thread tD = new Thread(test2);
		tA.start();
		tB.start();
		tC.start();
		tD.start();
	}
}

java多线程的艺术_就绪状态_06


b) 同步方法

public class CopyOfThreadSafeTest implements Runnable{
	int num = 10;
	public synchronized void doit() {
		if (num > 0) {
			try {
				Thread.sleep(10);
			} catch (Exception e) {
				// TODO: handle exception
				e.printStackTrace();
			}
			System.out.println("tickets"+--num);
		}
	}
	
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(true) {
			doit();
		}
	}
	public static void main(String[] args) {
		CopyOfThreadSafeTest copyOfThreadSafeTest = new CopyOfThreadSafeTest();
		Thread tAThread = new Thread(copyOfThreadSafeTest);
		Thread tBThread = new Thread(copyOfThreadSafeTest);
		Thread tCThread = new Thread(copyOfThreadSafeTest);
		Thread tDThread = new Thread(copyOfThreadSafeTest);
		tAThread.start();
		tBThread.start();
		tCThread.start();
		tDThread.start();
	}
}

java多线程的艺术_ide_07


对于synchronized关键字,会在今后单独的文章中出现。

我们发现,之前的问题得以解决,打印到最后,票数没有出现负数,这是因为将资源放置在了同步块/同步方法中。就好像是在售票厅,顾客进入后看到很多个窗口,顾客们选择了人少的窗口进行排队,每个窗口都是一个线程在同时工作着,当任意一个售票员借到购买信息后,都会到同一个仓库中去取票,仓库中每次只能进一个人,当一个售票员拿完了票,才能轮到下一个。【此处本人感觉,没有绝对的线程优化,就像实际生活中,并不是大量的增多售票窗口就可以提升效率,提升了成本,反而效率降低。但是单独开设售票量较大的窗口,比如单独开设只售通往北京的售票窗口,也许会提升效率。】

6.线程的通信

有时,执行的多个任务之间可能有一定的联系,这时就需要使这些线程进行交互。
这里我们以一个水塘为例,有两个线程,一个为“进水”,一个为“排水”,当水塘满时,进水行为不能再进行,当水塘没有水时,出水进程不再进行。主要使用上面图中所提到的wait()与notify()方法。线程A代表“进水”,线程B代表“排水”,这两个线程都对水塘有访权限。当水塘没水时,线程B试图排水,这时候只好让B先等一会,如下操作:
if(Water.isEmpty()){
	water.wait();
}

在线程A向水塘注水之前,线程B不能从这个队列中释放出来,不能再次运行。当线程A向水塘注水后,由线程A通知线程B水塘中有水了,线程B开始运行。此时,等待队列中的第一个被阻塞的线程在队列中被释放出来,并加入竞争。如下操作:

water.notify();

先上整体代码:
Water类:

public class Water {
	static Object waterObject = new Object();
	static int total = 6;
	static int mqsl = 3;
	static int ps = 0;
}

排水类:

public class ThreadA implements Runnable{
	
	public boolean isEmpty() {
		return Water.mqsl==0?true:false;
	}
	
	void pswork() {
		synchronized(Water.waterObject) {
			System.out.println("水塘是否为空: "+isEmpty());
			if (isEmpty()) {
				try {
					Water.waterObject.wait();
				} catch (InterruptedException e) {
					// TODO: handle exception
					e.printStackTrace();
				}
			}else {
				Water.ps++;
				Water.mqsl--;
				System.out.println("水塘目前排水量 "+Water.ps);
			}
			System.out.println("Water.mqsl"+Water.mqsl);
		}
	}
	
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(Water.mqsl<Water.total) {
			if (isEmpty()) {
				System.out.println("水塘目前没有水,排水线程被挂起");
			}
			System.out.println("排水工作开始");
			pswork();
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO: handle exception
				e.printStackTrace();
			}
		}
	}
}

进水类:

public class ThreadB implements Runnable{
	void jswork() {
		synchronized (Water.waterObject) {
			Water.mqsl++;
			Water.waterObject.notify();
			System.out.println("水塘目前水量为 "+Water.mqsl);
		}
	}
	@Override
	public void run() {
			// TODO Auto-generated method stub
			while(Water.mqsl < Water.total) {
				System.out.println("进水工作开始");
				jswork();
				try {
				Thread.sleep(3000);
				} catch (InterruptedException e) {
					// TODO: handle exception
					e.printStackTrace();
				}
		}
	}
}

主函数类:

public class WaterMain {
	public static void main(String[] args) {
		ThreadA threadA = new ThreadA();
		ThreadB threadB = new ThreadB();
		new Thread(threadA).start();
		new Thread(threadB).start();
	}
}

java多线程的艺术_ide_08


就这样,这么浪费水的项目就无限运行下去了。

在这段代码中,“水塘”抽象为两个线程的共享对象。notify()方法最多只能释放等待队列中的第一个线程,如果有多个线程在等待,可以使用notifyAll()方法释放所有线程。

另外,wait()方法除了可以被notify()方法释放外,还可以通过interrupt()方法来释放,如果通过interrupt()方法来释放,wait()方法将会抛出一个异常,因此需要放到try_catch块中。

**

7.从线程中产生返回值

**
【此处的例子来源于网络,望原作者见谅】
上面的方法虽然创建了独立的工作,但是它们都不返回任何值。如果你希望任务在完成时能够返回一个值,那么可以实现Callable接口而不是Runnable接口或继承Thread。
我们先模拟一个场景:我们现在要做饭,可是没有菜刀,并且也没有菜。这时我们需要网购一把菜刀,并且去超市买菜,才能继续做饭。这次我们用Thread的继承,代码如下:

public class CommonCook {

	public static void main(String[] args) throws InterruptedException {
		long startTime = System.currentTimeMillis();
		//第一步 网购厨具
		OnlineShopping threadOnlineShopping = new OnlineShopping();
		threadOnlineShopping.start();
		threadOnlineShopping.join(); 	//保证厨具送到
		//第二步 去超市购买食材
		System.out.println("第四步:等待去超市购买食材");
		System.out.println("买啊买啊买啊买");
		Thread.sleep(3000); 	//模拟购买食材的时间
		Shicai shicai = new Shicai("大白菜");
		System.out.println("第五步:食材到位了");
		//第三步 用厨具烹饪食材
		System.out.println("第六步:开始展现厨艺");
		cook(threadOnlineShopping.chuju, shicai);
		
		System.out.println("总共用时" + (System.currentTimeMillis() - startTime)+"ms");
	}
	
	static class OnlineShopping extends Thread{
		private Chuju chuju;
		@Override
		public void run() {
			// TODO Auto-generated method stub
			System.out.println("第一步:下单");
			System.out.println("第二步:等待送货");
			try {
				System.out.println("等啊等啊等啊等");
				Thread.sleep(5000);	//模拟送货时间
			} catch (Exception e) {
				// TODO: handle exception
				e.printStackTrace();
			}
			System.out.println("第三步:快递送到");
			chuju=new Chuju("菜刀");
		}
	}
	
	//用厨具烹饪食材
	static void cook(Chuju chuju, Shicai shicai) {
		System.out.println(chuju.nameString+"  "+shicai.nameString);
	}

	//厨具类
	static class Chuju {
		String nameString;
		public Chuju(String name){
			this.nameString=name;
		}
	}

	//食材
	static class Shicai {
		String nameString;
		public Shicai(String name) {
			this.nameString=name;
		}
	}
}

java多线程的艺术_System_09


我们原本希望,在网购送货期间去超时买菜,这样才能体现多线程的优势,但由于run()方法不能返回值,而使得多线程的意义没有了。在厨具送到期间,我们不能干任何事,通过调用join()方法阻塞主线程。用于保证厨具送到了,但是当我们去掉这一行,会发生什么呢?

java多线程的艺术_ide_10


我们订完订单后,出门买菜了,在这期间快递送到了,发现家里没人,于是便丢失了菜刀。我们只能在家等待或者在快递到来之前买菜回来,可以保证运行。不然从代码来看,run方法不执行完,属性chuju就没有被赋值,还是null。换句话说,没有厨具怎么做饭。

现在面临的问题是,run()方法是没有返回值的,如果想要保存run方法里的结果,就必须等待run方法计算完,无论计算过程多么耗时。

于是我们迎来了Callable接口与Future模式的结合:

public class FutureCook {
	public static void main(String[] args) throws InterruptedException, ExecutionException{
		long startTime = System.currentTimeMillis();
		//第一步 网购厨具
		Callable<Chuju> onlineShopping = new Callable<Chuju>() {
			@Override
			public Chuju call() throws Exception {
				// TODO Auto-generated method stub
				System.out.println("第一步:下单");
				System.out.println("第二步:等待送货");
				System.out.println("等啊等啊等啊等");
				Thread.sleep(5000);	//模拟送货时间
				System.out.println("第三步:快递送到");
				return new Chuju();
			}
		};
		FutureTask<Chuju> task = new FutureTask<Chuju>(onlineShopping);	//把Callable实例当作对象,生成一个FutureTask的对象
		new Thread(task).start();	//然后把这个对象当作一个Runnable,作为参数另起线程
		//第四步 去超市购买食材
		System.out.println("第四步:去超市购买食材");
		System.out.println("买啊买啊买啊买");
		Thread.sleep(3000);	//模拟购买食材时间
		Shicai shicai = new Shicai();
		System.out.println("第五步:食材到位");
		//第六步 用厨具烹饪食材
		if (!task.isDone()) {	//联系快递员,询问是否到货
			System.out.println("第六步:厨具还没到,心情好就等,心情不好就cancel取消掉订单");
		}
		Chuju chuju = task.get();
		System.out.println("第六步:厨具到位,开始做饭");
		cook(chuju, shicai);
		System.out.println("总共用时" + (System.currentTimeMillis() - startTime)+"ms");
	}
	
	
	//用厨具烹饪食材
	static void cook(Chuju chuju, Shicai shicai) {}
	
	//厨具类
	static class Chuju {}
	
	//食材类
	static class Shicai {}
}

java多线程的艺术_ide_11


经过这次的改进,我们可以在快递员送货期间去买菜;而且我们可以得知送货物流,看到没到,甚至可以在货没到的时候,取消订单。