10.1 相关概念的介绍
10.1.1 程序、进程与线程
程序:用来完成某些功能的一段静态代码,一组指令的集合。
进程:正在运行的一段程序。它是系统分配资源的基本单位,是一个动态过程,有它自己的声明周期。
线程:进程中的一个执行单元,它是系统执行与调度的基本单位。
10.1.2 单线程和多线程
单线程:每个正在运行的程序(即进程),至少包括一个线程,这个线程叫主线程。主线程在程序启动时被创建,用于执行main函数。只有一个主线程的程序,称作单线程程序,主线程负责执行程序的所有代码(UI展现以及刷新,网络请求,本地存储等等)。这些代码只能顺序执行,无法并发执行。
多线程:拥有多个线程的程序,称作多线程程序。一个进程可以创建一个或多个线程,线程还可以创建子线程。
10.1.3 并发与并行
并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
比如说对于一个单核cpu
来说,因为cpu
一次只能处理单条指令,所以即便是单核多线程cpu
,也只能做到并发,而非并行。并行只能由多核cpu
进行处理。
10.2 线程的基本使用
10.2.1 创建线程的两种方式
- 继承
Thread
类:Thread
类实现了Runnable
接口 - 实现
Runnable
接口
10.2.2 Thread类使用案例
public class Thread01 extends Thread{
public static void main(String[] args) throws InterruptedException {
Cat cat = new Cat();
cat.start();
// 主线程继续
System.out.println("主线程继续执行" + Thread.currentThread().getName());
for (int i = 0; i < 60; i++) {
System.out.println("主线程 i=" + i);
Thread.sleep(1000);
}
}
}
class Cat extends Thread{
@Override
public void run() {
int times = 0;
while (times < 80) {
System.out.println("喵喵,我是小猫咪");
times++;
System.out.println(times);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
代码分析: 这段代码可以演示主线程和子线程并行执行的过程。main方法相当于起了一个主线程,有主线程中实例化了Cat对象,由于Cat继承了Thread类并重写run方法,所以可以通过该类对象的start方法启动一个线程。
start
方法是Thread
类中的一个成员方法,它调用了本地方法native start0
用来启动线程(注意此时只是启动,而非线程能够立马执行,线程只是状态切换到了就绪态,但是依然要等系统时间片轮转到它时才会执行)。
run
方法是Runnable
接口中的一个抽象方法,由实现它的类来定义其功能,内容为线程具体要做的事情。当线程开始执行时,就开始执行run
方法里的代码,当run
方法体执行完毕后,线程就正常结束了。
10.2.3 Runnable接口使用案例
由于Java是单继承,当一个类已经有直接继承的父类时,就不能再继承Thread类了,所以还另外提供了实现Runnable接口的方式。
public class Thread02 {
public static void main(String[] args) throws InterruptedException {
Dog dog = new Dog();
Thread t1 = new Thread(new Dog());
t1.start();
System.out.println("what the fuck......");
for (int i = 0; i < 10; i++) {
System.out.println(i);
Thread.sleep(1000);
}
}
}
class Dog implements Runnable {
@Override
public void run() {
int times = 0;
while (times++ < 10) {
System.out.println("hi");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
代码分析:由于Runnable接口只有run方法,start方法定义在Thread类中,所以还是需要Thread类的支持。需要去实例化一个Thread对象,在构造Thread时,需要传入一个实现了Runnable接口的实现类的对象。
10.2.4 两种方式的比较
推荐使用实现Runnable
接口的方式来实现多线程,理由如下:
- 可以避免单继承的限制
- 使用继承接口的方式可以更好地共享资源
关于第二点使用一个案例来解释:
// 模拟三个售票窗口出售100张票
public class SellTicket {
public static void main(String[] args) {
// 1. 使用继承Thread类实现
Window_1 w1 = new Window_1();
Window_1 w2 = new Window_1();
Window_1 w3 = new Window_1();
w1.start();
w2.start();
w3.start();
// 2. 使用实现Runnable接口的方式实现
Window_1 w1 = new Window_1();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.start();
t2.start();
t3.start();
}
}
class Window_1 extends Thread {
// 1. 使用继承Thread类实现
private static int ticket = 100; // 共同资源使用static修饰
@Override
public void run() {
while (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":" + ticket);
ticket--;
}
}
}
class Window_1 implements Runnable {
// 2. 使用实现Runnable接口的方式实现
private int ticket = 100;
@Override
public void run() {
while (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":" + ticket);
ticket--;
}
}
}
代码分析:可以看出一个明显的区别是,当使用继承方式实现多线程时,实际上创建了多个类对象,由每个对象去调用start()
开启一个线程,即7-12行所示;而当使用实现接口方式时,只创建了一个类对象,通过将该对象分别放入三个构造器,开启了多线程。这直接导致了两种方式对于ticket
的修饰产生了差别,前者由于是多个对象共享ticket
,所以使用static
进行修饰;而后者始终在一个对象上操作,所以无需static
修饰。也正因为后者在一个对象上操作,所以说后者更适合资源的共享。
10.3 线程终止
当run()
正常结束时,线程应该正常结束;但是还可以通过通知的方式来结束线程,以下案例展示了在主线程中通过设置loop
的方式来通知子线程结束。
public class ThreadExit_ {
public static void main(String[] args) {
Person p = new Person();
Thread t = new Thread(p);
t.start();
// 主线程休眠10s后通知子线程结束
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
p.setLoop(false);
}
}
class Person implements Runnable {
private boolean loop = true;
int i = 0;
@Override
public void run() {
while (loop) {
System.out.println("i=" + i);
i++;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void setLoop(boolean loop) {
this.loop = loop;
}
}
10.4 线程的常用方法
- setName(String name): 设置线程名称
- getName(): 获取线程名称
- start(): 使线程处于就绪状态,底层调用start0
- run(): 线程实际执行的任务
- setPriority(): 设置线程优先级
- getPriority(): 获取线程优先级
- sleep(millisec): 线程休眠
- interrupt(): 中断线程(并不会终止线程,只是标明线程的状态为中断态)
- yield(): 当前线程让出cpu给别的线程执行
- join(): 等待当前线程结束
10.4.1 用户线程和守护线程
用户线程:也称为工作线程,当线程任务执行完或通知方式结束;
守护线程:一般是为工作线程服务的,当所有工作线层结束后,守护线程结束,比如GC
10.5 线程的生命周期
10.5.1 线程的几种状态
线程的状态定义在Thread
类中的State
枚举类中,共有6种状态:
NEW
:线程未启动时,处于该状态RUNNABLE
:对于一个可运行的线程,就处于这个状态BLOCKED
:等待monitor lock
的线程处于该状态WAITING
:处于等待状态的线程TIMED_WAITING
:等待另一个线程执行动作达到指定等待时间TERMINATED
:已结束的线程处于该状态
10.5.2 状态转换图
10.6 线程的同步
10.6.1 线程同步机制
在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就需要使用同步访问技术,保证数据再任一时刻,最多有一个线程访问,以保证数据的完整性。
10.6.2 同步具体方法-Synchronized
实现同步有很多方法,这里仅介绍同步方法和同步代码块。
同步方法即使用Synchronized
关键字修饰想要同步的方法;同步代码块同理,即将想要同步的代码块放在Synchronized
包含的子块中。
// 同步方法
public Synchronized void func(param...) {
......
}
// 同步代码块
public void func(param...) {
Synchronized ([object/class]){
// 要同步的代码
}
}
10.6.3 线程同步的原理-互斥锁
当一段代码或一个方法使用Synchronized
修饰时,当多个线程对方法或代码块进行访问时,它们首先需要获取一把锁,这个锁是在对象上的,用对象的一个位来表示的。当一个线程获取该对象锁后,就改变这个位的状态,此时其他对象就无法再获取该锁。
对于同步代码块,它的锁对象可以是当前对象,也可以是其他任何对象,但是需要保证多个线程操作的是同一把锁。这里又区分两种情况:
- 如果是静态方法,那么锁就是该类本身(
Classname.class
) - 如果是成员方法,那么锁可以是该对象(
this
),也可以是其他任何对象
对于代码块也是同理,看代码块是位于静态方法中还是非静态方法中。
10.7 死锁与释放锁
10.7.1 死锁
操作系统讲的很详细,这里不做介绍了
10.7.2 释放锁
下面的行为会释放锁:
- 同步方法或同步代码块执行完了
- 当前线程在同步方法或同步代码块中遇到了
break
、return
- 当前线程在同步方法或同步代码块中遇到了未处理的
Error
、Exception
- 当前线程在同步方法或同步代码块中执行了线程对象的
wait()
下面的行为不会释放锁:
- 线程执行同步方法或同步代码块时,程序调用了
Thread.sleep()
、Thread.yield()
- 线程执行同步代码块时,其他线程调用了该线程的
suspend()
,即当前线程被挂起