java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > java不同线程及线程池的使用

java不同线程解读以及线程池的使用方式

作者:200.OK

这篇文章主要介绍了java不同线程解读以及线程池的使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

java不同线程解读以及线程池的使用

线程池子类比一个水池,而每一个线程,都好比水池的水。因此,水池多大,看系统硬件配置。线程池主要为了并发,高效处理任务。而异步处理任务,可以有效提高处理任务的吞吐量。

线程池的常见应用场景

处理大量而短小的请求,请求数量很大,每一个请求开启一个线程,在请求完毕之后再对线程进行销毁,这样创建和销毁线程所消耗的时间往往比任务本身所需消耗的资源要大得多。

线程池做到线程复用,不需要频繁的创建和销毁线程,线程池中的线程一直存在于线程池中,线程从任务队列中取得任务来执行。而且这样做的另一个好处有,通过适当地调整线程池中的线程数目,也就是当请求的数目超过某个阈值时,就强制其它任何新到的请求一直等待,直到获得一个线程来处理为止,从而可以防止资源不足。

线程池是什么?

线程池用于多线程处理中,它可以根据系统的情况,可以控制线程执行的数量,优化运行效果。线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。

线程池的作用

创建对象和销毁对象是非常消耗时间和资源的。因此想要最小化这种消耗的一种思想就是『池化资源』,通过重用线程池中的资源来减少创建和销毁线程所需要耗费的时间和资源。

线程池的一个作用是创建和销毁线程的次数,每个工作线程可以多次使用;另一个作用是可根据系统情况调整执行的线程数量,防止消耗过多内存。另外,通过线程池,能有效的控制线程的最大并发数,提高系统资源利用率,同时避免过多的资源竞争,避免堵塞。

线程池的优点总结如下几个方面:

线程池组成

1、Async 和 @Async 很关键

两个注解很关键:

2、Thread和Runnable

Thread可以创建线程,但是线程使用完之后需要对线程资源进行销毁回收,消耗资源,且容易造成线程上下文切换(操作系统核心对CPU上对进程或者线程进行切换)问题,线程管理不当容易资源耗尽,不建议使用。、

一个类实现 Runnable 接口可以将该类的实例传递给一个 Thread 对象并启动一个新线程,这个新线程就会执行该类中 void run() 方法中所定义的逻辑。

3、数据类型—线程安全

如果有多线程共享数据的方式,要牢记各种使用场景的线程安全数据类型。

①List<T> vector = new Vector<>();
②<T> listSyn = Collections.synchronizedList(new ArrayList<>()); 
③List<T> copyList = new CopyOnWriteArrayList<>();

4、@Configuration配置类和 @Bean将实例对象交给IOC容器

5、ThreadPoolExecutor 和 ThreadPoolTaskExecutor

两种线程池方式,本质上一样,ThreadPoolTaskExecutor(我使用的) 源码上是在 ThreadPoolExecutor 上再加了一层包装,为了更方便在spring框架中使用。

使用@Bean(“beanName”)定义线程池 然后在@Async(“beanName”)中引用指定的线程池

@EnableAsync 用于启用整个应用程序的异步处理功能,包括所有通过 @Async 注解标记的方法。它不负责配置底层线程池。

ThreadPoolTaskExecutor Bean 配置则是用于配置具体的线程池实例,这个线程池会被 @Async 方法所使用。

如果你在配置类中同时使用了 @EnableAsync 注解和自定义的 ThreadPoolTaskExecutor Bean 配置,Spring 将会使用你配置的线程池执行异步方法。如果你只使用 @EnableAsync,Spring 将会使用默认的线程池配置。

注意

定义线程池的配置

package com.test.wll.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.ThreadPoolExecutor;

@EnableAsync//或者加在app类上,此处不加需要在启动类上加
@Configuration 
public class ThreadPoolTaskConfig {

    @Bean("threadPoolRedisTaskExecutor")
    public ThreadPoolTaskExecutor threadPoolRedisTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //线程池创建的核心线程数,线程池维护线程的最少数量,即使没有任务需要执行,也会一直存活
        executor.setCorePoolSize(8);
        //如果设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
        //executor.setAllowCoreThreadTimeOut(true);
        //阻塞队列 当核心线程数达到最大时,新任务会放在队列中排队等待执行
        executor.setQueueCapacity(124);
        //最大线程池数量,当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任
        //任务队列已满时, 且当线程数=maxPoolSize,,线程池会拒绝处理任务而抛出异常
        executor.setMaxPoolSize(64);
        //当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
        //允许线程空闲时间30秒,当maxPoolSize的线程在空闲时间到达的时候销毁
        //如果allowCoreThreadTimeout=true,则会直到线程数量=0
        executor.setKeepAliveSeconds(30);
        //spring 提供的 ThreadPoolTaskExecutor 线程池,是有setThreadNamePrefix() 方法的。
        //jdk 提供的ThreadPoolExecutor 线程池是没有 setThreadNamePrefix() 方法的
        executor.setThreadNamePrefix("threadPoolRedisTaskExecutor");
        // rejection-policy:拒绝策略:当线程数已经达到maxSize的时候,如何处理新任务
        // CallerRunsPolicy():交由调用方线程运行,比如 main 线程;如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行
        // AbortPolicy():该策略是线程池的默认策略,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。
        // DiscardPolicy():如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常
        // DiscardOldestPolicy():丢弃队列中最老的任务,队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
        executor.initialize();
        return executor;
    }
}
}
        //最大线程数
        executor.setMaxPoolSize(maxPoolSize);
        //核心线程数
        executor.setCorePoolSize(corePoolSize);
        //任务队列的大小
        executor.setQueueCapacity(queueCapacity);
        //线程前缀名
        executor.setThreadNamePrefix(namePrefix);
        //线程存活时间
        executor.setKeepAliveSeconds(keepAliveSeconds);
    //此处的名字要与线程池的名字一致,清晰的命名可以清晰的了解哪个线程报错
    @Async("threadName")
    @GetMapping("/方法名字")
    public Result<String> test() {
}

6、拒绝策略

rejectedExectutionHandler参数字段用于配置绝策略,常用拒绝策略如下

7、线程池处理流程

线程池的工作流程(必知必会)?

这个问题回答的时候,最好用讲故事的方式进行。 假如核心线程数是5,最大线程数是10,阻塞队列也是10

8、 Hutool 的ThreadUtil.execute

execute 方法是通过 GlobalThreadPool 来执行任务的,而 GlobalThreadPool 在 Hutool 中是一个全局的线程池。

它会使用默认的配置来初始化线程池,这些默认配置在 Hutool 内部已经设定好了,因此在使用 execute 方法时不需要手动指定核心池大小和最大池大小。

这种方法适合简单的任务执行,如果需要更灵活的线程池配置(比如自定义线程数、队列类型等),可以考虑使用 newExecutor 等方法手动创建线程池,并进行更详细的配置。

spring的 ThreadPoolTaskExecutor

Hutool ThreadUtil

常见线程池

核心概念:这四个线程池的本质都是ThreadPoolExecutor对象(看源码)

不同点在于:

线程池的主要参数有哪些(必知必会)?

线程池的工作流程(必知必会)?

这个问题回答的时候,最好用讲故事的方式进行。 假如核心线程数是5,最大线程数是10,阻塞队列也是10

线程安全

函数、函数库在并发环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。

eg:并发是两个队伍交替使用一台咖啡机。并行是两个队伍同时使用两台咖啡机。

保存在方法区中的变量就是Java中的共享变量

堆中存放new出来的对象,(包括实例变量); 栈中存放正在调用的方法中的局部变量 (包括方法的参数); 方法区中存储.class 字节码 文件(包括 静态变量 、静态方法)

public class Variables {
    // 类变量  共享变量
    private static int a;
    //成员变量  共享变量
    private int b;
    //局部变量  c和d 非共享变量
    public void test(int c){
        int d;
    }
}
//多线程场景,对于变量a和b的操作是需要考虑线程安全的,而对于线程c和d的操作是不需要考虑线程安全的。

线程不安全

多线程并发执行某个代码时,产生了逻辑上的错误,结果和预期值不相同 线程安全是指多线程执行时没有产生逻辑错误,结果和预期值相同

//开启了两个线程,每个线程执行1000次循环,循环中对count进行加1操作。等待两个线程都执行完成后,打印count的值。
public class Test {
     private  static  int count;
    private static class Thread1 extends Thread {
        public void run() {
            for (int i = 0; i < 1000; i++) {
                count ++;
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread1  t1 = new Thread1();
        Thread1  t2 = new Thread1();
        t1.start();
        t2.start();
        //main主线程内调用join()方法:休眠主线程,等待t1、t2线程执行完毕主线程再继续,即最后输出count值
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

输出结果应该是2000但是不有时候1996

count++的指令在实际执行的过程中不是原子性的,而是要分为读、改、写三步来进行;即先从内存中读出count的值,然后执行+1操作,再将结果写回内存中,如下图所示。

上图中线程1执行了两次自加操作,而线程2执行了一次自加操作,但是count却从6变成了8,只加了2。

我们看一下为什么会出现这种情况。当线程1读取count的值为6完成后,此时切换到了线程2执行,线程2同样读取到了count的值为6,而后进行改和写操作,count的值变为了7;此时线程又切回了线程1,但是线程1中count的值依然是线程2修改前的6,这就是问题所在!即线程2修改了count的值,但是这种修改对线程1不可见,导致了程序出现了线程不安全的问题,没有符合我们预期的逻辑。

导致线程不安全的原因

主要有三点:

Java中的原子操作包括:

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

补充:happens-before原则(先行发生原则)

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文