使用并发的原因
多核的CPU的背景下,催生了并发编程的趋势,通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升。
在特殊的业务场景下先天的就适合于并发编程。 比如在图像处理领域,一张1024X768像素的图片,包含达到78万6千多个像素。即时将所有的像素遍历一边都需要很长的时间, 面对如此复杂的计算量就需要充分利用多核的计算的能力。又比如当我们在网上购物时,为了提升响应速度,需要拆分,减库存, 生成订单等等这些操作,就可以进行拆分利用多线程的技术完成。 面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分。
Java线程的生命周期
- java线程的生命周期如下图所属
1、上下文切换
多线程的支持:CPU通过给每个线程分配CPU时间片来实现这个机制。
上下文切换:CPU通过分配时间片算法循环执行任务时,任务切换前会保存前一个任务的状态,以便切回任务,任务从保存到再次加载的过程就是一次上下文切换。上下文切换会影响多线程的执行速度。
1.1多线程一定快吗?
代码见part01中ConcurrencyTest类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package io.ilss.concurrency.part01;
/**
* className ConcurrencyTest
* description ConcurrencyTest
*
* @author feng
* @version 1.0
* @date 2019-01-21 12:47
*/
public class ConcurrencyTest {
private static final long count = 100001L;
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() {
long a = 0``;
for (``long i = 0``; i < count; i++) {
a += 5L;
}
}
});
thread.start();
long b = 0``;
for (``long i = 0``; i < count; i++) {
b--;
}
thread.join();
long time = System.currentTimeMillis() - start;
System.out.println(``"Concurrency : " + time + "ms, b = " + b);
}
private static void serial() {
long start = System.currentTimeMillis();
long a = 0``;
for (``long i = 0``; i < count; i++) {
a += 5``;
}
long 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);
}
}
通过更改count从1万到1亿可看成,执行count越低,串行执行效率更高。这是因为线程有创建和上下文切换的开销。但是到了一定的数量过后,多线程的优势就会体现出来。
1.2
可以用Lmbench3测量上下文切换的时间 (Lmbench3是一个性能分析工具)
1.3 如何减少上下文切换
减少上下文切换的方法:无锁并发编程、CAS算法、使用最少线程、使用协程
无锁并发编程:即用一些方法来避免使用锁。
CAS算法:Atomic包使用CAS算法更新数据
只用最少线程:避免创建不需要的线程
协程:在单线程中实现多任务调度,并在单线程中维持多个任务间的切换
1.4死锁
代码见part01中的DeadLockDemo类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package io.ilss.concurrency.part01;
/**
* className DeadLockDemo
* description DeadLockDemo
*
* @author feng
* @version 1.0
* @date 2019-01-21 16:28
*/
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(() -> {
synchronized (A) {
try {
Thread.sleep(``2000``);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) {
System.out.println(``"1"``);
}
}
});
Thread t2 = new Thread(() ->{
synchronized (B) {
synchronized (A) {
System.out.println(``"2"``);
}
}
});
t1.start();
t2.start();
}
}
t1(Thread-0)拿到A锁过后sleep2s,t2("Thread-1)同时进入B锁代码块,由于t1还在执行A锁的代码块阻塞进程,等t1线程2s时间到了想进入B的代码块时,由于t2还在执行B锁的代码块,就行形成了拿了A的t1要B,拿了B的t2要A,形成死锁。如下信息所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Java stack information for the threads listed above:
===================================================
"Thread-1"``:
at io.ilss.concurrency.part01.DeadLockDemo.lambda$deadLock$``1``(DeadLockDemo.java:``36``)
- waiting to lock <``0x000000076ac26378``> (a java.lang.String)
- locked <``0x000000076ac263a8``> (a java.lang.String)
at io.ilss.concurrency.part01.DeadLockDemo$$Lambda$``2``/``2129789493``.run(Unknown Source)
at java.lang.Thread.run(Thread.java:``748``)
"Thread-0"``:
at io.ilss.concurrency.part01.DeadLockDemo.lambda$deadLock$``0``(DeadLockDemo.java:``29``)
- waiting to lock <``0x000000076ac263a8``> (a java.lang.String)
- locked <``0x000000076ac26378``> (a java.lang.String)
at io.ilss.concurrency.part01.DeadLockDemo$$Lambda$``1``/``1607521710``.run(Unknown Source)
at java.lang.Thread.run(Thread.java:``748``)
Found 1 deadlock.
注:先使用jps找到运行所在的pid,然后用jstack 查看如上信息
避免死锁的几个常见的方法:
避免一个线程同时获取多个锁
避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
尝试使用定时锁,使用lock.tryLock(timeout)来替代内部锁机制
对于数据库锁,枷锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
1.3 资源限制的挑战
并发编程时需要考虑:硬件资源如带宽的上传/下载速度、硬盘的读写速度和CPU的处理速度。软件资源如数据库连接数、socket连接数等
资源限制引发的问题:多线程程序由于资源限制,仍然串行执行,会因为上下文切换和资源调度而降低效率
解决资源限制的问题:硬件上可以使用集群,不同的机器处理不同的数据;软件上可以考虑使用资源池将资源复用,如:数据库连接池、socket连接复用、调用webservice接口获取数据时,只建立一个连接。
在资源限制情况下进行并发线程:根据不同的资源限制调整程序的并发度。