我爱编程

多线程

2018-03-29  本文已影响22人  SingleDiego

创建线程

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,再对周边的表达式求值。

toStringObject 根类的方法,我们通过覆盖该方法,来将对象转换成字符串。当我们打印该对象时,Java 将自动调用该方法。(类似于 Python 的 __str__ 方法)。

Thread 基类的构建方法(super())可以接收一个字符串作为参数。该字符串是该线程的名字,并使用 getName() 返回。

定义类之后,我们在 main() 方法中创建线程对象。每个线程对象为一个线程。创建线程对象后,线程还没有开始执行。

我们调用线程对象的 start() 方法来启动线程。start() 方法可以在构造方法中调用。这样,我们一旦使用 new 创建线程对象,就立即执行。

Thread 类还提供了下面常用方法:

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 对象中的计数器,来同步花括号中的代码。

上一篇下一篇

猜你喜欢

热点阅读