Java 线程入门

648 查看

主线程

首先每个 Java 程序都是从主线程开始运行的,主线程就是执行 main() 方法的那个线程。在 main() 方法中获取当前线程很简单:

// 示例1
public static void main(String[] args) {
    Thread mainThread = Thread.currentThread();
    System.out.println("当前线程: " + mainThread.getName());
}

Thread 对象的文档在这里Thread 对象包含很多方法和属性,除了上面例子当中的 name 属性外,还有状态、优先级等等。现在我们只需要知道 main() 方法是在主线程中运行就可以了。

线程是可以暂停的

我们通常使用 sleep() 方法来使线程在指定的时间内暂停执行。下面是一个在主线程中执行循环和暂停的例子:

// 示例2
public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
        System.out.println(i);

        try {
            Thread.sleep(500);    // 暂停线程
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

注意:首先,sleep() 方法是静态的,因为它永远只能用在当前的线程上;其次,sleep() 方法会抛出异常,该异常通常发生在暂停状态被打断时。所以这里用 try-catch 代码块包围起来。

sleep() 方法接受一个 long 类型的参数,指明需要暂停多少毫秒。这个例子当中,我们循环输出 i 变量,共循环 10 次,每输出一次就暂停 500 毫秒。

你可能觉得整个程序的运行时间会是精确的 5000 毫秒,但请千万不要这么认为,首先 sleep() 方法并非十分精确,CPU 在各个线程之间切换会要花掉很微量的一点时间,如果这个例子循环次数不是 10 次而是十万次百万次,那么积累的误差就会比较大了;其次,代码中的 System.out.println() 方法和 for 循环本身也要花掉一点时间,所以每次循环不会是绝对精确的 500 毫秒。

创建新的线程

除了主线程外,我们还可以创建和执行另外的线程。执行新线程的方式是调用该线程对象的 start() 方法。

// 示例3
public static void main(String[] args) {
    Thread thread1 = new Thread();
    thread1.start();    // 启动线程
}

从这个例子中我们可以看到,thread1 是一个通过 new Thread() 创建出来的对象。把线程看作是对象这点十分重要,这意味着我们可以创建 Thread 的子类,而子类的对象仍然是线程对象。执行这段代码什么输出都没有,因为我们没有为 thread1 定义要执行什么操作。下面的例子中,我们让 thread1 来做循环输出。

// 示例4
public static void main(String[] args) {

    Thread thread1 = new Thread() {

        @Override
        public void run() {               // 指定线程要做的事
            for (int i = 0; i < 10; i++) {
                System.out.println(i);

                try {
                    Thread.sleep(500);    // 暂停线程
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    };

    thread1.start();    // 启动线程
}

这里我们创建了一个 Thread 类的匿名子类,并覆写了 run() 方法。通过覆写 run() 方法,我们可以指定线程要做哪些事情。

线程之间没有父子关系
线程与线程之间是平等的,并没有父子关系。不过 Java 为了方便管理线程,定义了一个叫线程组(ThreadGroup)的类,线程组之间可以存在父子关系。不过这个概念平常用的很少,所以这里只是顺带提下,不作详细介绍。

线程是并行执行的

在示例4中,thread1 其实是与主线程并行执行的。为了演示这点,我们首先将这个循环提取成一个方法:

private static void printNumbers(int start, int end) {
    for (int i = start; i < end; i++) {
        System.out.println(i);

        try {
            Thread.sleep(500);    // 暂停线程
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

然后示例4就变成了:

public static void main(String[] args) {

    Thread thread1 = new Thread() {

        @Override
        public void run() {
            printNumbers(0, 10);  // 提取出来的方法
        }
    };

    thread1.start();    // 启动线程
}

我们在最后面添加一行,让主线程在启动 thread1 之后也做一个循环输出:

// 示例5
public static void main(String[] args) {

    Thread thread1 = new Thread() {

        @Override
        public void run() {
            printNumbers(0, 10);  // 循环输出
        }
    };

    thread1.start();        // 启动线程
    printNumbers(100, 110); // 主线程也循环输出
}

执行 main() 方法,在输出中你可以看到 0~9 与 100~109 交替出现,这说明主线程和 thread1 在同时执行。

将线程中的逻辑独立出来

为了使线程中的逻辑能够被重用,我们通常将其声明为一个独立的类。在前面的代码示例中,我们都是以匿名类的方式来创建线程的。独立声明一个线程类的方式是这样的:

// 示例6
public class MyThread extends Thread {

    private int start;
    private int end;

    // 构造方法
    public MyThread(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    public void run() {
        printNumbers();
    }

    private void printNumbers() {
        for (int i = this.start; i < this.end; i++) {
            System.out.println(i);

            try {
                Thread.sleep(500);    // 暂停线程
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

在示例 6 中我们可以看到,执行这个线程所需的两个参数现在变成了 MyThread 的两个成员。这是我们向线程传递执行参数的一般方式。提取成独立的类之后,线程使用起来就非常简单了:

public static void main(String[] args) {
    new MyThread(0,10).start();
    new MyThread(100,110).start();
    new MyThread(1000,1010).start();
}

线程的返回值

我们有时候希望当线程执行完毕时,我们能得到一个结果。在示例 6 中我们了解了向线程传递参数的方式,类似的我们也可以为线程类定义一个成员用来保存线程的执行结果。下面是一个例子:

// 示例7
public class ThreadWithReturnValue extends Thread {

    public String result;

    @Override
    public void run() {
        try {
            Thread.sleep(3000);
            this.result = "result";  // 假设产生结果需要比较长的时间
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        ThreadWithReturnValue thread = new ThreadWithReturnValue();
        thread.start();
        Thread.sleep(3100);
        System.out.println(thread.result);  // 获得结果
    }
}

在这个例子当中,ThreadWithReturnValue 线程产生结果需要 3 秒钟,那么主线程就需要等待 3 秒以上才能得到 "result",否则就只能得到 null。在实际情况中,我们并不知道线程产生结果需要多长时间,而我们也不想无限制的等下去。

出于这样的目的,Thread 对象为我们提供了 join() 方法,用于等待指定的线程直到执行完毕。示例 7 当中的 main() 方法可以改造成这样子:

public static void main(String[] args) throws Exception {
    ThreadWithReturnValue thread = new ThreadWithReturnValue();
    thread.start();
    thread.join();      // 等待直到 thread 执行完毕
    System.out.println(thread.result);
}

这样我们就能在线程执行完毕时立刻得到结果了。我们可以运行多个线程,然后依次调用它们的 join() 方法,这样等待的时间就是它们当中运行最久的那个线程的运行时间。当它运行完毕时,我们就得到所有线程的执行结果了。线程的入门概念就介绍到这里,本文只是介绍非常基本的概念,Java 在处理多线程和并发方面还有很多很复杂的东西等待你去了解和尝试。