当前位置:   article > 正文

JAVA多线程使用,从入门到精通,看这一篇打开思路,豁然开朗_多线程入门到精通

多线程入门到精通

谈谈JAVA多线程使用

java开发中,当需求复杂度和并发量增大时,引入多线程来充分发挥机器的性能是很有必要的,但是引入多线程以后系统复杂度随之上升,使用不当带来OOM或者CPU疯狂飙升导致服务崩溃的风险增大,这时候理解多线程的使用变得尤为重要。下文谈谈我自身开发中,对java中使用多线程的理解。不过什么是线程,线程和进程的关系本文不再赘述。

1、提到多线程,就不得不提Thread类

在java语言基础库中,提供了一个Thread类,用来表示或操作线程,Thread类可以视为是Java标准库提供的API,Java是支持多线程编程的,在Java中创建好的Thread实例,其实就表示操作系统中存在的一个线程,是一一对应的关系,操作系统提供了一组关于线程的API(C语言),Java对于这组API进一步封装之后,把API变成适用java语言的各个方法,存在Thread类中。

查看Thread类的原码,我们可以看到几乎都是native方法,意味着底层是操作系统提供的c语言库函数

public static native Thread currentThread();
public static native void yield();
public static native void sleep(long millis) throws InterruptedException;
  • 1
  • 2
  • 3

1)那么如何快速创建一个线程用来异步执行一些业务呢?

或许你也曾疑惑或正在疑惑,很多人说创建线程有3种方法,有的人说是4种,那么到底有几种?如何合理选择创建线程的方式

我们知道在Java中Thread和操作系统的线程一一对应,而JAVA是面向对象的编程语言,万物皆对象,线程肯定也是一个对象,我们能不能通过new一个Thread的实例来创建线程呢,如果你从这个思路来考虑,那么你已经和JAVA之父的思路在同一个频道上了,也就是说是可以这么来创建线程的

public static void main(String[] args) {
    Thread thread = new Thread();
    // 注意,在Thread中有start()和run()两个方法十分相似,但却天差地别
    // 但只有调用start()方法才表示该线程进入就绪状态,等待操作系统运行,线程的状态大家可以自行了解下
    // 也就是说线程具体什么时候运行需要等待操作系统调用
    thread.start();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

但是你运行完会发现和没执行一样什么都没发生,为什么,这是因为我们没有让这个线程做事

// 创建一个类,继承Thread,重写run方法适配自己的业务逻辑
class MyThread1 extends Thread {
    @Override
    public void run() {
        System.out.println("这是第一种创建线程的方法,执行线程名:"+Thread.currentThread().getName());
    }
}

// 实例化一个对象并执行,这时控制台会打印出响应的执行信息
public static void main(String[] args) {
    Thread thread = new MyThread1();
	thread.start();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

当然,实际上继承Thread类只是其中一种创建线程的方式,我们来看看其他方案

a: 实现Runnable接口

// 实现Runnable接口,将业务代码写在run方法内
static class MyThread2 implements Runnable {
    @Override
    public void run() {
        System.out.println("这是第二种创建线程的方法,执行线程名:"+Thread.currentThread().getName());
    }
}

// 实例化一个对象,将对象作为target传入Thread类,利用Thread的start方法将线程提交给cpu调度
public static void main(String[] args) {
    Thread thread = new Thread(new MyThread2());
    thread.start();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

b: 通过Callable和Future接口创建线程

// 实现Callable接口,配合FutureTask创建线程
public static void main(String[] args) {
    FutureTask<Integer> task =  new FutureTask<>(new Callable<Integer>() {
        @Override
        public Integer call() throws ExecutionException {
            System.out.println("这是第3种创建线程的方法,执行线程名:"+Thread.currentThread().getName());
            return 1;
        }
    });
    // 同样需要将target传给Thread
    Thread thread = new Thread(task);
    thread.start();
    try {
        // 此方式创建线程可以拿到执行的返回值,并且业务方法报错时,可以捕获到
        // 需要注意的是调用get方法会阻塞主线程
        Integer integer = task.get();
        System.out.println(integer);
    } catch (InterruptedException interruptedException) {
        interruptedException.printStackTrace();
        System.out.println("执行中断");
    } catch (ExecutionException e) {
        e.printStackTrace();
        System.out.println("业务异常");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

归纳:由此可见,不管使用哪一种方式,最后都要通过Thread类来维护,笔者认为实际创建线程的方式就一种,那就是new一个Thread对象。不过实现Runnable带来的好处是可以继承别的父类,以便于扩展,实现Callable,配合FutureTask可以实现获取执行的返回值和捕获执行异常的场景。

2、使用线程池来管理线程

在上文我们知道了创建线程的各种方式,那么为什么还要引入线程池的概念呢。

对比一下数据库连接池,作用无非是复用已有的连接,避免每次请求都重新建立连接,从而减少建立和断开连接的开销,提升数据库操作的响应速度和稳定性,还可以监控连接情况等。线程池其实也是差不多的作用。

线程池相关的类基本都在java.util.concurrent包下,翻阅原码可以看到是Doug Lea这个人一手编写,太牛了。

本文我们着重谈谈java.util.concurrent.ThreadPoolExecutor,因为只要你实例化这个对象出来,就已经创建好了一个线程池,剩下的就是按照业务需要丢线程进去执行

1)、如何创建线程池

通过介绍我们知道new ThreadPoolExecutor即可创建,但是看源码得知构造方法有很多,我们具体用哪个更合适呢

corePoolSize 核心线程池大小 - 表示该线程池有多少个一直可以循环使用不会被销毁的线程资源
maximumPoolSize 最大线程池大小 - 表示该线程池的线程总数,当核心线程数慢时,再有线程被添加时会进入对列等待,队列满时,线程池最大会创建(maximumPoolSize - corePoolSize)个非核心线程来执行新添加的线程
keepAliveTime 非核心线程空闲多久会被释放
unit 超时时间单位
workQueue 阻塞队列 - 核心线程满时存放线程的队列
threadFactory 线程工厂 - 可以通过自定义工厂来定义线程名等
defaultHandler 拒绝策略 - 当线程池被占满时通过什么策略拒绝

2)、通过对构造方法的7个参数的理解,创建适合的线程池来使用

public static void main(String[] args) {
    // 自定义线程工厂,自定义线程名
    ThreadFactory factory = new ThreadFactoryBuilder().setNameFormat("my-thread-%d").build();
    ThreadPoolExecutor pool = new ThreadPoolExecutor(6, 10, 10,
                                                     TimeUnit.SECONDS, 
                                                     new LinkedBlockingQueue<>(), 
                                                     factory, 
                                                     new ThreadPoolExecutor.AbortPolicy());
    // 将上文中实现Runnable接口的一个对象丢进线程池,由线程池来管理和调度
    // 也可以调用pool.submit()传入Callable的实现类
    pool.execute(new MyThread2());
    // 注意,线程池被创建时一般不会主动关闭,需要手动shutdown()
    // 一般我们会将线程池创建在一个常量类中,当一个静态常量来使用
    pool.shutdown();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

3、通过使用现有的api创建线程池(不推荐)

// 比如
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(new MyThread1());
  • 1
  • 2
  • 3

注意到此方式创建的线程池是ExecutorService类型,为什么呢,有兴趣的同学可以看看ThreadPoolExecutor和ExecutorService的关系,有兴趣的朋友可以自己研究下

注:笔者想分享的已经结束了,本文如有不正确之处,欢迎大家批评纠正

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/酷酷是懒虫/article/detail/926166
推荐阅读
相关标签
  

闽ICP备14008679号