在开始多线程之前,首先要知道,程序、进程和线程这三者之间的关系。
程序
:为了完成某特定的任务,用某种语言编写的一组指令的集合。也就是我们所编写的一段静态的代码。进程
:指的是一个正在运行的程序,是资源分配的单位线程
:
- 是程序内部的一条执行路径
- 一个程序可以支持多个线程并行运行
- 线程有自己的生命周期
- 每个线程都有自己独立的运行栈和程序计数器
- 线程共享相同的内存单元,可以访问相同的变量和对象。
- 缺点:会操作共享数据,可能存在线程安全的问题
- 优点:线程之间的通信更加的方便
并行和并发也是两个常见的概念,那么这两个具体的含义是什么呢
并行
:真正意义上的多个CPU实例或者多台机器同时去执行一段逻辑代码,是真正意义上的同时并发
:是通过CPU调度算法实现了一种伪并行,并不是真正的同时
1、线程的创建方式
1.1 继承Thread类
流程
:
- 创建一个继承于
Thread
类的子类 - 重写
Thread
类的run
方法 - 创建子类对象
- 通过此对象调用
start
方法
顺序和注意点:- 启动当前线程
- 调用线程的
run
方法- 不可以直接调用
run
(相当于直接调用了一个对象的方法,还是在主线程中执行的)
- 不可以直接调用
- 一个线程的
start
方法只可以调用一次- 线程执行完
run
方法后,就会进入死亡状态
- 线程执行完
Thread类中的常用方法
start()
启动当前线程,调用当前线程的run()
方法run()
currentThread()
获取执行当前代码的线程getName()
获取当前线程的名字setName()
设置当前线程的名字yield()
释放当前CPU的执行权join()
- 作用:线程a中调用线程b的join方法,那么线程a就会进入阻塞状态,当线程b完全执行完之后,线程a才会结束阻塞状态
- 有时某个线程运行过程中需要的资源来自于另外一个线程,那么这个时候我们就需要先暂停该线程,等资源全部获取到了之后再继续执行此线程
- 作用:线程a中调用线程b的join方法,那么线程a就会进入阻塞状态,当线程b完全执行完之后,线程a才会结束阻塞状态
stop()
强制结束此线程sleep()
挂起(睡眠)当前线程,指定时间内该线程处于阻塞状态
举例
:三个窗口同时卖票
输出结果:
- 创建一个类
MyThread
实现Runnable
接口 - 实现
Runnable
接口中的抽象方法run
- 创建
MyThread
类的对象myThread1
- 创建
Thread
类的对象thread1
,并将myThread1
作为构造器的参数
举例
:窗口卖票问题
使用Runnable接口来实现多线程的时候,票数可以不用静态变量来存储,因为三个线程都是由同一个对象生成的,那么自然共用同一份票数。
两种创建方式的比较
优先选择实现Runnable
接口的方式,更方便实现对数据的共享。两种方式的联系
:Thread
类本身就实现了Runnable
接口
1.3 实现Callable接口
创建方法
- 创建一个实现了
Callable
接口的类 - 在实现类中重写
call
方法,将此线程需要实现的操作声明在call
方法中。 (必要的话可以通过泛型执行返回值的类型) - 创建实现类的对象
- 将创建好的对象传递给
FutureTask
的构造器中,创建FutureTask
类型的对象 - 将
FutureTask
的对象作为参数传递到Thread
类的构造器中,创建Thread
类型的对象,调用其start
方法来开启线程 - 通过
FutureTask
对象的get
方法,来获取Callable
中call
方法的返回值
示例:MyThread.java
package test9;
import java.util.concurrent.Callable;
/**
* Created By Intellij IDEA
*
* @author Xinrui Yu
* @date 2021/12/21 15:59 星期二
*/
public class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i = 1;i <= 100;i++){
sum += i;
}
return sum;
}
}
App.java
package test9;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* Created By Intellij IDEA
*
* @author Xinrui Yu
* @date 2021/12/21 15:58 星期二
*/
public class App {
public static void main(String[] args) {
MyThread myThread = new MyThread();
FutureTask<Integer> futureTask = new FutureTask<>(myThread);
Thread thread = new Thread(futureTask);
Integer result = null;
thread.start();
try {
result = futureTask.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
if(result != null){
System.out.println(result);
}
}
}
为什么说实现Callable方法来创建线程比实现Runnable接口创建多线程更加强大?
call()
可以有返回值call()
可以抛出异常,从而被外部进行捕获,进行对应的处理- 在启动线程的
start()
处进行try-catch
捕获处理
- 在启动线程的
-
1.4 使用线程池
好处
: 提高程序的响应速度,减少了新线程的创建时间。
- 降低资源消耗(线程池中的线程可以进行重复的利用,而不需要每次都重新进行创建)
- 便于对线程的统一集中管理。
corePoolSize
:核心池的大小maximumPoolSize
:最大线程数keepAliveTime
:线程没有任务的时候多长时间后会终止
创建线程池示例
:
package test12;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created By Intellij IDEA
*
* @author Xinrui Yu
* @date 2021/11/30 13:27 星期二
*/
public class Application {
public static void main(String[] args) {
// 1、提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//2、执行指定的线程的操作,需要提供实现了Runnable接口或者Callable接口的对象
service.execute(new CountNumber1());
service.execute(new CountNumber2());
// service.submit(Callable callable); 适合使用于实现了Callable接口的对象
//3、关闭线程池
service.shutdown();
}
}
class CountNumber1 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 1){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
class CountNumber2 implements Runnable{
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
2、线程安全问题
线程安全 - 掘金
线程安全问题在我们的日常生活中也有具体的体现,比如买票的过程中一定概率会出现重复票、错误票的现象,这种现象出现的根本原因在于,某一个线程在操作车票的过程中,在操作尚未全部完成的时候,由于CPU的调度,使得其他的线程对共享数据进行了操作,导致买票出错。那么我们接下来讨论线程安全的相关问题。
上述问题的解决方案:当线程A在操作票数的时候,其他的线程不可以参与进来,直到线程A操作完,其他的线程才能开始操作票数。在Java中,我们通过线程的同步机制,来解决线程的安全问题。但是这种方式也有一定的局限性:我们在操作同步代码的时候,只能有一个线程参与,其他的线程都只能处于等待状态,相当于只有一个线程,导致程序的效率比较低下。
处理方法
JDK5之前
在JDK5之前,我们可以使用同步代码块
或者同步方法
的方式来实现线程之间的同步。同步
在Java中的关键字是synchronized
同步监视器
:也就是锁的概念。事实上,任何一个类的对象都可以充当锁来使用。要实现线程的同步,那么多个线程之间必须共用同一把锁。
同步代码块
将多个线程操作共享数据的部分代码使用同步代码块的方式包裹起来。- 格式:
synchronized(锁){...}
- 注意:多个线程使用的锁一定要是相同的!!!
- 格式:
同步方法
如果操作共享数据的代码是可以写成完整的一个方法,那么我们不妨把这个方法声明为同步的- 注意:同步方法中的同步监视器是
this
。使用过程中要注意锁的共享问题
- 注意:同步方法中的同步监视器是
JDK5之后
JDK5
之后提供了Lock
接口,来实现对线程的同步。
创建实现了Lock
接口的ReentrantLock
对象
优点
:对线程同步的操作更加灵活
示例
:两个用户共用同一个银行账户,每个人分三次向账户里面存钱,每次存1000元,判断是否会有线程安全的问题,如果有线程安全的问题,那么请对此问题进行处理。
3、线程的死锁
死锁的原因
:两个或多个线程都在等待对方放弃自己所需要的同步资源,就形成了线程的死锁。出现了死锁之后,程序不会出现异常也不会提示,但是所有的线程都处于阻塞的状态,不会进行下去。
4、线程之间的通信
线程之间的通信主要涉及到这三个方法wait()
、notify()
、notifyAll()
wait()
一旦执行此方法,当前线程就会立即进入阻塞状态,并且释放同步监视器
sleep和wait的相同点和不同点- 相同点
- 一旦执行这两个方法,那么线程就会进入到阻塞状态
- 不同点
- 两个方法的定义位置不同
sleep
定义在Thread
类中wait
定义在Object
类中
- 调用的要求不同
wait()
只能用在同步代码块中sleep()
可以在任何需要的场景下使用
- 是否会释放同步监视器
sleep
不会释放wait
会释放
- 两个方法的定义位置不同
- 相同点
notify()
执行此方法之后,就会唤醒被wait的一个线程。如果当前有多个线程在wait,那么就会唤醒优先级高的那个线程notifyAll()
执行此方法可以唤醒所有被wait的线程
注意点:
- 上述三个方法都必须使用在同步代码块 / 同步方法中
- 上述三个方法的调用者都是同步代码块 / 同步方法中的同步监视器(锁),否则会出现异常
- 上述三个方法都是定义在
Object
类下的,进一步验证了任何一个对象都可以充当同步监视器(锁)