java多线程编程基础二(同步机制)

java提供的线程同步机制:锁、volatile关键字、final关键字、static关键字以及相关的API(如Object.wait/Object.notify)。

1、锁(Lock):

使多线程对共享数据(共享变量、共享资源)的并发访问,变为串行访问。

  • 锁具有排它性,一次只能被一个线程所持有。(互斥性)
  • 一个线程在访问共享数据时必须申请相应的锁,获得锁的线程称为锁的持有线程。
  • 锁的持有线程在获得锁之后到释放锁之前,所执行的代码,称为临界区
按照锁的实现方式不同分为:

a、内部锁(intrinsic Lock),通过synchronized关键字实现
b、显式锁(explicit lock),通过java.concurrent.locks.Lock接口的实现类实现

锁相关的概念:

a、可重入性:如何一个线程持有一个锁的时候,能够继续成功申请该锁,即为可重入(Reentrant),否则不可重入(Non-reentrant)
b、锁的争用和调度:锁可以看做多线程访问共享数据时所需要持有的一种排他性资源。

java中锁的调度策略:公平锁(内部锁),非公平锁(显式锁既支持公平锁,也支持非公平锁)

c、锁的粒度:一个锁可以保护一个或多个共享数据,一个锁所保护的共享数据的数量大小被称为锁粒度

锁的开销,及锁的问题:

锁的开销包括锁的申请和释放所产生的开销,以及锁可能导致上下文切换所产生的开销

锁存在被争用的情况,一旦发生争用,就会出现上下文切换的问题,未被争用的锁不会导致上下文切换

锁导致的线程活性故障:

a、锁泄露(lock leak):指一个线程在获取锁之后,由于程序错误,一致无法释放锁,从而导致其他线程无法正常获取锁。
b、死锁
c、锁死

内部锁和显式锁的区别:

a、灵活性,内部锁就是同步代码块,要么使用,要么不使用;显式锁是基于对象的锁,相对灵活。内部锁的锁获取和释放只能在代码块中执行,而显式锁能在一个方法中获取锁,而在另一个方法中释放锁。
b、易用性,内部锁简单易用,不会导致锁泄露;而显式锁由于锁的获取到释放灵活多变不好掌控
c、显式锁新增了针对锁的监控,而内部锁Thread.holdsLock(Object)只能检测当前线程是否持有指定的内部锁。
d、Java1.6/1.7 对内部锁做了一些优化,这些优化在特定情况下可以减少锁的开销。这些优化包括锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、偏向锁(Biased Lock)和适配性锁(Adaptive Lock)。而这些优化在Javal.6/1.7中并没有运用到显式锁上。
e、在Javal.5中,在高争用的情况下,内部锁的性能急剧下降,而显式锁的性能下降则少得多。换而育之,Java1.5中显式锁的可伸缩性(Scalability)比内部锁的可伸缩性要好。到了Java1.6,随着JDK对内部锁所做的一些改进,显式锁和内部锁之间的可伸缩性差异已经变得非常小了。

改进型锁:读写锁(read/wirte lock)是一种改进型的排他锁,也被称为共享/排他锁。

读写锁允许多个线程可以同时读取(只读)共享变量,而只允许一个线程更新共享变量;读取共享变量时,其它线程不允许更新共享变量,并且更新共享变量时,其它线程不可访问共享变量

对于同步在同一锁之上的线程而言,对共享变量仅进行读取而没有进行更新的线程被称为只读线程,简称读线程。对共享变量进行更新(包括先读取后更新)的线程就被称为写线程。

ReentrantReadWriteLock(读写锁),是一个可重入锁。其支持锁的降级(DownGrade),即一个线程获取写锁之后,可以继续获得相应的读锁;但是不支持锁的升级(UpGrade),即一个线程持有读锁的情况下,不能继续获取相应的写锁。

2、内存屏障(线程同步机制的底层实现):

锁保证可见性的实现方式(即获取锁和释放锁的内存逻辑):
a、刷新处理器缓存(保证在锁的持有线程在读取共享数据之前更新内存数据,保证数据的读写正确)
b、冲刷处理器缓存(保证在锁的持有线程在释放锁之后,共享数据对其它线程的可见性)
这两步操作是借助内存屏障实现的。

约定:对于同步在同一个锁之上的多个线程,我们称对共享变量进行更新的线程为写线程,对共享变量进行读取的线程为读线程。因此,一个线程可以既是写线程又是读线程。读线程、写线程在访问共享变量时必须持有相应的锁。

内存屏障是被插入到两个指令之间进行使用的,其作用是禁止编译器、处理器重排序从而保障有序性

3、锁与重排序:

多线程,使用锁状态下的编译器和处理器的重排序规则:
a、临界区内的操作不允许重排序到临界区之外
b、临界区内的操作之间允许被重排序
c、临界区外的操作之间允许被重排序
d、锁申请与锁释放不允许被重排序
e、两个锁申请操作不允许被重排序
f、两个锁释放操作不允许被重排序
g、临界区外的操作可以被重排序到临界区内(与编译器的实现规则有关,JIT是不会将临界区外的操作重排序的临界区内的)

4、volatile轻量级同步机制

volatile关键字修饰的变量不会被编译器分配到寄存器进行存储,并且读写操作都是内存访问(访问高速缓存相当于主内存)操作

volatile被称为轻量级锁的原因是:

  • volatile保证了可见性、有序性,但是没有锁的排它性。
  • volatile不会引起上下文的切换。

volatile 关键字的作用包括:保障可见性、保障有序性和保障long/double 型变量读写操作的原子性。

volatile 关键字在原子性方面仅保障对被修饰的变量的读操作、写操作本身的原子性。如果要保障对volatile变量的赋值操作的原子性,那么这个赋值操作不能涉及任何共享变量(包括被赋值的volatile变量本身)的访问。

5、对象的发布和逸出

对象的发布是指使对象能够在器作用域之外的线程访问

对象发布的形式:

a、private修饰的对象
b、将对象存储到public变量中
c、非private方法中返回一个对象
d、创建内部类,使得当前对象能够被这个内部类使用
e、通过方法调用将对象传递给外部方法

对象的初始化安全:

a、使用static 关键字修饰引用该对象的变量:仅保证读线程能读到相应字段的初始值,而不是最新值
b、使用final 关键字修饰引用该对象的变量:当一个对象的引用对其他线程可见的时候,这些线程所看到的该对象的final 字段必然是初始化完毕的。final c、关键字的作用仅是这种有序性的保障,它并不能保障包含final 字段的对象的引用自身对其他结程的可见性。
d、使用volatile关键字修饰引用该对象的变量。
e、使用Atomic Reference 来引用该对象。
f、对访问该对象的代码进行加锁。

你可能感兴趣的:(java多线程编程基础二(同步机制))