Java多线程

进程(process)

进程为正在运行中的程序。

  • 每个进程拥有自己的系统资源,内存空间和地址空间。

  • 每个进程内部数据和状态完全独立。

线程(thread)

线程为一个程序中的单个执行流。

  • 线程是进程的基本执行单位,每个进程由一个或多个线程组成。

  • 每个线程可以负责不同的任务而互不干扰。

  • Java语言中,一个进程中,堆内存和方法区内存在线程中共享,栈内存不共享,即一个线程一个栈。

线程的生命周期

线程的生命周期可分为:新建状态(new)、就绪状态(runnable)、运行状态(running)、

阻塞状态(blocked)、死亡状态(dead)。

新建状态(new)

当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时线程情况如下:

  1. 此时JVM为其分配内存,并初始化其成员变量的值。

  1. 此时线程对象没有表现出任何线程的动态特性,程序也不会执行线程的线程执行体。

就绪状态(runnable)

又叫可运行状态,当线程对象调用了start()方法之后,该线程处于就绪状态。此时线程情况如下:

  1. 此时JVM会为其创建方法调用栈和程序计数器。

  1. 该状态的线程一直处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为CPU的调度不一定是按照FIFO的顺序来调度的),线程并没有开始运行;

  1. 此时线程等待系统为其分配CPU时间片,并不是说执行了start()方法就立即执行

运行状态(running)

当CPU开始调度处于就绪状态的线程时,此时线程获得了CPU时间片才得以真正开始执行run()方法的线程执行体,该线程处于运行状态。

  1. 在一个多处理器的机器上,将会有多个线程并行执行,处于运行状态。

处于运行状态的线程不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就执行结束了),线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。

对于采用抢占式策略的系统而言,系统会给每个可执行的线程分配一个时间片来处理任务;当该时间片用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。线程会从运行状态变成就绪状态,重新等待系统分配资源。

对于采用协作式策略的系统而言,只有当一个线程调用了他的yield()方法后才会放弃所占用的资源,也就时必须由该线程主动放弃所占用的资源,线程就会又从运行状态变为就绪状态。

阻塞状态(blocked)

处于运行状态的线程在某些情况下,让出CPU并暂时停止自己的运行,进入阻塞状态。

进入阻塞状态的情况:

  1. 线程调用sleep()方法,主动放弃所占用的处理器资源,暂时进入中断状态(不会释放持有的对象锁),时间到后等待系统分配cpu继续执行。

  1. 线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。

  1. 线程视图获得一个同步监视器,但该同步监视器正被其他线程所持有。

  1. 程序调用了线程的suspend方法将线程挂起。

  1. 线程调用wait,等待notify/notifyAll唤醒时(会释放持有的对象锁)。

阻塞状态分类:

  1. 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态。

  1. 同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用)。

  1. 其他阻塞:通过调用线程的sleep()或join()或发出I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时或者I/O处理完毕时,线程重新传入就绪状态。

在阻塞状态的线程只能进入就绪状态,无法直接进入运行状态、而就绪和运行状态之前的转换通常不受程序控制,而是由系统线程调度所决定。当处于就绪状态的线程获得处理器资源时,该线程进入运行状态。

调用yield()方法可以让运行状态的线程转入就绪状态。

死亡状态(dead)

  1. run()或call()方法执行完成,线程正常结束;

  1. 线程抛出一个捕获的Exception或Error;

  1. 直接调用该线程stop()方法来结束该线程;

处于死亡状态的线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。

多线程的实现方式

  • 继承Thread类

  1. 自定义一个类用于继承Thread类;

  1. 在自定义类中重写run()方法;

  1. 使用自定义类对象调用start()方法。

public class Main {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start(); // 启动新线程
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("start new thread!");
    }
}

  • 实现Runnable接口

  1. 自定义类实现Runnable接口;

  1. 在自定义类中重写run()方法;

  1. 使用自定义类对象调用start()方法。

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start(); // 启动新线程
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("start new thread!");
    }
}

  • 线程池

为什么推荐使用线程池?

创建Java线程需要给线程分配堆栈内存以及初始化内存,还需要进行系统调用,频繁的创建和销毁线程会大大降低系统运行的效率。线程池通过对线程的重用,减少了创建和销毁线程的性能开销,使程序响应更快。但同时线程池更加容易造成并发风险,诸如同步错误和死锁,还容易遭受特定于线程池的少数其他风险,诸如与线程池有关的死锁,资源不足和线程泄露。

阿里的《Java开发手册》中强制线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。

JDK默认Executors提供四种线程池

  1. newFixedThreadPool:创建一个定长的线程池,可控制线程最大并发数,超出的线程会在队列中等待。

  1. newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则创建线程。

  1. newSingleThreadExecutor:创建一个单线程化的线程池,只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序进行。

  1. newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。

为什么推荐使用ThreadPoolExecutor创建线程池?

阿里的《Java开发手册》中强制使用ThreadPoolExecutor创建线程池,而不允许使用Executors 去创建。

Executors返回的线程池对象弊端如下:

  1. FixedThread和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM(内存溢出)。

  1. CachedThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

  1. ScheduledThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。

ThreadPoolExecutor的构造函数

通过Excutors的源码,JDK提供的4中默认线程池都是用ThreadPoolExecutor的构造函数实现的。所以我们需要了解下ThreadPoolExecutor的构造函数。

  1. corePoolSize:核心线程数,初始创建的线程都是核心线程数,线程池正常情况下始终保留该大小线程实例存活。

  1. maximumPoolSize:最大线程数,当核心线程都在执行任务,任务队列满的情况下会创建非核心线程来执行任务,当非核心线程处于空闲时间,且超过keepAliveTime时,会销毁非核心线程。

  1. keepAliveTime:存活时间,指非核心线程空闲时的存活时间。

  1. unit:存活时间的单位,具体是TimeUnit枚举,有毫秒、秒、分钟、小时等等。

  1. workQueue:线程池的任务队列,当线程池的核心线程都处于繁忙状态,且有新任务到来,则会进入任务队列,当任务队列满了,则会创建非核心线程执行新任务。

  1. threadFactory:线程池创建线程实例的线程工厂,一般默认为Executors.defaultThreadFactory。

  1. handler:线程池拒绝策略,当核心线程全部繁忙,任务队列已满,非核心线程全部繁忙,会出发线程池拒绝策略。默认为AbortPolicy,直接抛出异常并拒绝。此外还有CallerRunsPolicy,把任务交给当前线程来执行;DiscardPolicy,忽略此任务;DiscardOldestPolicy,忽略最先加入队列的任务。还可以自定义策略。

ThreadPoolTaskExecutor

ThreadPoolTaskExecutor是spring为我们提供的线程池类,他对ThreadPoolExecutor进行了封装处理。

ThreadPoolTaskExecutor配置

参数配置application.properties

# 核心线程池数
spring.task.execution.pool.core-size=5
# 最大线程池数
spring.task.execution.pool.max-size=10
# 任务队列的容量
spring.task.execution.pool.queue-capacity=5
# 非核心线程的存活时间
spring.task.execution.pool.keep-alive=60
# 线程池的前缀名称
spring.task.execution.thread-name-prefix=xxxxx

线程池配置类

@Configuration
public class ThreadPoolConfig {

    @Value("${spring.task.execution.pool.core-size}")
    private int corePoolSize;
    @Value("${spring.task.execution.pool.max-size}")
    private int maxPoolSize;
    @Value("${spring.task.execution.pool.queue-capacity}")
    private int queueCapacity;
    @Value("${spring.task.execution.thread-name-prefix}")
    private String namePrefix;
    @Value("${spring.task.execution.pool.keep-alive}")
    private int keepAliveSeconds;
    @Bean
    public Executor myAsync() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //最大线程数
        executor.setMaxPoolSize(maxPoolSize);
        //核心线程数
        executor.setCorePoolSize(corePoolSize);
        //任务队列的大小
        executor.setQueueCapacity(queueCapacity);
        //线程前缀名
        executor.setThreadNamePrefix(namePrefix);
        //线程存活时间
        executor.setKeepAliveSeconds(keepAliveSeconds);

        /**
         * 拒绝处理策略
         * CallerRunsPolicy():交由调用方线程运行,比如 main 线程。
         * AbortPolicy():直接抛出异常。
         * DiscardPolicy():直接丢弃。
         * DiscardOldestPolicy():丢弃队列中最老的任务。
         */
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        //线程初始化
        executor.initialize();
        return executor;
    }
}

使用时,在方法上加上@Async注解,然后还需要在@SpringBootApplication启动类或者@Configuration注解类上添加注解@EnableAsync启动多线程注解,@Async就会对标注的方法开启异步多线程调用,注意这个方法的类一定要交给Spring容器来管理。

@Component
public class ScheduleTask {

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    @Async("myAsync")
    @Scheduled(fixedRate = 2000)
    public void testScheduleTask(){
        try {
            Thread.sleep(6000);
            System.out.println("spring1自带线程池"+Thread.currentThread().getName() + "_" + sdf.format(new Date()));

        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
    
    @Async("myAsync")
    @Scheduled(cron = "*/2 * * * * ?")
    public void testAsync(){
        try {
            Thread.sleep(1000);
            System.out.println("spring2自带线程池" + Thread.currentThread().getName() + "_" + sdf.format(new Date()));
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    
}

输出:

怎么保证多线程按顺序执行?

  1. 在子线程中使用join()方法指定顺序

join()方法使当前线程阻塞,等待指定线程执行完后继续执行。

public class ThreadJoinDemo {
    public static void main(String[] args) throws InterruptedException {
        final Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("打开冰箱!");
            }
        });
  
        final Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    thread1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("拿出一瓶牛奶!");
            }
        });
  
        final Thread thread3 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    thread2.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("关上冰箱!");
            }
        });
  
        //下面三行代码顺序可随意调整,程序运行结果不受影响,因为我们在子线程中通过“join()方法”已经指定了运行顺序。
        thread3.start();
        thread2.start();
        thread1.start();
  
    }
}

  1. 通过倒数计时器CountDownLatch实现

CountDownLatch是一个同步工具类,是通过一个计数器来实现的,初始值为线程的数量,每当一个线程完成了自己的任务,计数器的值就相应的减1。当计数器到达0时,表示所有线程都执行完毕,然后等待的线程就可以恢复执行任务。

public class ThreadCountDownLatchDemo {
  
    private static CountDownLatch countDownLatch1 = new CountDownLatch(1);
  
    private static CountDownLatch countDownLatch2 = new CountDownLatch(1);
  
    public static void main(String[] args) {
        final Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("打开冰箱!");
                countDownLatch1.countDown();
            }
        });
  
        final Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    countDownLatch1.await();
                    System.out.println("拿出一瓶牛奶!");
                    countDownLatch2.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
  
        final Thread thread3 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    countDownLatch2.await();
                    System.out.println("关上冰箱!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
  
        //下面三行代码顺序可随意调整,程序运行结果不受影响
        thread3.start();
        thread1.start();
        thread2.start();
    }
}

  1. 使用单一化线程池

单线程化线程池(newSingleThreadExecutor)可以串行执行所有任务。

public class ThreadPoolDemo {
  
   static ExecutorService executorService = Executors.newSingleThreadExecutor();
  
    public static void main(String[] args) {
        final Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("打开冰箱!");
            }
        });
  
        final Thread thread2 =new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("拿出一瓶牛奶!");
            }
        });
  
        final Thread thread3 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("关上冰箱!");
            }
        });
        executorService.submit(thread1);
        executorService.submit(thread2);
        executorService.submit(thread3);
        executorService.shutdown();        //使用完毕记得关闭线程池
    }
  
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值