多线程详解-01实现方式

27 篇文章 0 订阅
3 篇文章 0 订阅

1.线程概述

1.1 进程和线程

当一个程序进入内存运行时,就变成一个进程。进程是处于运行过程中的程序,并具有一定的独立功能,进程是系统进行资源分配和调度的一个基本单位

进程特征

  • 独立性:进程是系统中独立存在的实体,可以拥有自己独立的资源,拥有自己私有的地址空间。在没有允许情况下,一个用户进程不能直接访问其他进程的地址空间。
  • 动态性:进程的实质是程序在多道程序系统中的一次执行过程,具有自己的生命周期和各种不同的状态,进程是动态产生,动态消亡的。
  • 结构特征:进程由程序、数据、和进程控制块组成。
  • 并发性;多个进程可以在单个处理器上并发执行,互不影响。

多个不同的进程可以包含相同的程序:一个程序在不同的数据集里就构成不同的进程,能得到不同的结果;但是执行过程中,程序不能发生改变。

并发:同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行
并行:同一时刻,多条指令在多个处理器中同时执行。

并发便是多进程了

多线程扩展了多进程的概念,使同一个进程可以同时并发处理多个任务。线程称为轻量级进程,是进程的执行单元。

一个程序运行后至少有一个进程,一个进程至少有一个线程,线程拥有自己的堆栈、自己的程序计数器和局部变量,但不拥有系统资源。同一个进程的线程共享该进程的全部资源。
一个线程可以创建和撤销另一个线程,同一个进程的多个线程可以并发执行。

归纳:操作系统可以同时执行多个任务(进程),每个进程可以同时执行多个任务,每个任务就是一个线程。
在这里插入图片描述

1.2 多线程的优势

  • 进程之间不能共享内存,但线程之间共享内存非常容易。
  • 系统创建进程时需要重新分配系统资源,但创建线程代价小得多,因此此案成实现任务并发比多进程效率高。
  • Java语言内置了多线程的功能支持。

2. 线程的创建和启动

Java虚拟机的主线程入口是main方法,自己创建线程两种方式:

  1. 继承Thread类
  2. 实现Runnable接口(推荐使用Runnable接口)

2.1 继承Thread

Thread 类中创建线程最重要的两个方法为:

public void run()
public void start()

采用 Thread 类创建线程,用户只需要继承 Thread,覆盖 Thread 中的run 方法,父类 Thread 中的run 方法没有抛出异常,那么子类也不能抛出异常,最后采用start 启动线程即可。

public class CThread extends Thread {
    private int i;
    @Override
    public void run(){
        for(;i<100;i++){
            System.out.println(this.getName()+" "+i);
        }
    }
    public static void main(String[] args) {
        for (int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+" "+i);
            if(i==20){
                new CThread().start();
                new CThread().start();
            }
        }
    }
}

运行结果:

main 98
main 99
Thread-0 0
Thread-1 0
Thread-1 1
Thread-1 2
Thread-1 3

新建的两个线程交替执行,且两个线程不共享数据。

2.2 实现Runnable接口创建线程类

  1. 定义实现类,重写fun()方法,
  2. 创建实现类的实例,并以此实例作为Thread的target来创建Thread对象。
public class CRunnable implements Runnable {
    private int i;
    @Override
    public void run() {
        for(;i<100;i++){
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }
    public static void main(String[] args) {
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+" "+i);
            if(i==20){
                CRunnable cRunnable = new CRunnable();
                new Thread(cRunnable,"子线程-01").start();
                new Thread(cRunnable,"子线程-02").start();
            }
        }
    }
}
子线程-02 84
子线程-01 77
子线程-02 85
子线程-01 86
子线程-02 87
子线程-01 88
子线程-02 89

结果发现,两个子线程共用资源

思考:看到Thread继承了Runnable,在我们实现Runnable的时候还要使用new Thread(cRunnable,"子线程-01").start();将Runnable作为参数传给Thread?这是为什么呢?
我的理解:光靠Runnable是不能实现的,她只是一个抽象接口,只有在Thread帮助她才可以实现,类似于静态代理模式,Thread代理Runnable,帮助Runnable做不能做的事。

2024-1-4补充
这是因为Runnable接口只定义了一个run()方法,它描述了线程的执行逻辑。但是,Runnable接口本身不能创建线程,而Thread类提供了创建和管理线程的功能。通过将实现了Runnable接口的对象传递给Thread类的构造函数,我们可以将这个Runnable对象与一个新的线程关联起来,并在新的线程中执行run()方法中定义的任务逻辑。

2.3 使用Callable和Future创建线程

Callable创建线程类似于Runnable。

与使用runnable方式相比

  • runnable重写的run方法不如callaalbe的call方法强大,call方法可以有返回值
  • 方法可以抛出异常
  • 支持泛型的返回值
  • 比runable多一个FutureTask类,用来接收call方法的返回值。适用于需要从线程中接收返回值的形式

创建步骤;
1.创建一个实现callable的实现类

  • 2.实现call方法,将此线程需要执行的操作声明在call()中
  • 3.创建callable实现类的对象
  • 4.将callable接口实现类的对象传递到FutureTask的构造器中,创建FutureTask的对象
  • 5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start方法启动(通过FutureTask的对象调用方法get获取线程中的call的返回值)
class NumThread implements Callable{
    private int sum=0;
    
    @Override
    public Object call() throws Exception {
        for(int i = 0;i<=100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName()+":"+i);
                sum += i;
            }
        }
        return sum;
    }
}

public class ThreadNew {

    public static void main(String[] args){
        //new一个实现callable接口的对象
        NumThread numThread = new NumThread();

        //通过futureTask对象的get方法来接收返回值
        FutureTask futureTask = new FutureTask(numThread);

        Thread t1 = new Thread(futureTask);
        t1.setName("线程1");
        t1.start();

        try {
            //get返回值即为FutureTask构造器参数callable实现类重写的call的返回值
           Object sum = futureTask.get();
           System.out.println(Thread.currentThread().getName()+":"+sum);
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

优缺点:

  • Runnable:多个线程共用一个target,适合多个相同线程来处理一份资源,从而将CPU,代码和数据分开,形成清晰的模型。

2.4 线程池

系统启动一个新线程的成本是比较高的,因为它涉及与操作系统交互。在这种情况下,使用线程池可以很好地提高性能,尤其是当线程中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。
总体来说,线程池有如下的优势:

(1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

(2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

(3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

使用线程池来执行线程任务的步骤:

  1. 调用Executors类的静态工厂方法创建一个ExecutorsService对象,该对象代表一个线程池。
  2. 创建Runnable实现类或Callable实现类的实例,作为线程执行任务。
  3. 调用ExecutorsService对象的**submit()**方法来提交Runnable实例或Callable实例。
  4. 当不想提交任何任务时,调用ExecutorsService对象的shutdown()方法关闭线程池。
    实例
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorsTest {
    public static void main(String[] args)  throws Exception{
        //创建一个具有固定线程数的线程池,适用于执行很快的代码
        //newFixedThreadPool创建线程数量有限,如果线程多且执行时间长,很容易造成堆积
        ExecutorService executorService = Executors.newFixedThreadPool(6);
        Runnable target = () -> {
            for (int i=0; i<100; i++) {
                System.out.println(Thread.currentThread().getName()+"的i值为:"+i);
            }
        };
        executorService.submit(target);
        //提交两个并发任务
        executorService.submit(target);
        //关闭
        executorService.shutdown();
    }
}

Executors接口下实现的线程种类

  • newSingleThreadExecutor:创建一个使用无界队列运行的单个工作线程的执行程序,期内多个任务相当于顺序执行。(如果线程在执行某个任务过程中发生故障而终止,如果还要其他后续任务需要执行,那么会再次创建一个线程来运行)
  • ScheduledExecutorService: 创建一个线程池,可以调度命令在给定的延迟之后运行,或定期执行(定期是指该线程池在延迟后工作,不是单个线程的延迟)
  • newFixedThreadPool(int nThreads): 创建一个定长线程池,该线程池重用固定数量的从共享无界队列中运行的线程,在任何时候最多有nTheread个线程处于运行状态。
  • newCachedThreadPool():创建一个定长线程池,超出的任务进入任务队列

注意:已不推荐使用Executors直接创建线程池,而是推荐使用ThreadPoolExecutor构造函数来自己创建线程池
具体原因观看
YourBatman的一篇博客:Java中的线程池,你真的用对了吗?(教你用正确的姿势使用线程池,Executors使用中的坑)

直接粘贴正确创建线程池的正确方式:
避免使用Executors创建线程池,主要是避免使用其中的默认实现,那么我们可以自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量就可以了。

private static ExecutorService executor = new ThreadPoolExecutor(10, 10,
        60L, TimeUnit.SECONDS,
        new ArrayBlockingQueue(10));

这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出java.util.concurrent.RejectedExecutionException,这是因为当前线程池使用的队列是有边界队列,队列已经满了便无法继续处理新的请求。但是异常(Exception)总比发生错误(Error)要好。

ThreadPoolExecutor 介绍

public class ThreadPoolExecutor
extends AbstractExecutorService implements ExecutorService

构造方法详解

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

参数介绍

  • corePoolSize - 即使空闲时仍保留在池中的线程数(核心线程池),除非设置 allowCoreThreadTimeOut
  • maximumPoolSize - 池中允许的最大线程数
  • keepAliveTime - 线程最大空闲时间
  • unit - keepAliveTime参数的时间单位
  • workQueue - 用于在执行任务之前使用的队列(线程等待队列)。 这个队列将仅保存execute方法提交的Runnable任务。
  • threadFactory - 执行程序创建新线程时使用的工厂 (非必要项)
  • handler - 执行被阻止时使用的处理程序,因为达到线程限制和队列容量 (饱和策略,非必要项)

下面来描述一下线程池工作的原理,同时对上面的参数有一个更深的了解。其工作原理流程图如下:在这里插入图片描述
该图出自大佬文章:Java 多线程:彻底搞懂线程池

线程池工作队列BlockingQueue

package java.util.concurrent;
public interface BlockingQueue<E> extends Queue<E>{
}

在这里插入图片描述
关于BlockingQueue的介绍和其实现类的简介:https://www.cnblogs.com/aspirant/p/8657801.html

该队列的实现是线程安全的,使用Lock锁

线程池工作队列饱和策略

Java线程池会将提交的任务先置于工作队列中,在从工作队列中获取(SynchronousQueue直接由生产者提交给工作线程)。
那么工作队列就有两种实现策略:无界队列有界队列
无界队列不存在饱和的问题,但是其问题是当请求持续高负载的话,任务会无脑的加入工作队列,那么很可能导致内存等资源溢出或者耗尽。
而有界队列不会带来高负载导致的内存耗尽的问题,但是有引发工作队列已满情况下,新提交的任务如何管理的难题,这就是线程池工作队列饱和策略要解决的问题。

饱和策略分为:Abort 策略, CallerRuns 策略,Discard策略,DiscardOlds策略

Abort
该策略是线程池的默认策略。使用该策略时,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。
源码如下

 public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
     //不做任何处理,直接抛出异常
      throw new RejectedExecutionException("Task " + r.toString() +
                                           " rejected from " +
                                           e.toString());
}

CallerRuns
使用此策略,如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行。

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                //直接执行run方法
                r.run();
            }
        }

Discard
这个策略同AbortPolicy的slient版本,如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常

   public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        	//就是一个空的方法
        }

DiscardOlds
这个策略从字面上也很好理解,丢弃最老的。也就是说如果队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列。
因为队列是队尾进,队头出,所以队头元素是最老的,因此每次都是移除对头元素后再尝试入队。

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
            	//移除队头元素
                e.getQueue().poll();
                //再尝试入队
                e.execute(r);
            }
        }

为什么要使用线程池

  • 服务器应用程序中经常出现的情况是:单个任务处理的时间很短(处理时间小于创建线程+销毁线程)而请求的数目却是巨大的。
  • 服务器应用于原型开发,每一个请求到达都会创建一个新线程:为每个请求创建一个新线程的开销很大;为每个请求创建新线程的服务器在创建和销毁线程上花费的时间和消耗的系统资源要比花在处理实际的用户请求的时间和资源更多。
  • 除了创建和销毁线程的开销之外,活动的线程也消耗系统资源。在一个 JVM 里创建太多的线程可能会导致系统由于过度消耗内存而用完内存或“切换过度”。为了防止资源不足,服务器应用程序需要一些办法来限制任何给定时刻处理的请求数目
  • 通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。其好处是,因为在请求到达时线程已经存在,所以无意中也消除了线程创建所带来的延迟。
  • 而且,通过适当地调整线程池中的线程数目,也就是当请求的数目超过某个阈值时,就强制其它任何新到的请求一直等待,直到获得一个线程来处理为止,从而可以防止资源不足。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值