从零开始创建一家公司

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. Python使用 Kubernetes API 访问集群

    通过将身份认证令牌直接传给 API 服务器,可以避免使用 kubectl 代理,像这样:使用 grep/cut 方式: 通过将身份认证令牌直接传给 API 服务器,可以避免使用 kubectl 代理, ...

  2. 最小生成树,Prim算法与Kruskal算法,408方向,思路与实现分析

    最小生成树,Prim算法与Kruskal算法,408方向,思路与实现分析 最小生成树,老生常谈了,生活中也总会有各种各样的问题,在这里,我来带你一起分析一下这个算法的思路与实现的方式吧~~ 在考研中呢 ...

  3. CRC校验原理简介及C代码实现说明

    1 原理 参考文档:CRC校验 (qq.com) 参考书籍:<计算机网络(第7版)-谢希仁> 1.1 原理简介 CRC是一种检错方法. 在发送端,先把数据划分为组,假定每组k个比特.现假定 ...

  4. 上传靶机实战之upload-labs解题

    前言 我们知道对靶机的渗透可以提高自己对知识的掌握能力,这篇文章就对上传靶机upload-labs做一个全面的思路分析,一共21个关卡.让我们开始吧,之前也写过关于上传的专题,分别为浅谈文件上传漏洞( ...

  5. Jmeter- 笔记6 - 负载测试

    普通场景介绍 1.线程数:并发用户数 2.Ramp-Up时间:启动时间(线程数的准备时间),在这个时间点结束时,所有用户都已运行起来 3.循环次数:每个线程数都要运行的次数.永远 和 调度器一起使用, ...

  6. python基础知识笔记(二) (出现语法以及颜色问题)

    以下是在学习Python时需要尤其要注意的点: 1.程序中出现中文,运行的时候出现如下错误: SyntaxError: Non-UTF-8 code starting with '\xc1' 则是编码 ...

  7. TOF摄像机可以替代Flash激光雷达吗?

    TOF摄像机可以替代Flash激光雷达吗? 一.基于ToF技术的Flash激光雷达 基本成像原理上ToF Camera与LiDAR相同,都采用飞行时间测距技术(包括利用APD或SPAD的直接测距法,和 ...

  8. Postman之newman的安装

    一.newman简介:newman是为Postman而生,专门用来运行Postman编写好的脚本:使用newman,你可以很方便的用命令行来执行postman collections. 二.newma ...

  9. JAVA复习题(一)基础知识

    类的构造方法描述正确的是( )A. 类中的构造方法不能省略B. 构造方法必须与类同名,但方法不能与class同名C. 构造方法在一个对象被new时执行D. 一个类只能有一个构造方法我的答案:C正确答案 ...

  10. CentOS:操作系统级监控及常用计数器解析

    我相信有一些人看到这篇文章的标题肯定有种不想看的感觉,因为这样的内容实在被写得太多太多了.操作系统分析嘛,无非就是 CPU 使用率.I/O 使用率.内存使用率.网络使用率等各种使用率的描述. 然而因为 ...