创建多线程的四种方式

这篇具有很好参考价值的文章主要介绍了创建多线程的四种方式。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、创建线程的四种方式

1. 继承Thread类

① 创建一个类继承Thread类,重写run()方法
② 调用start()方法启动线程
例:

/* 创建三个窗口卖票,总票数为100 */
public class TicketWindow  extends Thread {

    //线程共享资源(100张票)
    private static int ticket = 100;
    //同步锁必须是唯一的
    private static final Object obj = new Object();

    @Override
    public void run() {
        while(true){
            synchronized(obj){
                if(ticket > 0){

                    //暂停当前线程,允许其它具有相同优先级的线程获得运行机会
                    Thread.yield();

                    System.out.println(getName() + "卖票: 票号:" + ticket);
                    ticket--;
                }else {
                    break;
                }
            }
        }
    }

    public static void main(String[] args) {
        TicketWindow w1 = new TicketWindow();
        w1.setName("一号窗口");
        w1.start();

        TicketWindow w2 = new TicketWindow();
        w2.setName("二号窗口");
        w2.start();

        Thread w3 = new TicketWindow(); //TicketWindow继承了Thread,可以用Thread接收TicketWindow对象
        w3.setName("三号窗口");
        w3.start();
    }
}

2. 实现Runnable接口

① 创建类实现Runnable接口,重写run()方法
② 以实现类作为构造器参数,创建一个线程(Thread)对象
③ 调用start()方法启动线程

/* 创建三个窗口卖票,总票数为100 */
public class TicketWindow  implements Runnable {

    //线程共享资源(100张票)
    private static int ticket = 100;
    //同步锁必须是唯一的
    private static final Object obj = new Object();

    @Override
    public void run() {
        while(true){
            synchronized(obj){
                if(ticket > 0){
                    //暂停当前线程,允许其它具有相同优先级的线程执行(不能保证运行顺序)
                    Thread.yield();
                    System.out.println(Thread.currentThread().getName() + "卖票: 票号:" + ticket);
                    ticket--;
                }else {
                    break;
                }
            }
        }
    }

    public static void main(String[] args) {
        TicketWindow ticketWindow = new TicketWindow();

        new Thread(ticketWindow, "一号窗口").start();
        new Thread(ticketWindow, "二号窗口").start();
        new Thread(ticketWindow, "三号窗口").start();
    }
}

注意:实现Runnable接口方式中,调用的不是Thread类的run()方法,而是在线程启动后,去调用Runnable类型的run()方法,也就是我们传入的实现类中的run()方法

3. 实现Callable接口

① 创建类实现Callable接口,重写call()方法
② 创建实现类对象
③ 将实现类对象作为构造器参数,创建FutureTask对象
FutureTask对象作为构造器参数,创建Thread对象
⑤ 调用Thread对象的start()方法启动线程
⑥ 调用FutureTask对象的get()方法获取返回值
例:

// 1. 创建一个类来实现Callable接口
public class TicketWindow  implements Callable<Object> {

    //线程共享资源(100张票)
    private static int ticket = 100;
    //同步锁必须是唯一的
    private static final Object obj = new Object();

    // 2. 重写call()方法,返回值类型可以根据需求指定,但必须与创建FutureTask对象时里面的泛型一致
    @Override
    public Object call() {
        while(true){
            synchronized(obj){
                if(ticket > 0){

                    //暂停当前线程,允许其它具有相同优先级的线程获得运行机会
                    Thread.yield();

                    System.out.println(Thread.currentThread().getName() + "卖票: 票号:" + ticket);
                    ticket--;
                }else {
                    break;
                }
            }
        }
        return Thread.currentThread().getName() + "执行完毕 ~ ~";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 3. 创建Callable接口实现类的对象
        TicketWindow ticketWindow = new TicketWindow();
        
        // 4. 将实现类对象作为参数传到FutureTask类的构造器中,创建FutureTask类的对象
        FutureTask<Object> futureTask1 = new FutureTask<Object>(ticketWindow);
        // 5. 将FutureTask类对象传到Thread类的构造器中,创建线程对象(因为FutureTask实现了Runnable接口,所以可以这样传)
        Thread t1 = new Thread(futureTask1, "1号窗口");
        // 6. 通过线程对象调用start()方法开启线程
        t1.start();


        FutureTask<Object> futureTask2 = new FutureTask<Object>(ticketWindow);
        Thread t2 = new Thread(futureTask2, "2号窗口");
        t2.start();

		FutureTask<Object> futureTask3 = new FutureTask<Object>(ticketWindow);
        Thread t3 = new Thread(futureTask3, "3号窗口");
        t3.start();

        // 8. 调用FutureTask类的get()方法获取call()方法的返回值,如果不需要返回值可以省略这一步
        Object result1= futureTask1.get();
        Object result2= futureTask2.get();
        Object result3= futureTask3.get();
        System.out.println(result);
    }
}

4. 线程池

使用线程池创建线程,是实际项目开发中最常用的方式,它拥有许多好处:

  • 提高响应速度 (因为减少了创建新线程的时间)
  • 降低资源损耗 (重复利用线程池中的线程,不需要每次都创建和销毁)
  • 便于线程的管理
import java.util.concurrent.*;

/* 水果售卖窗口 */
class FruitWindow implements Runnable {

    private int fruitNumber = 100;

    @Override
    public void run() {
        while (true) {
            /* 1.任何一个类的对象,都可以充当锁,一般使用Object类对象 */
            /* 2.多个线程必须共用一把锁,多个线程抢一把锁,谁抢到谁执行 */
            synchronized (this) {
                if (fruitNumber > 0) {
                    try {
                        //休眠30毫秒
                        TimeUnit.MILLISECONDS.sleep(30);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + "卖第:" + fruitNumber + "份水果");
                    fruitNumber--;

                } else {
                    break;
                }
            }
        }
    }
}

/* 蔬菜售卖窗口 */
class VegetableWindow implements Callable<Object> {

    private int vegetableNumber = 100;

    @Override
    public Object call() {
        while (true) {
            /* 1.任何一个类的对象,都可以充当锁,一般使用Object类对象 */
            /* 2.多个线程必须共用一把锁,多个线程抢一把锁,谁抢到谁执行 */
            synchronized (this) {
                if (vegetableNumber > 0) {
                    try {
                        //休眠20毫秒
                        TimeUnit.MILLISECONDS.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + "卖第:" + vegetableNumber + "份蔬菜");
                    vegetableNumber--;

                } else {
                    break;
                }
            }
        }
        return null;
    }
}


public class ThreadPool {

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

        // 设置线程池的属性 (线程管理)
        ThreadPoolExecutor poolconfig = (ThreadPoolExecutor) pool; //设置属性前,需要将接口类型 强转为 实现类类型
        poolconfig .setCorePoolSize(10); //核心池的大小(最小线程数)
        poolconfig .setMaximumPoolSize(15); //最大线程数
        //参数1:时间值。时间值为零将导致多余的线程在执行任务后立即终止
        //参数2:时间单位,使用TimeUnit类指定,TimeUnit.DAYS为天、TimeUnit.HOURS为小时、TimeUnit.MINUTES为分钟、TimeUnit.SECONDS为秒,TimeUnit.MICROSECONDS为微秒
        poolconfig .setKeepAliveTime(5, TimeUnit.MINUTES); //空闲线程存活时间
        // .  .  .

        // 2.执行指定的线程操作 (需要提供实现了Runnable接口 或 Callable接口实现类的对象  )
        FruitWindow fruitWindow = new FruitWindow();
        /* 注意:操作共享数据的线程必须共用同一把锁 */
        pool.execute(fruitWindow); //适用于Runnable
        pool.execute(fruitWindow); //适用于Runnable
        pool.execute(fruitWindow); //适用于Runnable

        VegetableWindow vegetableWindow = new VegetableWindow();
        /* 注意:操作共享数据的线程必须共用同一把锁 */
        pool.submit(vegetableWindow); //适用于Callable
        pool.submit(vegetableWindow); //适用于Callable
        Future<Object> future = pool.submit(vegetableWindow); //适用于Callable

        // 获取call()方法的返回值
        Object result = future.get();
        System.out.println(result);

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

禁止使用 Executors 构建线程池

上面的例子用到了Executors静态工厂构建线程池,但一般不建议这样使用
Executors是一个Java中的工具类。提供工厂方法来创建不同类型的线程池。
创建多线程的四种方式
虽然它很大程度的简化的创建线程池的过程,但是它有一个致命缺陷:Java开发手册中提到,使用Executors创建线程池可能会导致OOM(Out-OfMemory , 内存溢出 )

原因:
Executors的静态方法创建线程池时,用的是 LinkedBlockingQueue阻塞队列,如创建固定线程池的方法newFixedThreadPool()

public static ExecutorService newFixedThreadPool(int nThreads) {
	return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}

Java 中的 BlockingQueue 主要有两种实现,分别是 :

  • ArrayBlockingQueue
  • LinkedBlockingQueue

ArrayBlockingQueue 是一个用数组实现的有界阻塞队列,必须设置容量。
LinkedBlockingQueue 是一个用链表实现的有界阻塞队列,在不设置的情况下,将是一个无边界的阻塞队列,最大长度为 Integer.MAX_VALUE

这里的问题就出在这里,newFixedThreadPool 中创建 LinkedBlockingQueue 时,并未指定容量。此时,LinkedBlockingQueue 就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。

上面提到的问题主要体现在 newFixedThreadPoolnewSingleThreadExecutor 两个工厂方法上
newCachedThreadPoolnewScheduledThreadPool 这两个方法虽然没有用LinkedBlockingQueue阻塞队列,但是它默认的最大线程数是Integer.MAX_VALUE,也会有导致OOM的风险。

// 缓存线程池
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}
// 周期线程池
public ScheduledThreadPoolExecutor(int corePoolSize) {
   super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue());
}

构建线程池的正确方式

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

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

还要指定拒绝策略,处理好任务队列溢出时的异常问题。

参考资料:文章来源地址https://www.toymoban.com/news/detail-496745.html

  1. CSDN CD4356 Java 多线程详解(二):创建线程的4种方式
    https://blog.csdn.net/weixin_42950079/article/details/124862582
  2. Java开发手册
    http://static.kancloud.cn/mtdev/java-manual/content/%E4%B8%BA%E4%BB%80%E4%B9%88%E7%A6%81%E6%AD%A2%E4%BD%BF%E7%94%A8Executors%E5%88%9B%E5%BB%BA%E7%BA%BF%E7%A8%8B%E6%B1%A0%EF%BC%9F.md

到了这里,关于创建多线程的四种方式的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 按照前序遍历创建二叉树及树的四种遍历方式

    一.二叉树的介绍         二叉树的特点是二叉树的每个结点的度都不大于2,可以视为每个结点都有左孩子和右孩子。故二叉树结点的数据结构为   二.二叉树的特点     1.设根结点所在的层数为第1层,则第i层最多有个结点。     2.深度为k的二叉树最多有个结点。    

    2024年02月04日
    浏览(35)
  • Java多线程(1)---多线程认识、四种创建方式以及线程状态

    目录 前言 一.Java的多线程 1.1多线程的认识  1.2Java多线程的创建方式 1.3Java多线程的生命周期 1.4Java多线程的执行机制 二.创建多线程的四种方式 2.1继承Thread类 ⭐创建线程  ⭐Thread的构造方法和常见属性  2.2.实现Runnable接口 ⭐创建线程 ⭐使用lambda表达式创建 2.3实现Callable接口

    2024年02月14日
    浏览(40)
  • View 的四种 OnClick 方式

    嗨喽,大家好!今天呢,我跟大家聊一聊Android 的View 的点击事件onClick 。额,有点拗口(^_^) 。 看过我的文章的人可能会好奇,你怎么写Android的文章了啊?说起这啊,就是我的血泪史了,此处省略一万字.................... 废话不多说,让我们代码走起,风里来,雨里去,唯有代

    2023年04月15日
    浏览(41)
  • CSS中的四种定位方式

    在CSS中定位有以下4种: 静态定位 - static 相对定位 - relative 绝对定位 - absolute 固定定位 - fixed 静态定位是css中的默认定位方式,也就是没有定位。在此定位方式中设置:top,bottom,left,right,z-index 这些属性都是无效的。 相对位置前的位置: 相对位置后的位置: 可以看到该

    2024年02月08日
    浏览(85)
  • JavaScript中的四种枚举方式

    字符串和数字具有无数个值,而其他类型如布尔值则是有限的集合。 一周的日子(星期一,星期二,...,星期日),一年的季节(冬季,春季,夏季,秋季)和基本方向(北,东,南,西)都是具有有限值集合的例子。 当一个变量有一个来自有限的预定义常量的值时,使用

    2024年02月03日
    浏览(56)
  • STM32的四种开发方式

    首先看下ST官方给出的四种开发方式的比较 寄存器开发 寄存器编程对于从51等等芯片过渡过来的小伙伴并不陌生,不管你是什么库,最终操作的还是寄存器,所以对于标准库、HAL库、LL库都是在寄存器上的编程,所以可以直接在各种库中直接操作寄存器。 但寄存器开发方法到

    2024年02月11日
    浏览(41)
  • gRpc的四种通信方式详细介绍

    🌷🍁 博主猫头虎 带您 Go to New World.✨🍁 🦄 博客首页——猫头虎的博客🎐 🐳《面试题大全专栏》 文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~🌺 🌊 《IDEA开发秘籍专栏》学会IDEA常用操作,工作效率翻倍~💐 🌊 《100天精通Golang(基础入门篇)》学会Golang语言

    2024年02月11日
    浏览(40)
  • C++文件读取的四种方式

    C++可以根据不同的目的来选取文件的读取方式,目前为止学习了C++中的四种文件读取方式。 C++文件读取的一般步骤: 1、包含头文件 #includefstream 2、创建流对象:ifstream ifs (这里的ifs是自己起的流对象名字) 3、打开文件:file.open(\\\"文件路径\\\",\\\"打开方式\\\"),打开文件后并判断文件是

    2024年02月11日
    浏览(40)
  • SpringBoot导出Excel的四种方式

           近期接到了一个小需求,要将系统中的数据导出为Excel,且能将Excel数据导入到系统。对于大多数研发人员来说,这算是一个最基本的操作了。但是……我居然有点方!         好多年没有实操这种基础的功能了。我对于excel导入导出的印象还停留在才入行时的工作经

    2024年02月03日
    浏览(41)
  • C#对象的四种比较方式

    1.ReferenceEquals(object o1, object o2): 静态方法: 比较两个对象的引用,引用相同返回true,否则返回false,同为null是返回true; ReferenceEquals进行值类型比较时总是返回false,因为两个值类型需要分别装箱到对象中,是不同的引用 ; 从名称中便可知它用来比较两者是否是相同的引

    2024年02月16日
    浏览(32)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包