多线程概述及线程的创建和启动
概述
我们之前写的程序都只是在做单线程的编程,所有的程序只有一条顺序执行流,程序从main方法开始执行,依次向下执行每行代码,如果程序执行过程中某行代码遇到了阻塞,则程序将会停滞在该处。
单个线程往往功能非常有限,所以我们引入了多线程来进行功能上的优化。多线程的概念听起来会让很多初学者或者说是不了解操作系统的同学感到特别的难。
举个简单的例子以前的单线程的程序就相当于我们开了一个餐厅,但是只雇佣了一个服务员,每次只能服务一个顾客,这样的效率就会很低,多线程就相当于是我们雇佣了多个服务员,他们可以同时服务多个顾客。这就是所谓的多线程。
Java提供了一套完整的多线程支持,包括线程的创建,线程的控制,线程同步,等等
定义
几乎所有的操作系统都会支持同时运行多个任务,一个任务通常就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每一个顺序执行流就是一个线程。首先我们要区分以下几个概念。
程序:
说起进程,就不得不说下程序。先看定义:程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。而进程则是在处理机上的一次执行过程,它是一个动态的概念。这个不难理解,其实进程是包含程序的,进程的执行离不开程序,进程中的文本区域就是代码区,也就是程序。的集合
进程:
狭义定义:进程就是一段程序的执行过程。
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
简单的来讲进程的概念主要有两点:
第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体,我们称其为进程。
线程:
通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
多线程
讲到多那就不得不谈一谈两个概念,一个是并发,一个是并行。
并发是指多个同一时刻,只能有一条指令被执行,但是多个指令被快速的轮换执行。
并行是指在同一时刻,多条指令在多个处理器上同时被执行。
为什么我们只谈了多线程而不谈多进程呢,因为相比于进程来讲,线程具有更高的性能,因为多线程的数据,内存等都是共享的,而进程,所有的都是独立的,因此多线程的 并发性能要比多进程高的多。
线程创建的三种方式
那么在Java中我们如何创建一个线程呢?Java提供了三种方式
继承Thread类
1.定义Thread类的子类,并重写run()方法。run()方法中的内容代表所要执行的任务。
2.创建子类实例。
3.调用start()方法启动线程。
package org.xupt.thread;
public class ThreadExtend extends Thread{
private int i;
//重写run()方法
@Override
public void run() {
for (; i < 100; i++) {
System.out.println(getName() + " " + i);
}
}
}
package org.xupt.thread;
public class Test {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if(i == 10){
new ThreadExtend().start();
new ThreadExtend().start();
}
}
}
}
运行上面的程序之后我们会看到虽然我们只创建了两个线程,但是实际的程序里有三个线程。这是因为我们的main方法的方法体本身本身也是一个线程。
上面的程序我们还用到了两个方法。
Thread.currentThread():返回正在执行的线程对象
getName():该方法返回线程的名字。默认为(Thread-0、Thread-1、…)
当然如果如觉得名字不够具体生动,我们可以用setName()方法设置你喜欢的名字。
**但是你有没有发现什么问题?**我们说进程中的数据是共享的但是两个进程输出时i都是从0开始的!!!这是为什么?
你以为我要说什么牛逼的答案,其实,就是因为你new了两次对象,每次都创建了一个新的实例,他们两个当然不会共享了,因为自己都有。所以这种方法创建的线程不能共享类的实例变量。
实现Runable接口
1.定义Runable的实现类,并重写run()方法。
2.创建Runable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread才是真正的线程对象。
3.调用start()方法启动线程。
package org.xupt.threadrunable;
public class ThreadRunable implements Runnable{
public int i;
@Override
public void run() {
for(;i<100;i++){
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
package org.xupt.threadrunable;
public class Test {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 10){
ThreadRunable threadRunable = new ThreadRunable();
new Thread(threadRunable,"Thread-1").start();
new Thread(threadRunable,"Thread-2").start();
}
}
}
}
运行后我们可以发现两个子线程的i是共享的。
Runable接口中只含有一个run()抽象方法。接口使用了@FunctionalInterface修饰。所以我们可以通过Lambda表达式创建Runable对象。
package org.xupt.threadrunable;
public class RunableLambda {
public static void main(String[] args) {
Runnable r = () -> {
int i = 0;
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
};
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 10) {
new Thread(r, "Thread-1").start();
new Thread(r, "Thread-2").start();
}
}
}
}
需要注意的是这里的i不是共享的!!!
使用Callable和Future创建线程
我们不难看出上面的方法中Thread类的作用就是把run()方法包装成线程执行体。那么是不是可以把任意方法(包含返回值)都直接包装成线程执行体呢?
从Java5开始,Java提供了一Callable接口(类似于Runable接口的增强版),提供了一个call()方法可以作为执行体,但是call()方法比run()方法的功能更加强大。
1.可以有返回值
2.可以抛出异常
Java5提供了一个Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,改实现类实现了Future接口,并且实现了Runable接口可以作为Thread的target。
接口主要方法:
1.boolean cancel(boolean mayInterfaceIfRunning):试图取消该Future里的关联的Callable任务
2.V get():返回Callable任务里call()方法的返回值。调用该方法将导致线程阻塞,必须等到子线程结束后才会有返回值。
3.V get(long timeout,TimeUnit unit):返回call()方法的返回值。指定时间内无返回,将抛出TimeoutException异常
4.boolean isCancelled():如果Callable任务正常完成前被取消返回True
5.boolean isDone():如果Callable任务正常完成,返回True
创建线程步骤如下:
1.创建Callable接口的实现类,重写call()方法(可以有返回值),然后创建Callable的实现类的实例对象(使用Lamabd表达式)。
2.试用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值。
3.使用FutureTask对象作为Thread的target创建并启动线程
4.调用FutureTask的get()方法来获得返回值。
package org.xupt.threadcallable;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadCallable {
public static void main(String[] args) {
FutureTask<Integer> integerFutureTask = new FutureTask<>((Callable<Integer>) () -> {
int i = 0;
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
return i;
});
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 10) {
new Thread(integerFutureTask, "Thread-1").start();
}
}
//接收返回值
try {
System.out.println("Thread-1的返回值: " + integerFutureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
三种创建方式的比较
采用实现接口的方法创建线程的优缺点
1.只是实现了接口,还可以继承其他类
2.多个线程可以共享一个target对象,所以适合多个线程来处理同一个资源,可以很好的将CPU、代码和数据分开,较好的体现面向对象的思想。
3.缺点就是编程比较复杂,如果需要访问线程就需要使用Thread.currentThread().getName()方法。
采用继承Thread类的方法创建线程的优缺点
1.编程简单,访问线程可以直接用this
2.缺点就是不可以再继承其他类