线程作为操作系统中最少调度单位,在当前系统的运行环境中,一般都拥有多核处理器,为了更好的充分利用 CPU,掌握其正确使用方式,能更高效的使程序运行。同时,在 Java 面试中,也是极其重要的一个模块。

线程简介

一个独立运行的程序是一个进程,一个进程中可以包含一个或多个线程,每个线程都有属于自己的一些属性,如堆栈,计数器等等。同时,一个线程在一个时间点上只能运行在一个 CPU 处理器核心上,不同线程之间也可以访问共享变量。线程在运行时,系统给每个线程分配一些 CPU 时间片,CPU 在时间片这段时间运行某个线程,当这个时间片运行完又跳转至下一段时间片线程,CPU 在这些线程中进行高速切换,使得程序像是在同时进行多个线程操作。

线程的实现

实现线程常用的两种方式:继承 java.lang.Thread 类、实现 java.lang.Runnable 接口。

继承 Thread 类方式

通过实例化 java.lang.Thread 类获得线程。创建 Thread 对象,一般使用继承 Thread 类的方式,然后通过方法重写覆盖 Thread 的某些方法。

首先创建一个继承 Thread 的子类。

public class DemoThread extends Thread{

    // 重写 Thread 类中的 run 方法
@Override
public void run() {
// currentThread().getName() 获取当前线程名称
System.out.println("java.lang.Thread 创建的"+ currentThread().getName() +"线程");
}
}

上面代码 DemoThread 实例化的对象就代表一个线程,通过重写 run 方法,在 run 方法中实现该线程的逻辑实现。

public class Main {

    public static void main(String[] args) {
// 实例化 DemoThread 得到新创建的线程实例
DemoThread thread = new DemoThread();
// 给创建的子线程命名
thread.setName("DemoThread 子线程");
// 启动线程
thread.start(); // 通过主线程打印信息
System.out.println("main 线程");
} }

在程序执行的主线程中创建子线程,并且命名为DemoThread 子线程,在程序的最后打印主线程打印的信息。调用线程必须调用start()方法,在调用此方法之前,子线程是不存在的,只有start()方法调用后,才是真正的创建了线程。

执行结果:

从结果可以看到,由于在主线程中创建了一个子线程,子线程相对于主线程就相当于是一个异步操作,所以打印结果就有可能main线程先于子线程执行打印操作。

实现 Runnable 接口方式

由于 Java 是单继承的特性,所以当创建线程的子类继承了其他的类,就无法实现继承操作。这时就可以通过实现 Runnable 接口,来实现线程创建的逻辑。

首先创建一个实现 Runnable 的类。

public class DemoRunnable implements Runnable {

    // 实现 Runnable 中的 run 方法
@Override
public void run() {
System.out.println("java.lang.Runnable 创建的 "+ Thread.currentThread().getName() +"线程");
}
}

Runnable 接口中定义有一个 run 方法,所以实现 Runnable 接口,就必须实现 run 方法。实际上 java.lang.Thread 类也实现了 Runnable 接口。

创建线程:

public class Main {

    public static void main(String[] args) {
// 创建 Thread 实例,并给将要创建的线程给命名
Thread thread = new Thread(new DemoRunnable(), "DemoRunnable 子线程");
// 创建一个线程
thread.start(); System.out.println("main 线程");
} }

执行结果

同样也实现了与继承 Thread 方式一样的结果。

创建 Thread 实例时,向新创建的 Thread 实例中传入了一个实现 Runnable 接口的对象的参数。

Thread 中初始化 Thread#init 的具体实现:

    private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
} // 给当前创建的 thread 实例中赋值线程名
this.name = name;
// 将要创建的线程的父线程即当前线程
Thread parent = currentThread();
// 添加到线程组操作
SecurityManager security = System.getSecurityManager();
if (g == null) {
if (security != null) {
g = security.getThreadGroup();
} if (g == null) {
g = parent.getThreadGroup();
}
}
g.checkAccess(); if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
// 线程组中添加为启动的线程数
g.addUnstarted();
this.group = g;
// 设置父线程的一些属性到当前将要创建的线程
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext(); // 将当前传入 target 的参数,赋值给当前 Thread 对象,使其持有 已实现 Runnable 接口的实例
this.target = target;
setPriority(priority);
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); // 设置线程的堆栈大小
this.stackSize = stackSize; // 给创建的线程一个 id
tid = nextThreadID();
}

上面代码创建 thread 对象时的 init 方法,通过传入 Runnable 的实例对象,thread 对象中就持有该对象。

创建 thread 对象后,调用 start() 方法,该线程就运行持有 Runnable 实现类对象的 run() 方法。

例如本文中案例,就会执行 DemoRunnable#run 方法的逻辑。

这两种方法创建线程的方式,具体使用哪种,根据自身需求选择。如果需要继承其他非 Thread 类,就需要使用 Runnable 接口。

线程状态

Java 线程每个时间点都存在于6种状态中一种。

状态 描述
NEW 初始状态,thread 对象调用 start() 方法前
RUNNABLE 运行状态,线程 start() 后的就绪或运行中
BLOCKED 阻塞状态,线程获得锁后的锁定状态
WAITING 等待状态,线程进入等待状态,不会被分配时间片,需要等待其他线程来唤醒
TIME_WAITING 超时等待状态,同样不分配时间片,当时间达到设定的等待时间后自动唤醒
TERMINATED 终止状态,表示当前线程执行完成

其中 NEW、RUNNABLE、TERMINATED 比较好理解,现在主要针对 BLOCKED、WAITING 和 TIME_WAITING 进行案例讲解。

BLOCKED

阻塞状态 是将两个线程之间处于竞争关系,同时在调用 run 时进行加锁。

首先还是使用上面 Runnable 实现的方式进行改造。

public class DemoRunnable implements Runnable {

    @Override
public void run() {
// 通过对DemoRunnable加同步锁,进行无限循环不退出
synchronized (DemoRunnable.class){
while (true){
System.out.println("java.lang.Runnable 创建的 "+ Thread.currentThread().getName() +"线程");
}
}
}
}

先竞争到 DemoRunnable 类的线程进入 run 会一直执行下去,未竞争到的线程则会一直处于阻塞状态。

创建两个线程

public class Main {

    public static void main(String[] args) {
// 创建两个线程测试
new Thread(new DemoRunnable(), "test-blocked-1")
.start();
new Thread(new DemoRunnable(), "test-blocked-2")
.start();
} }

通过分析执行后的线程如图:

可以得知线程test-blocked-1竞争到 DemoRunnable 类,一直都在运行 while 循环,所以状态为 RUNNABLE。由于 DemoRunnable#run 中加了同步锁锁住 DemoRunnable 类,所以test-blocked-2一直处于 BLOCKED 阻塞状态。

WAITING

等待状态 线程是不被分配 CPU 时间片,线程如果要重新被唤醒,必须显示被其它线程唤醒,否则会一直等待下去。

实现等待状态例子

public class DemoRunnable implements Runnable {

    @Override
public void run() {
while (true){
// 调用 wait 方法,使线程在当前实例上处于等待状态
synchronized (this){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("java.lang.Runnable 创建的 "+ Thread.currentThread().getName() +"线程");
}
}
}
} // 创建线程
public class Main { public static void main(String[] args) {
new Thread(new DemoRunnable(), "test-waiting")
.start();
} }

创建该实例线程后,分析 test-waiting 线程,该线程处于 WAITING 状态。

TIME_WAITING

超时等待状态 线程也是不被分配 CPU 时间片,但是它通过设置的间隔时间后,可以自动唤醒当前线程。也就是说,将等待状态的线程加个时间限制就是超时等待状态。

只需对上面 WAITING 状态案例增加 wait 时间限制。

public class DemoRunnable implements Runnable {

    @Override
public void run() {
while (true){
synchronized (this){
try {
// 增加等待时长
this.wait(1000000, 999999);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("java.lang.Runnable 创建的 "+ Thread.currentThread().getName() +"线程");
}
}
}
}

分析线程结果,可以看到 test-time_waiting 线程处于超时等待状态,使用 sleep 睡眠时,线程也是属于超时等待状态。

线程状态之间的转换,如图(来源网络):

Thread 常用方法

currentThread()

currentThread 是获取当前线程实例,返回 Thread 对象,这是一个静态方法,使用如下

Thread.currentThread();

start()

start 方法是启动线程的入口方法,这个就是上面实现创建线程例子中的 start 方法。

run()

run 方法是线程创建后,线程会主动调用 run 方法执行里面的逻辑。

join()

join 方法即线程同步,比如上继承 Thread 方法实现创建线程的例子中,如果在 thread.start() 后调用 thread.join() 方法,则 main 线程打印的信息一定在子线程打印的信息之后。这里的 main 线程会等待子线程执行完后,再继续执行。

getName()

getName 返回线程名称。

getId()

获取线程 Id,这是返回一个 long 类型的 Id 值。

setDaemon()

setDaemon(boolean on) 方法是设置线程类型,setDaemon 接受一个 boolean 类型参数。设置为 true 时,线程类型为守护线程,设置为 false 时,线程类型为用户线程。

yield()

yield 方法是线程让步,让当前线程进入就绪状态,去执行其它相同优先级的线程,但不一定会执行其他线程,有可能让步后的线程再次被执行。

setPriority()

setPriority(int newPriority) 是设置线程执行的优先级,数值为1~10,默认值为5,数值越大线程越先执行。

interrupt()

interrupt 方法的作用是中断线程,但是它还是会继续运行。它只是表示其他线程给打了个中断标志。

interrupted()

interrupted 方法是检查当前线程是否被中断。调用此方法时会清除该线程的中断标志。

isInterrupted()

isInterrupted 方法检测当前线程是否被中断,如果被中断了,也不会清除中断标志。

总结

本文对线程的常用功能及概念进行了分析,主要是讲解单线程的一些操作,线程操作的使用在生产中是极容易出现问题的,所以在掌握概念和使用后,需要多研究,多思考应用的设计及实现。在掌握多线程操作时,必须对这些的基本使用和概念进行掌握,今后会出进一步对多线程分析的文章。

推荐阅读

《你必须会的 JDK 动态代理和 CGLIB 动态代理》

《Dubbo 扩展点加载机制:从 Java SPI 到 Dubbo SPI》

《volatile 手摸手带你解析》

Java 线程基础,从这篇开始的更多相关文章

  1. Java 线程基础

    Java 线程基础

  2. java 线程基础篇,看这一篇就够了。

    前言: Java三大基础框架:集合,线程,io基本是开发必用,面试必问的核心内容,今天我们讲讲线程. 想要把线程理解透彻,这需要具备很多方面的知识和经验,本篇主要是关于线程基础包括线程状态和常用方法. ...

  3. java线程基础巩固---线程生命周期以及start方法源码剖析

    上篇中介绍了如何启动一个线程,通过调用start()方法才能创建并使用新线程,并且这个start()是非阻塞的,调用之后立马就返回的,实际上它是线程生命周期环节中的一种,所以这里阐述一下线程的一个完整 ...

  4. Java线程基础实例

    概述 Java线程是一个在实战开发中经常使用的基础功能,而在Java中线程相关的类在java.lang和java.util.concurrent里 Thread package thread.base ...

  5. Java线程基础知识(状态、共享与协作)

    1.基础概念 CPU核心数和线程数的关系 核心数:线程数=1:1 ;使用了超线程技术后---> 1:2 CPU时间片轮转机制 又称RR调度,会导致上下文切换 什么是进程和线程 进程:程序运行资源 ...

  6. java线程基础知识----线程与锁

    我们上一章已经谈到java线程的基础知识,我们学习了Thread的基础知识,今天我们开始学习java线程和锁. 1. 首先我们应该了解一下Object类的一些性质以其方法,首先我们知道Object类的 ...

  7. java线程基础知识----线程基础知识

    不知道从什么时候开始,学习知识变成了一个短期记忆的过程,总是容易忘记自己当初学懂的知识(fuck!),不知道是自己没有经常使用还是当初理解的不够深入.今天准备再对java的线程进行一下系统的学习,希望 ...

  8. Java 线程基础知识

    前言 什么是线程?线程,有时被称为轻量进程(Lightweight Process,LWP),是程序执行流的最小单元.一个标准的线程由线程 ID,当前指令指针 (PC),寄存器集合和堆栈组成.另外,线 ...

  9. JAVA线程基础

    一.线程状态 由于参考的维度不一样,线程状态划分也不一样,我这里简单的分为5大类,并且会说明状态变迁的详细过程:

随机推荐

  1. Liunx下使用wine容器实现跨平台使用软件

    首先在Liunx中使用QQ,网易云音乐,等这些软件是很痛苦的,某些软件可能会有Liunx版本,但是像腾讯QQ早年前也提供过Linux版本,后来就下架了!!! 这里我以ubuntu18.04版本为列,讲 ...

  2. Spring Boot笔记(五) SpringBoot 集成Lombok 插件

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 为了减少代码量,为当前项目添加 lombok 来优雅编码 Lombok 插件安装: a . 添加依赖: ...

  3. python3 pymysql查询结果包含字段名

    python2使用MySQLdb模块进行连接mysql数据库进行操作:python3则使用pymysql模块进行连接mysql数据库进行操作:两者在语法上有稍微的差别,其中就包括查询结果包含字段名,具 ...

  4. Java实现 蓝桥杯 算法提高 八数码(BFS)

    试题 算法提高 八数码 问题描述 RXY八数码 输入格式 输入两个33表格 第一个为目标表格 第二个为检索表格 输出格式 输出步数 样例输入 1 2 3 4 5 6 7 8 0 1 2 3 4 5 6 ...

  5. 第九届蓝桥杯JavaB组国(决)赛真题

    解题代码部分来自网友,如果有不对的地方,欢迎各位大佬评论 题目1.三角形面积 已知三角形三个顶点在直角坐标系下的坐标分别为: (2.3, 2.5) (6.4, 3.1) (5.1, 7.2) 求该三角 ...

  6. Java实现奇偶数排序

    1 问题描述 给定一个整数数组,请调整 数组中数的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分.要求时间复杂度为O(n). 2 解决方案 2.1 一头一尾指针往中间扫描法 pack ...

  7. java实现顺时针螺旋填入

    从键盘输入一个整数(1~20) 则以该数字为矩阵的大小,把 1,2,3-n*n 的数字按照顺时针螺旋的形式填入其中.例如: 输入数字 2,则程序输出: 1 2 4 3 输入数字 3,则程序输出: 1 ...

  8. vue axios封装

    前言: 对第三方库进行二次封装和抽离到统一模块,项目面对自己的模块进行开发.如果有一天更换库,只需要修改自己模块中的代码,无需对整个项目进行重构. 将axios网络请求库封装到network文件下的r ...

  9. HDU - 2546 饭卡 题解

    题目大意 电子科大本部食堂的饭卡有一种很诡异的设计,即在购买之前判断余额.如果购买一个商品之前,卡上的剩余金额大于或等于5元,就一定可以购买成功(即使购买后卡上余额为负),否则无法购买(即使金额足够) ...

  10. R调用python模块

    明明已经安装了sctransfer,但仍然显示没有该模块 Error in py_module_import(module, convert = convert) : ModuleNotFoundEr ...