Java并发编程的艺术
所有笔记:https://www.jianshu.com/nb/33534473
Github地址:https://github.com/ilssio/java-concurrency-programming-ilss
1.1上下文切换
多线程的支持:CPU通过给每个线程分配CPU时间片来实现这个机制。
上下文切换:CPU通过分配时间片算法循环执行任务时,任务切换前会保存前一个任务的状态,以便切回任务,任务从保存到再次加载的过程就是一次上下文切换。上下文切换会影响多线程的执行速度。
1.1.1多线程一定快吗?
-
代码见part01中ConcurrencyTest类。
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.1.2
- 可以用Lmbench3测量上下文切换的时间 (Lmbench3是一个性能分析工具)
1.1.3 如何减少上下文切换
减少上下文切换的方法:无锁并发编程、CAS算法、使用最少线程、使用协程
- 无锁并发编程:即用一些方法来避免使用锁。
- CAS算法:Atomic包使用CAS算法更新数据
- 只用最少线程:避免创建不需要的线程
- 协程:在单线程中实现多任务调度,并在单线程中维持多个任务间的切换
1.1.4 减少上下文切换实战
减少WAITING线程,系统上下文切换的次数就会少。线程从WAITING到RUNNABLE都会进行一次上下文切换。
1.2 死锁
代码见part01中的DeadLockDemo类
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,形成死锁。如下信息所示
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 <pid> 查看如上信息
避免死锁的几个常见的方法:
- 避免一个线程同时获取多个锁
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
- 尝试使用定时锁,使用lock.tryLock(timeout)来替代内部锁机制
- 对于数据库锁,枷锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
1.3 资源限制的挑战
- 并发编程时需要考虑:硬件资源如带宽的上传/下载速度、硬盘的读写速度和CPU的处理速度。软件资源如数据库连接数、socket连接数等
- 资源限制引发的问题:多线程程序由于资源限制,仍然串行执行,会因为上下文切换和资源调度而降低效率
- 解决资源限制的问题:硬件上可以使用集群,不同的机器处理不同的数据;软件上可以考虑使用资源池将资源复用,如:数据库连接池、socket连接复用、调用webservice接口获取数据时,只建立一个连接。
- 在资源限制情况下进行并发线程:根据不同的资源限制调整程序的并发度。