草庐IT

【Java 并发编程】一文详解 Java 中有几种创建线程的方式

没对象的指针 2023-07-13 原文

Java 中有几种创建线程的方式?

1. Java 程序天然就是多线程的

一个 Java 程序从 main() 方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与,但实际上 Java 程序天生就是多线程程序,因为执行 main() 方法的是一个名称为 main 的线程

public static void main(String[] args) {
    // Java 虚拟机线程系统的管理接口
    ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
    // 不需要获取同步的 monitor 和 synchronizer 信息,仅仅获取线程和线程堆栈信息
    ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
    // 遍历线程
    for (ThreadInfo threadInfo : threadInfos) {
        System.out.println("线程ID:" + threadInfo.getThreadId() + ",线程名:" + threadInfo.getThreadName());
    }
}

而一个 Java 程序的运行就算是没有用户自己开启的线程,实际也有有很多 JVM 自行启动的线程,一般来说有:

/Library/Java/JavaVirtualMachines/jdk1.8.0_301.jdk/Contents/Home/bin/java ...
线程ID:5,线程名:Monitor Ctrl-Break
线程ID:4,线程名:Signal Dispatcher
线程ID:3,线程名:Finalizer
线程ID:2,线程名:Reference Handler
线程ID:1,线程名:main

Process finished with exit code 0
  1. main - main 线程,用户程序入口;
  2. Reference Handler - 清除 Reference 的线程;
  3. Finalizer - 调用对象 finalize 方法的线程;
  4. Signal Dispatcher - 分发处理发送给 JVM 信号的线程;
  5. Monitor Ctrl-Break - 监控 Ctrl-Break 中断信号。

不同的 JDK 版本,启动的线程数会有差异,但这依然证明了 Java 程序天生就是多线程的。

2. 线程的启动与终止

2.1 线程的启动

(1)继承 Thread 类,重写 run() 方法

/**
 * @author pointer
 * @date 2023-03-26 14:57:49
 */
public class NewThread {

    static class MyThread extends Thread {
        @Override
        public void run() {
            // do something
            System.out.println("继承 Thread 类,重写 run() 方法创建线程");
        }
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

(2)实现 Runnable 接口,重写 run() 方法

/**
 * @author pointer
 * @date 2023-03-26 14:57:49
 */
public class NewThread {

    static class MyRunnable implements Runnable {
        @Override
        public void run() {
            // do something
            System.out.println("实现 Runnable 接口,重写 run() 方法");
        }
    }

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}

(3)Thread 和 Runnable 的区别

Thread 才是 Java 里对线程的唯一抽象,Runnable 只是对任务(业务逻辑)的抽象。Thread 可以接受任意一个 Runnable 的实例并执行。

Runnable 是一个接口,在它里面只声明了一个 run()方法,由于 run( )方法返回值为 void 类型,所以在执行完任务之后无法返回任何结果。

(4)Callable、Future 和 FutureTask

Callable 位于 java.util.concurrent 包下,它也是一个接口,与 Runnable 接口类似,因为两者都是为其实例可能由另一个线程执行的类设计的。然而,Runnable 不返回结果,也不能抛出异常。

Future 就是对于具体的 Runnable 或者 Callable 任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过 get() 方法获取执行结果,该方法会阻塞直到任务返回结果。


因为 Future 只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的 FutureTask:


FutureTask 类实现了 RunnableFuture 接口,RunnableFuture 继承了 Runnable 接口和 Future 接口,而 FutureTask 实现了 RunnableFuture 接口。所以它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。


因此我们通过一个线程运行 Callable,但是 Thread 不支持构造方法中传递 Callable 的实例,所以我们需要通过 FutureTask 把一个 Callable 包装成 Runnable,然后再通过这个 FutureTask 拿到 Callable 运行后的返回值。

/**
 * @author pointer
 * @date 2023-03-26 14:57:49
 */
public class NewThread {

    static class MyCallable implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            int sum = 0;
            for (int i = 0; i < 101; i++) {
                sum += i;
            }
            return sum;
        }
    }

    public static void main(String[] args) {
        MyCallable myCallable = new MyCallable();
        // 1. 用 FutureTask 接收结果
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
        new Thread(futureTask).start();

        // 2. 接收线程运算后的结果
        // futureTask.get() - 这个是堵塞性的等待
        try {
            Integer sum = futureTask.get();
            System.out.println("sum = " + sum);
            System.out.println("-------------------");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

2.2 中止线程

(1)线程自然终止

要么是 run() 执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。

(2)stop

暂停、恢复和停止操作对应在线程 Thread 的 API 就是 suspend()、resume() 和 stop()。但是这些 API 是过期的,也就是不建议使用的。

不建议使用的原因主要有:

以 suspend() 方法为例:在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop() 方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。正因为 suspend()、resume() 和 stop() 方法带来的副作用,这些方法才被标注为不建议使用的过期方法。

(3)中断

安全的中止则是其他线程通过调用某个线程 A 的 interrupt() 方法对其进行中断操作, 中断好比其他线程对该线程打了个招呼,“A,你要中断了”,不代表线程 A 会立即停止自己的工作,同样的 A 线程完全可以不理会这种中断请求。

线程通过检查自身的中断标志位是否被置为 true 来进行响应,线程通过方法 isInterrupted() 来进行判断是否被中断,也可以调用静态方法 Thread.interrupted() 来进行判断当前线程是否被中断,不过 Thread.interrupted() 会同时将中断标识位改写为 false。

如果一个线程处于了阻塞状态(如线程调用了 thread.sleep()、thread.join()、thread.wait() 等),则在线程在检查中断标示时如果发现中断标示为 true,则会在这些阻塞方法调用处抛出 InterruptedException 异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为 false。

不建议自定义一个取消标志位来中止线程的运行。因为 run() 方法里有阻塞调用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志。这种情况下,使用中断会更好,因为:1. 一般的阻塞方法,如 sleep 等本身就支持中断的检查;2. 检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可以避免声明取消标志位,减少资源的消耗。

注意:处于死锁状态的线程无法被中断

2.3 深入理解 run() 和 start()

Thread 类是 Java 里对线程概念的抽象,可以这样理解:我们通过 new Thread() 其实只是 new 出一个 Thread 的实例,还没有操作系统中真正的线程挂起钩来。只有执行了 start() 方法后,才实现了真正意义上的启动线程。

从 Thread 的源码可以看到,Thread 的 start() 方法中调用了 start0() 方法,而 start0() 是个 native 方法,这就说明 Thread.start() 一定和操作系统是密切相关的。

start() 方法让一个线程进入就绪队列等待分配 cpu,分到 cpu 后才调用实现的 run() 方法,start() 方法不能重复调用,如果重复调用会抛出异常(此处可能有面试题:多次调用一个线程的 start() 方法会怎么样?)。

而 run() 方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,也可以被单独调用。

3. Java 中有几种方式创建一个线程?

大家在学习 Java 线程或者面试的时候,肯定见过这个问题。这个问题的答案也是众说纷纭,有 2 种、3 种、4 种等等答案。那么一起来看一下 Java 源码是怎么说的:



官方说法是在 Java 中有两种方式创建一个线程用以执行,一种是派生自 Thread 类,另一种是实现 Runnable 接口

当然本质上 Java 中实现线程只有一种方式,都是通过 new Thread() 创建线程对象,调用 Thread.start() 启动线程。

至于基于 callable 接口的方式,因为最终是要把实现了 callable 接口的对象通过 FutureTask 包装成 Runnable,再交给 Thread 去执行,所以这个其实可以和实现 Runnable 接口看成同一类。

而线程池的方式,本质上是池化技术,是资源的复用,和新启线程没什么关系。

所以,比较赞同官方的说法,有两种方式创建一个线程用以执行。

有关【Java 并发编程】一文详解 Java 中有几种创建线程的方式的更多相关文章

  1. ruby - 如何在 Ruby 中顺序创建 PI - 2

    出于纯粹的兴趣,我很好奇如何按顺序创建PI,而不是在过程结果之后生成数字,而是让数字在过程本身生成时显示。如果是这种情况,那么数字可以自行产生,我可以对以前看到的数字实现垃圾收集,从而创建一个无限系列。结果只是在Pi系列之后每秒生成一个数字。这是我通过互联网筛选的结果:这是流行的计算机友好算法,类机器算法:defarccot(x,unity)xpow=unity/xn=1sign=1sum=0loopdoterm=xpow/nbreakifterm==0sum+=sign*(xpow/n)xpow/=x*xn+=2sign=-signendsumenddefcalc_pi(digits

  2. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  3. ruby - 如何以所有可能的方式将字符串拆分为长度最多为 3 的连续子字符串? - 2

    我试图获取一个长度在1到10之间的字符串,并输出将字符串分解为大小为1、2或3的连续子字符串的所有可能方式。例如:输入:123456将整数分割成单个字符,然后继续查找组合。该代码将返回以下所有数组。[1,2,3,4,5,6][12,3,4,5,6][1,23,4,5,6][1,2,34,5,6][1,2,3,45,6][1,2,3,4,56][12,34,5,6][12,3,45,6][12,3,4,56][1,23,45,6][1,2,34,56][1,23,4,56][12,34,56][123,4,5,6][1,234,5,6][1,2,345,6][1,2,3,456][123

  4. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  5. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  6. ruby-on-rails - 无法使用 Rails 3.2 创建插件? - 2

    我对最新版本的Rails有疑问。我创建了一个新应用程序(railsnewMyProject),但我没有脚本/生成,只有脚本/rails,当我输入ruby./script/railsgeneratepluginmy_plugin"Couldnotfindgeneratorplugin.".你知道如何生成插件模板吗?没有这个命令可以创建插件吗?PS:我正在使用Rails3.2.1和ruby​​1.8.7[universal-darwin11.0] 最佳答案 随着Rails3.2.0的发布,插件生成器已经被移除。查看变更日志here.现在

  7. ruby - 如何使用 RSpec::Core::RakeTask 创建 RSpec Rake 任务? - 2

    如何使用RSpec::Core::RakeTask初始化RSpecRake任务?require'rspec/core/rake_task'RSpec::Core::RakeTask.newdo|t|#whatdoIputinhere?endInitialize函数记录在http://rubydoc.info/github/rspec/rspec-core/RSpec/Core/RakeTask#initialize-instance_method没有很好的记录;它只是说:-(RakeTask)initialize(*args,&task_block)AnewinstanceofRake

  8. ruby - 为什么 SecureRandom.uuid 创建一个唯一的字符串? - 2

    关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?

  9. java - 等价于 Java 中的 Ruby Hash - 2

    我真的很习惯使用Ruby编写以下代码:my_hash={}my_hash['test']=1Java中对应的数据结构是什么? 最佳答案 HashMapmap=newHashMap();map.put("test",1);我假设? 关于java-等价于Java中的RubyHash,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/22737685/

  10. ruby-on-rails - 如何在 ruby​​ 交互式 shell 中有多行? - 2

    这可能是个愚蠢的问题。但是,我是一个新手......你怎么能在交互式ruby​​shell中有多行代码?好像你只能有一条长线。按回车键运行代码。无论如何我可以在不运行代码的情况下跳到下一行吗?再次抱歉,如果这是一个愚蠢的问题。谢谢。 最佳答案 这是一个例子:2.1.2:053>a=1=>12.1.2:054>b=2=>22.1.2:055>a+b=>32.1.2:056>ifa>b#Thecode‘if..."startsthedefinitionoftheconditionalstatement.2.1.2:057?>puts"f

随机推荐