前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【Java】线程、线程安全、线程状态

【Java】线程、线程安全、线程状态

作者头像
陶然同学
发布2023-02-27 13:08:42
1.7K0
发布2023-02-27 13:08:42
举报
文章被收录于专栏:陶然同学博客

👀专栏介绍

【Java】 目前主要更新Java,一起学习一起进步。

👀本期介绍

本期主要介绍线程、线程安全、线程状态

文章目录

第一章 线程

1.1 多线程原理

1.2 Thread类

1.3 创建线程方式二

1.4 Thread和Runnable的区别

1.5 匿名内部类方式实现线程的创建

第二章 线程安全

2.1 线程安全

2.2 线程同步

2.3 同步代码块

2.4 同步方法

2.5 Lock锁

第三章 线程状态

3.1 线程状态概述

3.2 Timed Waiting(计时等待)

3.3 BLOCKED(锁阻塞)

3.4 Waiting(无限等待)

3.5 补充知识点

第一章 线程

1.1 多线程原理

昨天的时候我们已经写过一版多线程的代码,很多同学对原理不是很清楚,那么我们今天先画个多

线程执行时序图

来体现一下多线程程序的执行流程。

代码如下:

自定义线程类:

测试类:

流程图:

程序启动运行 main 时候, java 虚拟机启动一个进程,主线程 main 在 main() 调用时候被创建。随着调

用 mt 的对象的

start 方法,另外一个新的线程也启动了,这样,整个应用就在多线程下运行。

通过这张图我们可以很清晰的看到多线程的执行流程,那么为什么可以完成并发执行呢?我们再来

讲一讲原理。

多线程执行时,到底在内存中是如何运行的呢?以上个程序为例,进行图解说明:

多线程执行时,在栈内存中,其实 每一个执行线程都有一片自己所属的栈内存空间 。进行方法的压

栈和弹栈。

当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进

程就结束了。

1.2 Thread类

在上一天内容中我们已经可以完成最基本的线程开启,那么在我们完成操作过程中用到了

java.lang.Thread 类,

API 中该类中定义了有关线程的一些方法,具体如下:

构造方法:

public Thread() : 分配一个新的线程对象。

public Thread(String name) : 分配一个指定名字的新的线程对象。

public Thread(Runnable target) : 分配一个带有指定目标新的线程对象。

public Thread(Runnable target,String name) : 分配一个带有指定目标新的线程对象并指定名字。

常用方法:

public String getName() : 获取当前线程名称。

public void start() : 导致此线程开始执行 ; Java 虚拟机调用此线程的 run 方法。

public void run() : 此线程要执行的任务在此处定义代码。

public static void sleep(long millis) : 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执

行)。

public static Thread currentThread() : 返回对当前正在执行的线程对象的引用。

翻阅 API 后得知创建线程的方式总共有两种,一种是继承 Thread 类方式,一种是实现 Runnable 接口

方式,方式一我

们上一天已经完成,接下来讲解方式二实现的方式。

1.3 创建线程方式二

采用 java.lang.Runnable 也是非常常见的一种,我们只需要重写 run 方法即可。

步骤如下:

1. 定义 Runnable 接口的实现类,并重写该接口的 run() 方法,该 run() 方法的方法体同样是该线程的

线程执行体。

2. 创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 来创建 Thread 对象,该 Thread 对

象才是真正

的线程对象。

3. 调用线程对象的 start() 方法来启动线程。

代码如下:

通过实现 Runnable 接口,使得该类有了多线程类的特征。 run() 方法是多线程程序的一个执行目

标。所有的多线程

代码都在 run 方法里面。 Thread 类实际上也是实现了 Runnable 接口的类。

在启动的多线程的时候,需要先通过 Thread 类的构造方法 Thread(Runnable target) 构造出对象,

然后调用 Thread

对象的 start() 方法来运行多线程代码。

实际上所有的多线程代码都是通过运行 Thread 的 start() 方法来运行的。因此,不管是继承 Thread 类

还是实现

Runnable 接口来实现多线程,最终还是通过 Thread 的对象的 API 来控制线程的,熟悉 Thread 类的

API 是进行多线程

编程的基础。

tips:Runnable 对象仅仅作为 Thread 对象的 target , Runnable 实现类里包含的 run() 方法仅作为线程

执行体。

而实际的线程对象依然是 Thread 实例,只是该 Thread 线程负责执行其 target 的 run() 方法。

1.4 Thread和Runnable的区别

如果一个类继承 Thread ,则不适合资源共享。但是如果实现了 Runable 接口的话,则很容易的实现

资源共享。

总结:

实现 Runnable 接口比继承 Thread 类所具有的优势:

1. 适合多个相同的程序代码的线程去共享同一个资源。

2. 可以避免 java 中的单继承的局限性。

3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。

4. 线程池只能放入实现 Runable 或 Callable 类线程,不能直接放入继承 Thread 的类。

扩充:在 java 中,每次程序运行至少启动 2 个线程。一个是 main 线程,一个是垃圾收集线程。因为

每当使用

java 命令执行一个类的时候,实际上都会启动一个 JVM ,每一个 JVM 其实在就是在操作系统中启动

了一个进

1.5 匿名内部类方式实现线程的创建

使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。

使用匿名内部类的方式实现 Runnable 接口,重新 Runnable 接口中的 run 方法:

第二章 线程安全

2.1 线程安全

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运

行的结果是一样

的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

我们通过一个案例,演示线程的安全问题:

电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “ 葫芦娃大战奥特曼 ” ,本次电影

的座位共 100 个

( 本场电影只能卖 100 张票 ) 。

我们来模拟电影院的售票窗口,实现多个窗口同时卖 “ 葫芦娃大战奥特曼 ” 这场电影票 ( 多个窗口一

起卖这 100 张票 )

需要窗口,采用线程对象来模拟;需要票, Runnable 接口子类来模拟

模拟票:

测试类:

结果中有一部分这样现象:

发现程序出现了两个问题:

1. 相同的票数 , 比如 5 这张票被卖了两回。

2. 不存在的票,比如 0 票与 -1 票,是不存在的。

这种问题,几个窗口 ( 线程 ) 票数不同步了,这种问题称为线程不安全。

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操

作,而无写

操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线

程同步,

否则的话就可能影响线程安全。

2.2 线程同步

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全

问题。

要解决上述多线程并发访问一个资源的安全性问题 : 也就是解决重复票与不存在票问题, Java 中提

供了同步机制

( synchronized ) 来解决。

根据案例简述:

为了保证每个线程都能正常执行原子操作 ,Java 引入了线程同步机制。

那么怎么去使用呢?有三种方式完成同步操作:

1. 同步代码块。

2. 同步方法。

3. 锁机制。

2.3 同步代码块

同步代码块 : synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行

互斥访问。

格式 :

同步锁 :

对象的同步锁只是一个概念 , 可以想象为在对象上标记了一个锁 .

1. 锁对象 可以是任意类型。

2. 多个线程对象 要使用同一把锁。

注意 : 在任何时候 , 最多允许一个线程拥有同步锁 , 谁拿到锁就进入代码块 , 其他的线程只能在外等着

(BLOCKED) 。

使用同步代码块解决代码:

当使用了同步代码块后,上述的线程的安全问题,解决了

2.4 同步方法

同步方法 : 使用 synchronized 修饰的方法 , 就叫做同步方法 , 保证 A 线程执行该方法的时候 , 其他线程只

能在方法外

等着。

格式:

同步锁是谁 ?

对于非 static 方法 , 同步锁就是 this 。

对于 static 方法 , 我们使用当前方法所在类的字节码对象 ( 类名 .class) 。

使用同步方法代码如下:

2.5 Lock锁

java.util.concurrent.locks.Lock 机制提供了比 synchronized 代码块和 synchronized 方法更广泛的

锁定操作 ,

同步代码块 / 同步方法具有的功能 Lock 都有 , 除此之外更强大 , 更体现面向对象。

Lock 锁也称同步锁,加锁与释放锁方法化了,如下:

public void lock() : 加同步锁。

public void unlock() : 释放同步锁。

使用如下:

第三章 线程状态

3.1 线程状态概述

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程

的生命周期中,

有几种状态呢?在 API 中 java.lang.Thread.State 这个枚举中给出了六种线程状态:

这里先列出各个线程状态发生的条件,下面将会对每种状态进行详细解析

我们不需要去研究这几种状态的实现原理,我们只需知道在做线程操作中存在这样的状态。那我们

怎么去理解这几

个状态呢,新建与被终止还是很容易理解的,我们就研究一下线程从 Runnable (可运行)状态与

非运行状态之间

的转换问题。

3.2 Timed Waiting(计时等待)

Timed Waiting 在 API 中的描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于

这一状态。单独

的去理解这句话,真是玄之又玄,其实我们在之前的操作中已经接触过这个状态了,在哪里呢?

在我们写卖票的案例中,为了减少线程执行太快,现象不明显等问题,我们在 run 方法中添加了

sleep 语句,这样就

强制当前正在执行的线程休眠( 暂停执行 ),以 “ 减慢线程 ” 。

其实当我们调用了 sleep 方法之后,当前执行的线程就进入到 “ 休眠状态 ” ,其实就是所谓的 Timed

Waiting( 计时等

待 ) ,那么我们通过一个案例加深对该状态的一个理解。

实现一个计数器,计数到 100 ,在每个数字之间暂停 1 秒,每隔 10 个数字输出一个字符串

代码:

通过案例可以发现, sleep 方法的使用还是很简单的。我们需要记住下面几点:

1. 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不

一定非要有协

作关系。

2. 为了让其他线程有机会执行,可以将 Thread.sleep() 的调用 放线程 run() 之内 。这样才能保证该线

程执行过程

中会睡眠

3. sleep 与锁无关,线程睡眠到期自动苏醒,并返回到 Runnable (可运行)状态。

小提示: sleep() 中指定的时间是线程不会运行的最短时间。因此, sleep() 方法不能保证该线程睡眠

到期后就

开始立刻执行。

Timed Waiting 线程状态图:

3.3 BLOCKED(锁阻塞)

Blocked 状态在 API 中的介绍为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状

态。

我们已经学完同步机制,那么这个状态是非常好理解的了。比如,线程 A 与线程 B 代码中使用同一

锁,如果线程 A 获

取到锁,线程 A 进入到 Runnable 状态,那么线程 B 就进入到 Blocked 锁阻塞状态。

这是由 Runnable 状态进入 Blocked 状态。除此 Waiting 以及 Time Waiting 状态也会在某种情况下进入

阻塞状态,而

这部分内容作为扩充知识点带领大家了解一下。

Blocked 线程状态图

3.4 Waiting(无限等待)

Wating 状态在 API 中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程

处于这一状态。

那么我们之前遇到过这种状态吗?答案是并没有,但并不妨碍我们进行一个简单深入的了解。我们

通过一段代码来

学习一下:

通过上述案例我们会发现,一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用

此对象的

Object.notify() 方法 或 Object.notifyAll() 方法。

其实 waiting 状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间

的协作关系,

多个线程会争取锁,同时相互之间又存在协作关系。就好比在公司里你和你的同事们,你们可能存

在晋升时的竞

争,但更多时候你们更多是一起合作以完成某些任务。

当多个线程协作时,比如 A , B 线程,如果 A 线程在 Runnable (可运行)状态中调用了 wait() 方法那

么 A 线程就进入

了 Waiting (无限等待)状态,同时失去了同步锁。假如这个时候 B 线程获取到了同步锁,在运行状

态中调用了

notify() 方法,那么就会将无限等待的 A 线程唤醒。注意是唤醒,如果获取到锁对象,那么 A 线程唤

醒后就进入

Runnable (可运行)状态;如果没有获取锁对象,那么就进入到 Blocked (锁阻塞状态)。

Waiting 线程状态图

3.5 补充知识点

到此为止我们已经对线程状态有了基本的认识,想要有更多的了解,详情可以见下图:

一条有意思的 tips:

我们在翻阅 API 的时候会发现 Timed Waiting (计时等待) 与 Waiting (无限等待) 状态联系还是很

紧密的,

比如 Waiting (无限等待) 状态中 wait 方法是空参的,而 timed waiting (计时等待) 中 wait 方法是

带参的。

这种带参的方法,其实是一种倒计时操作,相当于我们生活中的小闹钟,我们设定好时间,到时通

知,可是

如果提前得到(唤醒)通知,那么设定好时间在通知也就显得多此一举了,那么这种设计方案其实

是一举两

得。如果没有得到(唤醒)通知,那么线程就处于 Timed Waiting 状态 , 直到倒计时完毕自动醒来;

如果在倒

计时期间得到(唤醒)通知,那么线程从 Timed Waiting 状态立刻唤醒。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-11-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 👀专栏介绍
  • 👀本期介绍
  • 文章目录
  • 第一章 线程
    • 1.1 多线程原理
      • 1.2 Thread类
        • 1.3 创建线程方式二
          • 1.4 Thread和Runnable的区别
            • 1.5 匿名内部类方式实现线程的创建
            • 第二章 线程安全
              • 2.1 线程安全
                • 2.2 线程同步
                  • 2.3 同步代码块
                    • 2.4 同步方法
                      • 2.5 Lock锁
                      • 第三章 线程状态
                        • 3.1 线程状态概述
                          • 3.2 Timed Waiting(计时等待)
                            • 3.3 BLOCKED(锁阻塞)
                              • 3.4 Waiting(无限等待)
                                • 3.5 补充知识点
                                领券
                                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档