使用并发编程的挑战

时间:2021-06-20 18:00:24

编程中使用多线程的目的是为了让程序执行的更快,效率更高。所以很多人想当然的认为多线程的执行效率一定比单线程的高。但在进行并发编程时,会发现试图使用多线程来提高程序的整体运行效率,会面临很多挑战,下面来分析,何时使用多线程来编程,以及在多线程编程中如何最大化效率。

多线程中上下文切换的问题:

多线程执行代码在计算机的底层使用是通过CPU来给予每个线程时间片来实现这个机制。由于CPU分配的每个线程的时间片极为短暂(一般为几十毫秒),所有CPU通过不停地切换线程执行。这样就给程序员一种错觉,以为多个线程是在同时执行。既然CPU来不断的来回在几个线程中进行切换,所以CPU在从一个线程切换到另一个线程的时候肯定要记录一下当前线程执行到哪了,以遍下一次切换到该线程的时候可以继续执行。所以线程从保存在再次被执行的过程就是一次上下文切换。

并发编程的思考一:多线程一定快吗?

通过一个示例来观察,多线程是否一定优于单线程。


public class Con_Ser_Test {

private static final long count = 10001;

public static void main(String[] args) {
try {
concurrency();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
serial();
}
private static void concurrency() throws InterruptedException{
long start = System.currentTimeMillis();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int a = 5;
for(long i=0 ;i<count;i++){
a+=5;
}
}
});
thread.start();
int b=0;
for(int i=0;i<count;i++){
b--;
}
//等待thread线程执行结束后计时
thread.join();
long time = System.currentTimeMillis()-start;
System.out.println("多线程耗时:"+time);
}
private static void serial(){
long start = System.currentTimeMillis();
int a = 5;
for(long i=0 ;i<count;i++){
a+=5;
}
int b=0;
for(int i=0;i<count;i++){
b--;
}
long time = System.currentTimeMillis()-start;
System.out.println("单线程耗时:"+time);
}
}

测试数据为 10001,输出结果:

多线程耗时:1
单线程耗时:0

测试数据为 10000001,输出结果:

多线程耗时:16
单线程耗时:18

如上的测试结果不能看出,当数据量不是很大时。单线程的效率却优于多线程,因为多线程有线程的创建与上下文切换的开销。但数据足够大时当然多线程优于单线程。即使在多线程中,我们依然有办法减少上下文的切换,如:

  • 无锁并发编程,即避免使用锁
  • CAS算法
  • 使用最小线程和使用协程

并发编程的思考二:线程安全的问题

我们来运行如下一段代码:

import java.util.ArrayList;
import java.util.List;

public class SafeThread {
int i = 0;

public static void main(String[] args) {
final SafeThread sf = new SafeThread();
List<Thread> list = new ArrayList<Thread>();
for(int i=0;i<1000;i++){
Thread t1 = new Thread(new Runnable() {

@Override
public void run() {
for (int i = 0; i < 10; i++) {
sf.add(sf);
}
}
});
list.add(t1);
}
for(Thread t:list){
t.start();
}
try {
for(Thread t:list){
t.join();
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(sf.i);
}

public synchronized void add(SafeThread sf) {
sf.i++;
}
}

对于如上的程序,我们一般考虑的结果的最后i输出为1000*10。但你运行结果后会发现,结果并不是这样,而且结果有着不确定行。造成这种结果的原因就是因为多个线程试图操作一个被所有线程共享的变量,而出现的线程不安全问题

对于解决线程不安全的方法我们有多种,如使用 volatile修饰变量,使用synchronized来同步或者使用CAS算法等等。在后面的博客中我会从java内存模型中深入分析各个解决方法。

并发编程的思考三:线程的死锁

public class DieLock extends Thread {
private boolean judge;
DieLock(boolean judge) {
this.judge = judge;
}
public void run() {
if (judge) {
while (true) {
//拿到objA锁
synchronized (MyLock.objA) {
System.out.println("if objA");
//此时需要objB锁。但objB锁没有释放,线程死锁
synchronized (MyLock.objB) {
System.out.println("if objB");
}
}
}
} else {
while (true) {
//拿到objB锁
synchronized (MyLock.objB) {
System.out.println(" else objB");
//此时需要objA锁。但objA锁没有释放,线程死锁
synchronized (MyLock.objA) {
System.out.println("else objA");
}
}
}
}
}
}

public class MyLock {
static final Object objA = new Object();
static final Object objB = new Object();
}


public class Test {
public static void main(String[] args) {
DieLock d1 = new DieLock(true);
DieLock d2 = new DieLock(false);
d1.start();
d2.start();
}
}

以上代码只是模拟了线程死锁的场景。在实际开发不会出现这样的问题。但是,在一些复杂的场景中,可能会出现这样的问题,比如t1线程拿到锁之后,因为一些异常情况没有释放掉锁(死循环)。又或者在释放锁时出现异常,导致释放失败。出现死锁,程序则不能继续提供服务。此时通过dump线程来查看是哪个线程出现了问题。

如何避免死锁:

  • 避免一个线程同时获取多个锁
  • 尽量保证一个锁只占一个资源
  • 多尝试使用定时锁lock.tryLock来代替传统锁
  • 对于数据库锁,加锁和解锁必须在一个数据库连接中

后面的博问会一步步的从java内存模型来分析,理解并发编程的艺术。

参考书籍 《java并发编程的艺术》 方腾飞