并发基础概念
- 同步与异步
- 同步:同步调用需要等待方法执行完成后返回
- 异步:不用等待执行方法执行完成后就直接放回,但不会马上有返回值
- 并发与并行
- 并发:线程之间进行调度执行
- 并行:两个或多个线程同时执行
- 临界区
- 临界区:表示一个公共资源或共享数据,可以被多个线程共享,但是每一次只能有一个线程进入使用,一旦临界区资源被占用,其他线程就需要等待这个资源。
- 阻塞与非阻塞
- 阻塞:阻塞指的是当某个资源被某个线程占用时,其他线程想要取得该资源就会进入等待状态,等待会导致线程被挂起,这种情况被称为阻塞。一旦发生阻塞线程需要等待大约80000时钟周期 ;而关于时钟周期1GHz的在处理效率上讲1秒是10亿的时钟周期。
- 非阻塞:非阻塞是允许多个线程同时进入临界区
- 锁使用不当造成的问题
-
死锁:当多个线程在完成后需要释放资源进入下一资源时,结果另一资源中也在等待下一资源的释放,如此循环。导致程序无法进行。但是死锁静态的,因为当前的cpu占用为零不会耗费资源。
- 解决方案:
- 确保至少有一个必要条件不成立,通过限制如何申请资源的方法来进行预防。
- 获取线程在申请资源和使用资源的信息,通过对每个线程的信息的掌握确定这个线程是马上可以执行还是延迟执行,从而来避免死锁的发生。
- 对死锁进行检测,如果发现死锁存在则结束掉一个线程对其所拥有的资源进行回收。
- 解决方案:
-
饥饿:常见的是当一个线程优先级低一直没有被cpu进行调度;也可能是当出现死锁时,由于多个线程并不会进行阻塞倒是抢占不到cpu也会进入饥饿。
- 解决方案:
- 使用公平锁将阻塞的线程放入FIFO的队列中进行等待。
- 解决方案:
-
活锁:相当两个线程都占用了资源的情况下并都需要对方的资源,就都释放自身的资源,结果两个线程又抢占资源,发现又被占用了,如此循环导致活锁,会一直占用资源。
- 解决方案:
- 使用随机暂停的方式,就是在线程在释放资源的情况下,使之随机的暂停一段时间,从而脱离资源占用问题。
- 解决方案:
-
优先级反转:一块临界区,一个高优先级和一个低优先级的线程都有访问权限,当低优先级占用了临界区资源,由于低优先级被CPU的调度时间比较少,高优先级在临界区外就一直处于阻塞等待,而其他一般优先级的线程执行时因为没有访问临界区资源,高优先级的线程反而一直没有执行;还有如果说高优先级线程没有处于阻塞等待情况下,而是自旋的获取锁,这样低优先级的线程就更没有调度时间,继而造成程序直接卡死。
- 解决方案:
- 给临界区设置一个高优先级,凡是进入临界区的线程都获得该高优先级,其他试图进入临界区的线程都不得高于临界区的优先级,就不会造成优先级反转。
- 当有高优先级线程在临界区外等待低优先级线程释放临界区时,让低优先级线程暂时具有高优先级的优先级别,在结束时在进行调回。
- 解决方案:
-
护航现象(Lock Convoys)
- 该现象的引发是因为多线程环境下因为多个线程对锁的的使用造成系统运行性能下降,当多个相同优先级的线程频繁的对一个锁产生竞争时就会引发护航现象,因为其线程的优先级是相同的,CPU的调度时间就是相等的,当一个线程获取了锁的使用权时,可能还没开始执行时间片就使用完成了,然后调度的其他线程又来竞争这把锁,因为锁已经被占用了不得不在时间片没有完的情况下让出调度权,之后获取锁的线程才又进行执行,完成之后其他线程又对线程进行竞争,如此循环往复的执行。造成了程序性能降低。
- 解决:
- 由上面的可以看出现护航现象的原因是因为多个线程对频繁的获取一个锁,可以采用先进先出原则,避免多个线程对线程的竞争。
- 还有就是可以采用像ConcurrentHashMap一样使用try尝试获取锁,在一定的时间内没有获取到锁时进入阻塞状态。
-
JAVA内存模型与线程安全
- 原子性:原子性指的是一个操作不能被中断,即使是多个线程在一起执行的时候,一个操作一旦开始,就不会被其他线程干扰
- 有序性
- 在并发时,程序在执行代码时可能不会按照你写代码的顺序进行执行,实际就是JVM在对程序进行代码执行时,会对代码执行顺序进行优化(指令重排),如果两段代码之间没有任何的逻辑关系时,就会出现乱序。
- 举例:jvm在创建对象的时候通过new关键字进行触发首先会根据class类型开辟对应大小的空间,之后对类对象的变量进行初始化,之后将开辟的空间的内存地址赋值给栈空间的引用。以上时jvm创建对象的过程,但是空间的初始化与引用赋值之间没有太多的逻辑关联,先赋值在初始化也是可以的,这样在多线程的情况下就会因为赋值而没有初始化的情况下,另外一个线程获取到对象的值为错误的值,造成系统问题。
- 可见性:指的是一个线程修改了某个共享变量的值,其他线程是否能够立即知道,造成线程之间获取的值不能同步;其造成可见性问题的发生其实时因为多核时代的出现,因为线程的执行是由多个cpu进行调配的,每个cpu内都有一个寄存器来保存一个变量的快照,当一个cpu对变量进行修改后会放到寄存器中进行缓存,而另一个cpu则读不到该cpu内的寄存器,就会出现变量不同步的问题,也就是修改后的变量不可见,所以就需要cpu将数据及时的更新到主存中,使得每个cpu都能获取到变量的更新。
- volatile
- 该关键字提示编译器在做被volatile修饰的变量更新时会及时的更新到主存中使得每个线程都能共享,并且还提示编译器不需要做一些优化防止出现有序性的发生。
- Happen-Before规则是java内存模型中定义的两项操作之间的偏序关系,从而判断数据是否存在竞争、线程是否安全的主要一句。
- 程序顺序原则:一个线程内保证语义的串行性
- volatile规则:volatile变量的写,先发生于读,这保证volatile变量的可见性
- 锁规则:解锁必然发生在随后的加锁前
- 传递性:A先于B,B先于C,那么A必然先于C
- 线程的start()方法先于它的每一个动作
- 线程的所有操作先于线程的终结
- 线程的中断先于被中断线程的代码
- 对象的构造函数执行结束先于finalize()方法
- 线程安全概念
- 指某个函数、函数库在多线程环境中被调用时,能够正确的处理各个线程的局部变量,使程序功能正确完成。
无障碍
- 特性:
- 无障碍是最弱的非阻塞调度
- 所有线程都能自由出入临界区(宽进严出)
- 无竞争时,有限步内完成操作
- 有竞争则会回滚数据
无锁(保证线程的原子性)
-
特性:
- 无障碍的
- 但是需要保证一个线程能够胜出,否则会造成线程进入循环干扰
-
无锁编程:
- 本质上才采用CAS机制进行的,CAS操作保证了一个原子性操作;
- CAS算法:它包含3个参数CAS(V,E,N),V表示要更新的变量,E表示旧的预期值,N表示要修改的新值,仅当V值等于E值时,才会将V的值设为N,如果V值有和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做 ,最后,CAS返回当前V的真实值,CAS操作时抱着乐观 的态度进行的,它总认为自己可以成功完成操作,当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,失败的线程不会被挂起,仅时被告知失败,并且允许再次尝试,当然也运训失败的线程放弃操作,基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理
- CAS本质上是乐观锁的实现,但是使用CAS操作时可能引发ABA的问题:
- 举例:
线程A-->>通过获取到V的值。 线程B-->>通过CAS操作将V该为了B 线程C-->>发现V的值是B,通过CAS操作将V的值又改成了A 线程A-->>V的值还是A,没有发生改变?
- 以上的情况中可能在平常情况下并不会影响程序的执行,但是在某些情况下可能因为值的变更系统没有及时捕获出现问题,解决方案: 使用版本控制,每次对值进行更新的时候还要对版本号进行比较,如果版本号和V都相等的情况下才对值进行修改,否则不予修改
- 举例:
-
jdk提供的无锁类(原子类)
- Java.util.concurrent 中提供了一些实现的原子操作的类,包括:AtomicBoolean、AtomicInteger、AtomicIntegerArray、AtomicLong、AtomicReference、AtomicReferenceArray。
- Unsafe类直接提供了硬件级的原子操作(原子类内部使用)
- 根据偏移量设置数据
- Park()
- 底层的CAS操作
- 非公开的API,在不同版本中差异较大
无等待
- 特性:
- 无锁的
- 要求所有的线程都必须在有限步内完成
- 无饥饿的