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 接口(有返回值)
第二种方法比第一种方法得到了优化,但是仍然有一个问题,假如我们想要线程结束后需要有返回值,那么该怎么做呢?
步骤一:
- 实现Callable接口
- 重写接口的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开发手册》
本篇完~