并发编程概述初识

并发编程概述初识_第1张图片

  1. 分工:多线程并发最基本场景:各线程各司其职吗,完成不同的工作
  2. 同步:一个线程执行条件往往依赖于另一线程的执行结果
  3. 互斥:不同线程操作相同共享资源,比如访问相同的共享变量,强调线程安全问题

互斥问题很像数据库都是一样的,或者说天下的互斥都是一样的

并发的各种问题可以归结为三个源头:

  1. 缓存导致的可见性问题
  2. 线程切换带来的原子性问题
  3. 编译优化带来的有序性问题

1:缓存导致的可见性问题
CPU多核时代,同一变量再多个线程中有多个副本,线程对副本的操作对其他线程不可见
并发编程概述初识_第2张图片
2:线程切换带来的原子性问题
计算机允许运行多线程是因为操作系统的分时切换机制,提高了CPU的使用率,保证多线程可以相对公平的获取CPU,但是有一个不可避免的问题就是线程的切换,线程切换的时候,被休眠的线程会暂停,包括PC(程序计数器)与栈等,等到此线程再次被唤醒,可能发现已经物是人非了,因为一条高级语言可能对应多条CPU指令
并发编程概述初识_第3张图片
3:编译优化带来的有序性问题
JAVA为了优化性能,可能会对指令进行重排,这些重新排列再大部分时候是无害的,但是有时候,可能会导致意想不到的Bug,比较经典的问题是双重检查创建单例:在方法中先判断instance是否为空,为空就锁定整个类,创建前再判断一次是否为空,为空再创建。锁定以后再次进行判断,是为了避免在第一次判断之后,线程中断,其他线程完成了创建。

public class Singleton {
  static Singleton instance;
  static Singleton getInstance(){
    if (instance == null) {
      synchronized(Singleton.class) {
        if (instance == null)
          instance = new Singleton();
        }
    }
    return instance;
  }
}

一切看起来很完美,但实际上,这种单例模式的实现还是有问题的。因为new操作被重排了,我们认为new操作应该是:

分配一块内存;
在内存上完成Singleton对象的初始化;
把内存地址给instance;
但实际上优化后可能是这样的:

分配一块内存;
将内存地址给instance;
在内存上完成Singleton对象的初始化;
优化后,可能出现访问单例成员出现空指针异常。我们假设有两个线程A、B,A执行getIntance到指令2时,线程切换到线程B,线程B看到intance已经不为空,认为可用了。但这时还没有进行对象的初始化,这时使用instance,就可能出现问题。
并发编程概述初识_第4张图片

java并发问题归结为三类,安全性问题,活跃性问题和性能问题

  1. 安全性问题:本质为正确性问题,就是程序按照我们期望的方式执行。并发程序可能会因为上述的三种原因导致诡异的bug,但是可以确认bug的复现条件:多个线程共享一个数据,且数据可写。因此当这个条件出现的时候,我们就需要考虑程序是否存在安全性问题
  2. 活跃性问题:指的是某个操作无法执行下去,比如死锁,活锁,饥饿。
    死锁是因为两个线程均持有对方需要的资源,然后互相等待导致
    活锁则相反,是由于互相谦让导致,两个线程同时执行,发现冲突后会再次重试,然后一直谦让下去,解决办法:随机时间重试
    饥饿,指的是线程一直得不到执行的情况,
    如线程优先性低,系统忙时得不到执行,解决办法是公平锁,保证先排队先得到锁;
    或者所需资源被IO、锁等长时间占有资源,然后导致等待阻塞下去,解决办法是设计超时机制,超时处理
  3. 性能问题:滥用锁导致的,不需要用锁用了锁,用小锁的地方用了大锁。
    主要指标:吞吐(单位时间处理请求数)、时延(单次处理的平均耗时)、并发量(同一时刻可以介入的请求数)

你可能感兴趣的:(并发编程)