多线程
章节概述:
程序(program)是为了完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
进程(process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和小王的过程。————生命周期
- 如运行中的qq,运行中的MP3播放器
- 程序是静态的,进程是动态的
- 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径
- 若一个进程同一时间并行执行多个线程,就是支持多线程的
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
- 一个进程中的多个线程共享相同的内存单元/内存地址空间->他们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患
并行:多个cpu同时执行多个任务
并发:一个cpu采用时间片同时执行多个任务
背景:以单核CPU为例,只是用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程完成用的时间更短,为何仍需使用多线程呢?
多线程程序的优点:
1、提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
2、提高计算机系统CPU的利用率
4、改善程序结构,将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
线程的创建和使用:
java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来实现
/**
*多线程的创建,方式一:继承于Thread类
* 1.创建一个继承于Thread类的子类
* 2.重写Thread类的run()
* 3.创建Thread类的子类的对象
* 4.通过此对象调用start()
*
* 例子:遍历100以内的所有的偶数
*/
//1、创建一个继承于Thread类的子类
class MyThread extends Thread{
//2、重写Thread类的run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2==0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
//3.创建Thread类的子类的对象
MyThread t1 = new MyThread();
//4.通过此对象调用start():1、启动当前线程2、调用当前线程的run()
t1.start();
//问题一:我们不能通过直接调用run()的方式启动线程
//t1.run();
//问题二:再启动一个线程,遍历100内的偶数,不可以还让已经start()的线程去执行,报IllegalThreadStateException我们需要重新创建一个线程的对象
t1.start();
MyThread t2 = new MyThread();
t2.start();
//如下操作仍然实在main线程中执行
for (int i = 0; i < 100; i++) {
if (i%2==0){
System.out.println(Thread.currentThread().getName()+ i+"************main*****************");
}
}
}
}
Thread类的特性:
每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体。
通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
Thread类的有关方法:
void start():启动线程,并执行对象的run()方法
run():线程在被调度时执行的操作
String getName():返回线程的名称
void setName(String name):设置该线程名称
static Thread currentThread():返回当前线程。在Thread子类中就是this,通常于主线程和Runnable实现类
static void yield():线程让步
- 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
- 若队列中没有同优 先级的线程,忽略此方法
join(): 当某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,知道join()方法加入的join 线程执行完为止 低优先级的线程也可以获得执行
static void sleep(long millis):(指定时间:毫秒)令当前活动线程在指定时间段内放弃对CPU的控制,使其他线程有机会被执行,时间到后重排队,抛出InterruptedException异常
stop():强制线程生命期结束,不推荐使用
boolean isAlive():返回boolean,判断线程是否还活着。
class HelloThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2==0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
if (i%20==0){
yield();
}
}
}
HelloThread (String name){
super(name);
}
}
public class ThreadMethodTest {
public static void main(String[] args) {
HelloThread h1 = new HelloThread("Thread:1");
h1.start();
//给主线程命名
Thread.currentThread().setName("主线程");
for (int i = 0; i < 100; i++) {
if (i%2==0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
if (i==20){
try {
h1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
线程的调度:
调度策略:时间片,抢占式:高优先级的线程抢占CPU
Java的调度方式:
同优先级线程组成先进先出队列(先到先服务),使用时间片策略
对高优先级,使用优先调度的抢占式策略
线程的有限级等级:
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5
涉及的方法:
getPriority():返回线程有限制
setPriority(int newPriority):改变线程的优先级
说明:
线程创建时继承父线程的优先级
低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
创建多线程的方式二:实现Runnable接口
/*创建多线程的方式儿:实现Runnable接口
* 1.创建一个实现了Runnable接口的类
* 2.实现类去实现Runnable中的抽象方法:run()
* 3.创建实现类的对象
* 4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
* 5.通过Thread类的对象调用start()
* */
//1.创建一个实现Runnable接口的类
class MThread implements Runnable{
// 2.实现类去实现Runnable中的抽象方法:run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2==0){
System.out.println(i);
}
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
//3.创建实现类的对象
MyThread myThread= new MyThread();
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(myThread);
//5.通过Thread类的对象调用start()
t1.start();
//再启动一个线程
Thread t2 = new Thread(myThread);
t2.start();
}
}
解决线程安全问题
说明:
1、操作共享数据的代码,即为需要被同步的代码。共享数据:多个线程共同操作的变量。
2、同步监视器,俗称锁。任何一个类的对象都可以充当锁。要求:多个线程必须要公用同一把锁
补充:在实现Runnable接口创建多线程的方式中,我们考虑使用this充当同步监视器
方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
方式二:同步方法:如果操作共享数据的代码在一个方法中,我们不妨将此方法声明为同步的即在方法前加synchronized
1.同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
2.非同步的同步方法,同步监视器是:this。静态的同步方法,同步监视器是:当前类本身。
方式三:Lock(锁)
从JDK5.0开始,Java提供了更强大的线程同步机制————通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
Java.util.concurrent.locks.Lock接口时控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能由一个线程对Lock对象加锁。线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁
//1.实例化
private ReentrantLock lock = new ReentrantLock();
try{
//调用锁定的方法
lock.lock();
}finally{
//调用解锁方法,
lock.unlock();
}
线程的死锁问题
死锁:不同线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,只是所有线程都处于阻塞状态,无法继续
线程的通信
涉及到的三个方法:
wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器
notify():一旦执行此方法,就会唤醒被wait的一个线程,如果又多个线程被wait,就唤醒优先级高的
notifyAll():一旦执行此方法,就会唤醒所有被wait的线程
说明:
- wait(),notify(),notifyAll()三个方法必须是使用在同步代码块或同步方法中。
- wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则回出现异常
- wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中
面试题:sleep()和wait()的一同
1、相同点:一旦执行方法,都可以使用当前线程进入阻塞状态。
2、不同点:两个方法声明的位置不同,Thread类声明在sleep(),Object类中声明wait();调用的要求不同,sleep()可以在任意需要场景下调用,wait()必须使用在同步代码块或同步方法中;关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
JDK5.0新增线程创建方式
新增方式一:实现Callable接口
与Runnable相比,Callable功能更强大些,相比run()方法可以有返回值,方法可以抛出异常,支持泛型的返回值,需要借助FutureTask类,比如获取返回结果
Future接口:可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。FutureTask是Future接口的唯一的实现类。FutureTask同时实现了Ruannable,Future接口,它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
步骤:1.创建一个实现Callable的实现类。2.实现call方法,将此线程需要执行的操作声明在call()中。3、创建Callable接口实现类的对象。4、将此Callable接口实现类的对象传递到FutureTask的构造器中,创建FutureTask的对象。5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并start()调用。6.获取Callable中call方法的返回值(get()返回值几位FutureTask构造前期参数Callable实现类重写的call()的返回值。
本人在项目中遇到的多线程:
// 定义线程列表
List<CompletableFuture<ModelAndView>> futureList = new ArrayList<>();
// 循环查询,每次查询一页
for (int pageNo = 1; pageNo <= listSize / pageSize + 1; pageNo++) {
int number = pageNo;
// 开启线程查询
CompletableFuture<ModelAndView> completableFuture = CompletableFuture.supplyAsync(() -> {
return addDataToModelAndView(new ModelAndView(), queryWrapper, number, pageSize);
}, executorService);
futureList.add(completableFuture);
}
// 等待每个线程结束后
CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).join();
List<ParkBlackList> resultList = new ArrayList<>();
futureList.forEach(a -> {
try {
// 将结果添加到列表中
resultList.addAll((List<ParkBlackList>) a.join().getModel().get(NormalExcelConstants.DATA_LIST));
} catch (Exception e) {
e.printStackTrace();
}
});
// 将数据添加到Excel文件内
mv.addObject(NormalExcelConstants.CLASS, ParkBlackList.class);
mv.addObject(NormalExcelConstants.DATA_LIST, resultList);
如何理解实现Callable接口的方式比Runnable接口更强大:
1、call()可以有返回值的。2.call()方法可以跑出异常。3.callable是支持泛型的
新增方式二:使用线程池的方式
背景:经常创建和销毁、使用量特别打的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
好处:提高响应速度、降低资源消耗、便于线程管理
JDK5.0起提供了线程池相关的API:ExecutorService和Executors
ExecutorService:真正的线程池接口,常见子类ThreadPoolExcecutor:
void execute(Runnable command)
:执行任务/命令,没有返回值,一般用来执行Runnable
<T>Future<T> submit(Callable<T>task)
:执行任务,有返回值,一般用来执行Callable
void shutdown():关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool():创建一个根据需要创建新线程的线程池
Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或定期地执行。
步骤:
1.提供指定线程数量地线程池 2.执行指定地线程地操作,需要提供实现Runnable接口实现类地对象