文章目录

  • 前言
  • 基本使用
    • 创建线程的四种方式
    • 比较与区别
  • 一、通用的线程生命周期
  • 二、Java中线程的生命周期
  • 三、Java线程的状态转换
  • 四、度量程序运行性能指标和多线程意义
  • 五、创建多少个线程合适
  • 六、为什么局部变量是线程安全的?
  • 七、如何用面向对象思想写好并发程序?
    • 封装共享变量
    • 识别共享变量间的约束条件
    • 制定并发访问策略

主引用自:极客时间《Java并发编程实战》https://time.geekbang.org/column/intro/100023901

2)线程相关问题(必问):
创建线程的四种方式。
什么是线程安全。
Runnable接口和Callable接口的区别。
wait方法和sleep方法的区别。
synchronized、Lock、ReentrantLock、ReadWriteLock。
介绍下CAS(无锁技术)。
什么是 ThreadLocal 。
ThreadPoolExecutor 的内部工作原理。
分布式环境下,怎么保证线程安全。

前言

基本使用

创建线程的四种方式

  1. 继承Thread 类创建线程类
  2. 通过Runnable接口创建线程类
  3. 通过Callable和Future创建线程
  4. 线程池

实现的代码Demo见:创建线程有几种方式?我笑了

比较与区别

  1. 实现Runnable/Callable接口相比继承Thread类的优势
  • 适合多个线程进行资源共享(这个是为什么呐? 在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
  • 可以避免java中单继承的限制
  • 增加程序的健壮性,代码和数据独立
  • 线程池只能放入Runable或Callable接口实现类,不能直接放入继承Thread的类
  1. Callable和Runnable的区别
  • Callable重写的是call()方法,Runnable重写的方法是run()方法
  • call()方法执行后可以有返回值,run()方法没有返回值
  • call()方法可以抛出异常,run()方法不可以
  • 运行Callable任务可以拿到一个Future对象,表示异步计算的结果 。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果

一、通用的线程生命周期

  • 初始状态:,指的是线程已经被创建,但是还不允许分配 CPU 执行。在编程语言层面已经创建,但是真正的操作系统层面还没有创建
  • 可运行状态:操作系统真正创建好了线程,可以被分配CPU执行了
  • 运行状态:线程分配到CPU并执行的状态
  • 休眠状态:当运行状态的线程 调用一个阻塞的 API(例如以阻塞方式读文件)或者等待某个事件(例如条件变量),那么线程的状态就会转换到休眠状态,同时释放 CPU 使用权,休眠状态的线程永远没有机会获得 CPU 使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态。(所以sleep就是一直占用CPU,wait就会让出CPU)
  • 终止状态:线程执行完或者出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束了。

二、Java中线程的生命周期

Java 线程一共有六种生命状态:

  1. NEW(初始化状态)
  2. RUNNABLE(可运行 / 运行状态)
  3. BLOCKED(阻塞状态)
  4. WAITING(无限等待)
  5. TIMED_WAITING(有时限等待)
  6. TERMINATED(终止状态)

其中 3. BLOCKED(阻塞状态)4. WAITING(无时限等待)5. TIMED_WAITING(有时限等待)在操作系统层面都是属于休眠模式。

三、Java线程的状态转换

实际项目中死锁的线程栈例子:

四、度量程序运行性能指标和多线程意义

  • 时间维度:延迟(发出请求到收到响应这个过程的时间;延迟越短,意味着程序执行得越快,性能也就越好。)
  • 空间维度:吞吐(在单位时间内能处理请求的数量;吞吐量越大,意味着程序能处理的请求越多,性能也就越好。)

对于整个机器而言,其实最主要的还是如何提升CPU 和 I/O 设备综合利用率

在单核时代,多线程主要就是用来平衡 CPU 和 I/O 设备的。如图所示:

如果程序只有 CPU 计算,而没有 I/O 操作的话,多线程不但不会提升性能,还会使性能变得更差,原因是增加了线程切换的成本。但是在多核时代,这种纯计算型的程序也可以利用多线程来提升性能。为什么呢?因为利用多核可以降低响应时间。
比如:需要计算 1+2+… … +100 亿的值,如果在 4 核的 CPU 上利用 4 个线程执行,线程 A 计算[1,25 亿),线程 B 计算[25 亿,50 亿),线程 C 计算[50,75 亿),线程 D 计算[75 亿,100 亿],之后汇总,那么理论上应该比一个线程计算[1,100 亿]快将近 4 倍,响应时间能够降到 25%。一个线程,对于 4 核的 CPU,CPU 的利用率只有 25%,而 4 个线程,则能够将 CPU 的利用率提高到 100%。示意图如下所示:

五、创建多少个线程合适

就拿上面的计算 1+2+… … +100 亿的值的例子来说,创建了四个线程,性能提升了四倍,那我是不是可以多创建几个线程来达到更快的目的。这样问题就来了,创建多少个合适呐,是不是创建的越多越好?如果是六个线程,那么就意味着其中两个核会产生线程切换,这会带来一定的开销,这样想的话,是不是创建的线程数与CPU核的数目要完全对应上?他们之间有什么样子的关系?

其实创建多少线程合适,要看多线程具体的应用场景。我们的程序一般都是 CPU 计算和 I/O 操作交叉执行的,由于 I/O 设备的速度相对于 CPU 来说都很慢,所以大部分情况下,I/O 操作执行的时间相对于 CPU 计算来说都非常长,这种场景我们一般都称为 I/O 密集型计算;和 I/O 密集型计算相对的就是 CPU 密集型计算了,CPU 密集型计算大部分场景下都是纯 CPU 计算。I/O 密集型程序和 CPU 密集型程序,计算最佳线程数的方法都是不同的。

  • 对于 CPU 密集型计算,多线程本质上是提升多核 CPU 的利用率,所以对于一个 4 核的 CPU,每个核一个线程,理论上创建 4 个线程就可以了,再多创建线程也只是增加线程切换的成本。所以,对于 CPU 密集型的计算场景,理论上“线程的数量 =CPU 核数”就是最合适的。不过在工程上,线程的数量一般会设置为“CPU 核数 +1”,这样的话,当线程因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以顶上,从而保证 CPU 的利用率。

  • 对于 I/O 密集型的计算场景,比如前面我们的例子中,如果 CPU 计算和 I/O 操作的耗时是 1:1,那么 2 个线程是最合适的。如果 CPU 计算和 I/O 操作的耗时是 1:2,那多少个线程合适呢?是 3 个线程,如下图所示:CPU 在 A、B、C 三个线程之间切换,对于线程 A,当 CPU 从 B、C 切换回来时,线程 A 正好执行完 I/O 操作。这样 CPU 和 I/O 设备的利用率都达到了 100%。

    从上面可以看出,对于 I/O 密集型的计算场景,在选取线程数目的时候是根据CPU计算与I/O操作的比例而得出的!计算公式是:

单核:最佳线程数 =1 +(I/O 耗时 / CPU 耗时)
多核:最佳线程数 =CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]

当然,在工程上,该比例是很难确定并且动态变化的, 所以就需要压测来大致统计该比例。同时,需要重点关注 CPU、I/O 设备的利用率和性能指标(响应时间、吞吐量)之间的关系。

六、为什么局部变量是线程安全的?


每个线程都会有自己的独立的调用栈。所有局部变量不会有并发问题。

七、如何用面向对象思想写好并发程序?

主要有三点:

  1. 从封装共享变量
  2. 识别共享变量间的约束条件
  3. 制定并发访问策略

封装共享变量

将共享变量作为对象属性封装在内部,对所有公共方法制定并发访问策略。例如下面这样:

public class Counter {private long value;synchronized long get(){return value;}synchronized long addOne(){return ++value;}
}

对于这些不会发生变化的共享变量,建议你用 final 关键字来修饰。

识别共享变量间的约束条件

例如,库存管理里面有个合理库存的概念,库存量不能太高,也不能太低,它有一个上限和一个下限。关于这些约束条件,我们可以用下面的程序来模拟一下。


public class SafeWM {// 库存上限private final AtomicLong upper =new AtomicLong(0);// 库存下限private final AtomicLong lower =new AtomicLong(0);// 设置库存上限void setUpper(long v){// 检查参数合法性if (v < lower.get()) {throw new IllegalArgumentException();}upper.set(v);}// 设置库存下限void setLower(long v){// 检查参数合法性if (v > upper.get()) {throw new IllegalArgumentException();}lower.set(v);}// 省略其他业务代码
}

我们假设库存的下限和上限分别是 (2,10),线程 A 调用 setUpper(5) 将上限设置为 5,线程 B 调用 setLower(7) 将下限设置为 7,如果线程 A 和线程 B 完全同时执行,你会发现线程 A 能够通过参数校验,因为这个时候,下限还没有被线程 B 设置,还是 2,而 5>2;线程 B 也能够通过参数校验,因为这个时候,上限还没有被线程 A 设置,还是 10,而 7<10。当线程 A 和线程 B 都通过参数校验后,就把库存的下限和上限设置成 (7, 5) 了,显然此时的结果是不符合库存下限要小于库存上限这个约束条件的。

那么”正确“的代码应该是:

public class SafeWM {// 库存上限private final AtomicLong upper =new AtomicLong(0);// 库存下限private final AtomicLong lower =new AtomicLong(0);// 设置库存上限void setUpper(long v) {synchronized (this) { //对对象加锁即可!!!// 检查参数合法性if (v < lower.get()) {throw new IllegalArgumentException();}upper.set(v);}}// 设置库存下限void setLower(long v) {synchronized (this) {// 检查参数合法性if (v > upper.get()) {throw new IllegalArgumentException();}lower.set(v);}}// 省略其他业务代码
}

所以一定要识别出所有共享变量之间的约束条件,如果约束条件识别不足,很可能导致制定的并发访问策略南辕北辙。共享变量之间的约束条件,反映在代码里,基本上都会有 if 语句,所以,一定要特别注意竞态条件。

制定并发访问策略

主要有三种方式:

  1. 避免共享:避免共享的技术主要是利于线程本地存储以及为每个任务分配独立的线程。
  2. 不变模式:这个在 Java 领域应用的很少,但在其他领域却有着广泛的应用,例如 Actor 模式、CSP 模式以及函数式编程的基础都是不变模式。
  3. 管程及其他同步工具:Java 领域万能的解决方案是管程,但是对于很多特定场景,使用 Java 并发包提供的读写锁、并发容器等同步工具会更好。

有一些原则在编写这类代码时,建议遵守:

  1. 优先使用成熟的工具类:Java SDK 并发包里提供了丰富的工具类,基本上能满足你日常的需要,建议你熟悉它们,用好它们,而不是自己再“发明轮子”,毕竟并发工具类不是随随便便就能发明成功的。
  2. 迫不得已时才使用低级的同步原语:低级的同步原语主要指的是 synchronized、Lock、Semaphore 等,这些虽然感觉简单,但实际上并没那么简单,一定要小心使用。
  3. 避免过早优化:安全第一,并发程序首先要保证安全,出现性能瓶颈后再优化。

极客时间《Java并发编程实战》----Java线程相关推荐

  1. 视频教程-Java并发编程实战-Java

    Java并发编程实战 2018年以超过十倍的年业绩增长速度,从中高端IT技术在线教育行业中脱颖而出,成为在线教育领域一匹令人瞩目的黑马.咕泡学院以教学培养.职业规划为核心,旨在帮助学员提升技术技能,加 ...

  2. [Java 并发编程实战] 设计线程安全的类的三个方式(含代码)

    发奋忘食,乐以忘优,不知老之将至.---<论语> 前面几篇已经介绍了关于线程安全和同步的相关知识,那么有了这些概念,我们就可以开始着手设计线程安全的类.本文将介绍构建线程安全类的几个方法, ...

  3. Java并发编程实战之互斥锁

    文章目录 Java并发编程实战之互斥锁 如何解决原子性问题? 锁模型 Java synchronized 关键字 Java synchronized 关键字 只能解决原子性问题? 如何正确使用Java ...

  4. 【极客时间】《Java并发编程实战》学习笔记

    目录: 开篇词 | 你为什么需要学习并发编程? 内容来源:开篇词 | 你为什么需要学习并发编程?-极客时间 例如,Java 里 synchronized.wait()/notify() 相关的知识很琐 ...

  5. Java并发编程实战基础概要

    文章目录 Java并发编程实战基础概要 开篇 多线程问题有啥难点呢? 为啥要学习并发编程? 并发问题的根源是什么? CPU切换线程执导致的原子性问题是如何发生的? 缓存导致的可见性问题是如何发生的? ...

  6. 《Java 并发编程实战》--读书笔记

    Java 并发编程实战 注: 极客时间<Java 并发编程实战>–读书笔记 GitHub:https://github.com/ByrsH/Reading-notes/blob/maste ...

  7. Java并发编程实战————Executor框架与任务执行

    引言 本篇博客介绍通过"执行任务"的机制来设计应用程序时需要掌握的一些知识.所有的内容均提炼自<Java并发编程实战>中第六章的内容. 大多数并发应用程序都是围绕&qu ...

  8. JAVA并发编程实战-任务执行

    目录 思维导图 1 在线程中执行任务 1.1 顺序执行任务 1.2 显式的为任务创建线程 1.3 无限制创建线程的缺点 2 Executor框架 2.1 使用Executor实现WebServer 2 ...

  9. Java并发编程实战————Semaphore信号量的使用浅析

    引言 本篇博客讲解<Java并发编程实战>中的同步工具类:信号量 的使用和理解. 从概念.含义入手,突出重点,配以代码实例及讲解,并以生活中的案例做类比加强记忆. 什么是信号量 Java中 ...

  10. java并发编程实战学习(3)--基础构建模块

    转自:java并发编程实战 5.3阻塞队列和生产者-消费者模式 BlockingQueue阻塞队列提供可阻塞的put和take方法,以及支持定时的offer和poll方法.如果队列已经满了,那么put ...

最新文章

  1. js实现的时间轴效果
  2. 怎样提高自己的团队合作能力
  3. QTP和WinRunner区别
  4. java.util.zip_[Java 基础] 使用java.util.zip包压缩和解压缩文件
  5. 统计带头结点的单向链表的个数并存放在形参n所指的单元中。 欢迎评论 指点。
  6. android和网络连接相关的类URL,URLConnection,HttpURLConnection,HttpClient
  7. 神经网络API、Kotlin支持,那些你必须了解的Android 8.1预览版和Android Studio 3.0新特性
  8. 如何制作手绘地图?如何将图片图层精确地对准在地图上?
  9. 谷歌的下一个×××烦
  10. iocomp入门教程-以MFC中iplotx为例
  11. QT控件 之(TreeView)实现右键菜单栏功能,双击事件能实现区分不同的节点的点击效果
  12. Log4j2 漏洞检测工具清单
  13. 秋天远程控制V1.0源码(易语言)
  14. android实时声音信号波形_android绘制播放音频的波形图
  15. android 百度地图api切换城市,【百度地图API】关于如何进行城市切换的三种方式...
  16. sklearn-线性回归
  17. 一个简单的日内交易策略
  18. JVM,DVM,ART
  19. 2022下半年软件设计师中级考试通过
  20. FastDFS构成、特性、Linux下安装以及Java如何访问

热门文章

  1. 白嫖党进,全网最详细的信息安全术语合集终于来了
  2. Python深度学习---第1章 什么是深度学习
  3. Python小游戏及登录系统
  4. Java前端如何发送date类型的参数给后端
  5. 使用vue element-ui 打印组件
  6. MySQL四种SQL性能分析工具
  7. 视差图Disparity与深度图Depth Map的一点知识
  8. java se win10_Win10 JAVASE的下载和环境变量设置
  9. 小米/VIVO/OPPO全系列救砖+解锁+工具+教程+激活账户技术
  10. linux 用命令安装软件,Linux安装软件的三种常用命令