多线程并发执行的时候会遇到各种各样的挑战:
- 上下文切换问题
- 死锁问题
- 资源限制问题
上下文切换问题
CPU通过给每个线程分配CPU时间片来实现多线程任务之间的并发执行。CPU通过时间片分配算法来循环执行任务,当CPU为当前线程分配的时间片消耗完(时间片一般是几十ms),CPU会切出去执行另外的一个线程(**切换会有消耗时间**),但是在CPU切换出去之前,会保存上一个任务的状态,以便下次切换回这个线程的时候能够紧接着保存的状态继续向下执行任务。**我们称线程任务从保存到再次被执行的过程为一次上下文切换。**测试串行(顺序执行)和并发执行的时间效率:
public class ConcurrencyTest {
private static final long count = 100000000;
public static void main(String[] args) throws InterruptedException {
// 并发执行两个任务(a = a + 5 执行count次,b--执行coung次)
concurrency();
// 串行(顺序执行)这两个任务
serial();
}
private static void concurrency() throws InterruptedException {
long start = System.currentTimeMillis();
Thread thread = new Thread(new Runnable(){
@Override
public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a += 5;
}
}
});
// a = a + 5 执行count次
thread.start();
// b = b - 1 执行count次
int 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();
// a = a + 5 执行count次
int a = 0;
for (long i = 0; i < count; i++) {
a += 5;
}
// b = b - 1 执行count次
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
long time = System.currentTimeMillis() - start;
System.out.println("serial :" + time + "ms, b=" + b);
}
}
测试可得在count小于100w的时候串行要比并行快,大于100w时并行要比串行快,为什么数据量小的时候并发执行的效率会慢呢?
原因有两点:
- 线程创建会有开销
- 上下文切换也会有开销
线程创建的开销可以通过减少线程的创建来减少,那么如何来减少上下文切换的开销呢?
-
无锁并发编程。多线程竞争锁的时候会引起上下文切换,所以可以通过避免使用锁的方式来减少上下文切换,例如:
- 将数据的ID按照hash算法取模分段,不同的线程处理不同段的数据
- 也可以将大锁换成小锁,从原来的多个线程竞争一把锁,转变成多个线程竞争多把锁,即锁分段技术,例如ConcurrentHashMap就是采用的锁分段技术。
-
CAS算法。java的Atomic包使用CAS算法来更新数据,而不需要加锁。
CAS算法:
- 使用最少的线程。减少不必要线程的创建,如果任务少但是创建了很多线程就会造成大量线程等待。减少线程的数量不仅减少了线程创建的开销而且减少了上下文切换的开销(因为线程从每次等待到被执行都是一次上下文切换,而有些线程是无用空闲的线程,所以这种线程的上下文切换是没有意义的,所以减少这种线程的数量就可以减少上下文的切换的开销从而提高程序的运行效率)。
- 协程。在单线程例实现多个任务之间的调度,并在单个线程里维护多个任务之间的切换。
死锁问题
当两个线程或多个线程都在互相等待对方释放锁,那么就会产生死锁。死锁的代码示例如下:
package com.tz.jspstudy.test;
public class TestConcurrency {
private static String lockA = "A";
private static String lockB = "B";
public static void main(String[] args) {
new TestConcurrency().deadLock();
}
private void deadLock() {
Thread t1 = new Thread(new Runnable(){
@Override
public void run() {
synchronized (lockA) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// t1执行到这里持有lockA但是还想拿到lockB(此时已被t2持有)
synchronized (lockB) {
System.out.println("hello deadLockB");
}
}
}
});
Thread t2 = new Thread(new Runnable(){
@Override
public void run() {
synchronized (lockB) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// t2执行到这里持有lockB但是还想拿到lockA(此时已被t1已经持有)
synchronized (lockA) {
System.out.println("hello deadLockB");
}
}
}
});
t1.start();
t2.start();
}
}
那么如何避免死锁呢?
- 避免一个线程同时获得多个锁
- 避免一个线程在锁内同时占有多个资源,尽量保证每个锁只占用一个资源
- 尝试使用定时锁,使用lock.tryLock( timeout )来替代使用内部锁机制。
- 对于数据库的操作,加锁和解锁必须在同一个连接例,否则会出现解锁失败的情况
资源限制问题
资源限制是指在进行并发编程的时候,程序的执行速度受限于计算机的硬件资源和软件资源,比如服务器的带宽只有2Mb/s,某个资源的下载速度是1Mb/s,开启10个线程下载,速度不会编程10Mb/s。 **硬件资源限制:**- 带宽的上传/下载速度
- 硬盘的读写速度
- CPU的处理速度
软件资源限制:
- 数据库连接数
- socket连接数(就是多少个用户连接你这台服务器)
CPU利用率100%:CPU一直被某些进程或者线程占用着,无法处理其他的线程或者进程任务。
如何解决资源限制问题?
-
硬件方面:使用集群**并行(真正意义上的同时)**执行程序,比如使用ODPS、Hadoop或者自己搭建服务器,不同机器处理不同的数据。可以通过“数据ID%机器数”,计算得到一个机器编号,然后由对应编号的机器处理这笔数据。
-
软件方面:比如使用数据库连接池,线程池将资源复用。
总结:对于这些并发问题可以使用JDK并发包提供的并发容器和工具类来解决并发问题,如ConcurrentHashMap…