在学习使用多线程之前,首先要先明白为什么需要使用它。
使用多线程只有一个目的,那就是更好的利用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();
}
}
}
我们也做到了卖出火车票的任务,但是请我们结合实际的想一下,一个火车站,只有一个售票窗口,所有的消费者都来这一个窗口排队,是不是太耗时了。我们应该不难想到,开设多个售票窗口不就好了吗?在开设新的售票窗口前,先看一下如何创建线程(开设多个窗口)。
创建线程:
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();
}
}
在上例中,该类继承了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 线程在运行");
}
}
}
3.线程的生命周期
首先上一张典型的图:
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();
}
}
我们成果的开设了四个新的售票窗口,但是我们也发现了新的问题。在代码中我们判断当前未售出票数是否大于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();
}
}
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();
}
}
对于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();
}
}
就这样,这么浪费水的项目就无限运行下去了。
在这段代码中,“水塘”抽象为两个线程的共享对象。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;
}
}
}
我们原本希望,在网购送货期间去超时买菜,这样才能体现多线程的优势,但由于run()方法不能返回值,而使得多线程的意义没有了。在厨具送到期间,我们不能干任何事,通过调用join()方法阻塞主线程。用于保证厨具送到了,但是当我们去掉这一行,会发生什么呢?
我们订完订单后,出门买菜了,在这期间快递送到了,发现家里没人,于是便丢失了菜刀。我们只能在家等待或者在快递到来之前买菜回来,可以保证运行。不然从代码来看,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 {}
}
经过这次的改进,我们可以在快递员送货期间去买菜;而且我们可以得知送货物流,看到没到,甚至可以在货没到的时候,取消订单。