Java中的多线程-01

前言

之前我们说过了创建线程的两种方式。一个是继承java.lang.Thread类,重写run方法,然后使用start方法来启动这个线程。另一个就是实现Runnable接口,实现其中的run方法,然后通过构造函数将这个类传递给一个java.lang.Thread类,然后使用start方法启动线程。实际上这两种创建线程的方式是统一的,不过要说区别还是有的。

多个线程

之前我们使用多线程都是让多个线程都输出0-100的所有的奇数。那么如果我们想要多个线程同时不重复的输出多个奇数该怎么办呢?如果我们使用第一种继承Thread的方式。

public class Demo1 {
    public static void main(String[] args) {
        Thread thread = new Mythread("线程一");
        thread.start();
        Thread thread2 = new Mythread("线程二");
        thread2.start();

//        Thread.currentThread().setName("主线程");
//        for (int i = 0; i < 100; i++) {
//            if (i % 2 == 0) {
//                System.out.println(Thread.currentThread().getName()+": "+i);
//            }
//        }
    }
}

class Mythread extends Thread {

    Mythread(String name) {
        super(name);
    }

    Mythread() {
        super();
    }

    private static int num = 1;

    @Override
    public void run() {
        while (num <= 100) {
            System.out.println(getName()+": "+num);
            num += 2;
        }
    }
}

这里只是简单的遍历一下100以内的所有的奇数,没有加什么奇数的判断。不过多个线程之间是如果交流的。这里我们使用了一个静态字段private static int num。通过Thread继承的线程,他们都是属于Mythread这个类的对象。如果我们想要一个类的多个对象之间共享数据,那么就可以通过静态成员的方式,因为静态成员是属于类的,而类只有那一个。

如果我们使用的是Runnable接口?

public class Demo2 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread thread1 = new Thread(myThread);
        thread1.setName("线程一");
        thread1.start();
        Thread thread2 = new Thread(myThread);
        thread2.setName("线程二");
        thread2.start();

//        Thread.currentThread().setName("主线程");
//
//        for (int i = 0; i < 100; i++) {
//            if (i % 2 == 0) {
//                System.out.println(Thread.currentThread().getName() + ": " + i);
//            }
//        }

    }
}

class MyThread implements Runnable {

    private int num = 1;

    @Override
    public void run() {
        while (num <= 100) {
            System.out.println(Thread.currentThread().getName() + ": " + num);
            num += 2;
        }
    }
}

这里我们使用的是private int num,是一个实例成员而不是上面那样的那个静态成员。为什么呢?因为MyThread这个类其实只有一个,上面的多个线程都是通过同一个MyThread对象传参而创建的。其实使用实现Runnable接口这个方式是具有一定的优势的,因为是基于同一个对象,这些线程天生具有数据共享的优势。

不过,问题就在于上面的两个操作是否可以满足我们的期望呢?这个待我们学习一下线程的生命周期再说。

线程的生命周期

前面我们说过了线程就像一个人,有出生也有死亡。不过这个只是线程的开始和结束,那么线程是否有其他的中间状态呢?

线程总共分为一下的五个阶段。

上面的两张都形象的显示了线程的生命周期。下面我们就以下面的这张为例子进行说明吧。

新建

首先是新建状态。这个也就是之前我们说的线程的出生。调用start方法之后线程将会进入新建状态。

就绪

其实是就绪状态。当线程进入新建状态之后就会进入就绪状态。所谓的就绪状态就是等待cpu的调度。之前也说过,所谓的多线程其实并不是同时进行。而是每个线程都执行一段时间。那些没有被执行的线程就处于就绪状态等待cpu的调度。其实关于线程的调度这个还有一个知识点,就是线程的优先级。多个线程都处于就绪状态,等待线程的调度,不过这样的调度是否有优先级呢?就比如说我们都在排队买东西,这是突然来了一个VIP客户,直接插到我们前面去了,因为他拥有更高的优先级。

线程的优先级是可以设置的,就像设置线程的名字一样。有setPrioritygetPriority方法。setPriority的参数是0-10的int类型的书。不过Thread类中定义了三种线程优先级的常量Thread.MIN_PROORITY, Thread.NORM_PRIORITYThread.MAX_PRIORITY。他们对于的枚举值分别是0 5 10。其中如果我没有对线程的优先级进行设置的话,线程的优先级将就是Thread.NORM_PRIORITY。不过需要注意的一点是,线程优先级高并不是意味着他将总会抢到低优先级的线程的资源。即使线程优先级是10也有可能被优先级是0的给抢占了资源。就好比100度的水也有不少的分子运动的比0度的水中的分子还慢。这个东西满足的是一种统计规律。

运行

当处于就绪状态的线程得到了CPU的调度权之后,将会进入运行状态,此时将会运行线程中的run方法。不过之前我们也说过CPU的调度是轮换的,有可能CPU就调用你1ms就调用其他的线程去了。不过此时线程的run方法还没有结束,此时线程将会返回之前的就绪状态。还有另外的一种情况,不是cpu剥夺调度的资源,而是线程自身志愿放弃调度资源。在线程运行的时候调用yield方法线程就会自动放弃处理器资源。

不过上面有一个要点就是,即使线程失去了资源了。也就是从运行状态回到了就绪状态,他依旧可以参与下一轮线程的抢夺。也就是说我调用yield方法之后,我自愿放弃了我拥有的资源,不过下一轮又抢到了,你这个就没办法了,别的线程是真的抢不过你这个欧皇啊!

阻塞

不过运行的时候也不一定会安全的回到就绪状态。有可能在运行的过程睡着了进入阻塞状态。比如说调用了Thread.sleep方法,线程就会如同方法那样睡着了。此时就是进入了阻塞状态。不过此时这个线程不会占用CPU的资源。毕竟只是你自己睡着了,如果你还占用资源的话,其他资源不也就都跟着你睡着了嘛。Thread.sleep接受一个long参数,代表的是要睡着的时间。时间到了之后,线程就会回到就绪状态,重新去抢占资源进入运行状态。

不过还有其他的方式进入阻塞状态。比如说IO操作的时候。这个应该是最容易理解的。比如我们使用scanner.next()的时候,程序会停下来,知道我们通过控制台输出信息并且按下回车键之后才会继续运行。其实程序停下来,主线程就是进入了阻塞状态了。

还有一个比较重要的方式join方法,图中没有谈到。当某个程序执行流中调用其他线程的 join() 方法时,调用线程将 被阻塞,直到 join() 方法加入的 join 线程执行完为止。也就是说在线程二中调用线程一.join()的时候,线程二将会进入阻塞状态,知道线程一死亡。比如说线程二要做的时候必须在线程一做完他的事的基础上就可以使用join方法。

还有其他的进入阻塞的方式,比如上面写的,等待同步锁,等待通知。还有suspend方法,不过这个suspend方法已经被废弃了,建议不要使用。童谣的还有resume方法,可以强行从阻塞进入就绪状态,和suspend是一对的。不过都是存在安全隐患的,不建议使用。

死亡

人终有一死,线程也是如此,每一个线程的最终目的地就是死亡。只不过通往死亡的道理可能有差别罢了。可能是run方法结束了,或者是线程运行的时候出现了异常或者错误。亦可能是调用了stop方法。不过这个方法也是不建议使用的。

其他方法

上面说了线程的生命周期,同时也说了不少的方法。现在还有几个方法需要补充的。比如说isAlive方法,可以判断线程是否还存活着。getState方法可以返回线程的状态。Thread类中有一个枚举类State定义了不同的状态。有NEW RUNNABLE BLOCKED WAITING TIMED_WAITING TERMINATED。其中多了一个TIMED_WAITING,官方文档中的解释是,这个状态是长时间的处于WAITING状态。

还有一个比较重要的概念。守护线程。Java中的线程分为两种用户线程和守护线程。Java的垃圾回收器GC就是一个守护线程。简单的来说守护线程就是来守护其他的线程的,如果要守护的线程结束了,那么守护线程也就没有运行的意义了。我们可以是Java中的setDaemon(true)方法将线程设置为当前线程的守护线程。

public class Demo2 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread thread1 = new Thread(myThread);
        thread1.setName("线程一");
        thread1.setDaemon(true);
        thread1.start();
        Thread thread2 = new Thread(myThread);
        thread2.setName("线程二");
        thread2.setDaemon(true);
        thread2.start();
        System.out.println(thread1.getState());

        Thread.currentThread().setName("主线程");

        for (int i = 0; i < 10; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
        System.out.println("主线程结束");
    }
}

此时当主线程结束的时候,线程一、二都会相继结束,因为他们是主线程的守护线程。不过主线程结束之后可能还会有输出,那个只是输出的延迟而已。

线程同步问题

上面已经说完了线程的生命周期以及线程的常用的一些方法。现在我们该回到我的正题上面来了,使用静态成员或者是实例成员变量的方式是否可以正确的工作呢?

看一下输出的结果如何。

线程一: 1
线程二: 1
线程一: 3
线程二: 5
线程一: 7
线程二: 9
......
......
线程二: 93
线程一: 95
线程二: 97
线程一: 99

上面省略了中间的部分,其实那部分都非常的正常,不过前面的两个1是什么意思。其实这个结果还不是很明显,然后我们来线程中加入sleep来模拟网络的延迟。

class Mythread extends Thread {

    Mythread(String name) {
        super(name);
    }

    Mythread() {
        super();
    }

    private static int num = 1;

    @Override
    public void run() {
        while (num <= 100) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getName()+": "+num);
            num += 2;
        }
    }
}

看一下有了延迟之后的输出如何

线程一: 1
线程二: 1
线程一: 5
线程二: 5
线程一: 9
线程二: 9
线程一: 13
线程二: 13
线程二: 17
线程一: 17
......
......
线程二: 89
线程一: 89
线程一: 93
线程二: 93
线程一: 97
线程二: 97
线程一: 101

或许是我这个延迟设置的有点高???这个输出简直是瞎扯,全是bug?不过是什么导致的呢?

我们先来考虑开始的情况,首先线程一进入了while循环,不过遇到了sleep,就睡了一会,线程二也是如此,进入了while,之后睡了一会。然后线程一输出了1,在22行代码还没有执行的时候,线程二也输出了1。后面的道理也是如此,这就是为什么同样的数值我们总是输出了两次。

再来看看最好的情况。此时num的值是99,所以线程一进出了,但是线程二还在执行加二的操作。等到线程一进入了while之后,num就变成了101,所以就输出了101。

上面就是对这个结果的分析。从上面我们可以看到问题所在了。线程之间不是同步的。多个线程同时执行这段逻辑使得这个逻辑变得残破不看。

一般来说,当涉及多多个线程共享数据的时候,我们就要考虑线程同步的问题了。不过如何同步线程呢?我们说过这个逻辑是单线程的逻辑,那我们就只能做一点牺牲强行将执行这段代码的多线程变成单线程。把多行道的车变成单行道怎么做呢?就是将收费站那样做。限制每次只能有一个车过。

总结

上面主要介绍了线程的生命周期,还有Thread类的主要的方法,还有就是提出了待解决线程的同步问题。


一枚小菜鸡