《Java并发编程的艺术》 学习之路
记录阅读学习《Java并发编程的艺术》一书的历程,好记性不如烂笔头,边看边思考边记录有助于理解,由于精力有限,整理内容有节选,旨在首次快速通读,标注其中重要的知识点,便于以后复习或精读使用。
第一章 并发编程的挑战
1.1 上下文切换
多线程执行代码,需要cpu通过为每个线程分配 cpu时间片 来实现。
上下文切换: 当前任务执行完一个时间片后会切换到下一个任务,切换前需要保存上一个任务的状态以便下次切换回这个任务可以加载到任务状态,所以任务从保存到再加载的过程就是一次上下文切换。
1.1.1 多线程一定快么?
不一定,因为线程有创建和上下文切换的开销。
1.1.2 测试上下文切换次数和时长
- 使用Lmbench3可以测量上下文切换的时长。
- 使用vmstat可以测量上下文切换的次数。
vmstat命令:vmstat是Virtual Meomory Statistics(虚拟内存统计)的缩写,可对操作系统的虚拟内存、进程、CPU活动进行监控。是对系统的整体情况进行统计,不足之处是无法对某个进程进行深入分析。
一般vmstat工具的使用是通过两个数字参数来完成的,第一个参数是采样的时间间隔数,单位是秒,第二个参数是采样的次数。
字段说明:
Procs(进程):
r: 运行队列中进程数量
b: 等待IO的进程数量
Memory(内存):
swpd: 使用虚拟内存大小
free: 可用内存大小
buff: 用作缓冲的内存大小
cache: 用作缓存的内存大小
Swap:
si: 每秒从交换区写到内存的大小
so: 每秒写入交换区的内存大小
IO:(现在的Linux版本块的大小为1024bytes)
bi: 每秒读取的块数
bo: 每秒写入的块数
system:
in: 每秒中断数,包括时钟中断。【interrupt】
cs: 每秒上下文切换数。 【count/second】
CPU(以百分比表示):
us: 用户进程执行时间(user time)
sy: 系统进程执行时间(system time)
id: 空闲时间(包括IO等待时间),中央处理器的空闲时间 。以百分比表示。
wa: 等待IO时间
从上面的测试结果中我们可以看到,上下文每1秒切换1000多次
1.1.3 如何减少上下文切换
减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。
- 无锁并发编程:多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一
些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。(个人理解:对资源划分好规则,避免多个线程使用共享资源) - CAS算法 :Java的Atomic包使用CAS算法来更新数据,而不需要加锁。(有印象,后续研究下代码)
- 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这
样会造成大量线程都处于等待状态。 - 协程 (coroutine):在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。(线程 进程知道 这个协程是啥?)
协程 又称微线程,纤程。英文名Coroutine, 子程序,或者称为函数。特点在于是一个线程执行,那和多线程比,协程有何优势?
最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
1.2 死锁
下面示例代码会引起死锁,因为线程t1和线程t2互相等待对方释放锁。
public class DeadLockDemo {
private static String A = "A";
private static String B = "B";
public static void main(String[] args) {
new DeadLockDemo().deadLock();
}
private void deadLock() {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (A) { //锁住A后执行 后续代码 (1)
try { Thread.currentThread().sleep(2000); //等在这里 (2)
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) { //等待t2 释放B ...... (5)
System.out.println("1");
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (B) { //开始锁住B 后执行后续代码 (3)
synchronized (A) { //等待t1 释放A ...... (4)
System.out.println("2");
}
}
}
});
t1.start();
t2.start();
}
}
死锁产生例子: 比如t1拿到锁之后,因为一些异常情况没有释放锁(死循环)。又或者是t1拿到一个数据库锁,释放锁的时候抛出了异常,没释放掉。
避免死锁的几个常见方法。
- 避免一个线程同时获取多个锁。
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
- 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
1.3 资源限制的挑战
(1)什么是资源限制
资源限制 是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。
硬件资源 限制有带宽的上传/下载速度、硬盘读写速度和CPU的处理速度。
软件资源 限制有数据库的连接数和socket连接数等。
(2)资源限制引发的问题
在并发编程中,将代码执行速度加快的原则是将代码中 串行执行的部分 变成 并发执行,
但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这时候程序不仅不
会加快执行,反而会更慢,因为增加了上下文切换和资源调度的时间。
(3)如何解决资源限制的问题
对于硬件资源限制,可以考虑使用集群并行执行程序。比如使用ODPS、Hadoop或者自己搭建服务器集群,不同的机器处理不同的数据。可以通过“数据ID%机器数”,计算得到一个机器编号,然后由对应编号的机器处理这笔数据。(HASH取模之类的方法使得数据 尽可能均匀分布于不同的主机上进行处理提高并行能力)
对于软件资源限制,可以考虑使用资源池将资源复用。比如使用连接池将数据库和Socket
连接复用,或者在调用对方webservice接口获取数据时,只建立一个连接。
(4) 在资源限制情况下进行并发编程
根据不同的资源限制调整程序的并发度,比如下载文件程序依赖于两个资源——带宽和硬盘读写速度。有数据库操作时,涉及数据库连接数,如果SQL语句执行非常快,而线程的数量比数据库连接数大很多,则
某些线程会被阻塞,等待数据库连接。
未完待续…
本文整理内容出自《Java并发编程的艺术》一书 第二版, 大力推荐~~~