多线程

章节概述:

程序(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的线程
说明:

  1. wait(),notify(),notifyAll()三个方法必须是使用在同步代码块或同步方法中。
  2. wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则回出现异常
  3. 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接口实现类地对象

posted @ 2023-04-24 21:44  生活的样子就该是那样  阅读(26)  评论(0编辑  收藏  举报