Java并发篇(1)入坑并发编程的正确姿势

791 阅读5分钟

为什么要使用并发编程

随着当今CPU的高速发展,4核、8核甚至16核CPU已经面世了。在以往单核CPU的时代,每一个线程只能争抢一个CPU去获取运行的权利。在多核CPU的场景下,一个线程已经无法充分地利用多个CPU了,再者,数字化时代更加加剧了用户对应用的性能需求,传统的单线程应用已经逐渐被淘汰了,通过多线程并发执行的形式可以将CPU的计算能力发挥到极限,这是为什么需要学习并发编程的一个重要原因。

并发编程也有缺点

频繁的上下文切换

即使单核处理器也支持多线程执行代码,CPU通过给每个线程分配时间片来实现多线程并发执行。当任务A时间片执行完以后会切换到下一个任务,此时需要保存当前任务A的状态,在下一次再次轮到任务A执行的时候需要恢复这个状态,这样的一次保存和恢复称为上下文切换。频繁的上下文切换会耗费系统大量资源!

减少上下文切换的方法有

  1. 无锁并发编程:可以参照concurrentHashMap锁分段的思想,每个桶都对应一个数据段,让不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间。
  2. CAS算法:Java的Atomic包使用CAS算法来更新数据,不需要加锁。
  3. 使用最少线程:避免创建不必要的线程,以免造成大量线程处于等待状态。
  4. 协程:在单线程里实现多线程的调度,并在单线程里维持多个任务之间的切换。

线程安全问题

并发编程环境下,多线程有可能带来的安全问题有:

  1. 线程死锁:每个线程互相等待对方释放正在占有的资源。
  2. 线程饥饿:线程永远无法得到CPU的时间片,永远处于等待状态。
public class DeadLockDemo {
    private static String resource_a = "A";
    private static String resource_b = "B";

    public static void main(String[] args) {
        deadLock();
    }
    public static void deadLock() {
        Thread threadA = new Thread(()->{
            synchronized (resource_a) {
                    System.out.println("get resource a");
                    try {
                        Thread.sleep(3000);
                        synchronized (resource_b) {
                            System.out.println("get resource b");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
        });
        Thread threadB = new Thread(()->{
            synchronized (resource_b) {
                    System.out.println("get resource b");
                    synchronized (resource_a) {
                        System.out.println("get resource a");
                    }
                }
        });
        threadA.start();
        threadB.start();
    }
}

上面这段代码演示了死锁这个场景,通过jps查看进程号,jstack查看应用线程状态,可以看到两个线程都在等待对方正在占有的资源,自身却不释放资源,造成永久地相互等待现象。

避免死锁的几种常见方法:

  1. 避免一个线程同时请求获取多个锁
  2. 给资源加上编号,线程只能按照编号按顺序获取锁
  3. 获取锁的时间加上一个期限值,不要让线程永久地请求锁
  4. 银行家算法,在分配锁之前判断是否会造成死锁,如果是则拒绝本次获取锁的请求

在学习并发编程前需要了解的概念

同步和异步

同步:方法A调用方法B后,需要等待方法B执行结束后才能继续执行方法A。

异步:方法A调用方法B后,方法A可以继续处理自己的业务逻辑,无需等待方法B执行结束。

比如,在超时购物,如果一件物品没了,你得等仓库人员跟你调货,直到仓库人员跟你把货物送过来,你才能继续去收银台付款,这就类似同步调用。而异步调用了,就像网购,你在网上付款下单后,什么事就不用管了,该干嘛就干嘛去了,当货物到达后你收到通知去取就好。

并发与并行

并发:多个线程不断地交替执行,在同一时刻只有一个线程在执行

并行:真正意义上的多个线程同时执行,多个线程分配在多个CPU上在同一时刻一起执行

比如,在单核CPU中,不存在并行执行线程的概念,只存在并发执行的概念。因为多个线程必须共享一个CPU,CPU不断地切换线程上下文让不同的线程执行(真的累)。在多核CPU中,多个线程都可以分配在不同的CPU上在同一时刻同时执行。

阻塞和非阻塞

阻塞:如果一个线程A占用了资源X,当线程B请求操作资源X时必须等待线程A释放资源X,等待的过程就称为阻塞。

非阻塞:和上面的例子相似,不同的是线程B请求操作资源X时不需要等待线程A释放,多个线程可以对资源X进行随意地访问。

临界区

临界区用来表示一种公共资源或者说是共享数据,可以被多个线程访问。但是每个线程对临界区资源进行操作时,一旦临界区资源被一个线程占有,那么其他线程必须等待。

例如Java中的CopyOnWriteArrayList,在读数据时不会对这个临界资源加锁,但是在对它添加、删除或更新数据时就会把整个集合锁住,其它线程必须等待该线程操作完后才能够访问。

总结

这是入坑并发编程之前必须了解的一些重要概念,掌握了这些概念,对接下来的学习会有很好的帮助!之后会持续更新并发系列的文章,希望能与你们交流,如果这篇文章对你有一点点帮助,你的一个小小的点赞能让我高兴一整天!感谢你的阅读!

巨人的肩膀:

《Java并发编程的艺术》

github.com/CL0610/Java…