JAVA中的锁

为什么需要加锁

每个计算机中都拥有多个处理器(CPU),而只有一块内存。cpu处理指令的时候需要与内存进行交互完成对数据的相关操作。但是每个cpu对指令的处理速度远远高于内存对于数据的读写速度,如果直接让cpu与内存进行交互会极大的浪费cpu的性能。所以缓存就出现了,缓存拥有高速读写数据的能力能最大程度利用cpu的高性能。缓存位于内存与cpu之间,目前使用最多的是三级缓存结构。低级缓存数据都是从高级缓存中复制了一部分而来,复制规则如下,时间局部性:如果某个数据被访问,那么它在不久的将来有可能被再次访问,空间局部性:如果某个数据被访问,那么与它相邻的数据很快可能被访问。所以当cpu处理指令时会优先到一级缓存中查找,没有再去二级缓存中查找,再没有会去三级缓存中查找,当低级缓存数据更新之后,会逐步将数据同步至高级缓存最后同步到内存中,多级缓存都遵从缓存一致性协议。缓存级数越低保存数据量越少处理速度越快,一级缓存、二级缓存都是每个cpu独有的,其余的cpu不能访问,而三级缓存是所有cpu共享的。所以当多个cpu并发执行线程的时候,就会导致 ①原子性问题:因为每个cpu执行线程是根据时间片来切换的,当某个cpu线程执行一段代码的时候时间片被切换了,如果此时另一个cpu线程执行了该段代码并对其中的数据做了处理,那么当上一线程切回来执行代码时就会造成前后数据的差异。②可见性问题:当某个cpu线程对数据做了处理之后,将处理后的数据刷新到三级缓存中甚至是主存中时时需要时间的,而这一段时间内对于其他的cpu线程来说该处理后的数据是不可见的。 ③有序性问题:因为cpu是多核处理的,为了保证资源的充分利用,编译器、处理器都对代码指令进行了乱序处理,即一段代码被分成了不同指令后可以同时处理,防止不相关的指令需要等待上一个指令结束才能开始。对于单线程来说是不用担心有序性问题的,因为重排过程中会遵循as-if-serial语义(不会对存在数据依赖关系的操作做重排序)。为了解决以上问题java内存模型中规定了多线程并发执行程序时的一些行为规范,所以产生了java内存模型的概念:Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行(此处工作内存与主内存只是虚拟概念)。而java内存模型(JMM)的实现就是通过volatile、synchronized等关键字进行加锁处理,或者通过concurrent并发包中的类实现。

乐观锁与悲观锁

提到锁就不得不说锁的两大概念,乐观锁与悲观锁。乐观锁:认为当前操作不会修改数据,所以不会对数据进行加锁处理。悲观锁:则认为当前操作会对数据进行修改,所以对数据加锁,等当前操作完成释放锁,其它操作才可获取该锁。前面已经说到悲观锁的实现是通过加锁来处理,可以在代码中通过synchronized,volatile关键字实现也可通过数据库层面添加排他锁来实现。而乐观锁的实现主要通过以下两种方式①版本号机制(推荐):对每条数据加一个version字段,每次要更新数据前都先将数据和version字段取出,处理数据的同时将version字段进行加一操作,处理数据后开始更新数据前,先将version字段与此时数据库中该字段version值做比较,如果相等或者小于说明该数据以被更新过,则需重新获取数据处理并更新。②CAS机制(compare and swap): cas操作需要有三个参数需要读写的内存位置(V),进行比较的预期值(A),拟写入的新值(B)。更新前也是先读取当前内存中数据的值作为预期值A,然后将A值与原值V比较一致再将新值B写入内存,如果不成功会一直重试,同时需要注意比较操作和写入操作需要保证原子性。在java中通过java.util.concurrent.atomic包提供的原子类(AtomicInteger,AtomicBoolean、AtomicLong等)中调用的Unsafe中的方法利用CPU提供的CAS操作来保证原子性(cpu在硬件层面上保证了cas操作的原子性),同时Unsafe中的方法中对于变量使用了volatile关键字保证了变量的可见性。不过利用CAS机制实现乐观锁有一个著名的ABA问题:即当线程一拿到A数据做CAS操作时,如果此时线程二同时获取了该数据并将A数据改为了B数据最后又改为A数据,那么此时线程一仍然可以完成对A数据的更新操作。那么什么时候需要用乐观锁什么时候需要用悲观锁呢? 首先乐观锁使用的条件有限制,比如只能CAS操作只能针对单条数据等,具体情况还需具体分析。其次当两种锁都能使用的时候则需要判断该数据竞争的激烈情况,如果竞争较为激烈最好使用悲观锁,不然乐观锁频繁更新失败会浪费cpu资源。

可重入锁、阻塞锁、自旋锁

可重入锁是指当某个线程获取该锁之后,可以再次获取该锁而不会出现死锁的情况。
阻塞锁是某个线程获取锁后让其余线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。常用的synchronize和Lock(ReentrantLock)都是可重入锁和阻塞锁。自旋锁和阻塞锁相反,如果某个线程获取锁后会让其它线程进入等待状态直到锁释放其它线程获取该锁。线程状态的切换需要做内核态与用户态的切换,如果该锁持有时间不长便可使用自旋锁减轻线程状态切换造成的cpu消耗。

偏向锁、轻量级锁、重量级锁

偏向锁,轻量级锁,重量级锁都是JDK1.6中为了对synchronized同步关键字进行优化而产生的的锁机制。在java对象内存模型中java对象有一个部分名为对象头,其中又有一个部分名为Mark Word(储存自身运行时数据)。偏向锁膨胀为轻量级锁膨胀为重量级锁的信息就是储存在Mark Word中的(具体过程自行百度)。偏向锁即当只有一个线程运行的时候,当该线程运行完成后下次再运行的时候无需获得该锁。当有第二个线程获取该锁的时候偏向锁会在没有代码执行的时候取消,并升级为轻量级锁,轻量级锁即多个线程有序运行不存在竞争情况。当存在多个线程竞争该锁的时候轻量级锁会自动膨胀成重量级锁。其实synchronized膨胀过程与ReentrantLock加锁过程是一样的,都是通过自旋锁判断是否有锁竞争,有则阻塞同时synchronized膨胀为重量级锁,无则获取锁。

共享锁,排他锁

共享锁:读锁,可被不同线程重复加锁,只能读取数据。
排他锁:写锁,只能同时被一个线程加锁,不允许有其他锁存在,只能写入数据。

锁消除、锁粗化

每次加锁,释放锁都会消耗系统资源。所以就需要锁粗化,将多次加锁解锁的过程合并成一个,减少系统损耗。同理如果JVM检测到一些同步代码不可能发生共享数据的竞争就会自动释放掉锁。

分布式锁

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值