一、进程(Process)与线程(Thread)
1、进程(process)
进程是一个独立的程序,要占用系统的资源 (CPU 内存),具有以下特点:
- 独立性:不同的进程之间是相互独立,使用的资源或者说数据是不共享的
- 动态性:进程在系统中运行是一个动态的,是随着系统一起运作的
- 并发性:多个进程(程序)可以同时在电脑中运行,互不影响
2、线程(Thread)
线程是进程(程序)的组成部分,一个进程(程序)可以同时执行多个线程。
特点:
1. 线程的执行是【抢占式】,多个线程在同一个进程(程序)中运行时,会抢占当前进程的资源
2. CPU在不同的线程之间快速切换当一个线程执行时,其他线程会挂起等待
优点:
1. 可以让程序同时执行不同的任务
2. 可以提供资源的利用率
缺点:
1. 线程安全问题,共享资源问题: 比如迭代器和集合
2. 增加CPU的负担
3. 降低了其他程序的CPU执行占用率
4. 容易出现死锁
3、进程和线程的关系以及区别
- 一个程序运行后至少有一个进程
- 一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义的
- 进程间不能共享资源,但线程之间可以
- 系统创建进程需要为该进程重新分配系统资源,而创建线程则容易的多,因此使用线程实现多任务并发比多进程的效率高
二、线程的创建
创建线程的方式主要有三种:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
其中前两种是最常见的创建线程的方式。
1、创建Thread类
继承自Thread类,Thread类是所有线程类的父类,实现了对线程的抽取和封装。
继承Thread类创建并启动多线程的步骤:
- 定义一个类,继承自Thread类,并重写该类的run方法,该run方法的方法体就代表了线程需要完成的任务,因此,run方法的方法体被称为线程执行体
- 创建Thread子类的对象,即创建了子线程
- 用线程对象的start方法来启动该线程
注意事项:
- 程序运行时会自动创建一个线程 ,这个线程叫主线程;可以通过主线程(main)创建子线程
- 启动线程使用start()方法,不要直接调用run()方法
代码演示
//自定义线程类
public class MyThread extends Thread {
//重写run方法
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("自定义线程:" + i);
}
}
}
//测试类
public class Test {
public static void main(String[] args) {
//创建自定义线程对象
MyThread myThread = new MyThread();
//用start()方法启动线程
myThread.start();
for (int i = 0; i < 5; i++) {
System.out.println("main线程:" + i);
}
}
}
2、实现Runnable接口
实现Runnable接口创建并启动多线程的步骤:
- 定义一个Runnable接口的实现类,并重写该接口中的run方法,该run方法的方法体同样是该线程的线程执行体
- 创建Runnable实现类的实例,并以此实例作为Thread的target参数传入,来创建Thread对象,该Thread对象才是真正的线程对象
- 调用线程对象的start方法来启动该线程
代码演示
//这里采用匿名内部类演示
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("自定义线程:" + i);
}
}
}, "继承了Runnable的线程").start();
实现Runnable接口的线程内存原理
案例说明:Jack和Lucy共用一张银行卡,Jack存钱,Lucy取钱(暂不考虑同步问题)
/**
* 创建银行卡类
*/
public class BankCard {
private double money;
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
}
/**
* 创建存钱类,遵从Runnable接口
*/
public class SaveMoney implements Runnable {
private BankCard card;
public SaveMoney(BankCard card) {
this.card = card;
}
@Override
public void run() {
synchronized (card) {
//存钱
for (int i = 0; i < 10; i++) {
card.setMoney(card.getMoney() + 1000);
//currentThread()为获取当前线程方法
//getName()为获取线程名字方法
System.out.println(Thread.currentThread().getName()
+ "存了1000,余额是:" + card.getMoney());
}
}
}
}
/**
* 创建取钱类
*/
public class SubMoney implements Runnable {
private BankCard card;
public SubMoney(BankCard card) {
this.card = card;
}
@Override
public void run() {
synchronized (card) {
//取钱
for (int i = 0; i < 10; i++) {
if (card.getMoney() >= 1000) {
card.setMoney(card.getMoney() - 1000);
System.out.println(Thread.currentThread().getName()
+ "取了1000,余额是:" + card.getMoney());
} else {
System.out.println("余额不足,及时存钱");
i--; //避免因为余额不足取不到,一直++然后就结束循环,目的是为了取完为止
}
}
}
}
}
/**
* 测试类
*/
public class test {
public static void main(String[] args) {
BankCard card = new BankCard();
SaveMoney save = new SaveMoney(card);
SubMoney sub = new SubMoney(card);
Thread jack = new Thread(save, "Jack");
Thread lucy = new Thread(sub, "Lucy");
jack.start();
lucy.start();
}
}
多线程的实现,就是要保证共用一个资源,这里共用的就是银行卡类资源,通过对Thread源码的分析就可以知道能够共用的原因。
//Thread类其实是遵从Runnable接口的一个类,那么它就必然要重写run()方法
public class Thread implements Runnable {...}
@Override
public void run() {
if (target != null) {
target.run();
}
}
//target源码,是Runnbale类型的变量,也就是说,start启动的是Runnable实现类的run()方法
//Runnable实现类作为实参传入了Thread形参中,创建了Thread类线程
private Runnable target;
SaveMoney方法和SubMoney方法分别将唯一的card地址传入,保证了两者操作的是一个资源,而线程的启动,本质也是调用的这两个类对象的run()方法,所以实现了多线程。
3、继承Thread类和实现Runnable接口两种方式的比较
继承Thread类的方式
- 没有资源共享,编写简单
- 因为线程类已经继承了Thread类,则不能再继承其他类【单继承】,影响扩展
遵从Runnable接口
- 可以多个线程共享同一个资源,所以非常适合多个线程来处理同一份资源的情况
- 资源类实现了Runnable接口。如果资源类有多个操作,需要把功能提出来,单独实现Runnable接口【接口分离,功能单一】
- 编程稍微复杂,不直观,如果要访问当前线程,必须使用Thread.currentThread()
调用start()与直接调用run()的区别
当调用start()方法时将创建新的线程,并且执行run()方法里的代码,但是如果直接调用run()方法,不会创建新的线程,无法实现多线程。
4、第三种创建线程的方式:Callable接口
//继承Callable接口,重写call()方法,并规定泛型
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
Integer sum = 0;
for (int j = 1; j <= 100; j++) {
sum += j;
}
return sum;
}
}
public class Demo1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建可调用对象
MyCallable myCallable = new MyCallable();
//使用FutureTask类,将可调用对象改为一个任务
FutureTask<Integer> task = new FutureTask<>(myCallable);
//任务传入,创建线程
Thread callable = new Thread(task, "callable");
//启动线程
callable.start();
//获取结果并打印
Integer sum = task.get();
System.out.println(sum);
}
}
三、线程中常用的方法
设置和获取线程属性
//设置线程的名字
void setName(String name);
//获取线程的名字
String getName();
String super.getName();
String Thread.currentThread().getName(); //推荐
//设置线程优先级
/**
* 线程优先级的范围是1-10,1最低,10最高,默认是5
* 这个方法的设置一定要在start,线程启动之前完成
* 线程的优先级低并不意味着争抢不到时间片,只是抢到时间片的概率比较低而已
*/
void setPriority(int newPriority);
//获取线程优先级
int getPriority();
线程休眠
//使得当前正在执行的线程休眠一段时间,释放时间片,导致线程进入阻塞状态
//当对应的时间到了之后,还会再继续执行
static void sleep(long millis);
合并(加入)线程
主线程仅仅是程序(进程)的所有线程的一个,自己有一个栈,其他子线程也有自己的栈,各执行各的,互不影响,这也是线程相对于方法的优势所在
如果想让主线程在最后退出,这里可以使用join阻塞主线程。
/**
* 在执行原来线程的过程中,如果遇到了合并线程,则优先执行合并进来的线程,
* 执行完合并进来的线程后,再回到原来的任务中,继续执行原来的线程。
* 特点:
a.线程合并,当前线程一定会释放cpu时间片,cpu会将时间片分给要Join的线程
b.哪个线程需要合并就在当前线程中添加要合并的线程
c.join之前,一定要将线程处于准备状态start
*/
//启动
joinThread.start();
//加入线程(阻塞了主线程,等join执行完完毕才继续执行)
joinThread.join();
后台线程
/**
* 后台线程,也被称为守护线程
* 隐藏起来一直在默默运行的线程,直到进程结束,JVM的垃圾回收线程就是典型的后台线程
* 特征:如果所有的前台线程都死亡,后台线程会自动死亡,即使相关方法还未结束。
*
* 前台线程:默认的线程都是前台线程,如果前台不执行完毕,程序不会退出。
*/
//设置线程为后台线程
deamonThread.setDaemon(true);
//启动线程
deamonThread.start();
线程让步
/**
* 让当前正在执行的线程暂停,但它不会阻塞该线程,只是将该线程重回就绪状态。
* 完全可能出现的情况是:当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行,继续抢CPU
* 实际上,当某个线程调用了yield方法暂停之后,只有优先级与当前线程相同或更高的就绪状态的线程才会获得执行的机会
* 一般用于让多个线程均匀执行
*/
static void yield();
线程中断
/**
* 程序在等待过程中,可以使用interrupt方法打断
* sleep/wait/join方法都会抛出打断异常InterruptedException(),唤醒线程,抛出异常
*/
void interrupt();
线程调度方式
- 分时调度:让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的cpu的时间片
- 抢占式调度:JAVA虚拟机采用抢占式调度模式,优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那就随机选择一个线程,使其占用CPU.处于运行状态的线程会一直运行,直至它不得不放弃CPU
四、线程生命周期
线程五状态
线程七状态
在源码中,是通过state枚举来表示的线程的状态,一共六种,其中就绪和运行归为一种。
//State枚举表示线程的状态(六种)
NEW //新生
RUNNABLE //就绪+运行
BLOCKED //锁阻塞
WAITING //等待
TIMED_WAITING //限时等待
TERMINATED //终止死亡