Java并发(一)-相关概念

发布 : 2019-05-10 分类 : Java 浏览 :

Java 并发相关概念

并发

并发是在单核处理机上所任务运行的一种方式,根本上是串行的运行方式,通过时间多路复用算法(复用 CPU)达到部分并行的效果

并行

多核 cpu 运行方式

计算密集

大量计算

IO 密集

频繁 IO 读写

死锁

造成死锁的四个必要条件:

  1. 互斥
  2. 请求与等待
  3. 不可剥夺条件
  4. 循环与等待
    而破坏死锁的条件即是:破坏以上任意一个条件即可

饥饿

长时间未等待到 cpu 调度

竞争条件

不可抢占资源在某一时刻只能有一个线程占有,因此对于不可抢占资源的使用需要线程竞争

原子性

原子是世界上最小的单位,具有不可分割性
一个操作时原子操作,那么我们称他具有原子性

可见性

指当多个线程访问同一个变量时,一个线程修改了这个变量的指,其他线程能够立即看到修改的值

有序性

程序执行的顺序按照代码的先后顺序执行

内存栅栏

内存栅栏(memory barrier)就是从本地或工作内存到主存之间的拷贝动作。
仅当写操作先跨越内存栅栏,读操作后跨越内存栅栏的情况下,读操作才可以对写操作的更新可见。
关键字 synchronized 和 valatile 都强制规定了所有的变更必须全局可见,该特性有助于跨越内存栅栏
在程序运行过程中,所有的更改会在高速缓存或者寄存器中完成,然后才会拷贝到主存一跨越内存栅栏。这种跨越序列或顺序称之为 happen###before

缓存一致性

写操作更改发生在缓存中,缓存与主存数据需要保持一致性
如何解决缓存不一致的问题:

  1. 通过在总线 LOCK 加锁的方式:
    因为 CPU 和其他部件通信都是通过总线进行数据传输的,如果对总线加 LOCK 锁的话,也就是说阻塞了其他 CPU 对内存的访问,从而使得这一个只有一个 CPU 能只用这个变量的内存
  2. 通过缓存一致性协议
    最出名的就是 Intel 的 MESI 协议,MESI 协议保证了每个缓存中使用的共享变量的副本都是一致的。他的核心思想是:当 CPU 写数据时,如果发现操作的变量是共享变量,即在其他 CPU 缓存中也存在该变量的副本,会发出信号通知(广播)其他 CPU,将该变量的缓存行置为无效,因此,当其他 CPU 需要读取这个变量时候,发现自己缓存的变量是无效的,那么它会立即从内存中重新读取

CAS

CAS 算法是由硬件直接支持来保证原子性的,有三个操作数:内存位置 V、旧的预期值 A 和新值 B,当且仅当 V 符合预期值 A 时,CAS 用新值 B 原子化地更新 V 的值,否则,它什么都不做。

CAS 的 ABA 问题

当然 CAS 也并不完美,它存在”ABA”问题,假若一个变量初次读取是 A,在 compare 阶段依然是 A,但其实可能在此过程中,它先被改为 B,再被改回 A,而 CAS 是无法意识到这个问题的。CAS 只关注了比较前后的值是否改变,而无法清楚在此过程中变量的变更明细,这就是所谓的 ABA 漏洞。

锁相关

  • 可重入锁
    如果锁具备可重入性,则称作为可重入锁。像 synchronized 和 ReentrantLock 都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个 synchronized 方法时,比如说 method1,而在 method1 中会调用另外一个 synchronized 方法 method2,此时线程不必重新去申请锁,而是可以直接执行方法 method2。

    1
    2
    3
    4
    5
    6
    7
    8
    public MyClass{
    public synchronized void method1(){
    method2();
    }
    public synchronized void method2(){

    }
    }

    实现原理:每个锁都关联一个请求计数器和一个占有他的线程,当请求计数器为 0 时,这个锁可以被认为是 unheld 的,当一个线程请求一个 unheld 的锁时,JVM 记录锁的拥有者,并把锁的请求计数加 1,如果同一个线程再次请求这个锁时,请求计数器就会增加,当该线程退出 syncronized 块时,计数器减 1,当计数器为 0 时,锁被释放。

  • 可中断锁
    可中断锁:顾名思义,就是可以响应中断的锁。

    在 Java 中,synchronized 就不是可中断锁,而 Lock 是可中断锁。

    如果某一线程 A 正在执行锁中的代码,另一线程 B 正在等待获取该锁,可能由于等待时间过长,线程 B 不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

  • 公平锁
    公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。

    非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。

    在 Java 中,synchronized 就是非公平锁,它无法保证等待的线程获取锁的顺序。

    而对于 ReentrantLock 和 ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

    ReentrantLock lock = new ReentrantLock(true); \\公平锁

    实现原理: 通过链表记录线程请求锁的顺序

  • 读写锁
    更加细粒度的锁
    读写锁将对一个资源(比如文件)的访问分成了 2 个锁,一个读锁和一个写锁。

    正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。

    ReadWriteLock 就是读写锁,它是一个接口,ReentrantReadWriteLock 实现了这个接口。

    可以通过 readLock()获取读锁,通过 writeLock()获取写锁

  • 偏向锁
    java 偏向锁(Biased Locking)是 java6 引入的一项多线程优化.它通过消除资源无竞争 q 情况下的同步原语,进一步提高了程序的运行性能.

    偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。

    如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM 会尝试消除它身上的偏向锁,将锁恢复到标准的轻量级锁。(偏向锁只能在单线程下起作用)

    因此 流程是这样的 偏向锁->轻量级锁->重量级锁

    偏向锁,简单的讲,就是在锁对象的对象头中有个 ThreaddId 字段,这个字段如果是空的,第一次获取锁的时候,就将自身的 ThreadId 写入到锁的 ThreadId 字段内,将锁头内的是否偏向锁的状态位置 1.这样下次获取锁的时候,直接检查 ThreadId 是否和自身线程 Id 一致,如果一致,则认为当前线程已经获取了锁,因此不需再次获取锁,略过了轻量级锁和重量级锁的加锁阶段。提高了效率。

  • 乐观锁、悲观锁
    乐观锁

    总是认为不会产生并发问题,每次读取数据的时候认为不会有其他线程对数据进行修改,因此不会上锁。

    但是在更新的时候会判断其他线程在这之前有没有对数据进行修改,一般会使用版本控制机制或 CAS 实现。

    实现原理:

    • 版本控制机制

    一般是在数据表中加上一个数据版本号字段(version,或者版本更新时间字段),表示数据被修改的次数,当数据被修改时,version 值会加 1.

    当线程 A 要更新数据时,读取数据的同时也会读取 version,提交更新时对比 version,若之前读取的 version 与此刻数据库中 version 相等时进行更新,否则重试更新操作,直到更新成功。(类似锁的自旋)

    • CAS

    即 compare and swap 或者 compare and set,涉及到三个操作数:数据所在的内存值,预期值,新值。

    当需要更新时,判断内存值(公共)与旧的预期值(之前取的值)是否相等,若相等,则没有被其他线程修改过,使用新值进行更新,否则进行重试,一般情况下是一个自旋,即不断的重试。

本文作者 : 对六
原文链接 : http://duiliuliu.github.io/2019/05/10/java并发一/
版权声明 : 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!

你我共勉!

微信

微信

支付宝

支付宝

留下足迹