线程和进程一样,都是实现并发的一个基本单位。
线程是依赖进程存在的。先来说进程,进程就是程序的一次动态执行过程。通俗来讲,进程就是正在运行的程序,它是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。
那么,单进程就是一个时间段只能有一个程序执行。不难理解单进程的计算机只能做一件事。最早期的DOS系统就是单进程的,那么一旦系统中了病毒就会直接死机,到了后来的Windows,只要不是致命性的病毒,计算机也可以正常使用,因为它是多进程的,一个时间段上就可以运行多个程序。注意是一个时间段而不是一个时间点,由于CPU具备分时机制,所以每个进程都能循环获得自己的时间片。由于CPU的执行速度非常快,所以给人的感觉是同时在进行。
但是进程的启动所消耗的时间是非常长的(想象你打开一些比较大的软件),所以有必要对其进一步划分以提高性能。这就是线程,一个进程可以同时有多个线程(实际上也确实如此)。线程是比进程更小的执行单位,相当于一个应用程序中同时进行多个任务。
假如你现在想要听歌,是不是就要打开听歌软件?打开软件需要一段比较长的时间(即进程的启动),在这个听歌软件中你可以一边听歌,一边写评论,还可以同时进行搜索功能。这就是多线程。
多线程的作用不是提高执行速度,而是为了提高应用程序的使用率。
我们程序在运行的使用,都是在抢CPU的时间片(执行权),如果是多线程的程序,那么在抢到CPU的执行权的概率应该比较单线程程序抢到的概率要大。那么也就是说,CPU在多线程程序中执行的时间要比单线程多,所以就提高了程序的使用率。但是哪个线程能抢占到CPU的资源呢,这个是不确定的,因为多线程具有随机性。
重申一下,所有的线程一定要依附于进程才能够存在,一旦进程消失,线程一定也会消失。java是为数不多的支持多线程的开发语言之一。
java命令会启动java虚拟机,相当于启动了一个应用程序,相当于启动了一个进程。虚拟机会开启一个主线程去寻找main方法,所以说main方法是运行在主线程中的。但是虚拟机在工作时还会启动垃圾回收机制,也就相当于开启了另一个线程。所以说,我们的JVM虚拟机是多线程的。
简单来说,要实现多线程有三种方式:
三种方式怎么选择?
先上结论:尽量避免继承Thread类,优先考虑实现接口的方法。
原因:因为java采用的是单继承的模式,继承Thread类就会带来这种局限性,没法再继承其他类;另外,实现接口可以更方便的实现数据共享的概念,这个下面会解释。总之,记住一点就可以,使用实现接口的方式来写多线程更合理。
在java中,要想实现多线程,就必须依靠一个线程的主类。不管是实现Runnable或者Callable接口,还是直接继承Thread类,都是为了定义这个主类。
class 类名 extends Thread {
属性;
方法;
public void run(){
线程主体方法;
}
}
public class MyThread extends Thread {
private String name;
public MyThread(String name) {
this.name = name;
}
// 重写run方法,作为线程的主操作方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.name + "--->" + i);
}
}
}
好,我们按照模板写了一个简单的线程操作类,用来打印循环。
值得一提的是,所有的进程和线程一样,都必须轮流去抢占资源,所以多线程的执行应该是多个线程彼此之间交替进行。直接调用run()方法,并不能启动多线程,多线程启动的唯一方法就是Thread类中的start()方法。
public class Test {
public static void main(String[] args) {
// 实例化线程操作类
MyThread threadA = new MyThread("ThreadA");
MyThread threadB = new MyThread("ThreadB");
MyThread threadC = new MyThread("ThreadC");
// 开启多线程
threadA.start();
threadB.start();
threadC.start();
}
}
本程序中,我们实例化了三个线程类的对象,然后调用了通过Thread类继承而来的start()方法启动了多线程。
注意:多线程抢占CPU的时间片完全是随机的,也就是说,仅仅因为我们在代码中先开启了A线程并不能保证它一定会先执行,也就是说,打印出的结果完全是随机的,而且线程之间是交替进行的。
有人可能会问,线程不是依赖于进程存在的吗?没有进程,为什么可以直接实现线程呢?
这么来说吧,创建进程是系统级的操作,仅靠java语言是不能完成的,也就是说我们不可能靠java来开启一个进程,但是使用Java命令执行一个类是就相当于启动了一个JVM进程,主方法main就是主线程。
这其实也是多线程的开启是通过Thread类中的start方法而不是直接调用run()方法的原因。
我们看一下start()方法的原码:
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
可以发现,在start()方法里面会调用一个start0()的方法,而且这个方法是用native声明的。java中调用本机操作系统提供的函数的技术叫做JNI(Java Native Interface ),这个技术离不开特定的操作系统,因为多线程必须由操作系统来分配资源。这项操作是根据JVM负责根据不同的操作系统实现的。
java.lang.Thread 是专门负责线程操作的类,任何类只要继承了它就可以成为一个线程的主类。有了主类自然要有它的使用方法,那么主类中只需要重写run()方法就可以,这个run()方法就是线程的主体。
为什么必须是run()方法?
没有什么特殊原因,只是Thread类是如此规定的而已,它相当于线程的入口,我们使用Trread类中的start()方法开启线程后,run()方法里面的代码一定会被执行。
继承Thread的方法会带来单继承的问题,所以,实际开发中实现Runnable接口的方法更普遍。
这个接口非常简单:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
在Runnable接口中也定义了run()方法,所以线程的主类只需要重写此方法即可。
public class MyThread implements Runnable {
private String name;
public MyThread(String name) {
this.name = name;
}
//重写run方法,作为线程的主操作方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.name + "--->" + i);
}
}
}
好了,线程操作类写完了。但是有一个问题?怎么开启线程?接口中没有其他方法了呀!
这里说明一下:不管是以何种方法实现多线程,多线程的开启工作一定是由Thread类下的start()方法来完成的。
也就是,要想开启多线程,我们还需要实例化Thread对象,不一样的是,我们这次使用的是有参构造:public Thread(Runnable target)
,这个方法可以接收一个Runnable接口对象:
package com.xx.threadTest;
public class Test {
public static void main(String[] args) {
// 实例化线程操作类
MyThread threadA = new MyThread("ThreadA");
MyThread threadB = new MyThread("ThreadB");
MyThread threadC = new MyThread("ThreadC");
// 有参构造实例化Thread对象并开启线程
new Thread(threadA).start();
new Thread(threadB).start();
new Thread(threadC).start();
}
}
相信你已经发现了,Runnable接口使用了“@FunctionalInterface”的注解,证明Runnable是一个函数式接口,我们可以使用Lambda表达式风格来写:
public class LambdaThread {
public static void main(String[] args) {
String name = "线程对象";
new Thread(()->{
for (int i = 0; i < 100; i++) {
System.out.println(name + "--->" + i);
}
}).start();
}
}
它的效果和上面先写线程操作类再写线程启动的效果是完全一样的,它利用了Lambda表达式直接定义线程主体实现操作后启动,非常简便!
上面已经讲到,继承Thread类来定义线程操作类存在单继承的问题,那么除了这个意外,Thread类和Runnable接口还有什么练习呢?
这样的话,我们自己实现Runnable接口写的MyThread类和Thread类都继承了Runnable接口!这么模式类似于代理模式,但又不完全是,此处代理类Thread调用的start方法而不是接口中的run方法,这是本质差别。
但不是说继承Thread类就不能表现出,只是有些麻烦而已。
我们通过一个简单的卖票问题来说明:
package com.xx.ticket;
public class MyThread extends Thread {
private int ticket = 5;
@Override
public void run() {
for (int i = 0; i < 50; i++) {
if (this.ticket >0)
System.out.println("ticket=" + this.ticket--);
}
}
}
package com.xx.ticket;
public class Test1 {
public static void main(String[] args) {
MyThread mt1 = new MyThread();
MyThread mt2 = new MyThread();
MyThread mt3 = new MyThread();
mt1.start();
mt2.start();
mt3.start();
}
}
可能的执行结果:
ticket=5
ticket=4
ticket=3
ticket=2
ticket=1
ticket=5
ticket=4
ticket=3
ticket=2
ticket=1
ticket=5
ticket=4
ticket=3
ticket=2
ticket=1
可以看到,我们开启了三个线程,并希望他们共同卖着5张票,结果却不尽人意。
public class MyThread implements Runnable {
private int ticket = 5;
@Override
public void run() {
for (int i = 0; i < 50; i++) {
if (this.ticket >0)
System.out.println("ticket=" + this.ticket--);
}
}
}
package com.xx.ticket;
public class Test1 {
public static void main(String[] args) {
MyThread mt1 = new MyThread();
new Thread(mt1).start();
new Thread(mt1).start();
new Thread(mt1).start();
}
}
可能的运行结果:
ticket=5
ticket=3
ticket=2
ticket=1
ticket=4
开启了三个线程,但是与使用继承Thread操作不同的是,他们三个都占用着同一个Runnable接口对象的引用,所以是实现了数据共享的操作。
package com.xx.ticket;
public class Test {
public static void main(String[] args) {
MyThread mt1 = new MyThread();
new Thread(mt1).start();
new Thread(mt1).start();
new Thread(mt1).start();
}
}
这样一来,即使继承Thread类也可以实现数据共享,但我们不推荐这么做,因为mt1本身就是Thread类的子类,还要再实例化Thread类去开启多线程,明显不合适。
使用Runnable接口实现的多线程可以避免单继承的局限,但是Runnable接口存在一个问题就是没有办法返回run方法的操作结果(public void run()
)。为了解决这个问题,从JDK1.5开始,引入了这个接口java.util.concurrent.Callable
:
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
这个接口中只定义了一个call()方法,而且在call()方法上可以实现线程操作数据的返回,返回类型有Callable接口上的泛型决定。
package com.xx.callable;
import java.util.concurrent.Callable;
public class MyThread implements Callable<String> {
private int ticket = 10;
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
if (this.ticket > 0)
System.out.println("ticket=" + this.ticket--);
}
return "售完";
}
}
这个线程操作类继承了Callable接口,并指定了返回类型为String。
想要获取这个返回值,靠Thread类是不可以的。为了解决这个问题,从JDK1.5起,引入了java.util.concurrent.FutureTask
类,定义如下:
public class FutureTask<V> extends Object implements RunnableFuture<V>
FutureTask这个类实现了了RunnableFuture这个接口,而RunnableFuture接口又同时实现了Future和Runnable接口。这是它的常用方法:
方法 | 解释 |
---|---|
public FutureTask(Callable |
构造,接收Callable接口实例 |
public FutureTask(Runnable runnable,V result) |
构造,接收Runnable接口实例,并指定返回结果类型 |
public V get() |
取得线程操作结果,是由Future接口定义的 |
我们现在尝试把上面的MyThread类中的返回值接收一下:
package com.xx.callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 实例化多线程对象
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
// 实例化FutureTask对象
FutureTask<String> task1 = new FutureTask<>(myThread1);
FutureTask<String> task2 = new FutureTask<>(myThread2);
// FutureTask是Runnable接口子类,可以使用Thread接收构造
new Thread(task1).start();
new Thread(task2).start();
// 获取返回值
String msg1 = task1.get();
String msg2 = task2.get();
System.out.println("线程1返回的结果是:" + msg1 + "\t线程2返回的结果是:" + msg2);
}
}
运行结果:
ticket=10
ticket=10
ticket=9
ticket=8
ticket=7
ticket=6
ticket=5
ticket=4
ticket=3
ticket=9
ticket=2
ticket=8
ticket=7
ticket=6
ticket=5
ticket=4
ticket=3
ticket=2
ticket=1
ticket=1
线程1返回的结果是:售完 线程2返回的结果是:售完
我们利用FutureTask类实现Callable接口的子类包装,由于FutureTask是Runnable接口的子类,所以可以利用Thread类的start()方法启动多线程,并接收返回值。
总结一下:Callable解决了run()方法返回值的问题,FutureTask解决了接收返回值的问题。
输出结果有些小问题,这是由于线程之间的不同步问题造成的,也是多线程主要的学习内容之一,放在下次讨论。
全文完。