【Java】多线程-线程基础

Posted by 西维蜀黍 on 2019-01-31, Last Modified on 2021-09-21

线程的实现

Thread类是专门用来创建线程和对线程进行操作的类。当某个类继承了Thread类之后,该类就叫做一个线程类

在Java中通过run方法为线程指明要完成的任务,有两种方法来为线程提供run方法:

  • **继承Thread类并重写它的run方法。**之后创建这个子类的对象并调用start()方法。
  • **通过定义实现Runnable接口的类进而实现run方法。**这个类的对象在创建Thread的时候作为参数被传入,然后调用start()方法。

无论在哪种创建线程的两种方法中,均需调用线程对象的**start()方法以为线程分配必须的系统资源、调度线程运行并执行线程的run()**方法。

start()方法是启动线程的唯一的方法。start()方法首先为线程的执行准备好系统资源,然后再去调用run()方法。一个线程只能启动一次,再次启动就不合法了。

run()方法中放入了线程的工作,即我们要这个线程去做的所有事情。缺省状况下run()方法什么也不做。


在具体应用中,采用哪种方法来构造线程要视情况而定。通常,当一个线程已经继承了另一个类时,就应该用第二种方法来构造,即实现Runnable接口。

下面给出两个例子来说明线程的两种实现方法,每个例子中都有两个线程:

线程的运行与停止

例子1 - 创建线程对象

代码

public class Main {
    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        Thread2 thread2 = new Thread2();

        thread1.start();
        thread2.start();
    }
}

class Thread1 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; ++i) {
            System.out.println("Hello World: " + i);
        }
    }
}

class Thread2 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; ++i) {
            System.out.println("Welcome: " + i);
        }
    }
}

执行结果

Hello World: 0
Welcome: 0
Hello World: 1
Welcome: 1
Hello World: 2
Hello World: 3
Hello World: 4
Welcome: 2
Welcome: 3
Welcome: 4

由于这两个线程几乎在同一时间被置为runnable状态,因此 Hello World 和 Welcome 大打印顺序完全又 JVM 对这两个线程的调度决定,我们不可提前预知。

例子2 - 创建线程对象

代码

public class Main {
    public static void main(String[] args) {
        // 线程的另一种实现方法,也可以使用匿名的内部类
        Thread thread1 = new Thread(new MyThread1());
        thread1.start();

        Thread thread2 = new Thread(new MyThread2());
        thread2.start();
    }
}

class MyThread1 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; ++i) {
            System.out.println("Hello: " + i);
        }
    }
}

class MyThread2 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; ++i) {
            System.out.println("Welcome: " + i);
        }
    }
}

执行结果

Hello: 0
Hello: 1
Hello: 2
Hello: 3
Hello: 4
Welcome: 0
Welcome: 1
Welcome: 2
Welcome: 3
Welcome: 4

分析

Thread类也实现了Runnable接口,因此实现了接口中的run()方法。

当生成一个线程对象时,如果没有为其指定名字,那么线程对象的名字将使用如下形式:Thread-number,该number是自动增加的数字,并被所有的Thread对象所共享,因为它是一个static的成员变量。

当使用第一种方式(继承Thread的方式)来生成线程对象时,我们需要重写run()方法。

当使用第二种方式(实现Runnable接口的方式)来生成线程对象时,我们需要实现Runnable接口的run()方法,然后使用new Thread(new MyRunnableClass())来生成线程对象(MyRunnableClass已经实现了Runnable接口),这时的线程对象的run()方法会调用MyRunnableClass的run()方法。

停止线程

线程的消亡不能通过调用stop()命令,stop()方法是不安全的,已经废弃。

停止线程推荐的方式:设定一个标志变量,以让run()方法在执行完成时线程也结束执行。

具体的来说,在run()方法中是一个循环,由该标志变量控制循环是继续执行还是跳出;循环跳出,则线程结束。

代码演示

public class Main {
    public static void main(String[] args) {
        MyThreadClass r = new MyThreadClass();
        Thread t = new Thread(r);
        t.start();

        // 当希望线程停止执行时,调用以下方法
        r.stopRunning();
    }
}

class MyThreadClass implements Runnable {
    private boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            System.out.println("Do something.");
        }
    }

    public void stopRunning() {
        flag = false;
    }
}

多线程访问成员变量与局部变量

例子1 - 多线程访问成员变量

代码

public class Main {
    public static void main(String[] args) {
        HelloThread r = new HelloThread();

        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);

        t1.start();
        t2.start();
    }
}

class HelloThread implements Runnable {
    int i;

    @Override
    public void run() {

        while (true) {
            Thread t = Thread.currentThread();
            String name = t.getName();

            System.out.println(name + " say: hello number: " + i++);

            try {
                Thread.sleep((long) Math.random() * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if (10 == i) {
                break;
            }
        }
    }
}

运行结果

Thread-1 say: hello number: 0
Thread-0 say: hello number: 1
Thread-1 say: hello number: 2
Thread-0 say: hello number: 3
Thread-1 say: hello number: 4
Thread-0 say: hello number: 5
Thread-1 say: hello number: 6
Thread-0 say: hello number: 7
Thread-1 say: hello number: 8
Thread-0 say: hello number: 9

分析

由于HelloThread类的实例r对象被两个线程(t1与t2线程对象)共享,而i又为r对象的成员变量。因此,t1和t2线程会交替的数数(依次打印从0到9),直到i>10时结束。

例子2 - 多线程访问局部变量

当我们改变代码如下时(原先的成员变量i被注释掉,增加了方法中的局部变量i):

代码

public class Main {
    public static void main(String[] args) {
        HelloThread r = new HelloThread();

        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);

        t1.start();
        t2.start();
    }
}

class HelloThread implements Runnable {
    @Override
    public void run() {
        // 每一个线程都会拥有自己的一份局部变量的拷贝
        // 线程之间互不影响
        // 所以会打印20个数字,0到9每个数字都是两遍
        int i = 0;

        while (true) {
            Thread t = Thread.currentThread();
            String name = t.getName();

            System.out.println(name + " say: hello number: " + i++);

            try {
                Thread.sleep((long) Math.random() * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if (10 == i) {
                break;
            }
        }
    }
}

运行结果

Thread-0 say: hello number: 0
Thread-1 say: hello number: 0
Thread-1 say: hello number: 1
Thread-0 say: hello number: 1
Thread-1 say: hello number: 2
Thread-0 say: hello number: 2
Thread-1 say: hello number: 3
Thread-0 say: hello number: 3
Thread-1 say: hello number: 4
Thread-0 say: hello number: 4
Thread-1 say: hello number: 5
Thread-0 say: hello number: 5
Thread-1 say: hello number: 6
Thread-0 say: hello number: 6
Thread-1 say: hello number: 7
Thread-0 say: hello number: 7
Thread-1 say: hello number: 8
Thread-0 say: hello number: 8
Thread-1 say: hello number: 9
Thread-0 say: hello number: 9

分析

如注释中所述,由于局部变量对于每一个线程来说都有自己的拷贝,所以各个线程之间不再共享同一个变量,输出结果为20个数字,实际上是两组,每组都是0到9的10个数字,并且两组数字之间随意地穿插在一起。 

结论

  • 如果一个变量是成员变量,那么多个线程对同一个对象的成员变量进行操作时,它们对该成员变量是彼此影响的,也就是说一个线程对成员变量的改变会影响到另一个线程。
  • 如果一个变量是局部变量,那么每个线程都会有一个该局部变量的拷贝(即便是同一个对象中的方法的局部变量,也会对每一个线程有一个拷贝),一个线程对该局部变量的改变不会影响到其他线程。

Reference