并发编程知识点全面总结

 

目录

为什么会出现锁

锁的类型以及相关概念

synchronized:

synchronized的具体是怎么实现的呢?

现在回到synchornized代码实现上

Volatile:

synchronized与Volatile的区别:

Condition:

Lock:

synchronized与Lock的区别:

AQS(AbstractQueuedSynchronizer):

 ReentrantLock:

ReentrantLock使用场景:

ThreadLocal:

ThreadLocal使用场景:

synchronized与ThreadLocal

jdk1.6对锁的优化:

jdk1.8新特性:

轻量级锁实现:

偏向锁实现:

轮询锁:

乐观锁与悲观锁:


为什么会出现锁

死锁如何产生:

1.对共享资源竞争

2.进程推进顺序不当

产生死锁的必要条件:

1.循环等待:

2.不可剥夺:进程已获得的资源,在未使用之前,不能强行剥夺

3.占有并请求:

4.互斥:

锁的类型以及相关概念

讲到锁,就得先讲一下同步,每当我们要用同步是就会想到synchronized,

那么java线程同步的方式其实有五种:

1.同步方法,synchronized修饰方法

2.同步代码块,synchronized修饰语句块

3.使用特殊域变量(volatile)实现线程同步

4.使用ReentrantLock来实现线程同步

5.使用局域变量来实现线程同步,如果使用ThreadLocal来管理变量,那么每一个使用该变量的线程都会获得该变量的副本,副本之间是相互独立的,这样每个线程修改自己变量的时候不会影响其他线程。

synchronized我们用起来是同步的作用,其实内部是锁来实现的,具体来看看synchronized到底是什么

synchronized:

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性

当被synchronized修饰时,synchronized会锁定当前变量,只有当前线程能够访问该变量,其他线程会被阻塞。

synchronized可以使用在变量和方法中

synchronized可以实现变量的修改可见性和原子性

synchronized的具体是怎么实现的呢?

首先来看两个概念Java对象头monitor

Java对象头和monitor是实现synchronized的基础

synchronized用的锁就是存在Java对象头

对象头

Hotspot的对象头主要包括两部分,Mark Word(标记字段),Klass Pointer(类型指针)。

Klass Pointer:对象指向类元数据的指针,虚拟机通过这个来确定对象是那个类的实例

Mark Word:存储自身运行时数据,比如哈希码,GC分代年龄,锁标记位,线程持有的锁等

Mark Word它是实现轻量级锁和偏向锁的关键。

Monitor

什么是Monitor?我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。

与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。

Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

现在回到synchornized代码实现上

当被synchronized关键字修饰的代码块在被编译成字节码的时候,会在该代码块开始和结束的地方加入 monitorenter 和 moniterexist 指令,任何对象都有一个monitor相关联,当一个monitor被持有后,他就处于锁定状态,当线程执行到monitorenter指令时,会获尝试获取对象对应的monitor的所有权,即获取对象的锁。

虚拟机在执行这两个命令的时候,会检查对象的锁状态是否为空或当前线程是否已经拥有对象的锁,

如果是 则对象锁的计数器加 1 ,直接进入同步代码,

如果不是,则当前线程阻塞等待,等待锁释放。

synchornized类似的还有Volatile,Lock,ThreadLocal

Volatile:

volatile,final,synchronized都可以实现可见性

使用volatile要满足的条件:

1.运算结果只保证一个线程修改变量的值或者不依赖于变量的当前值

2.不需要与其他状态变量参与不变的约束

3.访问变量时不需要加锁

Volatile的不变性:

1.当前处理器缓存行的数据会写回到系统内存

2.这个缓存操作会引起其他cpu中缓存该内存地址的数据失效

Volatile禁止指令重排序,因为Volatile变量在赋值后会有一个lock add命令,这个命令相当于内存屏障,重排序时不能把屏障后的指令重排序到屏障之前。

Volatile保证可见性:add指令会使得其他工作线程的工作内存缓存的数据失效

synchronized与Volatile的区别:

volatile,final,synchronized都可以实现可见性

volatile的本质是告诉jvm这个变量是在寄存器中的值是不确定的,需要从主存中读取。

synchronized是锁定当前变量,只有当前线程能够访问该变量,其他线程被阻塞。

volatile仅能实现变量的修改可见性,不具备原子性,而synchronized都可以保证

volatile标记的变量不会被编译器优化,而synchronized可以

volatile使用时变量级别,而synchronized可以使用在变量和方法

volatile不会阻塞线程,而synchronized可能会

Condition:

condition将Object监视器方法(wait,notify和notifyAll),分解成了多个不同的对象,然后通过这些对象和任意Lock进行结合使用。

Lock就相当于替代了synchronized方法和语句的使用。

condition替代了Objective监视器的使用方法。

condition是java5以后出现的机制,一个对象可以有多个condition(对象监视器),线程可以注册在不同的condition,可以更灵活的调度线程,有选择的调度线程,而synchronized相当于只有一个condition,所有的线程都注册在他身上,线程调度得调度所有的注册线程。

Lock:

Lock是使用condition进行线程之间的调度的。

Lock的锁定是通过代码实现的,而synchronized是在JVM层面实现。

Lock提供的是一种显示的,可轮询的定时的以及可中断的锁获取操作。

synchronized与Lock的区别:

1.Lock是一个接口,而synchronized是关键字,是在jvm层面实现的

2.Lock发生异常时,如果没有unlock()释放锁,会造成死锁现象,需要在finally块中释放,synchronized发生异常时会自动释放线程占有的锁

3.Lock可以让等待锁的线程相应中断,他不行,他得一直等待下去,不能中断

4.Lock可以知道是否成功获取锁,而他不行

5.Lock可以提高多个线程的读效率

Lock的锁定是通过代码实现的,而synchronized是在JVM层面实现。

适用场景:

如果竞争资源不激烈,两者性能差不多,当有大量线程同时竞争时,Lock的性能高于synchronized。

AQS(AbstractQueuedSynchronizer):

提供了一个基于FIFO队列,可以用于建阻塞锁或者同步容器的基础框架,AQS基于FIFO队列实现,存在一个个节点,node就是一个节点。对于FIFO中队列的各种操作在AQS中已经实现,AQS的子类只需要重写 tryAcquire(int arg) 和 tryRelease(int arg) 方法,用int值表示状态,子类必须定义更改此状态的受保护方法,并定义那种状态表示被获取或者被释放。这些都实现后,此类的其他方法就可以实现所有排队和阻塞机制。

tryAcquire(int arg) 试图在独占模式下获取对象状态。

acquire() 以独占模式获取对象,忽略中断。

tryRelease(int arg) 试图设置状态来反映独占模式下的下一个释放。

release()以独占模式释放对象

tryAcquireShared(int arg)试图在共享模式下获取对象状态。

tryReleaseShared(int arg)试图设置状态反应共享模式下的一个释放     

 ReentrantLock:

ReentrantLock是java.utils.locks的一个可重入锁类,在高竞争的情况下有良好的性能,可以中断支持重人性,即对公享资源可以反复加锁,当前线程再次获取该锁时,不会被堵塞,ReentrantLock是基于AQS实现,而AQS又是基于FIFO实现的,整个AQS其实是模板模式的经典应用,FIFO队列中所有的操作,AQS已经实现,AQS的子类只需要重写tryAcquire和tryRelese。

ReentrantLock中有一个抽象类syc继承与AbstractQuenedSynchronizer,syc的实现类FairSync,NoFairSync,即公平锁,非公平锁,他们都是ReentrantLock的静态内部类。ReentrantLock中用的比较多的是非公平锁。

非公平锁流程,线程1调用ReentrantLock的lock(),线程一变成独占,第一个线程做了两件事

1.设置AbstractQuenedSynchronizer的satae为1

2.设置AbstractQuenedSynchronizer的thread为当前线程

这样当第二个线程获取锁时,就执行else,调用acquire方法,进而调用acquire中的tryacquire。

公平锁:按照时间顺序,先等待的线程,先得到锁,公平锁不会产生饥饿锁,只要排队最终都能获得锁

非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁。

ReentrantLock使用场景:

1.发现子程序正在运行,可以再次进入并执行

2.需要使用中断锁

3.尝试等待执行,就是发现操作已经在执行,尝试一段时间后,等待超时就不执行

4.发现操作已经在执行,就不执行了c

ThreadLocal

ThreadLoacl为每一个线程创建了一个独立的变量副本,这样每个线程都可以独立改变自己的副本,操作变量时互不影响。这块与线程同步机制不同,线程同步机制是多个线程共享一个变量。制造变量副本是会消耗内存的,所以他们不同之处可以说ThreadLoacl实际上是用空间换取时间策略,而线程同步是时间换取空间策略。

ThreadLoaclMap是ThreadLoacl的一个静态内部类,它是实现线程隔离机制的关键

ThreadLocalMap提供了一种用键值对方式存储每一个线程的变量副本的方法,key为当前ThreadLocal对象,value则是对应线程的变量副本。

ThreadLocal使用场景:

数据库连接,session管理

比如在数据库连接池中,有多个DAO要获取连接,但这时需要完成事务,只能这些DAO获取同一个Connection,这样才能完成一个事务。从ThreadLoacl中获取Connetion的话,Dao就会被列入同一个Connection.

synchronized与ThreadLocal

可以说ThreadLoacl用于线程间数据隔离,而synchronized是线程间数据共享。

锁升级:

无锁 --> 偏向锁 --> 轻量级锁(利用CAS原理,避免重量级锁的消耗) --> 重量级锁

注意:锁可以升级,但是不能降级,为了减少获得锁和释放锁所带来的消耗

jdk1.6对锁的优化:

1.6中出现了轻量级锁,偏向锁,锁消除,适应性自旋锁,锁粗化开启,这些操作都是为了线程之间更高效的共享数据,解决竞争问题,主要是优化synchronized中获取锁,释放锁的性能问题。

jdk1.8新特性:

1.允许为接口添加一个非抽象的方法实现,使用default方法,叫扩展方法

2.Lambda表达式,匿名函数

3.方法与构造函数引用

4.函数式接口

轻量级锁实现:

轻量级锁的引入为了提升在没有提升线程竞争的情况下,执行同步代码的效率。

虚拟机使用CAS操作将对象头中的Mark Word 更新为当前线程的Lock Word的指针

如果更新成功,表示已经持有这个锁,mark Word的标记位 00,为轻量级锁

如果更新失败,虚拟机会检查对象中的Mark Word是否指向当前线程的栈帧,如果指向则直接进入同步代码直接执行,不是,说明线程竞争。如果有两条以上的线程抢占资源,轻量级锁膨胀为重量级锁,锁状态改为10,mark Word中存储指向重量级锁的指针,后面的锁进入阻塞状态。

轻量级性能提升的依据就是“对于大多数锁”在整个同步周期是不存在竞争的,没有竞争时轻量级锁利用CAS操作避免使用系统互斥量的开销。

偏向锁实现:

当只有一个线程执行同步块时,这种情况下,使用轻量级锁也会有多个CAS操作。所以开启偏向锁后,虚拟机检查当前线程是否处于无锁状态01,且标记为0没有偏向锁,如果成功,线程会用CAS操作把锁的线程ID放入对象的Mark Word中,以后有偏向锁的线程进入同步块时,虚拟机只看锁ID是否一样,如果是直接进入,不用CAS操作了。如果有其他线程获取该对象的锁,偏向模式就结束。根据锁的状态,撤销偏向锁后看恢复成无锁还是偏向锁。如同轻量级锁的介绍。

轮询锁:

不能同时获得所有锁时,可以使用轮询锁或者定时锁避免死锁。当一个线程获得多个锁时,已经获得一部分,另一部分没获得,此时返回失败,释放已经获得的锁,重新尝试获得所有的锁。

乐观锁与悲观锁:

每次拿数据时,都认为别人不会去修改,所以不上锁,更新时采取判断别人更改数据了没。

jdk中对乐观锁的实现就是CAS

分为三步骤:获取数据,写入校验,数据写入

适用于写比较少的情况,冲突发生的很少,减少系统开销,增大系统的吞吐量。

乐观锁大多是基于数据版本记录机制实现的,读取数据时,将版本号,一同读出,之后更新版本号加一,版本数据在于数据库表的版本信息比对,如果提交的数据版本号大于数据库当前版本,则给予更新,否则认为是过期

 

 

 

 

 

 

 

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值