并发编程的目的是为了让程序运行的更快,但是,并不是启动更多的线程就能让程序最大限度的并发执行。在进行并发编程的时候,如果希望通过多线程执行任务让程序运行的更快,会面临非常多的挑战,比如:上下文切换的问题、死锁的问题以及受限于硬件和软件资源限制问题。
1.上线文切换
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文的切换。因此,并发执行速度不一定会比串行执行的操作快,这是因为线程有创建和上下文切换的开销,可以使用Linux命令vmstat测量上下文切换到次数。看如下的代码实例:
public class ConcurrentTest {
public static long endTag = 10L;
public static int result = 0;
public static void main(String[] args) throws InterruptedException {
concurrent();
serial();
}
public static void concurrent() throws InterruptedException {
long startTime = System.currentTimeMillis();
final Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < endTag; i++) {
result += 5;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
thread.start();
int b = 0;
for (long i = 0L; i < endTag; i++) {
b--;
}
thread.join();
long costTime = System.currentTimeMillis() - startTime;
System.out.println("concurrent:" + costTime);
}
public static void serial() throws InterruptedException {
long startTime = System.currentTimeMillis();
for (int i = 0; i < endTag; i++) {
result += 5;
Thread.sleep(1);
}
int b = 0;
for (long i = 0L; i < endTag; i++) {
b--;
}
long costTime = System.currentTimeMillis() - startTime;
System.out.println("serial:" + costTime);
}
}
上述代码的输出结果如下,结果表明使用串行化执行的速度要高于使用并发执行的速度。
concurrent:14
serial:12
减少上下文切换的方法主要有:无锁并发编程、CAS算法、使用最少线程和使用协程。
![](https://i-blog.csdnimg.cn/blog_migrate/fe377e2a4ce753410efe85eef2d17a3a.png)
2.死锁
锁是一个非常有用的工具,运用的场景非常多,但同时它也会带来一些困扰,那就是有可能会引起死锁,一旦产生死锁,就会造成系统功能不可用。例如下面的实例代码:
public class DeadLockDemo {
private static String A = "A";
private static String B = "B";
private void deadLock() {
final Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (A) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) {
System.out.println("线程1");
}
}
}
});
final Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (B) {
synchronized (A) {
System.out.println("线程2");
}
}
}
});
thread1.start();
thread2.start();
}
public static void main(String[] args) {
new DeadLockDemo().deadLock();
}
}
一旦出现死锁,业务是可以感知的,因为不能继续提供服务了,那么只能通过dump线程查看到底是哪个线程出现了问题,例如使用jstack命令输出下面的dump日志,从日志中可以看到,代码发生了线程死锁:
避免死锁的几个常用的方法如下:
- 避免一个线程同时获取多个锁。
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
- 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。