线程与进程 并行与并发
开始多线程之前,我们得先说一说这个线程与进程,并行与并发。因为我专业是信息安全,所以啊上课有学过这玩意且不止一门课讲过,所以我大概了解一点,记住的也就这几句话,如果你没看懂或者我才疏学浅的确实是讲不明白,你就先找个大佬博客学一学,肯定详细,起码有个概念。
进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。
线程:是进程的一个执行单元,是进程内的调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。
一个程序至少一个进程,一个进程至少一个线程。
并行 :是物理上同时发生,指在某一个时间点同时运行多个程序,两个或者多个事件在同一时刻发生,是真正意义上的同时发生。
并发 :是逻辑上同时发生,指在某一个时间内同时运行多个程序,两个或多个事件在同一时间间隔内发生,就是这几个程序在一个时间段内走走停停,你来一下我来一下,在一个时间段内都运行结束,相当于在时间段中同时进行,其实每个时刻只有一个,只是快到你感觉不到。
JVM虚拟机是多线程的吗
java命令会启动java虚拟机,相当于启动了一个应用程序,相当于启动了一个进程。虚拟机会开启一个主线程去寻找main方法,所以说main方法是运行在主线程中的。但是虚拟机在工作时还会启动垃圾回收机制,也就相当于开启了另一个线程。所以说,我们的JVM虚拟机是多线程的。
多线程实现
简单来说,要实现多线程有三种方式:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
三种方式怎么选择?
先上结论:尽量避免继承Thread类,优先考虑实现接口的方法。
原因:因为java采用的是单继承的模式,继承Thread类就会带来这种局限性,没法再继承其他类;另外,实现接口可以更方便的实现数据共享的概念,这个下面会解释。总之,记住一点就可以,使用实现接口的方式来写多线程更合理。
继承Thread类
在java中,要想实现多线程,就必须依靠一个线程的主类。不管是实现Runnable或者Callable接口,还是直接继承Thread类,都是为了定义这个主类。
线程主体类的定义格式:
class 类名 extends Thread {
属性;
方法;
public void run(){ //重写run方法
线程主体方法;
}
}
eg: 定义一个线程操作类
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()方法的源代码的话会发现,在start()方法里面会调用一个start0()的方法,而且这个方法是用native声明的。java中调用本机操作系统提供的函数的技术叫做JNI(Java Native Interface ),这个技术离不开特定的操作系统,因为多线程必须由操作系统来分配资源。这项操作是根据JVM负责根据不同的操作系统实现的。(当然我们不用多管,了解一下)
java.lang.Thread 是专门负责线程操作的类,任何类只要继承了它就可以成为一个线程的主类。有了主类自然要有它的使用方法,那么主类中只需要重写run()方法就可以,这个run()方法就是线程的主体。它相当于线程的入口,我们使用Tread类中的start()方法开启线程后,run()方法里面的代码一定会被执行。
实现Runnable接口
继承Thread的方法会带来单继承的问题,所以,实际开发中实现Runnable接口的方法更普遍。
这个接口非常简单:
public interface Runnable {
public abstract void run();
}
在Runnable接口中也定义了run()方法,所以线程的主类只需要重写此方法即可。
eg: Runnable接口来实现多线程
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接口对象:
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就阔以
new Thread(threadA).start();
new Thread(threadB).start();
new Thread(threadC).start();
}
}
两种方法的区别与联系:
上面已经讲到,继承Thread类来定义线程操作类存在单继承的问题,那么除了这个以外,Thread类和Runnable接口还有什么联系呢?
Thread类也是Runnable类的接口
这样的话,我们自己实现Runnable接口写的MyThread类和Thread类都继承了Runnable接口!这么模式类似于代理模式,但又不完全是,此处代理类Thread调用的start方法而不是接口中的run方法,这是本质差别。
使用Runnable接口可以更方便的表示出数据共享的概念
但不是说继承Thread类就不能表现出,只是有些麻烦而已。
我们通过一个简单的卖票问题来说明:
- 继承Thread类
public class MyThread extends Thread {
private int ticket = 5;
@Override
public void run() {
for (int i = 0; i < 15; i++) {
if (this.ticket >0)
System.out.println("ticket=" + this.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张票,结果却不尽人意。
- 实现Runnable接口
public class MyThread implements Runnable {
private int ticket = 5;
@Override
public void run() {
for (int i = 0; i <15; i++) {
if (this.ticket >0)
System.out.println("ticket=" + this.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接口对象的引用,所以是实现了数据共享的操作。
eg:继承Thread类实现数据共享:
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类去开启多线程,明显不合适。
利用Callable接口实现多线程
使用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接口上的泛型决定。
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<V> callable) 构造,接收Callable接口实例
public FutureTask(Runnable runnable,V result) 构造,接收Runnable接口实例,并指定返回结果类型
public V get() 取得线程操作结果,是由Future接口定义的
我们现在尝试把上面的MyThread类中的返回值接收一下:
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解决了接收返回值的问题。
输出结果有些小问题,这是由于线程之间的不同步问题造成的,也是多线程主要的学习内容之一,放在下次讨论。