第五阶段 多线程
前言:
一个场景:周末,带着并不存在的女票去看电影,无论是现场买票也好,又或是手机买票也好,上一秒还有位置,迟钝了一下以后,就显示该座位已经无法选中,一不留神就没有座位了,影院的票是一定的,但是究竟是如何做到,多个窗口或者用户同时出票而又不重复的呢?这就是我们今天所要讲解的多线程问题
(一) 线程和进程的概述
(1) 进程
- 进程:进程是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源
- 多线程:在同一个时间段内可以执行多个任务,提高了 CPU 的使用率
(2) 线程
- 线程:进程的执行单元,执行路径
- 单线程:一个应用程序只有一条执行路径
- 多线程:一个应用程序有多条执行路径
- 多进程的意义?—— 提高 CPU 的使用率
- 多线程的意义? —— 提高应用程序的使用率
(3) 补充
并行和并发
- 并行 是逻辑上同时发生,指在某一个时间段内同时运行多个程序
- 并发 是物理上同时发生,指在某一个时间点同时运行多个程序
Java 程序运行原理和 JVM 的启动是否是多线程的?
-
Java 程序的运行原理:
- 由 java 命令启动 JVM,JVM 启动就相当于启动了一个进程
- 接着有该进程创建了一个主线程去调用 main 方法
-
JVM 虚拟机的启动是单线程的还是多线程的 ?
- 垃圾回收线程也要先启动,否则很容易会出现内存溢出
- 现在的垃圾回收线程加上前面的主线程,最低启动了两个线程,所以,jvm 的启动其实是多线程的
- JVM 启动至少启动了垃圾回收线程和主线程,所以是多线程的
(二) 多线程代码实现
需求:我们要实现多线程的程序。
如何实现呢?
由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。
而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。
Java 是 不能直接调用系统功能 的,所以,我们 没有办法直接实现多线程 程序。
但是呢?Java 可以去调用 C /C++ 写好的程序来实现多线程程序。
由 C /C++ 去调用系统功能创建进程,然后由 Java 去调用这样的东西,
然后提供一些类供我们使用。我们就可以实现多线程程序了。
通过查看 API,我们知道了有 2 种 方式实现多线程程序。
方式 1:继承 Thread 类
步骤:
- 自定义 MyThread(自定义类名)继承 Thread 类
- MyThread 类中重写 run()
- 创建对象
- 启动线程
public class MyThread extends Thread{public MyThread() { }
@Override
public void run() {for (int i = 0; i < 100; i++){System.out.println(getName() + ":" + i);
}
}
}
public class MyThreadTest {public static void main(String[] args) {
// 创建线程对象
MyThread my = new MyThread();
// 启动线程,run()相当于普通方法的调用,单线程效果
//my.run();
// 首先启动了线程,然后再由 jvm 调用该线程的 run()方法,多线程效果
my.start();
// 两个线程演示,多线程效果需要创建多个对象而不是一个对象多次调用 start()方法
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
my1.start();
my2.start();}
}
// 运行结果
Thread-1:0
Thread-1:1
Thread-1:2
Thread-0:0
Thread-1:3
Thread-0:1
Thread-0:2
......
Thread-0:95
Thread-0:96
Thread-0:97
Thread-0:98
Thread-0:99
方式 2:实现 Runnable 接口 (推荐)
步骤:
- 自定义类 MyuRunnable 实现 Runnable 接口
- 重写 run()方法
- 创建 MyRunable 类的对象
- 创建 Thread 类的对象,并把 C 步骤的对象作为构造参数传递
public class MyRunnable implements Runnable {public MyRunnable() { }
@Override
public void run() {for (int i = 0; i < 100; i++){
// 由于实现接口的方式不能直接使用 Thread 类的方法了,但是可以间接的使用
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class MyRunnableTest {public static void main(String[] args) {
// 创建 MyRunnable 类的对象
MyRunnable my = new MyRunnable();
// 创建 Thread 类的对象,并把 C 步骤的对象作为构造参数传递
// Thread t1 = new Thread(my);
// Thread t2 = new Thread(my);
// 下面具体讲解如何设置线程对象名称
// t1.setName("User1");
// t1.setName("User2");
Thread t1 = new Thread(my,"User1");
Thread t2 = new Thread(my,"User2");
t1.start()
t2.start();}
}
实现接口方式的好处
可以避免由于 Java 单继承带来的局限性
适合多个相同程序的代码去处理同一个资源的情况,把线程同程序的代码,数据有效分离,较好的体现了面向对象的设计思想
如何理解 —— 可以避免由于 Java 单继承带来的局限性
比如说,某个类已经有父类了,而这个类想实现多线程,但是这个时候它已经不能直接继承 Thread 类了
(接口可以多实现 implements,但是继承 extends 只能单继承),它的父类也不想继承 Thread 因为不需要实现多线程
(三) 获取和设置线程对象
// 获取线程的名称
public final String getName()
// 设置线程的名称
public final void setName(String name)
设置线程的名称 (如果不设置名称的话,默认是 Thread-? (编号) )
方法一:无参构造 + setXxx (推荐)
// 创建 MyRunnable 类的对象
MyRunnable my = new MyRunnable();
// 创建 Thread 类的对象,并把 C 步骤的对象作为构造参数传递
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
t1.setName("User1");
t1.setName("User2");
// 与上面代码等价
Thread t1 = new Thread(my,"User1");
Thread t2 = new Thread(my,"User2");
方法二:(稍微麻烦,要手动写 MyThread 的带参构造方法,方法一不用)
//MyThread 类中
public MyThread(String name){super(name);// 直接调用父类的就好
}
//MyThreadTest 类中
MyThread my = new MyThread("admin");
获取线程名称
注意:重写 run 方法内获取线程名称的方式
//Thread
getName()
//Runnable
// 由于实现接口的方式不能直接使用 Thread 类的方法了,但是可以间接的使用
Thread.currentThread().getName()
使用实现 Runnable 接口方法的时候注意:main 方法所在的测试类并不继承 Thread 类,因此并不能直接使用 getName()方法来获取名称。
// 这种情况 Thread 类提供了一个方法:
//public static Thread currentThread():
// 返回当前正在执行的线程对象, 返回值是 Thread, 而 Thread 恰巧可以调用 getName()方法
System.out.println(Thread.currentThread().getName());
(四) 线程调度及获取和设置线程优先级
假如我们的计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到 CPU 时间片,也就是使用权,才可以执行指令。那么 Java 是如何对线程进行调用的呢?
线程有两种调度模型:
分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。
Java 使用的是抢占式调度模型
// 演示如何设置和获取线程优先级
// 返回线程对象的优先级
public final int getPriority()
// 更改线程的优先级
public final void setPriority(int newPriority)
线程默认优先级是 5。
线程优先级的范围是:1-10。
线程优先级高仅仅表示线程获取的 CPU 时间片的几率高,但是要在次数比较多,或者多次运行的时候才能看到比较好的效果。
(五) 线程控制
在后面的案例中会用到一些,这些控制功能不是很难,可以自行测试。
// 线程休眠
public static void sleep(long millis)
// 线程加入(等待该线程终止,主线程结束后,其余线程开始抢占资源)
public final void join()
// 线程礼让(暂停当前正在执行的线程对象,并且执行其他线程让多个线程的执行更加和谐,但是不能保证一人一次)
public static void yield()
// 后台线程(某线程结束后,其他线程也结束)public final void setDaemon(boolean on)
//(过时了但还可以用)public final void stop()
// 中断线程
public void interrupt()
(六) 线程的生命周期
新建 —— 创建线程对象
就绪 —— 线程对象已经启动,但是还没有获取到 CPU 的执行权
运行 —— 获取到了 CPU 的执行权
- 阻塞 —— 没有 CPU 的执权,回到就绪
死亡 —— 代码运行完毕,线程消亡
(七) 多线程电影院出票案例
public class SellTickets implements Runnable {
private int tickets = 100;
@Override
public void run() {while (true){if (tickets > 0){
try {Thread.sleep(100);
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票");
}
}
}
}
public class SellTicketsTest {public static void main(String[] args) {
// 创建资源对象
SellTickets st = new SellTickets();
// 创建线程对象
Thread t1 = new Thread(st, "窗口 1");
Thread t2 = new Thread(st, "窗口 2");
Thread t3 = new Thread(st, "窗口 3");
// 启动线程
t1.start();
t2.start();
t3.start();}
}
在 SellTicket 类中添加 sleep 方法,延迟一下线程,拖慢一下执行的速度
通过加入延迟后,就产生了连个问题:
A: 相同的票卖了多次
CPU 的一次操作必须是原子性(最简单的)的 (在读取 tickets– 的原来的数值和减 1 之后的中间挤进了两个线程而出现重复)
B: 出现了负数票
随机性和延迟导致的 (三个线程同时挤进一个循环里,tickets– 的减法操作有可能在同一个循环中被执行了多次而出现越界的情况,比如说 tickets 要大于 0 却越界到了 -1)
也就是说,线程 1 执行的同时线程 2 也可能在执行,而不是线程 1 执行的时候线程 2 不能执行。
我们先要知道一下哪些问题会导致出问题:
而且这些原因也是以后我们 判断一个程序是否会有线程安全问题的标准
A: 是否是多线程环境
B: 是否有共享数据
C: 是否有多条语句操作共享数据
我们对照起来,我们的程序确实存在上面的问题,因为它满足上面的条件
那我们怎么来解决这个问题呢?
把多条语句操作共享数据的代码给包成一个整体,让某个线程在执行的时候,别人不能来执行
Java 给我们提供了:同步机制
// 同步代码块:synchronized(对象){需要同步的代码;}
同步的好处
同步的出现解决了多线程的安全问题
同步的弊端
当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
概述:
A: 同步代码块的锁对象是谁呢?
任意对象
B: 同步方法的格式及锁对象问题?
把同步关键字加在方法上
同步方法的锁对象是谁呢?
this
C: 静态方法及锁对象问题?
静态方法的锁对象是谁呢?
类的字节码文件对象。
我们使用 synchronized 改进我们上面的程序,前面线程安全的问题,
public class SellTickets implements Runnable {
private int tickets = 100;
// 创建锁对象
// 把这个关键的锁对象定义到 run()方法(独立于线程之外),造成同一把锁
private Object obj = new Object();
@Override
public void run() {while (true) {synchronized (obj) {if (tickets > 0) {
try {Thread.sleep(100);
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票");
}
}
}
}
}
(八) lock 锁的概述和使用
为了更清晰的表达如何加锁和释放锁,JDK5 以后提供了一个新的锁对象 Lock
(可以更清晰的看到在哪里加上了锁,在哪里释放了锁,)
void lock() 加锁
void unlock() 释放锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SellTickets2 implements Runnable {
private int tickets = 100;
private Lock lock = new ReentrantLock();
@Override
public void run() {while (true) {
try {lock.lock();
;
if (tickets > 0) {
try {Thread.sleep(150);
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
}
} finally {lock.unlock();
}
}
}
}
(九) 死锁问题 (简单认识)
同步弊端
效率低
如果出现了同步嵌套,就容易产生死锁问题
死锁问题
是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象
(十) 等待唤醒机制
我们前面假定的电影院场景,其实还是有一定局限的,我们所假定的票数是一定的,但是实际生活中,往往是一种供需共存的状态,例如去买早点,当消费者买走一些后,而作为生产者的店家就会补充一些商品,为了研究这一种场景,我们所要学习的就是 Java 的等待唤醒机制
生产者消费者问题 (英语:Producer-consumer problem),也称 有限缓冲问题(英语:Bounded-buffer problem),是一个多进程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个进程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
我们用通俗一点的话来解释一下这个问题
Java 使用的是抢占式调度模型
- A:如果消费者先抢到了 CPU 的执行权,它就会去消费数据,但是现在的数据是默认值,如果没有意义,应该等数据有意义再消费。就好比买家进了店铺早点却还没有做出来,只能等早点做出来了再消费
- B:如果生产者先抢到 CPU 的执行权,它就回去生产数据,但是,当它产生完数据后,还继续拥有执行权,它还能继续产生数据,这是不合理的,你应该等待消费者将数据消费掉,再进行生产。这又好比,店铺不能无止境的做早点,卖一些,再做,避免亏本
梳理思路:
- A:生产者 —— 先看是否有数据,有就等待,没有就生产,生产完之后通知消费者来消费数据
- B:消费者 —— 先看是否有数据,有就消费,没有就等待,通知生产者生产数据
解释 : 唤醒——让线程池中的线程具备执行资格
Object 类提供了三个方法:
// 等待
wait()
// 唤醒单个线程
notify()
// 唤醒所有线程
notifyAll()
注意:这三个方法都必须在同步代码块中执行 (例如 synchronized 块),同时在使用时必须标明所属锁,这样才可以得出这些方法操作的到底是哪个锁上的线程
为什么这些方法不定义在 Thread 类中呢 ?
这些方法的调用必须通过锁对象调用,而我们刚才使用的锁对象是任意锁对象。
所以,这些方法必须定义在 Object 类中。
我们来写一段简单的代码 实现等待唤醒机制
public class Student {
String name;
int age;
boolean flag;// 默认情况是没有数据(false),如果是 true,说明有数据
public Student() {}
}
public class SetThread implements Runnable {
private Student s;
private int x = 0;
public SetThread(Student s) {this.s = s;}
@Override
public void run() {while (true){synchronized (s) {
// 判断有没有数据
// 如果有数据,就 wait
if (s.flag) {
try {s.wait(); //t1 等待,释放锁
} catch (InterruptedException e) {e.printStackTrace();
}
}
// 没有数据,就生产数据
if (x % 2 == 0) {
s.name = "admin";
s.age = 20;
} else {
s.name = "User";
s.age = 30;
}
x++;
// 现在数据就已经存在了,修改标记
s.flag = true;
// 唤醒线程
// 唤醒 t2, 唤醒并不表示你立马可以执行,必须还得抢 CPU 的执行权。s.notify();}
}
}
}
package cn.bwh_05_Notify;
public class GetThread implements Runnable {
private Student s;
public GetThread(Student s) {this.s = s;}
@Override
public void run() {while (true){synchronized (s){
// 如果没有数据,就等待
if (!s.flag){
try {s.wait();
} catch (InterruptedException e) {e.printStackTrace();
}
}
System.out.println(s.name + "---" + s.age);
// 修改标记
s.flag = false;
// 唤醒线程 t1
s.notify();}
}
}
}
package cn.bwh_05_Notify;
public class StudentTest {public static void main(String[] args) {Student s = new Student();
// 设置和获取的类
SetThread st = new SetThread(s);
GetThread gt = new GetThread(s);
// 线程类
Thread t1 = new Thread(st);
Thread t2 = new Thread(gt);
// 启动线程
t1.start();
t2.start();}
}
// 运行结果依次交替出现
生产者消费者之等待唤醒机制代码优化
最终版代码(在 Student 类中有大改动,然后 GetThread 类和 SetThread 类简洁很多)
public class Student {
private String name;
private int age;
private boolean flag;
public synchronized void set(String name, int age) {if (this.flag) {
try {this.wait();
} catch (InterruptedException e) {e.printStackTrace();
}
}
this.name = name;
this.age = age;
this.flag = true;
this.notify();}
public synchronized void get() {if (!this.flag) {
try {this.wait();
} catch (InterruptedException e) {e.printStackTrace();
}
}
System.out.println(this.name + "---" + this.age);
this.flag = false;
this.notify();}
}
public class SetThread implements Runnable {
private Student s;
private int x = 0;
public SetThread(Student s) {this.s = s;}
@Override
public void run() {while (true) {if (x % 2 == 0) {s.set("admin", 20);
} else {s.set("User", 30);
}
x++;
}
}
}
public class GetThread implements Runnable{
private Student s;
public GetThread(Student s) {this.s = s;}
@Override
public void run() {while (true){s.get();
}
}
}
public class StudentTest {public static void main(String[] args) {Student s = new Student();
// 设置和获取的类
SetThread st = new SetThread(s);
GetThread gt = new GetThread(s);
Thread t1 = new Thread(st);
Thread t2 = new Thread(gt);
t1.start();
t2.start();}
}
最终版代码特点:
- 把 Student 的成员变量给私有的了。
- 把设置和获取的操作给封装成了功能,并加了同步。
- 设置或者获取的线程里面只需要调用方法即可
(十一) 线程池
程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池
线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用
在 JDK5 之前,我们必须手动实现自己的线程池,从 JDK5 开始,Java 内置支持线程池
JDK5 新增了一个 Executors 工厂类来产生线程池,有如下几个方法
// 创建一个具有缓存功能的线程池
// 缓存:百度浏览过的信息再次访问
public static ExecutorService newCachedThreadPool()
// 创建一个可重用的,具有固定线程数的线程池
public static ExecutorService newFixedThreadPool(intnThreads)
// 创建一个只有单线程的线程池,相当于上个方法的参数是 1
public static ExecutorService newSingleThreadExecutor()
这些方法的返回值是 ExecutorService 对象,该对象表示一个线程池,可以执行 Runnable 对象或者 Callable 对象代表的线程。它提供了如下方法
Future<?> submit(Runnable task)
<T> Future<T> submit(Callable<T> task)
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorDemo {public static void main(String[] args) {
// 创建一个线程池对象,控制要创建几个线程对象
ExecutorService pool = Executors.newFixedThreadPool(2);
// 可以执行 Runnalble 对象或者 Callable 对象代表的线程
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
// 结束线程池
pool.shutdown();}
}
(十二) 匿名内部类的方式实现多线程程序
匿名内部类的格式:
new 类名或者接口名( ) {重写方法;};
本质:是该类或者接口的子类对象
public class ThreadDemo {public static void main(String[] args) {new Thread() {
@Override
public void run() {for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + i);
}
}
}.start();}
}
public class RunnableDemo {public static void main(String[] args) {new Thread(new Runnable() {
@Override
public void run() {for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + i);
}
}
}).start();}
}
(十三) 定时器
定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。在 Java 中,可以通过 Timer 和 TimerTask 类来实现定义调度的功能
Timer
·public Timer()
public void schedule(TimerTask task, long delay)
public void schedule(TimerTask task,long delay,long period)
TimerTask
abstract void run()
public boolean cancel()
开发中
Quartz 是一个完全由 java 编写的开源调度框架
结尾:
如果内容中有什么不足,或者错误的地方,欢迎大家给我留言提出意见, 蟹蟹大家!^_^
如果能帮到你的话,那就来关注我吧!(系列文章均会在公众号第一时间更新)
在这里的我们素不相识,却都在为了自己的梦而努力 ❤
一个坚持推送原创 Java 技术的公众号:理想二旬不止