并发理论基础

背景:

cpu、内存、I/O设备不断迭代,但是在快速发展过程中,有一个核心矛盾一直存在,就是三者的速度差异,为了提高计算机性能,合理利用cpu,做出了以下方案:

  1. cpu增加了缓存,均衡与内存的速度差异
  2. 操作系统增加了进程、线程,以分时复用cpu
  3. 编译程序优化了指令执行效率,使缓存更加合理的利用
出现的问题:

但是这也是并发程序异常的根源之处

  1. 缓存导致了可见性问题(一个线程对共享变量的修改,另外一个线程能立即看到)
    多核cpu,每个cpu都有自己的缓存,都会从各自的缓存中读值,另外值得注意的是两个线程不是同时启动的,有一个时差,数据量越大,错误率越高。
  2. 线程切换带来了原子性问题
    操作系统会进行基于时间片的任务切换,而做任务切换,可以发生在任何一条cpu指令执行完(不是高级语言的一条语句),这样也就带来了无法保证原子性的问题
  3. 编译优化带来的有序性问题
    经典案例:利用双重检查创建单例对象
    Singleton instance = new Singleton();
    new操作为:(分配一块内存M,在内存M上初始化对象Singleton,将M的地址赋值给instance变量),但是实际上优化后会先将M的地址赋值给instance变量,那如果在此时进行了线程切换,实际上并没有初始化instance,但是变量的引用不为空。
    【如果对instance进行volatile声明,可以禁止指令重排序,避免发生】
    【对于有volatile语义声明的变量,写入时线程执行完后会强制将值刷新到内存中,读时线程也会强制重新把内存中的内容写到自己的缓存,这就是写入屏障问题?也是happen-before问题?】
如何解决问题:
  1. 解决可见性和有序性—按需禁用缓存和编译优化
    java内存模型规范了jvm如何按需禁用缓存和编译优化的方法,对编译器和处理器进行限制,包括三个关键字volatile、synchronized和final,六项happens-before规则
    happens-before规则(A happends-before B:A的操作结果对B是可见的)
    1. 程序的顺序性规则:按照程序的顺序,前面的操作(修改变量的值)后面是可见的(顺序执行,限制了编译器的优化)
    2. volatile变量规则:对一个volatile变量的写操作对这个变量后续的读操作是可见的
    3. 传递性:A happends-before B ,且B happends-before C ,那么A happends-before C
    4. 锁的规则:java中synchronized是对管程的实现,对一个锁的解锁对后续的这个锁的加锁是可见的。在解锁时,jvm需要强制刷新缓存。
    5. 线程start()规则(启动):线程A中启动线程B,B能看到A在启动B前的操作
    6. 线程join()规则(终止):如果在线程A中,调用线程B的join()并成功返回,那么线程B中的操作Happends-Before于该join()操作的返回。(通过Thread.isAlive()检测到线程是都终止执行)
    7. 线程中断规则:线程interrupt()方法的调用先行于被中断线程的代码检测到中断时间的发生
  2. 解决原子性—同一时刻只有一个线程执行—互斥锁
    • synchronized—加锁的本质是在锁对象的对象头中写入当前线程的id
      重要的点:我们锁的是什么?我们保护的又是什么?
      锁的是:当修饰静态方法的时候,锁定的是当前类的Class对象
      当修饰非静态方法的时候,锁定的是当前实例对象this
      当修饰代码块,锁定的是传入的对象
      受保护资源和锁之间的关联关系是N:1的关系
      一个保护资源被多把锁保护,会出现并发问题(不能保证同一时刻只有一个线程执行)
      一把锁可以保护多个资源
      保护没有关联关系的多个资源:一把锁会导致串行,性能差。用不同的锁对受保护资源进行精细化管理,能提升性能,这种锁叫做细粒度锁。
      保护有关联关系的多个资源:使用this锁不能覆盖所有受保护的资源,要使用Class作为共享的锁,要选择粒度更大的锁(串行化,性能差)
      【性能优化:账户A向账户B转账,this为A加锁,账户B的实例为B加锁—但是会导致死锁问题】

    • 性能提升(细粒度锁)—导致死锁—一组互相竞争资源的线程因互相等待,导致“永久”阻塞
      如何预防死锁(解决死锁问题最好的办法就是规避死锁)
      死锁出现的4个必备条件:互斥,占有且等待,不可抢占,循环等待(破换一个就可避免)、
      占有且等待:一次性申请所有的资源(使用一个角色来管理临界区,同时申请资源和同时释放资源)
      不可抢占:如果申请不到,就主动释放占有资源(java.util.concurrent下提供的Lock可以解决)
      循环等待:按序申请资源(为资源进行排序,申请时按从小到大顺序申请)

    • “等待-通知”—对规避死锁的性能提升—线程首先获取互斥锁,当线程要求的条件不满足时,释放互斥锁,进入等待状态;当要求的条件满足时,通知等待的线程,重新获取互斥锁。
      实现:synchronized配合wait()、notify()、notifyAll()
      当线程拿锁进入临界区后,由于条件不满足,调用wait()方法,进入等待队列,释放持有的锁,当线程的条件满足时,调用notifyAll(),通知等待队列中的线程,条件曾经满足过(因为通知时条件满足,但是被通知的线程还要去获得锁去执行,这两个时间点不会重合)

未完待续

安全性、活跃性以及性能问题

管程:并发编程的万能钥匙

java线程上:java线程的生命周期

java线程中:创建多少线程才是合适的?

java线程下:为什么局部变量的线程是安全的?

如何用面向兑现思想写好并发程序?

猜你喜欢

转载自blog.csdn.net/qq_36741161/article/details/88427879