多线程模型是Java程序最基本的并发模型,本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。
一、进程和线程
进程
在计算机中,一个任务就是一个进程,比如打开Word就是打开一个进程。在进程内部还需要同时执行多个子任务。例如,Word可以让我们一边打字,一边进行拼写检查,我们把子任务称为线程。一个进程可以包含一个或多个线程,但至少会有一个线程。因为同一个应用程序,既可以有多个进程,也可以有多个线程。
线程
线程是cpu调度的最小单位,线程是进程的一个执行单元,负责当前进程中程序的执行。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
区别
进程和线程是包含关系,通常一个进程都有若干个线程,至少包含一个线程。 进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。 线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。 多进程是指操作系统能同时运行多个任务(程序)。
-
根本区别: 进程是操作系统资源分配的基本单位(资源分配的最小单位),而线程是处理器任务调度和执行的基本单位(cpu调度的最小单位)
-
资源开销: 每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
-
包含关系: 进程和线程是包含关系,一个进程至少包含一个线程。线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
-
内存分配: 同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
-
影响关系: 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
-
执行过程: 每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。
二、并行和并发
并发是指一个处理器同时处理多个任务。并行是指多个处理器同时处理多个不同的任务。 并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。 来个比喻:并发是一个人同时吃三个馒头,而并行是三个人同时吃三个馒头。
并行
指在同一时刻,有多条指令在多个处理器上同时执行。就好像两个人各拿一把铁锨在挖坑,一小时后,每人一个大坑。所以无论从微观还是从宏观来看,二者都是一起执行的。
并发
指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。这就好像两个人用同一把铁锨,轮流挖坑,一小时后,两个人各挖一个小一点的坑,要想挖两个大一点得坑,一定会用两个小时。
总结:
- 并行:你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行
- 并发:你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
三、多线程
多线程是指在同一程序中有多个顺序流在执行。和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。
Java语言内置了多线程支持,当Java程序启动的时候,实际上是启动了一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动其他线程。此外,JVM还有负责垃圾回收的其他工作线程等。
四、实现多线程
Java语言内置了多线程支持,其多线程实现方式主要有三种:继承Thread类、实现Runnable接口、使用ExecutorService、Callable、Future实现有返回结果的多线程。其中前两种方式线程执行完后都没有返回值,只有最后一种是带返回值的。
继承Thread类
只需要继承Thread类,重写run(),然后在run方法里写下线程要实现的任务即可。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。
通过继承Thread类实现多线程的步骤:
- 定义一个类,
继承Thread类,并重写该类的run方法,该run方法的方法体就代表了线程需要完成的任务,因此,run方法的方法体被称为线程执行体; - 创建Thread子类的对象,即创建了子线程;
- 用线程对象的start方法来启动该线程;
代码如下:
class MyThread extends Thread {
private String name;
static int count = 10;
MyThread(String name) {
this.name = name;
}
@Override
public void run() {
// 循环售票
while (count > 0) {
count--;
//System.out.println(Thread.currentThread().getName() + "售出一张票,剩余" + count);
System.out.println(name + " > 售出一张票,剩余:" + count);
}
}
}
class Main {
public static void main(String[] args) {
// 创建两个线程来售票
Thread th1 = new MyThread("1号窗口");
Thread th2 = new MyThread("2号窗口");
// 启动线程
th1.start();
th2.start();
}
}
注意到start()方法会在内部自动调用实例的run()方法。
实现Runable接口
通过实现Runable接口实现多线程是受欢迎。因为Java只能单继承,继承了Thread类就不能再继承其他类了。
通过Runnable接口实现多线程的步骤:
- 定义一个Runnable接口的实现类,并重写该接口中的run方法,该run方法的方法体同样是该线程的线程执行体;
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象;
- 调用线程对象的start方法来启动该线程;
代码如下:
class MyThread2 implements Runnable {
static int count = 10;
private String name;
public MyThread2(String name) {
this.name = name;
}
@Override
public void run() {
while (count > 0) {
count--;
System.out.println(name + " > 售出一张票,剩余:" + count);
}
}
}
class Main {
public static void main(String[] args) {
// 为了启动MyThread,需要首先实例化一个Thread,并传入自己的MyThread实例:
Thread th3 = new Thread(new MyThread2("3号窗口"));
Thread th4 = new Thread(new MyThread2("4号窗口"));
th3.start();
th4.start();
}
}
通过callable
使用ExecutorService、Callable、Future实现有返回结果的多线程 ExecutorService、Callable、Future这个对象实际上都是属于Executor框架中的功能类。
Thread与Runnable的比较
继承Thread类的方式
- 编写简单,如果要访问当前线程,除了可以通过Thread.currentThread()方式之外,还可以使用 super关键字
- 弊端:因为线程类已经继承了Thread类,则不能再继承其他类【单继承】 实际上大多数的多线程应用都可以采用实现Runnable接口的方式来实现【推荐使用匿名内部类】java类是单知继承的
实现Runnable接口的方式
- 线程类只是实现了Runnable接口,还可以继承其他类【一个类在实现接口的同时还可以继承另外一个类】
- 可以多个线程共享同一个target对象,所以非常适合多个线程来处理同一份资源的情况
- 弊端:编程稍微复杂,不直观,如果要访问当前线程,必须使用Thread.currentThread()
start()与run()方法的区别
必须调用Thread实例的start()方法才能启动新线程,查看Thread类的源代码,会看到start()方法内部调用了一个
private native void start0()方法,native修饰符表示这个方法是由JVM虚拟机内部的C代码实现的,不是由Java代码实现的。
start()方法会新建一个线程,并且让这个线程执行run()方法。
Thread thread = new Thread();
thread.start();
调用run()也能正常执行。但是,却不能新建一个线程,而是在当前线程中调用run()方法,只是作为一个普通的方法调用。
Thread thread = new Thread();
thread.run();
效果如下:
所以不要用run()来开启新线程,它只会在当前线程中,串行执行run()方法中的代码,相当于调用了一个普通的Java方法,当前线程并没有任何改变,也不会启动新线程。
五、线程的状态
在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:
New:新创建的线程,尚未执行;Runnable:运行中的线程,正在执行run()方法的Java代码;Blocked:运行中的线程,因为某些操作被阻塞而挂起;Waiting:运行中的线程,因为某些操作在等待中;Timed Waiting:运行中的线程,因为执行- sleep()方法正在计时等待;Terminated:线程已终止,因为run()方法执行完毕。
┌─────────────┐
│ New │
└─────────────┘
│
▼
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
┌─────────────┐ ┌─────────────┐
││ Runnable │ │ Blocked ││
└─────────────┘ └─────────────┘
│┌─────────────┐ ┌─────────────┐│
│ Waiting │ │Timed Waiting│
│└─────────────┘ └─────────────┘│
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
│
▼
┌─────────────┐
│ Terminated │
└─────────────┘
当线程启动后,它可以在Runnable、Blocked、Waiting和Timed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。
线程终止的原因
- 线程正常终止:run()方法执行到return语句返回;
- 线程意外终止:run()方法因为未捕获的异常导致线程终止;
- 对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)。
一个线程还可以等待另一个线程直到其运行结束。例如,main线程在启动t线程后,可以通过t.join()等待t线程结束后再继续运行:
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("hello");
});
System.out.println("start");
t.start();
t.join();
System.out.println("end");
}
}
当main线程对线程对象t调用join()方法时,主线程将等待变量t表示的线程运行结束,即join就是指等待该线程结束,然后才继续往下执行自身线程。所以,上述代码打印顺序可以肯定是main线程先打印start,t线程再打印hello,main线程最后再打印end。
如果t线程已经结束,对实例t调用join()会立刻返回。此外,join(long)的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。
感谢大家的耐心阅读,如有建议请私信或评论留言