本系列文章主要参考书籍和网上的资料来对JAVA并载编程进行自我总结,方便更好的学习和理解。
相关代码 https://gitee.com/dogman/concurrent-example.git
并发编程的最终目的是为了让程序运行的更快,但是线程开的多,并不代表着程序能够更快的运行,多线程在并发编程时受到各类并发所引起的各种问题和瓶颈,例如死锁问题,上下文优化问题,线程同步操作等限制,导致程序不能很好的运行,本文主要会对此类问题进行一些描述及解决方案输出。
本节内容
- 多线程比单线程慢的例子
- 死锁现象描述
- 资源限制的挑战
线程开的多一定就快吗?
学过操作系统的同学,都会知道,CPU在操作系统层面执行程序的时间,是以时间片为单位来给不同的任务分配运行,CPU通过不停的切换任务,而这个时间片又特别短,一般为几十MS。所以我们感觉是多个任务在同时运行。而多个任务在进行切换时,是需要保存该任务的上下文信息,也就是每一次切换,都会导致保存上下任务的上下文信息存储,和切换到的线程的上下文读取。所以开的线程多,不一定会快。
下面就是一个反例,
该程序测试了并发执行和串行执行同一个操作,我们可以通过
package com.ssj.java.demo.concourrent;
import org.junit.Test;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @author shuai.zhang on 2018/10/16
* @description
*/
public class TestConcourrentAndSerial {
static int repeatCount = 50000;
@Test
public void testConcourrentAndSerial() throws InterruptedException {
concourrent();
serial();
}
private void serial() {
long startTime = System.currentTimeMillis();
for (int i = 0; i < repeatCount; i++) {
doSomething();
}
long overTime = System.currentTimeMillis();
System.out.println(String.format("serial cost time %.3fs", (overTime - startTime)/1000.0));
}
private void doSomething() {
Long a = System.currentTimeMillis();
a = a % 100;
}
/*多线程操作*/
private void concourrent() throws InterruptedException {
long startTime = System.currentTimeMillis();
ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(100);
for (int i = 0; i < repeatCount; i++) {
executorService.execute(() -> {
doSomething();
});
}
while(executorService.getCompletedTaskCount() != repeatCount) {
Thread.sleep(1);
}
long overTime = System.currentTimeMillis();
System.out.println(String.format("concourrent cost time %.3fs", (overTime - startTime)/1000.0));
}
}
测试结果如下:
可以看到当特别简单的操作时,其实串行操作要比并发操作还要快。
如何查看上下文切换次数呢,可以通过操作系统LINUX(提供的命令)vmstat进行查询,其中cs(content switch)代表上下文切换,可以当作我们评价程序上下文切换的参考指标
如何减少上下文切换的次数?
1.尽量少使用锁
2.使用CAS算法(compare and swap)
3.尽量减少并发线程数(可参考当前CPU数)
4.使用协程
死锁现象
死锁现象,在并发编程中,是一种很常见的问题,会出来在有多个锁出现的场景,
package com.ssj.java.demo.concourrent;
import org.junit.Test;
/**
* @author shuai.zhang on 2018/10/16
* @description
*/
public class TestDeadLock {
final static Integer lockA = new Integer(1);
final static Integer lockB = new Integer(1);
@Test
public void testDeadLock() throws InterruptedException {
Thread testOne = new Thread(() -> {
synchronized (lockA) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
synchronized (lockB) {
System.out.println("testOne in content");
}
}
});
Thread testTwo = new Thread(() -> {
synchronized (lockB) {
synchronized (lockA) {
System.out.println("testTwo in content");
}
}
});
testOne.start();
testTwo.start();
testOne.join();
testTwo.join();
}
}
执行后,可见程序进行到不可完成的阻塞状态,通过jstack 链接到该程序,我们可以看到该程序的两个线程都在等待lock操作
资源限制对并发的限制
在资源有限的情况下,并发并不能够突破已存在的资源的限制,如CPU计算力,硬件读写能力,网络吞吐能力,数据库操作能力等。所以我们要结合实际的硬件资源,来对线程数等进行合理的调整,已达到在一定资源限制的情况下,合理调整线程数,来达到程序能力的最大话。
参考书籍
JAVA并发编程艺术