Java:多线程概述与创建方式
Java:多线程概述与创建方式
在之前的学习过程中,已经不止一次地提到了并发啊,线程啊,同步异步的内容,但是出于内容的局部一体,之前总是几笔带过,并附上:以后学习的时候再细说。
那么,现在到了细说的时候,在翻阅并参考了介绍Java并发编程的书之后,突然感觉压力有些大,因为有些概念确实比较抽象。所以之后的内容不定长短,但是每天都会试着输出一些。
进程和线程
一个进程可以拥有多个线程,一个线程必须拥有一个父进程。
进程:当前操作系统正在执行的任务,也是操作系统运行程序的一次执行过程。
线程:是进程的执行单元,是进程中正在执行的子任务。
就好像我们正在使用的QQ,正在放歌的音乐软件,正在打的游戏,就是一个个的进程。我们在QQ进程中执行的各种操作,就是一个个的线程。
每个Java的应用程序运行的时候其实就是个进程,JVM启动之后,会创建一些进行自身常规管理的线程,如垃圾回收和终结管理,和一个运行main函数的主线程。

并发与并行
现在大部分的操作系统都是支持多进程并发运行的,就像我们现在正在使用电脑,可以通过任务管理器查查看,会发现有几十个几百个进程在“同时执行”。”同时执行“被打上了引号,显然事实上并不是。
并发:就拿进程来说,在同一个时刻,只能有一条指令执行,但是多个进程可以被快速地轮换执行,CPU的执行速度之快,让人产生这些个进程就是在同时执行。
并行:就是同一时刻,多条进程指令在多个处理器上同时执行。
看看下面的图就懂了:

接下来是我对于并发和并行假想场景:
并发场景:假设现在有一台只能一个人玩的电脑,老大和老二兄弟俩都想玩一小会儿,那没办法,得想办法解决啊。打一架吧,谁抢到算谁的。不管是谁抢到,他们一定玩到满足才会罢休,这就是现在操作系统所采用的高效率的抢占式多任务操作策略。
并行场景:现在有两台电脑,老大老二都各自玩各自的电脑,不争也不抢。
多线程的优势
线程被称为轻量级进程,大多数情况下,进程中的多线程的执行是抢占式的,就和操作系统的并发多进程一样。
线程拥有自己的堆栈、程序计数器和局部变量,允许程序控制流的多重分支同时存在于一个线程,共享进程范围内的资源,因此,同一进程中的线程访问相同的变量,并从同一个堆中分配对象,实现良好的数据共享,但是如果处理不当,会为线程安全造成一定的隐患。
多线程相比于多进程的优势:
- 多个线程之间可以共享内存,而进程之间不可以。
- 操作系统创建线程的代价比进程小,实现多任务并发效率更高。
以下参考自《JAVA并发编程实战》:
- 一个单线程应用程序一次之能运行在一个处理器上。在双处理器系统中只运行一个应用程序,相当于其中一个处理器空闲,50%的CPU资源没有利用上。随着处理器的增多,单线程的应用程序放弃的CPU资源将会更多。这一点,正好也侧面反映了多线程能够更有效地利用空闲的处理器资源。
- 处理器在某些情况是空闲的,如在等待一个同步IO操作完成的时候。这个时候,暂且不论多处理器,仅仅针对单处理器,多线程的优势也是相当明显的,可以很好地利用处理器空闲的时间运行另外一个线程。
线程的创建和启动
先来看看多线程编程中这个相当关键的类,java.lang.Thread,官方文档说了:有两种方式创建线程,就是下面这俩:
继承Thread类
- 将一个类声明为
Thread的子类。 - 这个子类应该覆盖类
Thread的run()方法。
创建线程如下:
/*创建线程*/
//创建一个类继承Thread类
class TDemo extends Thread{
//线程要执行的任务在run方法中
@Override
public void run(){
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
}
}
启动线程如下:
public static void main(String[] args){
//创建了TDemo的实例
TDemo t1 = new TDemo();
//启动线程,并调用run方法
t1.start();
System.out.print("main");
}
//输出结果:main01234
创建TDemo的实例对象不等于启动了该实例所对应的线程,启动需要调用线程对象的start()方法。
start()和run()
new创建了TDemo的实例,只是创建了一个线程,此时它处于新建状态,有JVM分配内存,并初始化成员变量的值,是个配置的过程。
线程对象调用
start()方法之后,线程就会处于就绪状态,JVM会为其创建方法调用栈和程序计数器,表示这个线程可以执行,但真正啥时候开始执行取决于JVM中线程调度器的调度。之后才进入运行状态,执行
run()方法中的方法体。
我们试着把start()方法换成run()方法看看结果:01234main
我们通过输出结果可以看到,调用start()方法,系统会把run()方法当成线程执行体处理,主线程和我们创建的线程将并发执行。但如果单纯调用run()方法,系统会把线程对象当成一个普通的对象,run()方法也只是普通对象方法的一部分,是主线程的一部分。

实现Runnable接口
这是Runnable接口的内容,@FunctionalInterface注解表示函数式接口,和Java8新特性lambda表达式相关,之后再做学习总结。
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
- 创建线程的另一种方法是声明一个实现Runnable接口的类。
- 然后,该类实现run方法。然后可以分配类的实例,
- 在创建线程时作为参数传递,并启动它。
//实现Runnable接口
class RDemo implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
}
}
//创建并启动线程
Thread t = new Thread(new RDemo());
t.start();
调用public Thread(Runnable target)构造器,将Runnble接口类型对象传入作为参数,构建线程对象。
当然还可以用匿名内部类的形式:
//匿名内部类创建并启动线程
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.print(i);
}
}
}).start();
实现Callable接口
这是Callable接口的内容:
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
除了上面两种方法之外,从书上看到还有一种Java5新增的方法,利用Callable接口,官方文档是这样描述的:
- Callable接口类似于Runnable,需要实现接口中的call()方法。但是,Runnable不返回结果,也不能抛出已检查的异常。
- Runnable接口提供run()方法支持用户定义线程的执行体,而Callable中提供call()方法。
- 拥有返回值。
- 允许抛出异常。
- 通过泛型我们可以知道,Callable接口中的形参类型需要和call方法返回值类型相同:
光有Callable接口还不行,毕竟隔了5年才出来,为了尽量避免修改之前的代码,适应当前环境,Java5还新增了配套的Future接口:
public interface Future<V> {
//试图取消Callable中任务的执行,如果任务已经完成、已经被取消、或因其他原因无法被取消,返回false。
boolean cancel(boolean mayInterruptIfRunning);
//如果此任务在正常完成之前被取消,则返回true
boolean isCancelled();
//如果此任务已完成(正常的终止、异常或取消),则返回true
boolean isDone();
//如果需要,则等待计算完成,然后检索其结果。
V get() throws InterruptedException, ExecutionException;
//如果需要,将等待最多给定的时间以完成计算,然后检索其结果。
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
通过继承关系可以发现,RunnableFuture接口同时继承了Runnable和Future接口,意味着实现RunnableFuture接口的类既是Runnable的是实现类,又是Future的实现类。FutureTask就是充当这样的角色,它的实例可以作为target传入Thread的构造器中。
通过查看源码,可以发现FutureTask内部维护了一个Callable的对象,可以通过下面的这个构造器初始化Callable对象。
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
- 用匿名内部类的方式,将实现call()方法的Callable实现类对象作为参数传递给FutureTask的构造器中,构建一个FutureTask类的对象。
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int i = 0;
while(i<10){
System.out.println(Thread.currentThread().getName());
i++;
}
return i;
}
});
- 之后可以通过Thread类构造器:
public Thread(Runnable target, String name)将task对象作为参数创建新线程并启动。name参数是可以自定义线程的名字。
new Thread(task,"name").start();
- 最后可以通过task对象调用get()方法得到call()方法的返回值,需要注意处理抛出的异常。
try {
System.out.println(task.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
创建方式的区别
继承类Thread和实现接口(Runnable或Callable)这两种方式的区别?
- 前者需要定义子类继承Thread类,可以直接通过创建子类对象作为线程对象,而后者创建的Runnable对象只是线程对象的target。
- 同样的,获取当前对象的方法也不同,前者可以直接使用this获取当前对象的引用。后者则需要调用Thread的静态方法
currentThread()。
下面是两个获取当前线程名的示例:
//继承Thread
System.out.print(this.getName()+i);
//实现Runnable接口
System.out.print(Thread.currentThread().getName()+i);
前者线程类每创建一个线程都需要创建一个对象,对象之间不能共享实例变量。而后者通过接口的实现类创建的多个线程可以共享同一个Runnable类型的target,也就是这个线程类的实例变量。
前者定义线程类需要继承Thread,而Java只支持单继承,支持接口多实现,显然在灵活性方面,后者优于前者。
本文作为个人学习笔记,仍停留在比较浅显的层面,还需要大量的实践去感悟并发编程的奥义。
参考资料:《JAVA并发编程实战》、《疯狂Java讲义》、《JAVA多线程设计模式》
Java:多线程概述与创建方式的更多相关文章
- Java多线程——线程的创建方式
Java多线程——线程的创建方式 摘要:本文主要学习了线程的创建方式,线程的常用属性和方法,以及线程的几个基本状态. 部分内容来自以下博客: https://www.cnblogs.com/dolph ...
- 面试官:小伙子,说一说Java多线程有哪些创建方式吧
第一种 继承Thread类 自定义类,继承Thread类,并重写run()方法. class MyThread1 extends Thread { @Override public void run( ...
- java 多线程5(创建方式)
实现Runnable接口: 问题1:Runnable实现类的对象是线程对象吗? 答:不是,该对象只不过是实现了Runnable接口的对象而已,只有是Thread或Thread的子类才是线程对象. 问题 ...
- Java多线程-Java多线程概述
第一章 Java多线程概述 线程的启动 线程的暂停 线程的优先级 线程安全相关问题 1.1 进程与线程 进程:可以将运行在内存中的程序(如exe文件)理解为进程,进程是受操作系统管理的基本的运行单元. ...
- 使用goroutine+channel和java多线程+queue队列的方式开发各有什么优缺点?
我感觉很多项目使用java或者c的多线程库+线程安全的queue数据结构基本上可以实现goroutine+channel开发能达到的需求,所以请问一下为什么说golang更适合并发服务端的开发呢?使用 ...
- 【JAVA多线程概述】
一.多线程概述 一个进程中至少有一个线程,每一个线程都有自己运行的内容,这个内容可以称为线程要执行的任务. 不能没一个问题都使用多线程,能使用单线程解决的问题就不要使用多线程解决. 使用多线程的弊端: ...
- Java多线程--两种实现方式
进程概述: 在这之前,有必要了解一下什么是进程? 在一个操作系统中,每个独立的执行的程序都可称为一个进程,也就是"正在运行的程序".如图所示: 线程概述: 如上所述,每个运行的程序 ...
- JAVA多线程三种实现方式
JAVA多线程实现方式主要有三种:继承Thread类.实现Runnable接口.使用ExecutorService.Callable.Future实现有返回结果的多线程.其中前两种方式线程执行完后都没 ...
- Java 多线程 三种实现方式
Java多线程实现方式主要有三种:继承Thread类.实现Runnable接口.使用ExecutorService.Callable.Future实现有返回结果的多线程.其中前两种方式线程执行完后都没 ...
随机推荐
- 电信NBIOT平台的CA证书上传-消息订阅回调地址检测503错误
在NBIOT北向开发过程中,遇到消息订阅回调地址检测503错误,经过论坛查询与文档查阅一直都没有解决问题,大多人都说是RESTful地址格式问题,但其实不是.最终发现是我们在电信平台创建应用时,上传C ...
- 微服务统计,分析,图表,监控一体化的HttpReports项目在.Net Core 中的使用
简单介绍 HttpReports 是 .Net Core 下的一个Web项目, 适用于WebAPI,Ocelot网关应用,MVC项目,非常适合针对微服务应用使用,通过中间件的形式集成到您的项目中,可以 ...
- Node.js 模块系统入门
在编程领域中,模块是自包含的功能单元,可以跨项目共享和重用.它们使开发人员的生活更加轻松,因为我们可以使用它来增加应用程序的功能,而不必亲自编写这些功能.它还让我们可以组织和解耦代码,从而使应用程序更 ...
- 24.python中xlwt模块用法详解
1.创建并保存一个excel 创建一个工作簿,设置编码格式为“utf-8”,默认格式是ASCII,为了方便写入中文,一般都要设置成UTF-8 import xlwt wb = xlwt.Workboo ...
- spring get方法 中文(UTF-8)乱码
问题: 前端用Get方法进行如下请求: 在浏览器中输入:http://localhost:8080/dmaList/ExportBySQL?sql=&names=分区级别&size=1 ...
- 【转】常见Java面试题 – 第一部分:非可变性(Immutability)和对象引用(Object reference)
ImportNew注: 本文是ImportNew编译整理的Java面试题系列文章之一.请看此系列相关面试题.你可以从这里查看全部的Java面试系列. 一些比较核心的Java问题经常会用来考验面试者的J ...
- express框架中使用nodemon自启动服务
1.安装nodemon //全局安装 npm install -g nodemon //本地安装 npm install nodemon --save 2.修改package.json配置 " ...
- TCP/IP协议与HTTP协议(一)
1.什么是TCP/IP 如果要了解一个人,可以从他归属的集体聊起来.我们的HTTP协议就属于TCP/IP协议家族中的一员,了解HTTP协议再整个网络流程中的地位,也能更加充分的理解HTTP协议. 要 ...
- 史上最简单的vi教程,10分钟包教会
从第一次接触vi/vim到现在已经十几年了,在这个过程中,来来回回,反反复复,学习vi很多次了. 虽然关于vi的使用,我还远未达到"专家"的水平,但对于vi的使用,我有话说. 1. ...
- Nginx作为负载均衡服务器——server参数讲解
upstream举例 upstream backend { server backend1.ecample.com weight = 5; # wwight 代表权重 server backend2. ...