Java并发实战一:线程与线程安全
从零开始创建一家公司
Java并发编程是Java的基础之一,为了能在实践中学习并发编程,我们跟着创建一家公司的旅途,一起来学习Java并发编程。
进程与线程
由于我们的目标是学习并发编程,所以我不会把很多时间放在底层原理和复杂的概念上。操作系统上的进程就像是全国各地的公司,而每个公司又都有许多员工--线程。关于进程与线程的关系先了解这么多。
创建一个线程
想象你现在成立了一个互联网公司,你准备先设立一个总经理的岗位,而你自己是幕后Boss,Main线程。
public class App {
public static void main(String[] args) throws Exception {
Thread manager = new Thread(new manager());
manager.start();
}
}
class manager implements Runnable {
@Override
public void run() {
System.out.println("我是总经理");
}
}
让我们一步步看上面的代码,首先为了区分公司的员工和其他人,每个员工要有统一的标识,都属于 Thread 类。这个Thread就是我们公司的员工标识,只要是这个类的对象都属于你的公司。
虽然已经有了 Thread 标识,但公司的每个人的职责都不同,所以还要进一步的细分。向Thread构造函数传入不同的实现Runnable接口的对象,可以获得不同职责的员工。
不同员工的职责由不同的 run 方法实现区分。manager.start()方法就是默认调用Runnable接口中的run方法。同时Runnable之所以是个接口,是因为继承只能单一,而接口可以实现多个,就像我们公司的员工,在社会上还可能有其他位置一样。
终止线程
我们公司当然要有下班制度,重新实现如下。
public class App {
public static void main(String[] args) throws Exception {
Thread manager = new Thread(new manager());
manager.start();
manager.interrupt();
}
}
class manager implements Runnable {
@Override
public void run() {
while(true) {
if(Thread.currentThread().isInterrupted()) break;
System.out.println("正在上班中");
}
}
}
可以看到 manager 类中加了个循环表示持续的上班状态。当到下班时间时,你(Main线程)调用经理的 interrupt 方法通知他该下班了。
注意,这里的 interrupt方法不会直接结束上班状态,只是通知。而经理根据自己 run 方法的实现来决定到底怎么下班。
用 Thread.currentThread().isInterrupted() 方法来判断是否收到通知。 Thread.currentThread()代表当前对象,之所以不是当前类,是因为你有时候会想只通知某些特定的员工下班,而不是每次通知都只能让所有员工下班。
还有一个 interrupted 方法,和 isInterrupted 作用相同,都是查询当前状态,不过前一个方法查询的同时会清除状态。如果员工都是收到中断请求就下班,那二者没有什么区别。但对某些需要收到特定次数下班通知才会下班的员工来说,用 interrupted方法就特别合适。
线程休眠
当员工在上班时间却感觉疲倦怎么办,幸好我们有休息制度。
class manager implements Runnable {
@Override
public void run() {
while(true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
if(Thread.currentThread().isInterrupted()) break;
System.out.println("我是总经理");
}
}
}
可以看到代码中增加了静态方法 Thread.sleep 和一个异常处理机制。线程会先休眠1秒后在执行下面的步骤。
虽然 sleep 是静态方法,但是只对当前线程其作用。这样设计的原因是为了防止别的线程调用该线程的休眠方法,也就是说,只有当前线程才能控制当前线程的休眠状态。
由于线程休眠过程中无法处理中断,所以当线程休眠时收到中断请求,就会抛出异常,在异常处理中决定如何中断。
总结
关于线程的基本情况基本这么多,可以看到麻雀虽小,五脏俱全。有唯一的标识Thread,可以实现自己的方法,可以响应中断,可以进行休眠等待。一个员工的工作周期已经初步成型,接下来我们看看简单的员工之间的合作。
等待(wait)和通知(notify)
等待与通知这两个方法和前面介绍的最大的不同在于,由于要负责线程之间的协作,这两个方法是属于object的而不是Thread的。
什么意思呢?这使得可以调用 Main 线程中的对象的wait方法来中断 Main线程。
具体过程如下 当一个线程调用等待方法时,它会加入一个等待队列。由于有多个线程可能拥有该对象,当不同线程先后调用这个方法时,都会加入这个对象的等待队列中。
当 notify 方法被某一线程调用时,就会在这个等待队列中随机挑出一个线程唤醒(并不是先到先得)。
要注意的是 使用 wai() 和 notify 的关键字必须要加锁,代码加粗部分
public class App {
public static void main(String[] args) throws Exception {
Thread manager = new Thread(new manager());
manager.start();
manager.interrupt();
synchronized(manager){ manager.notify(); }
}
}
class manager implements Runnable {
@Override
public void run() {
while(true) {
synchronized(this) {
try {
this.wait();
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
if(Thread.currentThread().isInterrupted()) break;
System.out.println("我是总经理");
}
}
}
}
如果不加锁编译不会报错,但执行会会有 current thread is not owner 异常,意思是当前线程没有获得对象的锁就调用了 wait 方法,notify 的方法同理,也必须要先获取锁才能执行。
过程就是 加锁---- 等待(wait)-----释放锁 -----加锁------通知(notify)------ 继续执行。但通知后不会释放锁,所以调用 wait 方法的线程要先等该线程执行完释放锁后才能继续执行。
至于为什么要加锁呢?如果没有加锁,就会出现丢失唤醒问题,既 notify 方法早于 wait 调用,导致 wait 的线程一直接收不到唤醒信号。
为什么加锁就能避免,难道 notify 线程没有可能先执行吗?其实确实即使加锁后 notify 也可能先于 wait 执行。因为这两个方法是负责线程协作的,所以一般代码逻辑是用户来写出,用户来避免 notify 先于wait执行,但如果没有加锁,即使用户的逻辑正确也可能导致 notify 先于 wait 执行,这也是个并发问题。
等待线程结束(join)和谦让(yeild)
等待结束和谦让是另外的一种线程间协作的方式,上文提到的等待和通知是基于线程内部方法的,而等待结束是等待线程整体的,可以说是线程协作的一种补充。
看个代码
public class App {
public static void main(String[] args) throws Exception {
Thread manager = new Thread(new manager());
manager.start();
manager.join();
}
}
}
class manager implements Runnable {
@Override
public void run() {
Thread.sleep(1000);
}
}
}
在这个代码中 Main 线程会等待 manger 睡眠结束后才会继续执行,期间一直处于阻塞状态。
还有一点很有趣,join 本质是让当前线程调用该对象的 wait 方法,比如上文代码,本质是 Main 线程调用 manager 的 wait 方法,再此对象上等待,而该被等待的线程结束后,会调用 notifyAll 方法告诉所有被等待的线程结束等待。所以最好不要在线程上调用 wait 方法,因为可能被 join 的 notifyAll 意外唤醒。
最后思考一下 jion 和 wait notify 之间的使用场景是很有意思的,wait 和 notify 是 线程调用对象(由于join的存在,这个对象不能是线程!)的方法来进行协作,一个线程调用wait进入阻塞,另一个线程调用notify方法唤醒,一共三个对象(两个线程,一个协作对象)。而 jion 的场景则是 一个线程调用 wait 方法等待一个线程,该线程调用notifyAll 方法唤醒该线程,没有第三者。所以 jion 可以理解为两个线程的互相协作,而 wait notify 是两个线程通过一个对象进行协作,当然只是可以这样理解,具体本质还需要好好在实践生活中使用才能慢慢领会到。
Java并发实战一:线程与线程安全的更多相关文章
- Java并发编程:如何创建线程?
Java并发编程:如何创建线程? 在前面一篇文章中已经讲述了在进程和线程的由来,今天就来讲一下在Java中如何创建线程,让线程去执行一个子任务.下面先讲述一下Java中的应用程序和进程相关的概念知识, ...
- Java 并发编程——Executor框架和线程池原理
Eexecutor作为灵活且强大的异步执行框架,其支持多种不同类型的任务执行策略,提供了一种标准的方法将任务的提交过程和执行过程解耦开发,基于生产者-消费者模式,其提交任务的线程相当于生产者,执行任务 ...
- 【转】Java并发编程:如何创建线程?
一.Java中关于应用程序和进程相关的概念 在Java中,一个应用程序对应着一个JVM实例(也有地方称为JVM进程),一般来说名字默认是java.exe或者javaw.exe(windows下可以通过 ...
- [Java并发编程(二)] 线程池 FixedThreadPool、CachedThreadPool、ForkJoinPool?为后台任务选择合适的 Java executors
[Java并发编程(二)] 线程池 FixedThreadPool.CachedThreadPool.ForkJoinPool?为后台任务选择合适的 Java executors ... 摘要 Jav ...
- [Java并发编程(一)] 线程池 FixedThreadPool vs CachedThreadPool ...
[Java并发编程(一)] 线程池 FixedThreadPool vs CachedThreadPool ... 摘要 介绍 Java 并发包里的几个主要 ExecutorService . 正文 ...
- Java 并发编程——Executor框架和线程池原理
Java 并发编程系列文章 Java 并发基础——线程安全性 Java 并发编程——Callable+Future+FutureTask java 并发编程——Thread 源码重新学习 java并发 ...
- 2、Java并发编程:如何创建线程
Java并发编程:如何创建线程? 在前面一篇文章中已经讲述了在进程和线程的由来,今天就来讲一下在Java中如何创建线程,让线程去执行一个子任务.下面先讲述一下Java中的应用程序和进程相关的概念知识, ...
- 和朱晔一起复习Java并发(一):线程池
和我之前的Spring系列文章一样,我们会以做一些Demo做实验的方式来复习一些知识点. 本文我们先从Java并发中最最常用的线程池开始. 从一个线程池实验开始 首先我们写一个方法来每秒一次定时输出线 ...
- 原创】Java并发编程系列2:线程概念与基础操作
[原创]Java并发编程系列2:线程概念与基础操作 伟大的理想只有经过忘我的斗争和牺牲才能胜利实现. 本篇为[Dali王的技术博客]Java并发编程系列第二篇,讲讲有关线程的那些事儿.主要内容是如下这 ...
随机推荐
- [leetcode] 33. 搜索旋转排序数组(Java)
33. 搜索旋转排序数组 说实话这题我连题都没有看懂....真是醉了 二分,没意思,直接交了- - https://www.jiuzhang.com/solutions/search-in-rotat ...
- Go语言流程控制03--goto跳转到任意标签位置
package main import ( "fmt" "time" ) func main() { STUDYHARD: fmt.Println(" ...
- C# 移除字符串头尾指定字符
1 private void button1_Click(object sender, EventArgs e) 2 {//去掉字符串头尾指定字符 3 string MyInfo= "--中 ...
- 201871030129-魏琦 实验三 结对项目—《D{0-1}KP 实例数据集算法实验平台》项目报告
项目 内容 课程班级博客链接 班级博客 这个作业要求链接 作业链接 我的课程学习目标 (1)掌握Github协作开发程序的操作方法:(2)理解并掌握代码风格规范.代码设计规范.代码复审.结对编程概念: ...
- 通过 DLPack 构建跨框架深度学习编译器
通过 DLPack 构建跨框架深度学习编译器 深度学习框架,如Tensorflow, PyTorch, and ApacheMxNet,快速原型化和部署深度学习模型提供了强大的工具箱.不幸的是,易用性 ...
- ICCV2019论文点评:3D Object Detect疏密度点云三维目标检测
ICCV2019论文点评:3D Object Detect疏密度点云三维目标检测 STD: Sparse-to-Dense 3D Object Detector for Point Cloud 论文链 ...
- RGB-D对红外热像仪和毫米波雷达标定
RGB-D对红外热像仪和毫米波雷达标定 Extrinsic Calibration of Thermal IR Camera and mmWave Radar by Exploiting Depth ...
- Pipe Utilization管道利用率
Pipe Utilization管道利用率 概述 CUDA设备的每个流式多处理器(SM)都具有许多专门用于执行特定任务的硬件单元.在芯片级,这些单元提供执行管道,翘曲调度程序将指令发送到这些管道.例如 ...
- Task05:SQL高级处理
5.1 窗口函数 5.1.1 窗口函数概念及基本的使用方法 窗口函数也称为OLAP函数.OLAP 是OnLine AnalyticalProcessing 的简称,意思是对数据库数据进行实时分析处理. ...
- 【NX二次开发】Block UI 线性尺寸
属性说明 常规 类型 描述 BlockID String 控件ID Enable Logical 是否可操作 Group Logical ...