9 第九章 线程
9.1 进程与线程
进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,即进程空间或(虚空间)。进程不依赖于线程而独立存在,一个进程中可以启动多个线程。比如在Windows系统中,一个运行的exe就是一个进程。
线程是指进程中的一个执行流程,一个进程中可以运行多个线程。比如java.exe进程中可以运行很多线程。线程总是属于某个进程,线程没有自己的虚拟地址空间,与进程内的其他线程一起共享分配给该进程的所有资源。
“同时”执行是人的感觉,在线程之间实际上轮换执行。
进程在执行过程中拥有独立的内存单元,进程有独立的地址空间,而多个线程共享内存,从而极大地提高了程序的运行效率。
线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程包含以下内容:
- 一个指向当前被执行指令的指令指针;
- 一个栈;
- 一个寄存器值的集合,定义了一部分描述正在执行线程的处理器状态的值
- 一个私有的数据区。
我们使用Join()方法挂起当前线程,直到调用Join()方法的线程执行完毕。该方法还存在包含参数的重载版本,其中的参数用于指定等待线程结束的最长时间(即超时)所花费的毫秒数。如果线程中的工作在规定的超时时段内结束,该版本的Join()方法将返回一个布尔量True。
简而言之:
- 一个程序至少有一个进程,一个进程至少有一个线程。
- 线程的划分尺度小于进程,使得多进程程序的并发性高。
- 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
- 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
- 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
在Java中,每次程序运行至少启动2个线程:一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM实际上就是在操作系统中启动了一个进程。
9.2 Java中的线程
在Java中,“线程”指两件不同的事情:
1、java.lang.Thread类的一个实例;
2、线程的执行。
在 Java程序中,有两种方法创建线程:
一是对 Thread 类进行派生并覆盖 run方法;
二是通过实现Runnable接口创建。
使用java.lang.Thread类或者java.lang.Runnable接口编写代码来定义、实例化和启动新线程。
一个Thread类实例只是一个对象,像Java中的任何其他对象一样,具有变量和方法,生死于堆上。
Java中,每个线程都有一个调用栈,即使不在程序中创建任何新的线程,线程也在后台运行着。
一个Java应用总是从main()方法开始运行,main()方法运行在一个线程内,他被称为主线程。
一旦创建一个新的线程,就产生一个新的调用栈。
线程总体分两类:用户线程和守候线程。
当所有用户线程执行完毕的时候,JVM自动关闭。但是守候线程却不独立于JVM,守候线程一般是由操作系统或者用户自己创建的。
一、定义线程
1、扩展java.lang.Thread类。
此类中有个run()方法,应该注意其用法:public void run()
如果该线程是使用独立的Runnable运行对象构造的,则调用该Runnable对象的run方法;否则,该方法不执行任何操作并返回。
Thread的子类应该重写该方法。
2、实现java.lang.Runnable接口。
void run()
使用实现接口Runnable的对象创建一个线程时,启动该线程将导致在独立执行的线程中调用对象的run方法。
方法run的常规协定是,它可能执行任何所需的操作。
二、实例化线程
1、如果是扩展java.lang.Thread类的线程,则直接new即可。
2、如果是实现了java.lang.Runnable接口的类,则用Thread的构造方法:
Thread(Runnabletarget)
Thread(Runnabletarget, String name)
Thread(ThreadGroupgroup, Runnable target)
Thread(ThreadGroupgroup, Runnable target, String name)
Thread(ThreadGroupgroup, Runnable target, String name, long stackSize)
/*
其中:
Runnable target:实现了Runnable接口的类的实例。
Thread类也实现了Runnable接口,因此,从Thread类继承的类的实例也可以作为target传入这个构造方法。
直接实现Runnable接口类的实例。
线程池建立多线程。
String name:线程的名子。这个名子可以在建立Thread实例后通过Thread类的setName方法设置。默认线程名:Thread-N,N是线程建立的顺序,是一个不重复的正整数。
ThreadGroup group:当前建立的线程所属的线程组。如果不指定线程组,所有的线程都被加到一个默认的线程组中。
long stackSize:线程栈的大小,这个值一般是CPU页面的整数倍。如x86的页面大小是4KB.在x86平台下,默认的线程栈大小是12KB。
*/
三、启动线程
在线程的Thread对象上调用start()方法,而不是run()或者别的方法。
在调用start()方法之前:线程处于新状态中,新状态指有一个Thread对象,但还没有一个真正的线程。
在调用start()方法之后:发生了一系列复杂的事情——
启动新的执行线程(具有新的调用栈);
该线程从新状态转移到可运行状态;
当该线程获得机会执行时,其目标run()方法将运行。
注意:对Java来说,run()方法没有任何特别之处。像main()方法一样,它只是新线程知道调用的方法名称(和签名)。因此,在Runnable上或者Thread上调用run方法是合法的。但并不启动新的线程。
9.3 多线程的概念
9.3.1 概念
多线程是在一个程序中可以同时运行多个不同的线程来执行不同的任务。
9.3.2 优点
- 提高程序的响应.
- 提高CPU的利用率.
- 改善程序结构,将复杂任务分为多个线程,独立运行.
9.3.3 缺点
- 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
- 多线程需要协调和管理,所以需要CPU时间跟踪线程;
- 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题;
9.3.4 何时需要多线程
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、
- 文件读写操作、网络操作、搜索等。
- 需要一些后台运行的程序时。
9.4 创建线程
9.4.1 继承Thread类的方式
实现线程,最简单的方法就是继承Thread类,重写其中的run()方法。
当线程启动时,会自动执行run()方法。
public class ThreadExtent extends Thread{
public ThreadExtent() {}
@Override
public void run() {
//当线程启动时,会自动执行run()方法。
}
}
Thread类中的构造方法
构造方法 | 说明 |
---|---|
Thread( ) | 无参构造方法, 创建一个新线程。 |
Thread(String name) | 创建一个拥有指定名称的线程。 |
Thread(Runnable target) | 利用Runnable类的对象,创建一个线程。 |
Thread(String name ,Runnable target) | 利用Runnable类的对象,创建一个拥有指定名称的线程。 |
Thread类中的方法
方法 | 说明 |
---|---|
方法 | 说明 |
---|---|
void start() | 启动线程 |
final void setName(String name) | 设置线程的名称 |
final String getName() | 返回线程的名称 |
final void setPriority(int newPriority) | 设置线程的优先级 |
final int getPriority() | |
final void join()throws InterruptedException | 等待线程终止 |
static Thread currentThread() | 返回对当前正在执行的线程对象的引用 |
static void sleep(long millis)throws InterruptedException | 让当前正在执行的线程休眠(暂停执行),休眠时间由millis(毫秒)指定 |
9.4.2 实现Runnable接口的方式
创建线程的另一种方法就是实现Runnable接口中的run()方法。
Runnable接口的存在主要是为了解决Java中不允许多继承的问题。
public class RunnableImpl001 implements Runnable{
public RunnableImpl001() { }
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("RunnableImpl001 " + i);
}
}
}
启动线程需要把其包裹在Thread中,作为一个target去执行。例:
Thread t = new Thread(new RunnableImpl001());
t.start(); //线程启动
两者的区别:
继承Thread :代码存放在继承Thread的子类中的run()方法中。
实现Runnable :代码放在实现Runnable接口的子类中的run()方法。
实现Runnable的好处:
- 避免了单继承的局限性。
- 不需要使用静态的方式,就能让多个线程共享同一个接口实现子类中的对象。
9.5 线程的优先级
事实上,计算机上只有一个CPU,各个线程轮流获得cpu的资源(使用权)。
这样线程才能执行任务。
线程的优先级越高,获得CPU资源的机会越高。反之亦然。
但这并不意味着线程的优先级越高,CPU就会优先执行该线程。
线程的执行顺序取决于超系统的进程调度算法,有时候优先级低的线程反而会先执行。
一般情况下,新创建的线程的优先级默认是 5 ;范围是1~10。
可以通过setPriority()和setPriority()来设置和返回优先级。
9.5.1 调度策略
时间片:就是排队 先进先出。
抢占式:高优先级的线程抢占CPU。
调度方法:
- 同优先级的组成先进先出队列。
- 对高优先级使用优先调度的抢占式策略。
9.6 线程的生命周期
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hh3YgAJ7-1610430112331)(image-20210112105592.png)]
-
调用start()方法时,该线程就会进入就绪队列,等待执行。
-
CPU分配资源,线程开始执行。
- 如果执行过程中,使用了yield方法或者失去了CPU的资源,直接回到就绪队列。(小病,不用住院)
- 如果执行过程中,使用了sleep, join, wait 则会进入阻塞队列。(生病了,去住院了)
- 如果执行过程中,出现了异常并且没处理的话,线程就会进入死亡状态。(遭受意外事故死亡)
- 最终线程执行完毕后,也会进入死亡状态(安乐死)
-
线程进入阻塞队列后(住院)
- sleep() 时间到
- join() 结束
- notify()或notifyAll() 唤醒线程
当遇到上述三种情况线程才会重新进入就绪队列(出院了),等待CPU分配资源。
-
最终线程(run方法调用完毕)执行完毕后,也会进入死亡状态(安乐死)。
9.7 线程的分类
9.7.1 用户线程
9.7.2 守护线程
任何一个守护线程都是整个JVM中所有非守护线程的保姆;
- 只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;
- 只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。
- 守护线程的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。
注意:设置线程为守护线程必须在启动线程之前,否则会跑出一个IllegalThreadStateException异常。
9.8 线程同步
9.8.1 并行和并发
并行:多个CPU同时执行多个任务(一个人写作业的同时还在听音乐,这就叫并行)。
并发:一个CPU在同一时间段内执行多个任务(一个人在一天内完成了多项工作任务,但单看某一瞬间,这个人还是在执行一个任务)。
9.8.2 多线程同步
多个线程使用同一份共享资源的时候,难免会起冲突。
所以引进同步的概念,即多个线程访问同一份共享资源时要有先来后到。
同步就是 排队+锁,只有持有锁的钥匙的线程,才能访问该共享资源。
- 几个线程之间要排队,一个个对共享资源进行操作,而不是同时进行操作;
- 为了保证数据在方法中被访问时的正确性,在访问时加入锁机制。
9.8.3 锁关键字synchronized
同步代码块:
/*
同步监视器
同步监视器可以是任何对象,必须唯一,保证多个线程获得是同一个对象(锁).
同步监视器的执行过程
1.第一个线程访问,锁定同步监视器,执行其中代码.
2.第二个线程访问,发现同步监视器被锁定,无法访问.
3.第一个线程访问完毕,解锁同步监视器.
4.第二个线程访问,发现同步监视器没有锁,然后锁定并访问.
*/
synchronized (同步监视器){
// 需要被同步的代码;
}
同步方法(不推荐):
public synchronized void show (String name){
// 需要被同步的代码;
}
注意:一个线程持有锁会导致其他访问该资源的线程挂起,在多线程竞争的情况下,频繁的加锁和释放锁,会导致比较多的调度切换和上下文切换,影响性能。
9.8.4 死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步 资源,就形成了线程的死锁.
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续.
避免死锁:
让程序每次至多只能获得一个锁(钥匙)。当然,在多线程环境下,这种情况通常并不现实。
设计时考虑清楚锁的顺序,尽量减少嵌套的加锁交互数量。
9.9 线程锁Lock
从JDK 5.0开始,Java提供了更强大的线程同步机制-通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
9.9.1 ReentrantLock
ReentrantLock类实现了Lock,它拥有与synchronized关键字相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁.
9.9.2 synchronized和Lock的区别
-
Lock是显式锁(手动开启和关闭锁,别忘记关闭锁哦!),
synchronized是隐式锁,出了作用域自动释放 -
Lock只有代码块锁,使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
synchronized有代码块锁和方法锁。
线程同步优先使用顺序:Lock >同步代码块(已经进入了方法体,分配了相应资源)>同步方法
9.10 线程通信
线程通讯指的是多个线程通过消息传递实现相互牵制,相互调度,即线程间的相互作用。
涉及三个方法:
方法 | 说明 |
---|---|
wait() | 执行此方法,线程就会进入阻塞状态,同时释放锁 |
notify() | 唤醒因wait而进入阻塞队列的线程,仅仅能唤醒一个,如果有多个,则唤醒优先级最高的线程。 |
notifyAll() | 唤醒所有(全部)因wait而进入阻塞队列的线程 |
注意: wait(),notify(),notifyAll()三个方法必须使用在同步代码块(还有Lock)或同步方法中。
9.11 新增的创建线程的方式
9.11.1 实现Callable接口,重写call方法
实现Callable接口重写call方法与使用Runnable相比,Callable功能更强大些。
- 相比run()方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,获取返回结果
// 返回值的类型
public class ThreadCallable implements Callable<Integer> {
public ThreadCallable() { } //构造方法
@Override
public Integer call() throws Exception {
int i = 0;
for (; i < 10; i++) {
System.out.println(Thread.currentThread().getName());
}
return i; //返回值
}
}
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Test {
public Test() {}
public static void main(String[] args) {
ThreadCallable p = new ThreadCallable(); //创建Callable 对象
FutureTask f = new FutureTask(p); //放进FutureTask对象中 该类实现了Runnable接口
Thread t = new Thread(f); // 把futureTask包进进程里。
t.start(); //启动进程
//在线程的执行过程中,可以在main线程中获取到该进程的返回值
//f.get()方法 FutureTask类中的方法。 得到返回值
try {
System.out.println(f.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
9.11.2 线程池
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
ExecutorService:真正的线程池接口。
**其常见子类ThreadPoolExecutor **
方法 | 说明 |
---|---|
execute(Runnable command) | 执行任务/命令,没有返回值,一般用来执行Runnable. |
submit (Callable task) | 执行任务,有返回值,一般又来执行Callable . |
shutdown() | 关闭线程池. |
Executors : 线程池工具类,用于创建并返回不同类型的线程池。
方法 | 说明 |
---|---|
Executors.newCachedThreadPool() | 创建一个可根据需要创建新线程的线程池. |
Executors.newFixedThreadPool(10) | 创建一个可重用固定线程数的线程池 |
Executors.newSingleThreadExecutor() | 创建一个只有一个线程的线程池 |
Executors.newScheduledThreadPool(n) | 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。 |
public class Solution {
public Solution() {}
public static void main(String[] args) {
//创建一个可重用固定线程数的线程池
ExecutorService exec = Executors.newFixedThreadPool(10);
//创建一个任务对象
ThreadCallable c = new ThreadCallable();
//放进submit中交给线程池执行 并返回Future对象
Future<Integer> submit1 = exec.submit(c);
Future<Integer> submit2 = exec.submit(c);
Future<Integer> submit3 = exec.submit(c);
try {
System.out.println(submit1.get());
System.out.println(submit2.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}