Java Spring多线程

开启一个线程

https://blog.csdn.net/qq_44715943/article/details/116714584

1 继承java.lang.Thread类

  1. 编写一个类,直接 继承 java.lang.Thread,重写 run方法,run方法为这个线程要做的事情。
  2. 创建线程对象, new继承线程的类。
  3. 启动线程,调用线程对象的 start() 方法。
public class ThreadTest02 {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        // 启动线程
        //t.run(); // 不会启动线程,不会分配新的分支栈。(这种方式就是单线程。)
        t.start();
        // 这里的代码还是运行在主线程中。
        for(int i = 0; i < 1000; i++){
            System.out.println("主线程--->" + i);
        }
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        // 编写程序,这段程序运行在分支线程中(分支栈)。
        for(int i = 0; i < 1000; i++){
            System.out.println("分支线程--->" + i);
        }
    }
}

注意:

  • t.run() 不会启动线程,只是普通的调用方法而已。不会分配新的分支栈。(这种方式就是单线程。)
  • t.start() 方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。启动成功的线程会自动调用run方法。 run方法在分支栈的栈底部(压栈)main方法在主栈的栈底部,run和main是平级的。

2 实现java.lang.Runnable接口

  1. 编写一个类,实现 java.lang.Runnable 接口,实现run方法,run方法为这个线程要做的事情(同1)。
  2. 创建线程对象,new线程类传入可运行的类/接口
  3. 启动线程呢,调用线程对象的 start() 方法(同1)。
public class ThreadTest03 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable()); 
        // 启动线程
        t.start();
        
        for(int i = 0; i < 100; i++){
            System.out.println("主线程--->" + i);
        }
    }
}

// 这并不是一个线程类,是一个可运行的类。它还不是一个线程。
class MyRunnable implements Runnable {
    @Override
    public void run() {
        for(int i = 0; i < 100; i++){
            System.out.println("分支线程--->" + i);
        }
    }
}

优势:

  • 实现的方式没有类的单继承性的局限性:一个类实现了接口,它还可以去继承其它的类,更灵活。
  • 实现的方式更适合来处理多个线程有共享数据的情况

3 实现Callable接口

  1. 创建一个实现Callable的实现类
  2. 实现call方法,将此线程需要执行的操作声明在call()中
  3. 创建Callable接口实现类的对象
  4. 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
  5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
  6. 获取Callable中call方法的返回值
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

//1.创建一个实现Callable的实现类
class NumThread implements Callable {
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        //把100以内的偶数相加
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}

public class ThreadNew {
    public static void main(String[] args) {    
        //3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
        
        //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(numThread);

        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        new Thread(futureTask).start();

        try {
            //6.获取Callable中call方法的返回值
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
            Object sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

优势:

  • call()可以有返回值的
  • call()可以抛出异常,被外面的操作捕获,获取异常的信息
  • Callable是支持泛型的

4 实现线程池ThreadPoolExecutor

  1. 创建好实现了Runnable接口的类或实现Callable的实现类
  2. 实现run或call方法
  3. 创建线程池
  4. 调用线程池的execute方法执行某个线程,参数是之前实现Runnable或Callable接口的对象
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

class NumberThread implements Runnable {
    @Override
    public void run() {
        //遍历100以内的偶数
        for (int i = 0; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

class NumberThread1 implements Runnable {
    @Override
    public void run() {
        //遍历100以内的奇数
        for (int i = 0; i <= 100; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

public class ThreadPool {

    public static void main(String[] args) {
        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);

        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        //自定义线程池的属性
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();

        //2. 执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适用于Runnable
        service.execute(new NumberThread1());//适用于Runnable
//        service.submit(Callable callable);//适合使用于Callable

        //3. 关闭连接池
        service.shutdown();
    }
}

操作线程

线程的状态

在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:

New:新创建的线程,尚未执行;
Runnable:运行中的线程,正在执行run()方法的Java代码;
Blocked:运行中的线程,因为某些操作被阻塞而挂起;
Waiting:运行中的线程,因为某些操作在等待中;
Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
Terminated:线程已终止,因为run()方法执行完毕。
在这里插入图片描述

1 等待线程 join()

一个线程还可以等待另一个线程直到其运行结束。例如,main线程在启动t线程后,可以通过t.join()等待t线程结束后再继续运行:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("hello");
        });
        System.out.println("start");
        t.start();
        t.join();
        System.out.println("end");
    }
}

如果t线程已经结束,对实例t调用join()会立刻返回。此外,join(long)的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。

2 中断线程 interrupt()

中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.start();
        Thread.sleep(1); // 暂停1毫秒
        t.interrupt(); // 中断t线程
        t.join(); // 等待t线程结束
        System.out.println("end");
    }
}

class MyThread extends Thread {
    public void run() {
        int n = 0;
        while (! isInterrupted()) {
            n ++;
            System.out.println(n + " hello!");
        }
    }
}

main线程通过调用t.interrupt()方法中断t线程,但是要注意,interrupt()方法仅仅向t线程发出了“中断请求”,至于t线程是否能立刻响应,要看具体代码。而t线程的while循环会检测isInterrupted(),所以上述代码能正确响应interrupt()请求,使得自身立刻结束运行run()方法。

3 守护线程(Daemon Thread)

如果有一个线程没有退出,JVM进程就不会退出。但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程。然而这类线程经常没有负责人来负责结束它们。但是,当其他线程结束时,JVM进程又必须要结束,怎么办?

守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出

因此,JVM退出时,不必关心守护线程是否已结束

Thread t = new MyThread();
t.setDaemon(true);
t.start();

注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。

4 线程同步 synchronized

两个线程同时对一个int变量进行操作,一个加10000次,一个减10000次,最后结果应该是0,但是,每次运行,结果实际上都是不一样的。

这是因为对变量进行读取和写入时,结果要正确,必须保证是原子操作。原子操作是指不能被中断的一个或一系列操作。

n = n + 1;

看上去是一行语句,实际上对应了3条指令:

ILOAD
IADD
ISTORE

JVM规范定义了几种原子操作:
-基本类型赋值,例如:int n = m;
-引用类型赋值,例如:List list = anotherList。

在这里插入图片描述
这说明多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待
在这里插入图片描述
通过加锁和解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。

一段代码的原子性就是通过加锁和解锁实现的。Java程序使用synchronized关键字对一个对象进行加锁:

public class Main {
    public static void main(String[] args) throws Exception {
        var add = new AddThread();
        var dec = new DecThread();
        add.start();
        dec.start();
        add.join();
        dec.join();
        System.out.println(Counter.count);
    }
}

class Counter {
    public static final Object lock = new Object();		 2. 共享实例作为锁
    public static int count = 0;	 1. 共享修改变量
}

class AddThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized(Counter.lock) {	// 3. 获取锁
                Counter.count += 1;
            }	// 无论有无异常,都会在此释放锁
        }
    }
}

class DecThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized(Counter.lock) {	// 3. 获取锁
                Counter.count -= 1;
            }	// 无论有无异常,都会在此释放锁
        }
    }
}

如何使用synchronized:

  1. 找出修改共享变量的线程代码块;
  2. 选择一个共享实例作为锁,使用同一个实例作为锁;可以锁住的对象是this,即当前实例,这又使得创建多个Counter实例的时候,它们之间互不影响,可以并发执行
  3. 使用synchronized(lockObject) { … }。

在使用synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁。

Java线程池

如果我们不采用线程池,为每一个请求都创建一个线程的话:

  1. 管理线程的生命周期开销非常高。管理这些线程的生命周期会明显增加 CPU 的执行时间,会消耗大量计算资源。
  2. 线程间上下文切换造成大量资源浪费。
  3. 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗
  4. 程序稳定性会受到影响。我们知道,创建线程的数量存在一个限制,这个限制将随着平台的不同而不同,并且受多个因素制约,包括jvm的启动参数、Thread构造函数中请求的栈大小,以及底层操作的限制等。如果超过了这个限制,那么很可能抛出OutOfMemoryError异常,这对于运行中的应用来说是非常危险的。

Java自JDK1.5 提供了自己的多线程框架,称为 Executor框架.
在这里插入图片描述

  • Executor:java线程池框架的最上层父接口,在Executor这个接口中只有一个execute方法,该方法的作用是向线程池提交任务并执行。
  • ThreadPoolExecutor:这是Java线程池最核心的一个类,该类继承自AbstractExecutorService,主要功能是创建线程池,给任务分配线程资源,执行任务。
  • ScheduledExecutorSerivceScheduledThreadPoolExecutor 提供了另一种线程池:延迟执行和周期性执行的线程池。
  • Executors:这是一个静态工厂类,该类定义了一系列静态工厂方法,通过这些工厂方法可以返回各种不同的线程池
  • ExecutorService:该接口继承自Executor接口,添加了shutdown、shutdownAll、submit、invokeAll等一系列对线程的操作方法,该接口比较重要,在使用线程池框架的时候,经常用到该接口。
  • AbstractExecutorService:这是一个抽象类,实现ExecuotrService接口

Executors 线程池类型

  1. SingleThreadExecutor: 此线程池 Executor 只有一个线程。它用于以顺序方式的形式执行任务。如果此线程在执行任务时因异常而挂掉,则会创建一个新线程来替换此线程,后续任务将在新线程中执行。
ExecutorService executorService = Executors.newSingleThreadExecutor()
  1. FixedThreadPool(n): 它是一个拥有固定数量线程的线程池,创建一个核心线程和最大线程均为固定值的线程池。提交给 Executor 的任务由固定的 n 个线程执行,如果有更多的任务,它们存储在无长度限制的等待队列 LinkedBlockingQueue 里。这个数字 n 通常跟底层处理器支持的线程总数有关。
ExecutorService executorService = Executors.newFixedThreadPool(4);

如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1 如果是IO密集型任务,参考值可以设置为2*NCPU

注意⚠️:
由于FixedThreadPool采用无界的等待队列,一旦空闲线程被用尽,就会向队列中加入任务,这时一旦任务进入速度远高于线程处理能力,就有出现 OOM 的可能。

  1. CachedThreadPool: 该线程池主要用于执行大量短期并行任务、响应时间要求高、数据量可控的场景。与固定线程池不同,此线程池的线程数不受限制。如果所有的线程都在忙于执行任务并且又有新的任务到来了,这个线程池将创建一个新的线程并将其提交到 Executor。只要其中一个线程变为空闲,它就会执行新的任务。 如果一个线程有 60 秒的时间都是空闲的,它们将被结束生命周期并从缓存中删除。创建一个核心线程数为0、最大线程数为整型最大值的线程池。

    注意⚠️:
    但是,如果管理得不合理,或者任务不是很短的,则线程池将包含大量的活动线程。这可能导致资源紊乱并因此导致性能下降。由于其不限制创建线程的个数,故若数据量不可控,会造成程序 OOM

ExecutorService executorService = Executors.newCachedThreadPool();
  1. ScheduledExecutor:当我们有一个需要定期运行的任务或者我们希望延迟某个任务时,就会使用此类型的 executor。
ScheduledExecutorService scheduledExecService = Executors.newScheduledThreadPool(1);
scheduledExecService.scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
scheduledExecService.scheduleWithFixedDelay(Runnable command, long initialDelay, long period, TimeUnit unit)

Future与线程池

由于提交给Executor 的任务是异步的,需要有一个对象来接收Executor 的处理结果:

Future<String> result = executorService.submit(callableTask);

调用者可以继续执行主程序,当需要提交任务的结果时,他可以在这个 Future对象上调用.get() 方法来获取。如果任务完成,结果将立即返回给调用者,否则调用者将被阻塞,直到 Executor 完成此操作的执行并计算出结果。

如果调用者不能无限期地等待任务执行的结果,那么这个等待时间也可以设置为定时地。可以通过 Future.get(long timeout,TimeUnit unit) 方法实现,如果在规定的时间范围内没有返回结果,则抛出 TimeoutException。调用者可以处理此异常并继续执行该程序。

如果在执行任务时出现异常,则对 get 方法的调用将抛出一个ExecutionException。对于 Future.get()方法返回的结果,一个重要的事情是,只有提交的任务实现了java.util.concurrent.Callable接口时才返回 Future。如果任务实现了Runnable接口,那么一旦任务完成,对 .get() 方法的调用将返回 null。

另一点是 Future.cancel(boolean mayInterruptIfRunning) 方法。此方法用于取消已提交任务的执行。如果任务已在执行,则 Executor 将尝试在mayInterruptIfRunning 标志为 true 时中断任务执行。

创建Callable任务:

public class Task implements Callable<String> {

    private String message;

    public Task(String message) {
        this.message = message;
    }

    @Override
    public String call() throws Exception {
        return "Hello " + message + "!";
    }
}

异步执行任务:

public class ExecutorExample {  
    public static void main(String[] args) {
    	// 1. 实例化了 Task 类
        Task task = new Task("World");
        // 2. 创建了一个具有4个线程数的 FixedThreadPool Executors
        // 3. 提交给 Executors 执行
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        // 4. 结果由 Future 对象返回
        Future<String> result = executorService.submit(task);
        try {
            System.out.println(result.get());
        } catch (InterruptedException | ExecutionException e) {
            System.out.println("Error occured while executing the submitted task");
            e.printStackTrace();
        }
        // 调用 executorService 对象上的 shutdown 来终止所有线程并将资源返回给 OS
        executorService.shutdown();
    }
}

shutdown() 方法等待 Executor 完成当前提交的任务。 但是,如果要求是立即关闭 Executor 而不等待,那么我们可以使用 shutdownNow() 方法。

或者创建Runnable任务:

public class Task implements Runnable{

    private String message;

    public Task(String message) {
        this.message = message;
    }

    public void run() {
    	// 由于Runnable任务没有返回值,不能得到任务执行结果。需要在任务内完成所有操作(打印)
        System.out.println("Hello " + message + "!");
    }
}

线程间通信

1 公共变量

用一个running标志位来标识线程是否应该继续运行,在外部线程中,通过把HelloThread.running置为false,就可以让线程结束

public class Main {
    public static void main(String[] args)  throws InterruptedException {
        HelloThread t = new HelloThread();
        t.start();
        Thread.sleep(1);
        t.running = false; // 标志位置为false
    }
}

class HelloThread extends Thread {
    public volatile boolean running = true;
    public void run() {
        int n = 0;
        while (running) {
            n ++;
            System.out.println(n + " hello!");
        }
        System.out.println("end!");
    }
}

volatile
为什么要对线程间共享的变量用关键字volatile声明?这涉及到Java的内存模型。在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!

这会导致如果一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。例如,主内存的变量a = true,线程1执行a = false时,它在此刻仅仅是把变量a的副本变成了false,主内存的变量a还是true,在JVM把修改后的a回写到主内存之前,其他线程读取到的a的值仍然是true,这就造成了多线程之间共享的变量不一致。

因此,volatile关键字的目的是告诉虚拟机:

  • 每次访问变量时,总是获取主内存的最新值;
  • 每次修改变量后,立刻回写到主内存。

volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值