0
点赞
收藏
分享

微信扫一扫

IO流及流的分类

Android开发指南 23小时前 阅读 0

目录

java中常见的锁策略

synchronized的优化策略

关于CAS


java中常见的锁策略  

锁策略可以理解成一把锁在加锁/解锁/遇到锁冲突的时候,都会怎么做

 本节将介绍几种常见的锁策略:

一、悲观锁VS乐观锁

我们在加锁的时候,要预测当前锁冲突的概率是大还是小。

悲观锁:如果预测当前锁冲突概率大,后续要做的工作就会更多。加锁开销就更大(时间,系统资源 )。

乐观锁:如果预测当前锁冲突概率不大,后续要做的工作往往就更少。加锁开销就更小。

值得注意的是,java中常用的synchronized既是乐观锁,也是悲观锁。原因是synchronized能够自动的统计出当前是锁冲突概率低,还是概率高。当冲突概率低的时候,就是按照乐观锁的方式来执行的(速度更快)。当冲突概率高的时候,就会升级成悲观锁的方式来执行(做的工作更多)。

二、重量级锁VS轻量级锁

一般来说,悲观锁往往就是重量级锁。乐观锁往往就是轻量级锁。

加锁过程中做的事情多,就是重量级锁。加锁过程中做的事情少,就是轻量级锁。

一般来说,锁冲突概率高得时候,就得多做处理,锁冲突概率低的时候,就可以少做处理。

所以实际交流过程中,这两组概念可能会混着用。

三、自旋锁VS挂起等待锁

自旋锁是轻量级锁的一种典型实现方式。挂起等待锁是重量级锁的一种典型实现方式。

自旋锁的内部可以理解为以下伪代码:

在加锁的过程中,自旋锁一直在判断锁是否被占用,这个过程消耗了更多的cpu资源,但是一旦锁被释放,就能第一时间拿到锁,拿到锁的速度更快,但是会消耗更多的cpu资源。

挂起等待锁借助了系统中的线程调度机制。当尝试加锁,并且锁被占用了,出现锁冲突,就会让当前这个尝试加锁的线程,被挂起(进入阻塞状态)。此时这个线程就不会参与调度了。知道这个锁被释放,然后系统才能唤醒这个线程,去尝试重新获取锁。

挂起等待锁消耗的时间更长,一旦线程被阻塞了,啥时候被唤醒,这个过程是不可控的,可能会经历很长很长的时间。拿到锁的速度更慢,节省cpu。

四、可重入锁VS不可重入锁

synchronized就是一个可重入锁,一个线程,针对这把锁,连续加锁两次,不会死锁。

例如C++的std::mutex就是不可重入锁,一个线程针对这把锁,连续加锁两次,会死锁。

五、公平锁VS非公平锁

公平锁:严格按照先来后到的顺序来获取锁。哪个线程等待的时间长,哪个线程就拿到锁。

非公平锁:若干个线程,各凭本事,随机的获取到锁,和线程等待时间无关了。

synchronized属于非公平锁,多个线程尝试获取到这个锁。此时是按概率均等的方式来进行获取的。主要原因是系统本身线程调度的顺序就是随机的。如果需要实现公平锁,就需要引入额外的队列,按照加锁顺序把这些获取锁的线程入队列,再一个个的取。

六、互斥锁VS读写锁

synchronized本身就是普通的互斥锁。提供加锁和解锁两个操作。

而读写锁提供加读锁,加写锁,解锁三种操作。

java中的读写锁是这样设定的:

1)读锁和写锁之间,不会产生互斥。

2)写锁和写锁之间,会产生互斥。

3)读锁和写锁之间,会产生互斥。

突出体现的是“读操作和读操作”之间是共享的(不会互斥的),有利于降低锁冲突的概率。提高并发能力。日常开发中,有很多场景,属于“读多,写少”,大部分操作都是读,偶尔有写操作。如果使用普通的互斥锁,此时,每次读操作之间,都会互斥。此时,就会比较影响效率。如果使用读写锁,就能够有效的降低锁冲突的概率,提高效率。

synchronized的优化策略

synchronized既是悲观锁,也是乐观锁,既是轻量级锁,也是重量级锁。自旋锁是轻量级锁的实现,挂起等待锁是重量级锁的实现。synchronized是可重入锁,不是读写锁,是非公平锁。

synchronized的“自适应”是根据当前锁冲突的概率来调整锁策略。调整的过程分为以下阶段:

偏向锁:首次使用synchronized对对象进行加锁的时候,不是真的加锁。而只是做一个“标记”(非常轻量非常快,几乎没有开销)。如果没有别的线程尝试对这个对象加锁,就可以保持这个暧昧状态,一直到解锁。(解锁也就是修改一下上述标记,也几乎没有开销)上述过程,就相当于没有任何的加锁操作,速度是非常快的,也可以保证锁能够正常生效。但是,如果再偏向锁状态下,有某个线程也尝试来对这个对象加锁,立马把偏向锁升级成轻量级锁(真的有锁,真的有互斥了)

上述升级过程,针对一个锁对象来说,是不可逆的。一旦升级成了重量级锁,不会退回到轻量级锁。

除了以上锁升级,线程的优化策略还有锁消除和锁粗化。

锁消除:代码里写了加锁操作,在编译阶段,编译器&JVM会对你当前的代码做出判定,看这个地方到底是不是真的需要加锁。如果这里不需要加锁,就会自动的把加锁操作给优化掉。

锁粗化:

锁的粒度:加锁的范围内,包含代码越多,就认为锁的粒度更粗,反之,锁的粒度就越细。

有些逻辑中,需要频繁的加锁解锁,编译器就会自动的把多次细粒度的锁,合并成一次粗粒度的锁。

关于CAS

compare and swap 比较和交换,简称CAS。这是一条cpu指令,可以完成比较和交换的一套操作。由于是一条cpu指令,所以是原子的,不会出现线程安全问题。

CAS的执行流程大体如下:

这里说的“交换”,实际上更多的是用来“赋值”,一般更关心内存中,交换后的数据,而不关心reg2寄存器里交换后的数据。所以此处也可以近似的认为把reg2中的值赋值给内存中。

在java中,也有一些类,对CAS进行了进一步的封装。典型的就是“原子类”。

AtomicInteger类中提供了各种方法来使程序在多个线程修改同一变量的时候,保证线程安全

 //count++;
                count.getAndIncrement();
                //++count;
                count.incrementAndGet();
                //count--;
                count.getAndDecrement();
                //--count;
                count.decrementAndGet();
                //count +=10
                count.getAndAdd(10);

通过原子类,我们能够实现“无锁编程”,提高程序运行效率。

但是CAS的适用范围没有锁更广泛,只针对一些特殊场景。

以上关于锁策略,希望对你有所帮助。

举报

相关推荐

0 条评论