写在前边:
这篇博客比较系统的介绍了 Java 多线程以及相关知识,刚入 Java 的小白或者 基础不牢 的可以来看看。内容虽然很广泛,但是深度还不够,还请各位多多指点。由于篇幅问题有些知识点没有写上,在看这篇博客的有不明白的当时一定要记下来,及时去搜索一下。
【友情提示】
概念性的东西很多,文章也不太短,要耐心看鸭!
Java 多线程
一、线程相关概念
1.并发与并行
并发:同一时间段内发生( 两个任务交替执行
)
并行:同一时刻发生( 两个任务同时执行
)
2.线程与进程
进程:指一个内存中运行的应用程序
(进入到内存的程序叫做进程)
线程:线程是进程中的一个执行单元
,负责当前进程中的程序执行,一个进程至少有一个线程。
如果一个进程中有多个线程,这个应用程序可称之为多线程程序
3.线程的调度
分 时 调 度:平均分配 每个线程占用 CPU 的时间
抢占式调度:优先级高的线程 优先使用 CPU
如果优先级相同,会随机选择一个(线程随机性)
Java为抢占式调度(线程执行的先后顺序是随机的)
4.主线程 (指Java中的主线程)
概念:执行 main 方法的线程
程序的执行:
- 1.JVM执行 main 方法,main 方法进入栈内存
- 2.JVM会请求 CPU 分配资源,用于执行程序
- 3.程序文件从硬盘读取到内存中,然后交给CPU执行程序
Java中默认只有一个线程,执行从main方法开始。可手动创建多个线程
二、线程的实现方式
创建新线程的三种方式:
继承Thread类
、实现Runable接口
、通过 Callable 和 Future 接口创建
1.继承 Thread 类
Thread类:来自于java.lang.Thread,使用时不需要导包
实现步骤
1.新建一个 Thread 类的子类 public class Xxx extends Thread { }
2.在 Thread 类的子类中重写 run 方法
,设置线程任务
3.创建 Thread 类的子类对象
4.调用 Thread 类的start
方法 ,开启新的线程,自动执行run
方法
代码实例
1.创建一个 Thread 类的子类
public class NewThread extends Thread{
//重写 run 方法,设置线程(开启线程要做什么)
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("run"+i);
}
}
}
2.创建 Thread 类的子类,并开启线程
【PS】是调用 start 方法开启线程,而不是调用 run 方法。调用start方法,会自动执行run方法
public class Demo {
public static void main(String[] args) {
NewThread nt = new NewThread();
nt.start();
//nt.run();//调用run方法不会开启线程
for (int i = 0; i < 5; i++) {
System.out.println("main"+i);
}
}
}
程序运行结果:
【注意】
调用 Thread 子类对象的 start 方法开启新线程
,调用 run 只会在当前线程继续执行 run 方法,不会开启新线程- Java程序属于抢占式调度,哪个线程优先级高优先执行那个线程的程序,优先级相同则随机选择一个
- 由 start() 创建了新线程,Java 虚拟机调用该线程的 run 方法。
- 多次启动一个线程是非法的。线程已经结束执行后,不能再重新启动。
会抛出 java.lang.IllegalThreadStateException 异常
Thread 类的常用方法
静态方法
方法信息 | 方法介绍 |
---|---|
public static void yield() | 暂停当前正在执行的线程对象,并执行其他线程。 |
public static void sleep(long millisec) | 在指定的毫秒数内让当前正在执行的线程休眠。 |
public static boolean holdsLock(Object x) | 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。 |
public static Thread currentThread() | 返回对当前正在执行的线程对象的引用。 |
public static void dumpStack() | 将当前线程的堆栈跟踪打印至标准错误流。 |
成员方法
方法信息 | 方法介绍 |
---|---|
public void start() | 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 |
public void run() | 如果该线程是使用独立的 Runnable 运行对象构造的, 则调用该 Runnable 对象的 run 方法; 否则,该方法不执行任何操作并返回。 |
public final void setName(String name) | 改变线程名称,使之与参数 name 相同。 |
public final void setPriority(int priority) | 更改线程的优先级。 |
public final void setDaemon(boolean on) | 将该线程标记为守护线程或用户线程。 |
public final void join(long millisec) | 等待该线程终止的时间最长为 millis 毫秒。 |
public void interrupt() | 中断线程。 |
public final boolean isAlive() | 测试线程是否处于活动状态。 |
2.实现 Runnable 接口
Runnable接口:来自于java.lang.Runnable,使用时不需要导包
实现步骤
1.新建 Runnable 的实现类
2.重写 Runnable 的 run 方法
3.通过 Thread 的构造方法,将 Runnable 的实现类对象传递给 Thread 类
4.通过 Thread 对象调用start方法开启线程
因为 Runnable 接口中没有 start 方法
代码实例
1.新建Runable的实现类
public class NewThread implements Runnable{
@Override
public void run() {
System.out.println("使用Runnable接口创建线程");
}
}
2.开启新线程
public class Demo {
public static void main(String[] args) {
//创建 Runable 的实现类对象
NewThread nt = new NewThread();
//创建 Thread 对象,并通过构造方法将实现类对象传递给Thread类
Thread t = new Thread(nt);
//调用 start 方法开启新线程
t.start();
//=======下方一行代码和上方三行是等价的=========
new Thread(new NewThread()).start();//使用 匿名对象 开启线程避免繁琐
System.out.println("main主线程");
}
}
程序运行结果:
两线程优先级相同,两条语句的打印顺序依旧是随机执行,不再截图演示
3.使用 Callable 和 Future 接口
通过此方式创建线程,可以获取新线程任务的返回值
实现步骤
1.创建 Callable<V> 接口的实现类
2.在实现类中重写 Callable 接口中的 call 方法
(注意这里不是 run 方法)
3.创建 FutureTask<V> 类的对象
4.创建 Thread 类对象,将 FutureTask 对象传递到 Thread 的构造方法
5.使用 Thread 对象调用 start 方法,开启新线程
代码实例
1.新建Callable的实现类,并重写call方法
public class CallableThread implements Callable<Integer>{
//重写call方法,编写线程任务
//call方法的返回值类型由Callable接口的泛型决定
@Override
public Integer call() throws Exception {
int i = 0;
for (; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
return i;
}
}
2.开启新线程
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建Callable实现类对象
CallableThread ct = new CallableThread();
//创建FutureTask对象,将Callable实现类对象传递到其构造方法中
FutureTask<Integer> ft = new FutureTask<>(ct);
//将FutureTask对象传递给Thread对象,并调用start方法开启新线程
new Thread(ft).start();
//main方法的线程任务
int i = 0;
for (; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
//获取call方法的返回值
Integer num = ft.get();
System.out.println("call方法的返回值为:" + num);
}
}
程序运行结果
【tips】
传递给
Callable<V>接口的泛型是作为call方法返回值类型的
- 获取新线程任务的返回值,可以
使用
FutureTask的
get方法
- get方法存在两个异常,分别是ExecutionException, InterruptedException
需要进行处理(throws或者try/catch)
4.创建线程三种方式的对比
继承Thread类创建新线程
优势:
- 实现过程简便,直接创建Thread对象调用start方法
- 可直接使用this获取当前线程的信息
劣势:
- 程序扩展性低,继承Thread类就不能再继承其它类
实现Runnable、Callable接口的方式创建新线程
优势:
- 增强了程序的扩展性,可以继承其它类
- 把设置线程任务和开启线程任务进行了分离(解耦)
- 更适合多个线程共享一份资源的情况(便于理解和代码编写),
可很好的将CPU代码及数据分离开劣势:
- 编写过程稍微繁琐,需要创建实现类对象,并将对象传递给Thread
- 只能使用Thread.currentThread()访问当前线程信息
Thread和Runable的区别
写法上的区别,本质并没有什么不同
。
Thread是Runnable接口的实现和扩展,
不论使用Thread还是Runnable开启新线程,都需要 new 出一个 Thread 对象来执行 start 方法
。
Thread同样可以简单的实现资源共享使用Thread类新建线程任务:
- 可以通过子类本身调用start方法(每开启一个线程,都是操作新的子类对象)
- 也可新建Thread对象开启新线程(将子类对象作为参数传递给Thread类)
- 已经继承了Thread类不能继承其它类了
使用Runnable接口新建线程任务:
- 必须通过Thread对象开启新线程(将实现类对象作为参数传递给Thread类)
使用建议:
如果有复杂的线程操作需求,那就选择继承Thread
如果只是简单的执行一个任务,那就实现Runnable
给大家配上代码,方便理解
public class Demo {
public static void main(String[] args) {
//继承Thread类创建线程
NewThread1 n1 = new NewThread1();
//子类对象直接开启线程
//操作了两个子类对象
n1.start();
n1.start();
//通过新的Thread对象开启线程
//操作了一个子类对象
new Thread(n1).start();
new Thread(n1).start();
//实现Runnable接口创建线程
//操作了一个实现类对象
NewThread2 n2 = new NewThread2();
new Thread(n2).start();
new Thread(n2).start();
}
}
class NewThread1 extends Thread {
@Override
public void run() {
synchronized (this) {
System.out.println("使用Thread类创建线程");
}
}
}
class NewThread2 implements Runnable {
@Override
public void run() {
synchronized (this) {
System.out.println("继承Runnable接口创建线程");
}
}
}
Runnable和Callable的区别
- Callable规定
重写
的方法是call()
,Runnable规定重写
的方法是run()
。Callable
的任务执行后有返回值
,而Runnable
的任务是不能有返回值
的。call 方法 可以抛出异常 ,run 方法不可以
。- 运行 Callable 任务可以拿到一个 Future 对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过 Future 对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
5.线程优先级
每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
Java 线程的优先级是一个整数,其取值范围是1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。
默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。
具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。
但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
三、线程安全问题
什么是线程安全问题:
在多线程环境下,不同线程对同一份数据操作,就可能会产生不同线程中数据状态不一致的情况,这就是线程安全问题的定义或者说原因。
保证线程安全的方式:
使用
同步代码块
、同步方法
、锁机制
。
1.使用 同步代码块 保证线程安全
格式:
synchronized (对象锁[对象检测器]) {
可能出现线程安全的代码;
}
代码实例:
public class Ticket01 implements Runnable{
//设置线程任务:卖票
private Integer ticket = 10;
Object obj = new Object();
@Override
public void run() {
//判断票是否存在
while(true){
//使用synchronized关键字保证线程安全
synchronized (obj){//obj可以换成this,也可以换成 本类类名.class(也可是继承的父类或者实现的接口)
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票。");
ticket--;
}else{
System.out.println(Thread.currentThread().getName()+"票已售空");
break;
}
}
}
}
}
注意:
1.
锁对象可以使用任意对象
(例如:this、super、本类类名.class、实现的接口 等)
2.必须保证多个线程使用的锁对象是同一个
3.锁对象可以把同步代码块锁住,只让一个线程在同步代码块中执行
2.使用 同步方法 保证线程安全
格式:
在方法声明时加 synchronized 关键字
代码实例:
public class Ticket02 {
//设置线程任务:卖票
private Integer ticket = 10;
//使用 synchronized 关键字修饰方法,保证线程安全
//同步方法的锁对象是 this
public synchronized void playTicket(){
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票。");
ticket--;
}else{
System.out.println(Thread.currentThread().getName()+"票已售空");
}
}
//静态同步方法
//静态方法的锁对象是 本类 的 class 属性
static int i = 100;
public static synchronized void playTicketStatic(){
if(i > 0){
System.out.println(Thread.currentThread().getName()+"正在卖第"+i+"张票。");
i--;
}else{
System.out.println(Thread.currentThread().getName()+"票已售空");
}
}
}
注意:
1.
同步方法的锁对象是 this
2.静态方法的锁对象是 本类 的 class 属性
3.使用 锁机制 保证线程安全
相关知识:
需要使用的到 Lock 接口(来自java.util.concurrent.locks包)
获取锁的方法 void lock()
释放锁的方法 void unlock()
Lock的实现类 ReentrantLock
使用步骤:
1.·在成员位置
创建一个ReentrantLock对象·
2.在可能出现线程安全问题的代码之前调用lock()方法
获取锁
3.在可能出现线程安全问题的代码之后调用unlock()方法
释放锁
代码实例:
class Ticket03 implements Runnable{
private Integer ticket = 10;
Lock lock = new ReentrantLock();
@Override
public void run() {
//判断票是否存在
while(true){
//调用lock方法获取锁
lock.lock();
try{
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票。");
ticket--;
}else{
System.out.println(Thread.currentThread().getName()+"票已售空");
break;
}
}catch (Exception e){
System.out.println("怎么可能有异常~");
System.out.println("try{}catch{}流程控制");
e.printStackTrace();
}finally {
//调用unlock方法释放锁
lock.unlock();
}
}
}
}
注意:
获取锁对象后一定要释放锁对象
,否则当前线程会一直持有锁,从而导致其它线程无法访问
四、等待唤醒机制
1.线程间的通信
多个线程并发执行时,CPU的执行时随机的,当需要多个线程共同执行一项任务时,需要有规律的进行,那么就需要协调通信,共同操作一份数据
2.线程的生命周期
下图概括一下就是:任何线程一般具有 5 种状态,即创建、就绪、运行、阻塞、终止。
图是 菜鸟教程 偷来的:https://www.runoob.com/java/java-multithreading.html
3.相关方法
Object类的 wait、notify、notifyall
方法
Thread类的 sleep、yield、join
方法
所属类 | 方法信息 | 方法介绍 |
---|---|---|
Object | final void wait() | 在其他线程调用此对象的 notify() 方法 或 notifyAll() 方法前,导致当前线程等待。 |
final void notify() | 唤醒在此对象监视器上等待的单个线程。 | |
final void notifyAll() | 唤醒在此对象监视器上等待的所有线程。 | |
Thread | static void sleep(long millis) | 指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。 |
static void yield() | 暂停当前正在执行的线程对象,并执行其他线程。 | |
final void join() | 等待该线程终止。 |
【tips】
注意 wait、sleep、join 方法有重载形式
(参数为时间)
- wait() 、wait(long timeout) 、wait(long timeout, int nanos)
- sleep(long millis) 、sleep(long millis, int nanos)
- join() 、join(long millis) 、join(long millis, int nanos)
4.等待唤醒实例
做包子
案例分析:
1.顾客要买包子,首先需要告知包子铺老板
2.顾客要等待
包子制作完毕
3.老板做好了包子,通知
顾客可以享用了
public class Demo01 {
public static void main(String[] args) {
//创建锁对象
Object obj= new Object();
new Thread(){
@Override
public void run() {
//保证等待和唤醒只有一个在执行
synchronized (obj){
System.out.println("告诉老板包子的种类和数量");
try {
//阻塞线程,等待老板做包子
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒之后执行的代码
System.out.println("包子已经做好了");
}
}
}.start();
new Thread(() -> {
try {
//阻塞线程,花 5 秒钟做包子
Thread.sleep(5*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj){
System.out.println("老板用 5 秒做好了包子");
//唤醒等待线程,告诉顾客包子已经做好了
obj.notify();
}
}).start();
}
}
【tips】
哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,
因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,
所以需要再次尝试去获取锁(很可能面临其它线程的竞争),
成功后才能在当初调用 wait 方法之后的地方恢复执行。
调用wait和notify方法需要注意的细节
1.wait方法与notify方法必须要由同一个锁对象调用。
因为∶对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
2.wait方法与notify方法是属于Object类的方法的。
因为∶锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
3.wait方法与notify方法必须要在同步代码块或者是同步函数中使用。
因为∶必须要通过锁对象调用这2个方法。
注意事项
1.
wait 方法如果有参数
,则到了指定时间可以自动唤醒
,否则只能阻塞到被 notify 方法唤醒
2.sleep 和 wait 方法都会阻塞线程,但是wait 会释放 CPU 资源
,而sleep 不会 释放 CPU 资源
3.join 方法主要是在主线程中等待其他子线程执行完 run 方法后再继续执行主线程的后续代码,当然也可以用到其他线程中
4.yield 方法把现在正在执行的线程的状态由执行转成就绪状态,等待资源重新执行。如果 CPU 资源不紧张,会立即继续执行
五、线程池
概念
线程池:可以理解为存放线程的集合,需要开启新线程时,从线程池里边直接取,用完再放回线程池
相关类和方法
Executors 类
创建线程池的方法:
static ExecutorService newFixedThreadPool(int nThreads)
创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。
参数:
nThreads - 池中的线程数
返回:
新创建的线程池(ExecutorService(接口)对象)
ExecutorService接口
取出线程的方法:
Future<?> submit(Runnable task)
提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。 该 Future 的 get 方法在成功 完成时将会返回 null。
参数:
task - 要提交的任务
返回:
表示任务等待完成的 Future
销毁线程的方法:
void shutdown()
启动一次顺序关闭,执行以前提交的任务,但不接受新任务。
线程池的使用步骤:
1.使用线程池 Executors 工厂类里的 newFixedThreadPool 静态方法生产一个指定数量的线程池
2.创建一个实现类实现 Runnable 接口,重写run方法,设置线程任务
3.调用 ExecutorService 接口中的 submit 方法,传递线程任务(Runnable的实现类),开启线程
4.调用 ExecutorService 接口中的 shutdown 方法销毁线程池(不建议使用)
代码实例:
public class Demo01 {
public static void main(String[] args) {
//开启线程池,并放入 5 个线程
ExecutorService executorService = Executors.newFixedThreadPool(5);
int i = 1;
//使用while循环从线程池开启 10 个线程
while(i<=10){
i++;
//开启一个线程
executorService.submit(new Thread(() -> {
System.out.println("线程池开启线程!"+Thread.currentThread().getName());
}));
}
//销毁线程池
executorService.shutdown();
}
}
运行结果:
线程池的优势
对已存在的线程进行复用,减低系统资源的开销
提高系统的相应速度,无需等待线程建立再执行新任务
方便并发管控,控制线程数量,增强系统可靠性与安全性
就到这里,没了没了…