从零开始创建一家公司

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 线程调用 managerwait 方法,再此对象上等待,而该被等待的线程结束后,会调用 notifyAll 方法告诉所有被等待的线程结束等待。所以最好不要在线程上调用 wait 方法,因为可能被 joinnotifyAll 意外唤醒。

最后思考一下 jionwait notify 之间的使用场景是很有意思的,wait notify 是 线程调用对象(由于join的存在,这个对象不能是线程!)的方法来进行协作,一个线程调用wait进入阻塞,另一个线程调用notify方法唤醒,一共三个对象(两个线程,一个协作对象)。而 jion 的场景则是 一个线程调用 wait 方法等待一个线程,该线程调用notifyAll 方法唤醒该线程,没有第三者。所以 jion 可以理解为两个线程的互相协作,而 wait notify 是两个线程通过一个对象进行协作,当然只是可以这样理解,具体本质还需要好好在实践生活中使用才能慢慢领会到。

Java并发实战一:线程与线程安全的更多相关文章

  1. Java并发编程:如何创建线程?

    Java并发编程:如何创建线程? 在前面一篇文章中已经讲述了在进程和线程的由来,今天就来讲一下在Java中如何创建线程,让线程去执行一个子任务.下面先讲述一下Java中的应用程序和进程相关的概念知识, ...

  2. Java 并发编程——Executor框架和线程池原理

    Eexecutor作为灵活且强大的异步执行框架,其支持多种不同类型的任务执行策略,提供了一种标准的方法将任务的提交过程和执行过程解耦开发,基于生产者-消费者模式,其提交任务的线程相当于生产者,执行任务 ...

  3. 【转】Java并发编程:如何创建线程?

    一.Java中关于应用程序和进程相关的概念 在Java中,一个应用程序对应着一个JVM实例(也有地方称为JVM进程),一般来说名字默认是java.exe或者javaw.exe(windows下可以通过 ...

  4. [Java并发编程(二)] 线程池 FixedThreadPool、CachedThreadPool、ForkJoinPool?为后台任务选择合适的 Java executors

    [Java并发编程(二)] 线程池 FixedThreadPool.CachedThreadPool.ForkJoinPool?为后台任务选择合适的 Java executors ... 摘要 Jav ...

  5. [Java并发编程(一)] 线程池 FixedThreadPool vs CachedThreadPool ...

    [Java并发编程(一)] 线程池 FixedThreadPool vs CachedThreadPool ... 摘要 介绍 Java 并发包里的几个主要 ExecutorService . 正文 ...

  6. Java 并发编程——Executor框架和线程池原理

    Java 并发编程系列文章 Java 并发基础——线程安全性 Java 并发编程——Callable+Future+FutureTask java 并发编程——Thread 源码重新学习 java并发 ...

  7. 2、Java并发编程:如何创建线程

    Java并发编程:如何创建线程? 在前面一篇文章中已经讲述了在进程和线程的由来,今天就来讲一下在Java中如何创建线程,让线程去执行一个子任务.下面先讲述一下Java中的应用程序和进程相关的概念知识, ...

  8. 和朱晔一起复习Java并发(一):线程池

    和我之前的Spring系列文章一样,我们会以做一些Demo做实验的方式来复习一些知识点. 本文我们先从Java并发中最最常用的线程池开始. 从一个线程池实验开始 首先我们写一个方法来每秒一次定时输出线 ...

  9. 原创】Java并发编程系列2:线程概念与基础操作

    [原创]Java并发编程系列2:线程概念与基础操作 伟大的理想只有经过忘我的斗争和牺牲才能胜利实现. 本篇为[Dali王的技术博客]Java并发编程系列第二篇,讲讲有关线程的那些事儿.主要内容是如下这 ...

随机推荐

  1. [leetcode] 33. 搜索旋转排序数组(Java)

    33. 搜索旋转排序数组 说实话这题我连题都没有看懂....真是醉了 二分,没意思,直接交了- - https://www.jiuzhang.com/solutions/search-in-rotat ...

  2. Go语言流程控制03--goto跳转到任意标签位置

    package main import ( "fmt" "time" ) func main() { STUDYHARD: fmt.Println(" ...

  3. C# 移除字符串头尾指定字符

    1 private void button1_Click(object sender, EventArgs e) 2 {//去掉字符串头尾指定字符 3 string MyInfo= "--中 ...

  4. 201871030129-魏琦 实验三 结对项目—《D{0-1}KP 实例数据集算法实验平台》项目报告

    项目 内容 课程班级博客链接 班级博客 这个作业要求链接 作业链接 我的课程学习目标 (1)掌握Github协作开发程序的操作方法:(2)理解并掌握代码风格规范.代码设计规范.代码复审.结对编程概念: ...

  5. 通过 DLPack 构建跨框架深度学习编译器

    通过 DLPack 构建跨框架深度学习编译器 深度学习框架,如Tensorflow, PyTorch, and ApacheMxNet,快速原型化和部署深度学习模型提供了强大的工具箱.不幸的是,易用性 ...

  6. ICCV2019论文点评:3D Object Detect疏密度点云三维目标检测

    ICCV2019论文点评:3D Object Detect疏密度点云三维目标检测 STD: Sparse-to-Dense 3D Object Detector for Point Cloud 论文链 ...

  7. RGB-D对红外热像仪和毫米波雷达标定

    RGB-D对红外热像仪和毫米波雷达标定 Extrinsic Calibration of Thermal IR Camera and mmWave Radar by Exploiting Depth ...

  8. Pipe Utilization管道利用率

    Pipe Utilization管道利用率 概述 CUDA设备的每个流式多处理器(SM)都具有许多专门用于执行特定任务的硬件单元.在芯片级,这些单元提供执行管道,翘曲调度程序将指令发送到这些管道.例如 ...

  9. Task05:SQL高级处理

    5.1 窗口函数 5.1.1 窗口函数概念及基本的使用方法 窗口函数也称为OLAP函数.OLAP 是OnLine AnalyticalProcessing 的简称,意思是对数据库数据进行实时分析处理. ...

  10. 【NX二次开发】Block UI 线性尺寸

    属性说明 常规         类型 描述     BlockID     String 控件ID     Enable     Logical 是否可操作     Group     Logical ...