Java中的多线程-01

前言

前面我们学习了Java8中一些新的特性,不过那些基本上都是支线剧情。主线剧情我们已经走完了Java中IO,现在就要开始另一个主线剧情了——Java中的多线程。这也是非常重要的一个部分。下面就来简单的学习一下吧。

什么是多线程

说到多线程,这又得说到我们说设计模式的时候说到的那个单例模式了。正常情况下,饿汉式单例模式是不会出现问题的,但是一旦进入了多线程的环境的时候,代码就会出现很大的问题。那么究竟是为什么会出问题呢?多线程到底又是什么呢?

计算机中有很多的进程,比如说现在我的电脑中开着很多软件。qq是一个进程,微信也是一个进程。每一个程序都是一个进程。我们的计算机就是多进程的。每个进程都有多个线程。比如QQ中和不同的人聊天,每一个聊天窗口就可以看出是一个线程(其实聊天窗口也应该是多线程的)。如果QQ是单线程的,我们每次只能和一个人聊天,而不能同时和多个人聊天,这该多无聊。(不对啊,有人找我用QQ聊过天吗?笑cry)

把上面的例子类比到我们的Java程序中来。也就是说如果我们的Java程序一次只可以干一件事情,那么就是单线程的,如果同时干多件事情,那么就是多线程的。简单一点来说就是,如果我们可以只用一条线就可以将代码中所有的操作连接在一起,那么就是单线程,若是出现了分叉,那就是多线程的。不过众所周知,Java中程序运行的都是按照从上到下的顺序来执行的,怎么可以同时执行多个操作呢?

我们当然是不可能通过之前是方式来创建多线程的操作的,所以Java提供了一个类java.lang.Thread,通过这个类,就可以实现多线程的操作。

java.lang.Thread

上面我们说到了多线程的实现和这个类有关,那么我们就来学习一下这个类的有关的信息。

java.lang.Object 
    java.lang.Thread 
public class Thread
extends Object
implements Runnable
  • 线程是程序中执行的线程。Java虚拟机允许应用程序同时执行多个执行线程。
  • 每个线程都有优先权。 具有较高优先级的线程优先于优先级较低的线程执行。 每个线程可能也可能不会被标记为守护程序。 当在某个线程中运行的代码创建一个新的Thread对象时,新线程的优先级最初设置为等于创建线程的优先级,并且当且仅当创建线程是守护进程时才是守护线程。
  • 当Java虚拟机启动时,通常有一个非守护进程线程(通常调用某些指定类的名为main的方法)。 Java虚拟机将继续执行线程,直到发生以下任一情况:
    • 已经调用了Runtime类的exit方法,并且安全管理器已经允许进行退出操作。
    • 所有不是守护进程线程的线程都已经死亡,无论是从调用返回到run方法还是抛出超出run方法的run

至于这个类的构战方法和实例方法现在暂且不看了。其实上面的东西也没有什么看的必要。不过官方文档下面给我们的信息是非常重要的。在jdk1.5之前,我们可以使用两种方式创建线程。一种方式是继承Thread类,另一种方式是实现Runnable接口。

创建一个线程

继承Thread

现在我们想要使用多线程的方式输出0-100之中所有的偶数。首先是第一中方式,继承Thread类。

但我们需要创建一个线程的时候,需要一下的几个步骤。

  • 写一个类继承Thread
  • 重写类中的run()方法。
  • 创建这个类的实例
  • 调用类中的start()方法。
public class Demo1 {
    public static void main(String[] args) {
        Thread thread = new Mythread();
        thread.start();
        System.out.println("Main Function End!");
    }
}

class Mythread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
            }
        }
    }
}

比如说上面的代码就是按照上面的步骤创建的一个多线程。那么如何证明这个是多线程呢?Java程序中原来就是一个单线程执行的,这个线程是主线程。主线程从main函数开始执行。执行到run之后,开启了一个新的新的线程。此时这个新的线程和主线程是同时开始运行的。所以此时我们执行这个代码,输出的结果可能就是Main Functin End先开始输出了。

不过为什么我们重写的run方法,要启动线程却要调用start方法呢?我们是否可以直接调用run方法?确实是可以直接调用run方法的,不过这个就不是多线程了,而就是一个普通的类的调用。此时Main Function End肯定是最后输出的。所谓的start方法其实是java.lang.Thread这个类的专门用来启动线程的方法,我们不通过这个start方法是没有办法启动一个线程的。至于run方法,在java.lang.Thread类中其实也是有实现的,不过按道理来说应该是一个抽象函数,为什么会有实现呢?这里可以以后再说。

现在我们要是两个线程去输出结果,现在应该怎么办呢?很简单,再次调用一下start方式不就可以了?不过很遗憾,这个方式是大错特错的。这样做会抛出一个java.lang.IllegalThreadStateException异常,一个线程只能够被启动一次。其实之后我们就会明白了,一个线程就像一个人,调用start就相当于出生了,run方法执行完毕就相当于死亡了。你想要一个线程起死回生这明显是不可能的事情。不过我们可以再生一个线程。(正如你的爸妈觉得你太笨了,是没办法把你塞会娘胎里重新生一次,不过他们可以选择再生一个,蛤蛤蛤)。

不过现在还是有问题,我们搞不清楚现在是谁输出的啊?其实我们可以通过getName()方法得到线程的名字。我们可以让主线程也参与到输出中来。不过如何获取主线程的名字?我们可以使用Thread.currentThread().getName()方法获取运行当前的代码的代码的线程名字。对于前面的Thread及其子类说,可以直接使用getName()方式。(相当于this.getName())

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

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

    }
}

class Mythread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(getName()+": "+i);
            }
        }
    }
}

上面其实就是三线程的代码了。观察其中的一段输出结果。

main: 18
Thread-1: 38
Thread-0: 20
Thread-1: 40
main: 20
Thread-1: 42
Thread-0: 22
Thread-1: 44

Thread-0Thread-1是自动给我们直接创建的线程的编号。当然我们也可以通过setName()方式给我们的线程起一个名字。不过这个操作我们必须要在这个线程调用start方法之前使用。给主线程起名字就和上面得到主线程的名字一样,可以使用Thread.currentThraed().setName()。不过除了上面的方法之外,我们还可以直接在构战函数中给定线程的名字。因为我发现Thread类有如下的构造函数。

Thread(String name) 
// 分配一个新的 Thread对象。 

不过构造函数式无法继承的。我们需要在MyThread中写一个Mythread(String name)构造函数,不过空参构造函数也是必须的。

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

Mythread() {
    super();
}

上面是关于线程名字设置的一些bb,不过现在让我回到输出的内容中来。我们发现这三个线程的输出是有交互的。这足以说明这三个线程是并行的了,也就是这三个线程是同时进行的。

*注意: *

上面我说这三个线程的同时进行的。其实这是一个误解。其实同一个时间上其实还是只有一个线程被执行。因为我们的CPU同一个时间段只可以执行一个操作。那我们为什么说他们是并行的呢?那是因为CPU切换的速度非常快,他可能是执行这个线程一点点时间,然后去执行另外的线程,然后再次切回到这个线程。这个切换的时间非常的短。正是因为时间非常的短,我们才会说这个是可以看成是多线程的。正如我们看的电影,其实就是一张张图片连续切换。有的是一秒六十帧,有的是八十帧,还有的是一百二十帧。正是因为切换的速度非常的快,我们才感觉这个是连续的。如果每秒就放几张 。。。emm,这不是PPT吗?

实现Runnable接口

上面我们使用的方式是继承的方式来实现创建线程。不过这可能会带来一个比较大的问题。众所周知,Java中的类都是单继承了。如果我们继承了Thread这个类,那么我们就无法继承其他的类。比如上面我们的Mythread如果应该是MyTinythread的子类,但是因为继承Thread就无法继承MyTinythread了。所以说使用继承的方式来实现多线程倒是有点限制。不过Java中接口是没有个数限制的,你要是本事大,你实现一百个接口也没有人管你。所以现在我们需要使用接口的方式来实现多线程的操作。

因为说实在的,上面我们创建多线程的操作。除了写个run函数,其余的操作都是没有用的操作。我们甚至可以使用匿名内部类的方式来创建一个多线程,比如。

new Thread(){
            @Override
            public void run() {
                System.out.println("Hello World");
            }
        }.start();

经过我们之前的学习,匿名内部类这个玩意是和接口有点儿关系的。这个接口应该是只有一个void run()方法来需要我们实现的。这个接口就是Runnable接口。

public class Demo2 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread);
        thread.start();

    }
}

class MyThread implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

我们可以通过上面的方式来创建一个多线程。

要注意的是,虽然我们的MyThread类实现了Runnable接口并且实现了其中的run方法。我们是不可以使用myThread.start()方法来启动这个线程的。因为MyThread没有启动线程的能力。而接口也只是一个协议,这只是告诉别人,我实现了一个方法void run()。并没有其他的信息。我们想要启动线程,还是要借助java.lang.Thread类。

我们发现java.lang.Thread中还是有这样的构造函数。

Thread(Runnable target) 
// 分配一个新的 Thread对象。  
Thread(Runnable target, String name) 
// 分配一个新的 Thread对象。  

不过现在问题也来了,我们实现了这个接口,但是我们调用start方法启动的不应该是Thread中的run(上面我们推测应该是一个抽象方法的)吗?和接口中的run又有什么联系呢?我们来看一下源码就好了。

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

可以看到如果我们没有传递Runnable接口的话,这个函数确实是什么都不会做。如果我们使用继承的方式的话,这个函数应该是被覆盖了的,当时我们猜测父类中的函数应该是抽象函数就是没有考虑到构造函数中还可以使用Runnable接口。如果使用Runnable接口的话,target就不会是null。然后就会调用接口中的run方法。

下面我们继续来探讨一下上面谈过的问题。

第一个设置线程的名字。可以通过上面的第二个构造函数来传递名字参数。(此时不需要我们写其他的构造函数了)。不过也可以通过在调用start方法前,使用thread.setName()的方式。

第二个获取线程的名字。依旧可以使用getName()的方式。不过需要注意的是,在MyThread中,我们不可以使用getName()的方式来获取线程的名字了。因为此时这个类已经不是Thread的子类,也不存在getName()这个方法了。如果在这个类中,我们需要使用像获取主线程的名字那样的形式Thread.currentThread().getName()的方式。

第三个就是如果我们此时还想要开始一个同样的线程应该如何?继续new一个MyThread??当然不是,我们说过线程有生有死,不能起死回生。但是MyThread并不是一个线程。我们想要重新创建一个线程的话,只需要new一个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 {

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

总结

上面只是简单的介绍了一下两种创建线程的方式,至于其他更加深入的就要等到下次再讲了。


一枚小菜鸡