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并发编程系列第二篇,讲讲有关线程的那些事儿.主要内容是如下这 ...
随机推荐
- Keil编译后的Code,RO,RW,ZI分别表示什么以及和芯片Flash、SRAM的对应关系
在使用keil开发STM32应用程序时,点击Build后在Build Output窗口中经常会有如下信息:<ignore_js_op> 以前一直好奇这几个参数和实际使用的STM32芯片中F ...
- 基于Jira的运维发布平台的设计与实现
作者:乔克 公众号:运维开发故事 上线发布是运维的日常工作,常见的发布方式有: 手动发布 Jenkins发布平台 Gitlab CI ...... 除此之外还有需要开源软件,他们都有非常不错的发布管理 ...
- .NET平台系列13 .NET5 统一平台
系列目录 [已更新最新开发文章,点击查看详细] 时机决定一切,对于 .NET5 也是如此.实际上微软.NET团队在开始开发 .NET Core 时,对 .NET Framework 的全面重写 ...
- 本地软件仓库配置及NFS安装
[root@localhost ~]# mount /dev/cdrom /mnt mount: /dev/sr0 is write-protected, mounting read-only [ro ...
- 关于一类docker容器闪退问题定位
背景:正在学习docker期间,接到一个任务,通过docker部署一个应用A.该应用A类似于之前部署的应用B,结果很自然地犯了形而上学的错误. 思路:基于dockerfile+docker-compo ...
- GO学习-(21) Go语言基础之Go性能调优
Go性能调优 在计算机性能调试领域里,profiling 是指对应用程序的画像,画像就是应用程序使用 CPU 和内存的情况. Go语言是一个对性能特别看重的语言,因此语言中自带了 profiling ...
- MongoDB学习笔记:文档Crud Shell
MongoDB学习笔记:文档Crud Shell 文档插入 一.插入语法 db.collection.insertOne() 将单个文档插入到集合中.db.collection.insertMan ...
- 适用于AMD ROC GPU的Numba概述
适用于AMD ROC GPU的Numba概述 Numba通过按照HSA执行模型将Python代码的受限子集直接编译到HSA内核和设备功能中,从而支持AMD ROC GPU编程.用Numba编写的内核似 ...
- 如何使用TensorCores优化卷积
如何使用TensorCores优化卷积 本文将演示如何在TVM中使用TensorCores编写高性能的卷积计划.假设卷积的输入有大量数据.首先介绍如何在GPU上优化卷积. TensorCore简介 每 ...
- halcon——缺陷检测常用方法总结(光度立体)
引言 机器视觉中缺陷检测分为一下几种: blob+特征(官方示例surface_scratch.hdev) blob+差分+特征(官方示例pcb_inspection.hdev) 光度立体 特征训练 ...