3. Java并发编程的魅力之线程的创建方法

1.1 写在前面的话

曾听闻一些业界的前辈说,如果你想学习一样技术,先去理解What(它是什么),然后了解Why(为什么这样),然后你再去学习HOW(怎么用它)就简单多了。

然而在我真实的学习过程中,却发现这条路似乎并不适合我,因为我发现往往在我去理解Why 的路上就被枯燥的理论给玩死了。。。

这个很像业界的一个玩笑,从入门到放弃,大致就是这样。

于是后来我尝试换了一种方式去学习,我先去了解它是什么,然后学习怎么干,最后再思考它为什么这么做,这样似乎愉快了很多。

这种方法的产生其实是深受马士兵老师的当年的一句话的影响。

马士兵老师曾说,
“如果你对于书本上的一些理论知识点不知道什么意思,那就先写,写着写着说不定有些知识点你就深入理解了。“”

1.2 并发编程学习环境搭建

好了,废话不多说,我们开始搭建我们的实战学习环境,通过写代码来讲解那些枯燥的理论知识。

如果可以的话,这里非常推荐搭建构建一个基于Maven 的Java 项目,当然,我也是这么干的。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.xingyun</groupId>
    <artifactId>learning-thread-tutorial-sample</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <!-- 设置当前项目源码使用字符编码为UTF-8 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <!-- 设置当前项目所需要的JDK版本 Open JDK下载地址:https://jdk.java.net/ -->
        <java.version>1.8</java.version>
        <!-- 设置当前项目编译所需要的JDK版本 Open JDK下载地址:https://jdk.java.net/ -->
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
        <!-- 设置maven编译插件版本,可通过下面网址查看最新的版本-->
        <!-- https://mvnrepository.com/artifact/org.apache.maven.plugins/maven-compiler-plugin -->
        <maven.compiler.plugin.version>3.5.1</maven.compiler.plugin.version>
        <!-- 项目所使用第三方依赖jar包的版本,建议以后都使用这种方式,方便今后维护和升级 -->
    </properties>

    <build>
        <plugins>
            <!--该插件限定Maven打包时所使用的版本,避免出现版本不匹配问题-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${maven.compiler.plugin.version}</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

1.3 从Hello World 看什么是线程

上节课我们了解到,在操作系统中,每当我们启动一个Java程序,操作系统就会创建一个Java进程。

而且,一个Java进程中,往往会包含很多线程。

如果你对这句话不是很理解,那么让我们一起来写一个Hello World 程序来看看。

package com.xingyun.main;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

/**
 * @author qing-feng.zhao
 * @功能 一个Java程序的运行不仅仅是main()方法的运行,而是main线程和多个线程的同时运行
 * @时间 2019/12/18 15:36
 */
public class MultiThread {
    public static void main(String[] args) {
        showThreadInfo();
        System.out.println("Hello World");
    }
    private static void showThreadInfo(){
        //获取Java线程管理MXBean
        ThreadMXBean threadMXBean= ManagementFactory.getThreadMXBean();

        //不需要获取同步的monitor和 synchronized 信息,仅获取线程和线程堆栈信息
        ThreadInfo[] threadInfoArray=threadMXBean.dumpAllThreads(false,false);

        //遍历线程信息,仅打印线程ID 和线程名称信息
        for (ThreadInfo threadInfo:threadInfoArray
             ) {
            System.out.println("["+threadInfo.getThreadId()+"]"+threadInfo.getThreadName()+"["+threadInfo.getThreadState()+"]");
        }
    }
}

打印结果如下:

[6]Monitor Ctrl-Break[RUNNABLE]
[5]Attach Listener[RUNNABLE]
[4]Signal Dispatcher[RUNNABLE] // 分发处理发送给JVM信号的线程
[3]Finalizer[WAITING] //调用对象finalize 方法的线程
[2]Reference Handler[WAITING] // 清除Reference 的线程
[1]main[RUNNABLE] //main 线程,用户程序入口
 Hello World

上面的输出,我们可以看到,

一个Java程序的运行不仅仅是main()方法的运行,而是main线程和多个其他线程的同时运行

1.4 为什么要用多线程?

我们打开电商网站,应该很容易看到如下的内容。
在这里插入图片描述
我们可以看到,如今的电脑,CPU很多都是多核多线程的。

  • 线程是大多数操作系统调度的基本单元,一个程序作为一个进程来运行,程序运行过程中能够创建多个线程
  • 一个线程在一个时刻只能运行在一个处理器核心上
  • 一个单线程程序在运行时只能使用一个处理器核心,那么再多的处理器核心加入也无法显著提升该程序的执行效率

相反,如果该程序使用多线程技术,将计算逻辑分配到多个处理器核心上,就会显著减少程序的处理时间,并且随着更多处理器核心的加入而变得更有效率

除此之外,针对一些有阻塞的任务,使用多线程,也可以帮我们节省很多时间。

值得注意的是,

  • 多线程不是银弹,并不是所有场景都适合用多线程技术、
  • 只有处理有阻塞的任务或在拥有多核心的CPU 上使用多线程才有意义。
  • 如果只是单一的简单的任务,使用多线程,因为线程切换也需要时间,所以在这种情况,速度不仅得不到提升,反而可能会下降。

1.5 如何创建线程?

创建线程的几种方法

1.5.1 继承自Thread 类(不推荐)

步骤:
1.继承自Thread类
2.重写run方法

public class MyThreadOne extends  Thread {

    @Override
    public void run() {
        //我们可以通过调用 Thread.currentThread().getName(); 获取当前线程名称
        System.out.println("当前线程名称:"+Thread.currentThread().getName()+":Do Somthing");
    }
}

调用方式

public class MyThreadOneTest {

    public static void main(String[] args) {

        //创建一个继承自Thread类的实例对象
        MyThreadOne myThreadOne=new MyThreadOne();

        //创建一个线程使用默认的线程名称thread-1
       //Thread myThread=new Thread(myThreadOne);
        
        //创建一个线程并指定线程名称
        Thread myThread=new Thread(myThreadOne,"My Thread One");
        
        //启动线程
        myThread.start();
    }
}

打印结果:

当前线程名称:My Thread One:Do Somthing

这种方式弊端很明显,因为一旦我们当前类已经继承了其他类作为父类,那么就没有办法再继承Thread类了(Java中类是单继承的),因此这种方式几乎不会使用。

有没有更好的方法呢? 有,那就是接下来讲的第二种方法。

1.5.2 实现Runnable 接口(无返回值)

我们知道Java之中虽然类不可以多继承,但是接口却可以多继承,并且一个类可以实现多个接口。

步骤:
1.实现Runnbale接口
2.重写run方法

public class MyThreadTwo implements  Runnable {

    @Override
    public void run() {
        //我们可以通过调用 Thread.currentThread().getName(); 获取当前线程名称
        System.out.println("当前线程名称:"+Thread.currentThread().getName()+":Do Somthing");
    }
}

调用方法如下

public class MyThreadTwoTest {

    public static void main(String[] args) {

        //创建一个实现Runnable接口类的实例对象
        MyThreadTwo myThreadTwo=new MyThreadTwo();

        //创建一个线程使用默认的线程名称thread-1
        //Thread myThread=new Thread(myThreadTwo);

        //创建一个线程并指定线程名称
        Thread myThread=new Thread(myThreadTwo,"My Thread Two");

        //启动线程
        myThread.start();
    }
}

执行结果如下:

当前线程名称:My Thread Two:Do Somthing

1.5.3 实现Callable 接口(有返回值)

第二种方法比第一种方法得到了优化,但是仍然有一个问题,假如我们想要线程结束后需要有返回值,那么该怎么做呢?

步骤一:

  1. 实现Callable接口
  2. 重写接口的call()方法

V是泛型,可以是String,也可以是任意Object对象

public class MyThreadThree implements Callable<String> {

    @Override
    public String call() throws Exception {

        //我们可以通过调用 Thread.currentThread().getName(); 获取当前线程名称
        System.out.println("当前线程名称:"+Thread.currentThread().getName()+":Do Somthing");

        return "thread three result";
    }
}

3.调用如下:

这里我们注意到和之前有些改动。

  • 1.我们不能再使用Thread类直接包装我们的自定义类
  • 2.需要new FutureTask<>(myThreadThree)包装我们的自定义类
FutureTask<String> futureTask=new FutureTask<>(myThreadThree); 
  • 3.然后使用Thread类包装下FutureTask<V>
Thread myThread=new Thread(futureTask,"My Thread Three");
  • 4.futureTask.get() 获取线程返回的结果

代码如下:

public class MyThreadThreeTest {

    public static void main(String[] args) {

        //创建一个实现Callable接口类的实例对象
        MyThreadThree myThreadThree=new MyThreadThree();

        //V 是泛型,可以是String 也可以是任意Object类型
        //FutureTask<V> 中的V要和 Callable<V> V类型保持一致
        //使用FutureTask<String>对象来包装实现Callable接口类的实例对象
        FutureTask<String> futureTask=new FutureTask<>(myThreadThree);

        //创建一个线程使用默认的线程名称thread-1
        //Thread myThread=new Thread(futureTask);

        //创建一个线程并指定线程名称
        Thread myThread=new Thread(futureTask,"My Thread Three");

        //启动线程
        myThread.start();

        try {

           //获取线程返回的结果
           String result= futureTask.get();
           //打印线程返回的结果
           System.out.println(result);

        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

执行结果:

当前线程名称:My Thread Three:Do Somthing
thread three result

参考资料

《Java并发编程的艺术》
《Java编程思想》
《码出高效Java开发手册》


本篇完~

发布了162 篇原创文章 · 获赞 219 · 访问量 40万+

猜你喜欢

转载自blog.csdn.net/hadues/article/details/103613274