多线程
创建线程
Java 中“一切皆对象”,线程也被封装成一个对象。我们可以通过继承 Thread
类来创建线程。
线程类中的的 run()
方法包含了该线程应该执行的指令。我们在衍生类中覆盖该方法,以便向线程说明要做的任务:
class NewThread extends Thread {
private static int threadID = 0; // shared by all
// 构造器
public NewThread() {
super("ID:" + (++threadID));
}
// 将对象转为字符串
// 当我们打印该对象时,Java 将自动调用该方法
public String toString() {
return super.getName();
}
// 该线程进行什么操作
public void run() {
System.out.println(this);
}
}
public class Test
{
public static void main(String[] args)
{
NewThread thread1 = new NewThread();
NewThread thread2 = new NewThread();
thread1.start(); // start thread1
thread2.start(); // start thread2
}
}
输出:
ID:1
ID:2
++
是 Java 中的累加运算符,即让变量加 1。这里 ++
出现在 threadID
之前,说明先将 threadID
加 1,再对周边的表达式求值。
toString
是 Object
根类的方法,我们通过覆盖该方法,来将对象转换成字符串。当我们打印该对象时,Java 将自动调用该方法。(类似于 Python 的 __str__
方法)。
Thread
基类的构建方法(super()
)可以接收一个字符串作为参数。该字符串是该线程的名字,并使用 getName()
返回。
定义类之后,我们在 main()
方法中创建线程对象。每个线程对象为一个线程。创建线程对象后,线程还没有开始执行。
我们调用线程对象的 start()
方法来启动线程。start()
方法可以在构造方法中调用。这样,我们一旦使用 new
创建线程对象,就立即执行。
Thread
类还提供了下面常用方法:
-
join(Thread tr)
等待线程 tr 完成 -
setDaemon()
设置当前线程为后台daemon (进程结束不受daemon线程的影响)
Thread类官方文档: http://docs.oracle.com/javase/6/docs/api/java/lang/Thread.html
Runnable
实现多线程的另一个方式是实施 Runnable
接口,并提供 run()
方法。实施接口的好处是容易实现多重继承(multiple inheritance)。然而,由于内部类语法,继承 ··Thread·· 创建线程可以实现类似的功能。我们在下面给出一个简单的例子:
class NewThread implements Runnable {
public String toString() {
return Thread.currentThread().getName();
}
public void run() {
System.out.println(this);
}
}
public class Test {
public static void main(String[] args) {
Thread thread1 = new Thread(new NewThread(), "first");
Thread thread2 = new Thread(new NewThread(), "second");
thread1.start(); // start thread1
thread2.start(); // start thread2
}
}
输出:
first
second
synchronized(同步)
多任务编程的难点在于多任务共享资源。对于同一个进程空间中的多个线程来说,它们都共享堆中的对象。某个线程对对象的操作,将影响到其它的线程。
在多线程编程中,要尽力避免竞争条件(racing condition),即运行结果依赖于不同线程执行的先后。线程是并发执行的,无法确定线程的先后,所以我们的程序中不应该出现竞争条件。
然而,当多任务共享资源时,就很容易造成竞争条件。我们需要将共享资源,并造成竞争条件的多个线程线性化执行,即同一时间只允许一个线程执行。
下面是一个售票程序。3个售票亭(Booth)共同售卖 100 张票(Reservoir)。
每个售票亭要先判断是否有余票,然后再卖出一张票。如果只剩下一张票,在一个售票亭的判断和售出两个动作之间,另一个售票亭卖出该票,那么第一个售票亭(由于已经执行过判断)依然会齿形卖出,造成票的超卖。
为了解决该问题,判断和售出两个动作之间不能有“空隙”。也就是说,在一个线程完成了这两个动作之后,才能有另一个线程执行。
在 Java 中,我们将共享的资源置于一个对象中,比如下面 Reservoir
对象。它包含了总共的票数;将可能造成竞争条件的,针对共享资源的操作,放在 synchronized
方法中,比如下面的 sellTicket()
。
synchronized
关键字,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用 synchronized
方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。
使用 synchronized
修饰符。我们就能排除了竞争条件的可能。
class Reservoir {
private int total; // 票数总量
// 构造器
public Reservoir(int t)
{
this.total = t;
}
// 使用 synchronized(同步) 方法
// 该方法用于保证线程安全
public synchronized boolean sellTicket()
{
if(this.total > 0) {
this.total = this.total - 1;
return true; // 售出一张票
}
else {
return false; // 余票不足
}
}
}
// 继承 Thread 类,用于多线程
class Booth extends Thread {
private static int threadID = 0; // 表示线程ID的类属性
private Reservoir release; // sell this reservoir
private int count = 0; // owned by this thread object
// 构造器
public Booth(Reservoir r) {
super("ID:" + (++threadID));
this.release = r; // all threads share the same reservoir
this.start();
}
public String toString() {
return super.getName();
}
public void run() {
while(true) {
// 如果符合售票条件
if(this.release.sellTicket()) {
this.count = this.count + 1;
System.out.println(this.getName() + ": sell 1");
try {
sleep((int) Math.random()*100); // 休眠(随机数)
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
else {
break;
}
}
System.out.println(this.getName() + " I sold:" + count);
}
}
public class Test {
public static void main(String[] args) {
Reservoir r = new Reservoir(10);
Booth b1 = new Booth(r);
Booth b2 = new Booth(r);
Booth b3 = new Booth(r);
}
}
Java 的每个对象都自动包含有一个用于支持同步的计数器,记录 synchronized
方法的调用次数。执行synchronized
方法时线程获得该计数器,计数器加 1。
当 synchronized
方法调用结束并退出时,计数器减 1。其他线程如果也调用了同一对象的 synchronized
方法,必须等待该计数器变为 0,才能锁定该计数器,开始执行。
关键代码
上面,我们利用 synchronized
修饰符同步了整个方法。我们可以同步部分代码,而不是整个方法。这样的代码被称为 关键代码(critical section)。我们使用下面的语法:
synchronized (syncObj) {
...;
}
花括号中包含的是想要同步的代码,syncObj
是任意对象。我们将使用 syncObj
对象中的计数器,来同步花括号中的代码。