锁的初体验
Posted farmersun
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了锁的初体验相关的知识,希望对你有一定的参考价值。
synchronized
synchronized 关键字声明的方法同一时间只能被一个线程访问。
synchronized 锁的是对象而不是代码,锁方法锁的是this,锁static方法锁的是class。
锁定方法和非锁定方法是可以同步执行的。
public synchronized void doSomething(){
.......
}
synchronized原理,锁升级
偏向锁:如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。
自旋锁:偏向锁有多个线程(相对较多)竞争的时候就会升级为自旋锁。自旋形象的比喻就是在锁的边上等,不在锁的队列中。
重量级锁:在自旋多次后还是拿不到锁救护i升级为重量级锁。重量级锁需要到操作系统申请资源,效率比较低。
系统锁:加锁代码执行时间长、线程数多
自旋锁:加锁代码执行时间段、线程数少
注意事项
控制加锁代码的范围,避免资源浪费。
synchronized(object)不能用在string常量、Integer、Long等类型上
volatile
一个 volatile 对象引用可能是 null。
volatile作用
线程的可见性
- MESI:CPU高速缓存一致性协议
- volatile 修饰的成员变量在每次被线程访问时,都强制从共享内存中(堆内存)重新读取该成员变量的值。当成员变量发生变化时,会强制线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
禁止指令重排序(CPU)
- CPU为了提高效率,会将指令并发执行。volatile关键字会在指令之间加上读屏障和写屏障,保证指令线性执行。
- 懒汉式单利模式用到了volition关键字
public class MyRunnable implements Runnable
{
private volatile boolean active;
public void run()
{
active = true;
while (active) // 第一行
{
// 代码
}
}
public void stop()
{
active = false; // 第二行
}
}
通常情况下,在一个线程调用 run() 方法(在 Runnable 开启的线程),在另一个线程调用 stop() 方法。
如果 第一行 中缓冲区的 active 值被使用,那么在 第二行 的 active 值为 false 时循环不会停止。
但是以上代码中我们使用了 volatile 修饰 active,所以该循环会停止。
CAS(CompareAndSet)
无锁优化,自旋锁、乐观锁
Atomic类
java提供了一些原子类,内部自带锁,这些锁的实现不是synchronized重量级的,是通过CAS实现的。
比如有:AtomicBoolean、AtomicInteger、AtomicLong
这些类的API操作可以实现同步。
通过原子类的代码跟踪,就会发现原子类的实现是依赖与unsafe类。Atomic类内部调用unsafe类的方法CompareAndSet(对比和设定),这是一种乐观锁的实现方式
cas(Value,Expection,NewValue){
if(V==E) V=NewValue;
}
//Value是需要修改的值,Expection是期望修改值Value的现在值,NewValue是要设定的新值。
//CAS乐观锁就是在修改值的时候,对比要修改的值和期望值是不是一致,是才能修改,不是不能修改,会进行下一次尝试
CAS是CPU原语级别实现的,中间不能被打断
ABA问题
在CAS算法中,需要取出内存中某时刻的数据(由用户完成),在下一时刻比较并交换(CPU保证原子操作),这个时间差会导致数据的变化。 假设有以下顺序事件: > 1、线程1从内存位置V中取出A > 2、线程2从内存位置V中取出A > 3、线程2进行了写操作,将B写入内存位置V > 4、线程2将A再次写入内存位置V > 5、线程1进行CAS操作,发现V中仍然是A,交换成功
尽管线程1的CAS操作成功,但线程1并不知道内存位置V的数据发生过改变
解决ABA问题
要解决ABA问题,可以增加一个版本号,当内存位置V的值每次被修改后,版本号都加1
AtomicStampedReference内部维护了对象值和版本号,在创建AtomicStampedReference对象时,需要传入初始值和初始版本号,
当AtomicStampedReference设置对象值时,对象值以及状态戳都必须满足期望值,写入才会成功
unsafe类
Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。
Unsafe可以直接操作内存,直接生成类实例,直接操作类或者实例变量
LongAdder、AtomicLong、synchronized比较
为什么sync比atomic快?
atomic不加锁,cas实现的原子操作
为什么longadder比atomic快?
LongAdder内部做了一个分段锁,高并发时将对单一变量的CAS操作分散为对数组cells中多个元素的CAS操作,取值时进行求和;
并发较低时仅对base变量进行CAS操作,与AtomicLong类原理相同。
RenntrantLock 可重入锁
synchronized本身也是一种可重入锁。否则子类调用父类不能实现。
可重入:比如同一个类的两个方法都上锁this,方法一在执行期间调用了方法二,因为两把锁的对象是同一个,所以可以执行方法二。
RenntrantLock 可以替代 synchronized
可以在使用synchronized(synchronized(this))的地方替换成RenntrantLock(lock.lock())。
不同点在于RenntrantLock 需要手动解锁(lock.unlock),手动解锁一定写在finally里面避免死锁。
tryLock()尝试锁定,不管锁定与否,方法都会继续执行,可以自己决定要不要wait
lockInterruptibly() 会对Interrupt()做出响应,是一个可以被打断的锁。lock.lock()是不能被打断的。
公平锁 RenntrantLock的构造方法可以传一个boolean值,传true即为公平锁。公平锁会优先之前在排队的线程先执行。
RenntrantLock默认是非公平锁。
CountDownLatch
latch是门栓的意思,计数到了,门就开了。
当一个线程执行到latch.await()方法的时候会进行等待,并在在构造 new CountDownLatch(num)传的值num上减1,一直等这个num值减到0的时候程序才会继续向下执行。
countDown要比join灵活,join需要等线程执行结束之后才会继续执行。
CyslicBarrier
循环栅栏。barry.await()会等待在构造new CyslicBarrier(num,new Runnable(){})设置的num满了才会继续执行。
有一种等人齐发车的意思。
Phaser
阶段。结合了CountDownLatch 和 CyslicBarrier。
Phaser按照不同的阶段执行线程,本身维护着一个成员变量。
phaser.arriveAndAwaitAdvance()方法意思是到达等待继续向下一个阶段走。
phaser.arriveAndDeregister()方法可以在某一阶段解除某个线程的注册
phaser.register()在某一阶段注册添加线程
ReadWriteLock
读写锁。读锁是共享锁,写锁是排他锁。
ReentranReadWriteLock是ReadWriteLock锁的一种实现,在里面又分为readLock和WriteLock。
读锁可以一起读,写锁需要锁等待。
Semaphore
信号灯。可以传一个参数,permits是允许的数量,可以做限流。
s.acquire()方法叫阻塞方法,线程执行前先用该方法判断下是不是能获取到允许继续执行,不能获取到就得等待。
s.release()方法是线程结束后释放允许的权限,将权限还回去给下一个线程使用。
Semaphore默认是非公平锁。它的第二个参数可以传递一个boolean值确定是不是公平锁。
Exchanger
交换器。可以在两个线程间交换数据。
s是线程的某信息,使用e.exchange(s)可以获取到另一个线程交换的信数据。
以上是关于锁的初体验的主要内容,如果未能解决你的问题,请参考以下文章