1. 线程

到目前为止介绍过的各种范例都是单线程程序,也就是启动的程序从main()程序进入点开始至结束只有一个流程。有时候需要设计程序可以拥有多个流程,也就是所谓的多线程(Multi-thread)程序。

1.1 线程简介

如果要设计一个龟兔赛跑游戏,

赛程长度为10步,每经过一秒,乌龟会前进一步,兔子则可能前进两步或睡觉.

那该怎么设计呢?如果用目前所学过的单线程程序来说,你可能会这样设计:

public class TortoiseHareRace {
    public static void main(String[] args) throws InterruptedException {
        boolean[] flags = {true, false};
        int totalStep = 10;
        int tortoiseStep = 0;
        int hareStep = 0;
        System.out.println("比赛开始...");
        while(tortoiseStep < totalStep && hareStep < totalStep) {
            Thread.sleep(1000);
            tortoiseStep++;
            System.out.printf("乌龟跑了 %d 步...%n", tortoiseStep);
            boolean isHareSleep = flags[((int) (Math.random() * 10)) % 2];
            if(isHareSleep) {
                System.out.println("兔子睡着了zzzz");
            } else {
                hareStep += 2;
                System.out.printf("免子跑了 %d 步...%n", hareStep);
            }
        }
    }
}

由于程序只有一个流程,所以只能将乌龟与兔子的行为混杂在这个流程中写,

而且为什么每次都先递增乌龟再递增兔子步数呢?

这样对兔子很不公平啊!

如果可以撰写程序再启动两个流程,一个是乌龟流程,一个兔子流程,程序逻辑会比较清楚。

在Java中,如果想在main()以外独立设计流程,可以撰写类操作java.lang.Runnable接口,流程的进入点是操作在run()方法中。

例如,可以这样设计乌龟的流程:

public class Tortoise implements Runnable {
    private int totalStep;
    private int step;
    
    public Tortoise(int totalStep) {
        this.totalStep = totalStep;
    }
    
    @Override
    public void run() {
        try {
            while(step < totalStep) {
                Thread.sleep(1000);
                step++;
                System.out.printf("乌龟跑了 %d 步...%n", step);
            }
        } catch(InterruptedException ex) {
            throw new RuntimeException(ex);
        }
    }
}

在Tortoise类中,乌龟的流程会从run()开始,乌龟只要专心负责每秒走一步就可以了,不会混杂兔子的流程。同样地,可以这样设计兔子的流程:

public class Hare implements Runnable {
	private boolean[] flags = { true, false };
	private int totalStep;
	private int step;

	public Hare(int totalStep) {
		this.totalStep = totalStep;
	}

	@Override
	public void run() {
		try {
			while (step < totalStep) {
				Thread.sleep(1000);
				boolean isHareSleep = flags[((int) (Math.random() * 10)) % 2];
				if (isHareSleep) {
					System.out.println("兔子睡着zzzz");
				} else {
					step += 2;
					System.out.printf("免子跑了 %d 步...%n", step);
				}
			}
		} catch (InterruptedException ex) {
			throw new RuntimeException(ex);
		}
	}
}

在Hare类中,兔子的流程会从run()开始,兔子只要专心负责每秒睡觉或走两步就可以了,不会混杂乌龟的流程。

在Java中,从main()开始的流程会由主线程(Main thread)执行,那么刚才设计的Tortoise与Hare,run()方法定义的流程该由谁执行呢?可以创建Thread实例来执行Runnable实例定义的run()方法。例如:

public class TortoiseHareRace2 {
    public static void main(String[] args) {
        Tortoise tortoise = new Tortoise(10);
        Hare hare = new Hare(10);
        Thread tortoiseThread = new Thread(tortoise);
        Thread hareThread = new Thread(hare);
        tortoiseThread.start();
        hareThread.start();
    }
}

要启动线程执行指定流程,必须调用Thread实例的start()方法。

1.2 Thread与Runnable

从抽象观点与开发者的角度来看,JVM是台虚拟计算机,

只安装一颗称为主线程的CPU,可执行main()定义的执行流程。

如果想要为JVM加装CPU,就是创建Thread实例,

要启动额外CPU就是调用Thread实例的start()方法。

额外CPU执行流程的进入点,可以定义在Runnable接口的run()方法中。

除了将流程定义在Runnable的run()方法中之外,另一个写多线程程序的方式,就是继承Thread类,重新定义run()方法。从写程序的角度来看,前面的龟兔赛跑也可以改写为以下:

public class HareThread extends Thread {
    private boolean[] flags = {true, false};
    private int totalStep;
    private int step;
    
    public HareThread(int totalStep) {
        this.totalStep = totalStep;
    }
    
    @Override
    public void run() {
        try {
            while(step < totalStep) {
                Thread.sleep(1000);
                boolean isHareSleep = flags[((int) (Math.random() * 10)) % 2];
                if(isHareSleep) {
                    System.out.println("兔子睡着了zzzz");
                } else {
                    step += 2;
                    System.out.printf("免子跑了 %d 步...%n", step);
                }
            }
        } catch(InterruptedException ex) {
            throw new RuntimeException(ex);
        }
    }
    
}

public class TortoiseThread extends Thread {
    private int totalStep;
    private int step;
    
    public TortoiseThread(int totalStep) {
        this.totalStep = totalStep;
    }
    
    @Override
    public void run() {
        try {
            while(step < totalStep) {
                Thread.sleep(1000);
                step++;
                System.out.printf("乌龟跑了 %d 步...%n", step);
            }
        } catch(InterruptedException ex) {
            throw new RuntimeException(ex);
        }
    }
}


public class TortoiseHareRace3 {
    public static void main(String[] args) {
        new TortoiseThread(10).start();
        new HareThread(10).start();
    }
}

在Java中,任何线程可执行的流程都要定义在Runnable的run()方法。事实上,Thread类本身也操作了Runnable接口。

那么是操作Runnable在run()中定义额外流程好,还是继承Thread在run()中定义额外流程好?

  • 操作Runnable接口的好处就是较有弹性,你的类还有机会继承其他类。
  • 若继承了Thread,那该类就是一种Thread,通常是为了直接利用Thread中定义的一些方法,才会继承Thread来操作。

2.线程生命周期

​ 线程生命周期颇为复杂,下面将从最简单的开始介绍。

2.1 Daemon线程

主线程会从main()方法开始执行,直到main()方法结束后停止JVM。

如果主线程中启动了额外线程,默认会等待被启动的所有线程都执行完run()方法才中止JVM。

如果一个Thread被标示为Daemon线程,在所有的非Daemon线程都结束时,JVM自动就会终止。

从main()方法开始的就是一个非Daemon线程,可以使用setDaemon()方法来设定一个线程是否为Daemon线程。

比如

public class DaemonDemo {
    public static void main(String[] args) {
        Thread thread = new Thread() {
            public void run() {
                while(true) {
                    System.out.println("Orz");
                }
            }  
        };
         thread.setDaemon(true);
        thread.start();
    }
}

如果没有使用setDaemon()设定为true,则程序会不断地输出"Orz"而不终止;使用isDaemon()方法可以判断线程是否为Daemon线程。

默认所有从Daemon线程产生的线程也是Daemon线程,因为基本上由一个背景服务线程衍生出来的线程,也应该是为了在背景服务而产生的,所以在产生它的线程停止时,也应该一并跟着停止。

2.2 Thread基本状态图

在调用Thread实例start()方法后,基本状态为可执行(Runnable)、被阻断(Blocked)、执行中(Running),状态间的转移如下图

image-20190628215544126

实例化Thread并执行start()之后,线程进入Runnable状态,

此时线程尚未真正开始执行run()方法,必须等待排班器(Scheduler)排入CPU执行,

线程才会执行run()方法,进入Running状态。

线程看起来像是同时执行,但事实上同一时间点上,一个CPU还是只能执行一个线程,

只是CPU会不断切换线程,且切换动作很快,所以看来像是同时执行。

线程有其优先权,可使用Thread的setPriority()方法设定优先权,可设定值为1(Thread.MIN_PRIORITY)到10(Thread.MAX_PRIORITY),默认是5(Thread.NORM_PRIORITY),超出1到10外的设定值会抛出IllegalArgumentException。数字越大优先权越高,排班器越优先排入CPU,

如果优先权相同,则输流执行(Round-robin)。

有几种状况会让线程进入Blocked状态,例如前面调用Thread.sleep()方法,就会让线程进入Blocked(其他还有进入synchronized前竞争对象锁定的阻断、调用wait()的阻断等,之后就会介绍);

等待输入/输出完成也会进入Blocked,之前控制台模式下等待用户输入时就是。

运用多线程,当某线程进入Blocked时,让另一线程排入CPU执行(成为Running状态),避免CPU空闲下来,经常是改进效能的方式之一。

我们来一个例子

首先是单线程下载

public class Download {
    public static void main(String[] args) throws Exception {
        URL[] urls = {
            new URL("https://www.scala-lang.org/"),
            new URL("https://developer.mozilla.org/zh-CN/docs/Web/JavaScript"),
            new URL("https://www.python.org/"),
            new URL("https://groovy-lang.org/")
        };
        
        String[] fileNames = {
            "Scala.html",
            "JavaScript.html",
            "Python.html",
            "Groovy.html"
        };
        long start = System.currentTimeMillis();
        for(int i = 0; i < urls.length; i++) {
            dump(urls[i].openStream(), new FileOutputStream(fileNames[i]));
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
    
    private static void dump(InputStream src, OutputStream dest)
                               throws IOException {
        try (InputStream input = src; OutputStream output = dest) {
            byte[] data = new byte[1024];
            int length = -1;
            while ((length = input.read(data)) != -1) {
                output.write(data, 0, length);
            }
        }
    }
}

然后是多线程下载

public class Download2 {
    public static void main(String[] args) throws Exception {
    	final long start = System.currentTimeMillis();
    	// 为了测试结束的时间,添加了一个JVM关闭的钩子。 
    	Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
			@Override
			public void run()
			{	
				final long stop = System.currentTimeMillis();
				System.out.println(stop - start);
			}
		}));
        final URL[] urls = {
                new URL("https://www.scala-lang.org/"),
                new URL("https://developer.mozilla.org/zh-CN/docs/Web/JavaScript"),
                new URL("https://www.python.org/"),
                new URL("https://groovy-lang.org/")
            };
            
        final  String[] fileNames = {
                "Scala.html",
                "JavaScript.html",
                "Python.html",
                "Groovy.html"
            };

        for(int i = 0; i < urls.length; i++) {
            final int index = i;
            new Thread() {
                @Override
                public void run() {
                    try {
                        dump(urls[index].openStream(), new FileOutputStream(fileNames[index]));
                    } catch(IOException ex) {
                        throw new RuntimeException(ex);
                    }
                }
            }.start();
        }
    }
    
    private static void dump(InputStream src, OutputStream dest)
                               throws IOException {
        try (InputStream input = src; OutputStream output = dest) {
            byte[] data = new byte[1024];
            int length = -1;
            while ((length = input.read(data)) != -1) {
                output.write(data, 0, length);
            }
        }
    }
}

线程因输入/输出进入Blocked状态,在完成输入/输出后,会回到Runnable状态,等待排班器排入执行(Running状态)。

一个进入Blocked状态的线程,可以由另一个线程调用该线程的interrupt()方法,让它离开Blocked状态。

举个例子来说,使用Thread.sleep()会让线程进入Blocked状态,若此时有其他线程调用该线程的interrupt()方法,会抛出InterruptedException异常对象,这是让线程“醒过来”的方式。以下是个简单示范:

2.4 安插线程

如果A线程正在运行,流程中允许B线程加入,等到B线程执行完毕后再继续A线程流程,则可以使用join()方法完成这个需求。

这就好比你手上有份工作正在进行,老板安插另一工作要求先做好,然后你再进行原本正在进行的工作。

当线程使用join()加入至另一线程时,另一线程会等待被加入的线程工作完毕,然后再继续它的动作,join()的意思表示将线程加入成为另一线程的流程中。

public class JoinDemo {
    public static void main(String[] args) {
        System.out.println("Main thread 开始...");
        Thread threadB = new Thread() {
            @Override
            public void run() { 
                try { 
                    System.out.println("Thread B 开始..."); 
                    for(int i = 0; i < 5; i++) { 
                        Thread.sleep(1000); 
                        System.out.println("Thread B 执行..."); 
                    }
                    System.out.println("Thread B 将结束..."); 
                } 
                catch(InterruptedException e) { 
                    e.printStackTrace(); 
                } 
            } 
        };
        
        threadB.start();

        try {
            // Thread B 加入 Main thread 流程
            threadB.join();
        } 
        catch(InterruptedException e) { 
            e.printStackTrace(); 
        } 
        System.out.println("Main thread 将结束...");
    }    
}

程序启动后主线程就开始,在主线程中新建threadB,并在启动threadB后,将之加入(join())主线程流程中,所以threadB会先执行完毕,主线程才会再继续原本的流程。

执行结果如下

Main thread 开始...
Thread B 开始...
Thread B 执行...
Thread B 执行...
Thread B 执行...
Thread B 执行...
Thread B 执行...
Thread B 将结束...
Main thread 将结束...


有时候加入的线程可能处理太久,你不想无止境等待这个线程工作完毕,则可以在join()时指定时间,如join(10000),这表示加入成为流程的线程至多可处理10000毫秒,也就是10秒,

如果加入的线程还没执行完毕就不理它了,目前线程可继续执行原本工作流程。

2.3 停止线程

线程完成run()方法后,就会进入Dead,进入Dead(或已经调用过start()方法)的线程不可以再次调用start()方法,否则会抛出IllegalThreadStateException。

Thread类上定义有stop()方法,不过被标示为Deprecated,被标示为Deprecated的API,表示过去确实定义过,后来因为会引发某些问题,为了确保向前兼容性,这些API没有直接剔除,但不建议新写的程序再使用它。

直接调用Thread的stop()方法,将不理会所设定的释放、取得锁定流程,线程会直接释放所有已锁定对象(锁定的概念稍后会谈到),这有可能使对象陷入无法预期状态。

除了stop()方法外,Thread的resume()、suspend()、destroy()等方法也不建议再使用

如果要停止线程,最好自行操作,让线程跑完应有的流程,而非调用Thread的stop()方法。

比如

class SomeRunner implements Runnable {

	private boolean isContinue = true;

	public void stop() {
		isContinue = false;
	}

	@Override
	public void run() {
		while (isContinue) {
			try {
				Thread.sleep(50);
				System.out.println("SomeRunner");
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
};

public class StopDemo {
	public static void main(String[] args) throws InterruptedException {
		SomeRunner someRunner = new SomeRunner();
		Thread thread = new Thread(someRunner);
		thread.start();
		Thread.sleep(1000);
		someRunner.stop();
	}

}

在这个程序片段中,若线程执行了run()方法,就会进入while循环,想要停止线程,就是调用Some的stop(),这会将isContinue设为false,在跑完此次while循环,下次while条件测试为false就会离开循环,执行完run()方法,线程也就进入Dead状态而停止。

因此不仅有停止线程必须自行根据条件操作,线程的暂停、重启,也必须视需求操作,而不是直接调用suspend()、resume()等方法。