目录
多进程与多线程
现代操作系统是多进程的,会同时执行多个程序
许多应用程序是多线程运行的,比如:在使用微信是,发送消息、接收消息、打开文档等操作,会让我们觉得这些是并发运行的
多进程
- 操作系统的多任务:指操作系统同时运行多个应用程序的能力
这些程序看起来像是同时运行,但是对于CPU而言,操作系统在同一时间只能运行一个程序。它将CPU的时间片轮流分配给不同的程序,给用户一种并发处理的感觉,因为CPU轮转的速度很快,人感觉不出来。
- 进程:操作系统中的每一个任务
当一个程序进入内存时,就变成了一个进程
多线程
- 线程:线程是进程的执行单元,进程中的线程是独立的、并发的执行流
- 当进程被初始化后,主线程就被创建了
- 多线程:一个进程中可以创建多个线程,它们相互独立,使得一个进程可以并发处理多个线程
进程与线程的关系
- 进程是操作系统进行资源分配和调度的独立单元。线程共享进程的资源(方法区、堆内存区等等)
- 每个线程有自己的独立的数据区(程序计数器、栈内存区等),在创建新线程时,这些数据区会一并创建
- 程序计数器:记录每个每个线程执行的指令位置
- 栈内存区:存放线程中每个调用方法的相关信息
- 线程的优势:
- 操作系统创建进程时,需要创建独立的内存单元,并且分配大量资源。相对来说,线程的创建比较容易,,因此多线程任务要比多进程任务的效率更好。
- 线程之间是可以共享内存的,进程之间不可以共享
- Java语言提供了多线程的支持,不需本地操作系统的直接参与,简化了多线程编程。
线程的创建与执行
- 线程:java.lang.Thread实例
- 创建线程的方式:
- 继承Thread类
- 实现Runnable接口
继承Thread类创建线程
- 定义Thread的子类,并重写该类的run()方法。run()方法的方法体代表了线程需要完成的任务。
- 创建线程对象。
- 线程对象调用start()方法启动该线程。
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行");
}
}
// 使用
MyThread t = new MyThread();
t.start();
实现Runnable接口创建线程
Runnable接口只有一个run()方法:
- 因为Runnable接口和Thread类之间没有继承关系,所以不能直接赋值
- 为了使run()方法中的代码在单独的线程中运行,仍需要一个Thread实例,实现对Runnable对象的包装。这样,线程相当于由两部分代码组成:Thread提供线程的支持,Runnable实现类提供线程执行体,即线程任务部分的代码
- Thread(Runnable thread)构造方法用于包装Runnable实现类对象,并创建线程
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程执行");
}
}
// 使用
Thread t = new Thread(new MyRunnable());
t.start();
线程的生命状态与周期
线程由新建、就绪、运行、阻塞、死亡这些状态构成了它的生命周期,呈现了其工作的过程。
-
新建和就绪状态
- 创建Thread实例:“新建”(new)
- start()方法启动线程:“就绪”状态(runnable)
-
运行状态
- 线程在就绪状态就获得了CPU的时间片:运行状态(Running):
- 操作系统大都采用抢占式方法调配资源,即操作系统会为每一个线程分配一个时间片,时间片用完后线程会被换下。当选择下一个线程的时候,会考虑线程的优先级
阻塞状态
- 阻塞状态是三个状态的结合体(睡眠、资源堵塞、等待)
- 共同点:此时线程依然是“活”的,只是缺少运行它的条件,即当前是不可运行的,但是当发生某个特定的事件,就能够重新回到Runnable状态
- 当前正在执行的线程处于阻塞状态,那么其他的线程就获得了执行的机会。当线程解除阻塞状态回到Runnable状态时,必须要等待再次被操作系统调度
线程在运行状态时,遇到如下状况将会进入各种阻塞状态:
- 睡眠:线程调用sleep()方法睡眠一段时间
- 资源阻塞:线程在等待一种资源,例如线程调用了阻塞式的I/O方法(等待输入流等),在该方法返回之前该线程被阻止;线程试图获得一个同步锁,但同步锁正被其他线程持有(11.5节详述)
- 等待:线程调用wait()方法后等候其他线程的唤醒通知
死亡状态
- 线程的run()方法执行完毕,线程正常结束
- 或者线程执行过程中抛出一个未捕获的异常或错误,线程异常结束。结束后线程处于“死亡”状态(dead)
线程状态转换图
[新建NEW] → [可运行RUNNABLE] → [运行中(操作系统层面)]
↗ ↓ ↖
[阻塞BLOCKED] ← [等待WAITING/TIMED_WAITING] → [终止TERMINATED]
NEW → RUNNABLE
Thread t = new Thread(); // 状态:NEW
t.start(); // 状态:RUNNABLE
-
触发条件:调用线程的
start()
方法 -
说明:线程对象创建后处于NEW状态,调用
start()
后进入就绪状态
RUNNABLE → RUNNING
- 如果处于就绪状态的线程获得了CPU时间片,就开始执行run()方法中的线程执行体,线程进入“运行”状态。
- 除非线程的执行体特别短,在一个CPU时间片内就可以执行完毕,否则在运行过程中它将会被中断,以使其他的线程获得执行的机会。线程因失去时间片而中断时返回就绪状态。
- running状态的线程可以调用yield()方法主动放弃执行,从running转入runnable。
RUNNABLE → BLOCKED
“阻塞”状态是三种状态的组合体:睡眠/资源阻塞/等待。
DEAD
线程优先级与线程调度策略
线程调度器:JVM的一部分
- JVM通常将Java线程直接映射为本地操作系统上的本机线程。
- 处于runnable状态的线程被放在可运行池中,它们都有资格被调度
- 线程调度器决定在某个时刻应该运行哪个线程。
- 决定实际运行哪个线程是线程调度器的职权
注意!!!我们无法控制线程调度器,不能要求、指定某个线程去被运行。
线程调度器:基于优先级的抢先式调度机制
- 如果线程进入了runnable状态,而且它比可运行池中的任何线程以及当前运行的线程具有更高的优先级,则具有最高优先级的线程将被选择运行,较低优先级的运行中线程撤回到runnable状态。
- 当池内线程具有相同的优先级,或者当前运行线程与池内线程具有相同优先级时,线程调度器将随意选择它“喜欢”的线程
线程同步
当多个线程共享同一个数据时,如果处理不当,很容易出现线程的安全隐患,所以多线程编程时经常需要解决线程同步问题:
public static void main(String[] args) {
Runnable target = new SecondThread();
Thread t1 = new Thread(target);
Thread t2 = new Thread(target);
t1.start();
t2.start();
}
数据共享问题
实际问题举例:
夫妻共同进行取钱,
- 妻子线程首先执行,检查账户发现账户余额满足取款条件(妻子线程的步骤(1)完成),但在妻子取款之前线程被换下;
- 丈夫线程上来后检查账户余额,此时妻子还未取款,丈夫看到的是妻子取款前的账户余额,账户余额也可以满足他的取款要求(丈夫线程的步骤(1)完成),在丈夫取款之前线程被换下;
- 妻子线程换上来后接着运行,取走了账户中的全部余额(妻子线程的步骤(2)完成),妻子线程结束;
- 丈夫线程换上来后接着运行,因为之前丈夫线程已经确认账户有足够的余额可以支取,于是,丈夫在没有足够余额的情况下仍然进行了取款。
同步与锁机制
-
锁机制
Java中每个对象都有一个内置锁,当对象具有同步代码时,内置锁启用。
Java使用关键字synchonized修饰同步代码块或同步方法,为对象加锁。 加锁后的同步代码块或同步方法,形成“原子”操作,即该操作是不可分割的。
// 同步方法
public synchronized void method() {}
// 同步代码块
synchronized(lockObject) {}
-
线程与锁的关系
- 锁是属于对象的,拥有该对象锁的线程可以执行同步代码。当同步代码执行完成后,锁被释放。
- 一个对象只有一个锁,所以当一个对象被加锁后,当另一个线程想要执行同步代码时,就会因为锁获取失败而进入阻塞状态,从而保证线程安全。
- 锁不属于线程,只属于对象,所以一个线程可有多个对象的锁,但是只有同一个对象的锁之间才会互斥。
同步代码块
同步锁形成的原子操作会极大破坏并发性,所以不要同步原子操作之外的其他代码
案例分析
public class MyRunnable implements Runnable{
private int i=0; //i作为属性
public void run(){
while(i<5){
i++;
for (int j = 0; j < 20000000; j++);
System.out.print(Thread.currentThread().getName()+" ");
System.out.println("i="+i);
}
}
}
public class Test {
public static void main(String[] args) {
Runnable target = new MyRunnable();
Thread t1 = new Thread(target, "A");
Thread t2 = new Thread(target, "B");
t1.start();
t2.start();
}
}
-
情况一:为什么没有输出 i = 1 ?
要理解这的错误,要先明确循环程序的操作顺序:
while(i < 5) { // 检查条件
i++; // 递增操作
// ...其他代码
}
-
情况二:i = 5 为什么输出了两次?
添加同步代码之后:
public class MyRunnable implements Runnable{
private int i=0; //i作为属性
public void run(){
while(i<5){
synchronized(this){
i++;
for (int j=0; j<20000000; j++);
System.out.print(Thread.currentThread().getName()+" ");
System.out.println("i="+i);
}
}
}
}
public class Test {
public static void main(String[] args) {
Runnable target = new MyRunnable();
Thread t1 = new Thread(target, "A");
Thread t2 = new Thread(target, "B");
t1.start();
t2.start();
}
}
-
情况三:为什么输出 i = 6 ?
假设以下执行顺序:
线程A读取i=4,满足i<5条件
线程B读取i=4,满足i<5条件
线程A执行i++,i变为5
线程A完成循环,退出
线程B执行i++,i变为6
线程B继续执行打印语句,输出i=6
要彻底解决这三个问题应该将代码修改成为下面这样子:
public class MyRunnable implements Runnable{
private int i=0; //i作为属性
public void run(){
while(i<5){
synchronized(this){
if(i==5) break; //这一步很关键,起到了再次判断的效果
i++;
for (int j = 0; j < 20000000; j++);
System.out.print(Thread.currentThread().getName()+" ");
System.out.println("i="+i);
}
}
}
}
同步方法
如果一个方法内的所有代码组成“原子”操作,那么可以将该方法定义为同步方法,使用synchronized关键字修饰。
线程间的通信
- 在多线程环境中,线程之间经常需要协调通信从而共同完成一件任务。Java传统的线程通信是通过Object类中的wait()和notify()方法完成
- Java SE5.0中增加了阻塞队列BlockingQueue等方式控制线程通信
wait()和notify()方法
- Object类中提供了wait(),notify()和notifyAll()3个方法来操作线程。
- 它们只能在同步代码块或者同步方法内使用,而且只能通过进行同步控制的对象(同步监视器)来调用。
wait()方法
- 在线程已获得对象锁的情形下,如果该线程需要再满足一些条件才能继续执行线程任务,此时该线程可调用wait()方法进入等待池(阻塞状态的一种)
- 线程调用wait()方法会解除对象的锁,让出CPU资源,并使该线程处于等待状态,使其它线程可以获取该对象的锁,执行该对象的同步代码块或方法。
- void wait():线程会一直等待,直到其他线程调用同步监视器的notify()或notifyAll()方法后苏醒
- void wait(long timeout),wait(long timeout,int nanos):方法指定了等待时间,所以如果线程在等待时间内没有被同步监视器的notify()方法唤醒,则在等待指定时间后自动苏醒。
notify()和notifyAll()方法
- notify()方法唤醒一个处于等待状态的线程,使之进入runnable状态。
- 某个线程执行完同步代码,或该线程使另一个线程所等待的条件得到满足,这时它利用同步监视器调用notify()方法,以唤醒一个因该同步监视器而处于等待状态的线程再次进入runnable状态。
- 从等待状态进入runnable状态的线程,将再次尝试获得同步监视器的锁。
- notifyAll()方法:使因该同步监视器而处于等待状态的全部线程进入runnable状态。
更完整的线程转换图
wait()和notify()方法
实例讲解:
写两个线程,线程A“做”10个披萨,线程B“做”20份意大利面,要求线程A每做一个披萨,就通知线程B去做两份意大利面,线程B完成两份意大利面后通知线程A继续做披萨……。
public class SimplePizzaPastaProduction {
// 使用一个简单的标志对象来控制流程
private static class Kitchen {
boolean isPizzaTime = true;
}
public static void main(String[] args) {
Kitchen kitchen = new Kitchen();
// 线程A:制作披萨
Thread pizzaChef = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
synchronized (kitchen) {
// 等待轮到做披萨
while (!kitchen.isPizzaTime) {
try {
kitchen.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// 制作一个披萨
System.out.println("🍕 制作第" + i + "个披萨");
// 通知做意大利面
kitchen.isPizzaTime = false;
kitchen.notifyAll();
}
}
});
// 线程B:制作意大利面
Thread pastaChef = new Thread(() -> {
for (int i = 1; i <= 20; i += 2) {
synchronized (kitchen) {
// 等待轮到做意大利面
while (kitchen.isPizzaTime) {
try {
kitchen.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// 制作两份意大利面
System.out.println("🍝 制作第" + i + "和" + (i+1) + "份意大利面");
// 通知做披萨
kitchen.isPizzaTime = true;
kitchen.notifyAll();
}
}
});
pizzaChef.start();
pastaChef.start();
}
}