1.1 上下文切换
CPU 通 过时间 片分配算法来循 环执 行任 务 ,当前任 务执 行一个 时间 片后会切 换 到下一个 任务 。但是,在切 换 前会保存上一个任 务 的状 态 ,以便下次切 换 回 这 个任 务时 ,可以再加 载这 个任务 的状 态 。所以任 务 从保存到再加 载 的 过 程就是一次上下文切 换 。
1.1.1 多线程一定快吗
public class Test01 {
private static final long count = 10000l;
public static void main(String[] args) throws InterruptedException {
concurrency();
serial();
}
//多线程执行
private static void concurrency() throws InterruptedException {
long start = System.currentTimeMillis();
Thread thread = new Thread(new Runnable() {
public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a += 5;
}
}
});
thread.start();
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
long time = System.currentTimeMillis() - start;
//join() 的作用:让“主线程”等待“子线程”结束之后才能继续运行。
thread.join();
System.out.println("concurrency :" + time + "ms,b=" + b);
}
//穿行执行
private static void serial() {
long start = System.currentTimeMillis();
int a = 0;
for (long i = 0; i < count; i++) {
a += 5;
}
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
long time = System.currentTimeMillis() - start;
System.out.println("serial:" + time + "ms,b=" + b + ",a=" + a);
}
}
1.1.2 测试上下文切换次数和时长
·
使用
Lmbench3
[1]
可以
测
量上下文切
换
的
时长
。
·
使用
vmstat
可以
测
量上下文切
换
的次数。
1.1.3 如何减少上下文切换
减少上下文切 换 的方法有无 锁 并 发编 程、 CAS 算法、使用最少 线 程和使用 协 程。· 无 锁 并 发编 程。多 线 程 竞 争 锁时 ,会引起上下文切 换 ,所以多 线 程 处 理数据 时 ,可以用一 些办 法来避免使用 锁 ,如将数据的 ID 按照 Hash 算法取模分段,不同的 线 程 处 理不同段的数据。·CAS 算法。 Java 的 Atomic 包使用 CAS 算法来更新数据,而不需要加 锁 。· 使用最少 线 程。避免 创 建不需要的 线 程,比如任 务 很少,但是 创 建了很多 线 程来 处 理, 这 样会造成大量 线 程都 处 于等待状 态 。· 协 程:在 单线 程里 实现 多任 务 的 调 度,并在 单线 程里 维 持多个任 务间 的切 换 。
1.1.4 减少上下文切换实战
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() {
public void run() {
synchronized (A) {
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) {
System.out.println("1");
}
}
}
});
Thread t2 = new Thread(new Runnable() {
public void run() {
synchronized (B) {
synchronized (A) {
System.out.println("2");
}
}
}
});
t1.start();
t2.start();
}
}
这 段代 码 只是演示死 锁 的 场 景,在 现实 中你可能不会写出 这样 的代 码 。但是,在一些更 为 复杂 的 场 景中,你可能会遇到 这样 的 问题 ,比如 t1拿到锁之后,因为一些异常情况没有释放锁 (死循环)。又或者是t1拿到一个数据库锁,释放锁的时候抛出了异常,没释放掉。
一旦出
现
死
锁
,
业务
是可感知的,因
为
不能
继续
提供服
务
了,那么只能通
过
dump
线
程
查
看到底是哪个线
程出
现
了
问题
现 在我 们 介 绍 避免死 锁 的几个常 见 方法。· 避免一个 线 程同 时获 取多个 锁 。· 避免一个 线 程在 锁 内同 时 占用多个 资 源,尽量保 证 每个 锁 只占用一个 资 源。· 尝试 使用定 时锁 ,使用 lock.tryLock ( timeout )来替代使用内部 锁 机制。· 对 于数据 库锁 ,加 锁 和解 锁 必 须 在一个数据 库连 接里,否 则 会出 现 解 锁 失 败 的情况。
1.3 资源限制的挑战
(1)什么是资源限制
资
源限制是指在
进
行并
发编
程
时
,程序的
执
行速度受限于
计
算机硬件
资
源或
软
件
资源。例如,服务器的带宽只有2Mb/s,某个资源的下载速度是1Mb/s每秒,系统启动10个线程下载资源,下载速度不会变成10Mb/s,所以在进行并发编程时,要考虑这些资源的限制。硬件资源限制有带宽的上传/下载速度、硬盘读写速度和CPU的处理速度。软件资源限制有数据库的连接数和socket连接数等
(
2
)
资
源限制引
发
的
问题
在并
发编
程中,将代
码执
行速度加快的原
则
是将代
码
中串行
执
行的部分
变
成并
发执
行,
但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这时候程序不仅不会加快执行,反而会更慢,因为增加了上下文切换和资源调度的时间。
(
3
)如何解决
资
源限制的
问题
对于硬件资源限制,可以考虑使用集群并行执行程序
。既然
单
机的
资
源有限制,那么就
让程序在多机上运行。比如使用ODPS
、
Hadoop
或者自己搭建服
务
器集群,不同的机器
处
理不同的数据。可以通过
“
数据
ID%
机器数
”
,
计
算得到一个机器
编
号,然后由
对应编
号的机器
处
理
这笔数据。
对于软件资源限制,可以考虑使用资源池将资源复用
。比如使用
连
接池将数据
库
和
Socket连接复用,或者在
调
用
对
方
webservice
接口
获
取数据
时
,只建立一个
连
接。
(
4
)在
资
源限制情况下
进
行并
发编
程
如何在
资
源限制的情况下,
让
程序
执
行得更快呢?方法就是,
根据不同的资源限制调整程序的并发度
,比如下
载
文件程序依
赖
于两个
资
源
——
带宽
和硬
盘读
写速度。有数据
库
操作 时,涉及数据
库连
接数,如果
SQL
语
句
执
行非常快,而
线
程的数量比数据
库连
接数大很多,
则某些线
程会被阻塞,等待数据
库连
接。