文章目录
前言
对java众多锁机制的学习,予以记录!
学习三板斧:理论、代码、小总结
缓冲的三大基本操作:读、写,清空
一、公平锁与非公平锁
公平锁: 多个线程按照申请锁的顺序来获取锁,类似排队打饭,队列,FIFO先来后到。
非公平锁: 多个线程获取锁顺序并不是按照申请锁的顺序来获取锁,有可能后申请的线程比先申请的线程优先获取锁。在高并发情况下可能造成优先级反转或饥饿现象。抢占式,先抢先得,抢不到才排队。
1、优先级反转
一个线程执行完毕,本该为优先级高的2号线程执行,但由于非公平锁,使得8号线程获取到锁
2、饥饿现象
优先级低的反而频繁获取锁,造成其他线程因为没有获取到锁,而产生“饥饿”现象
3、两者区别
公平锁,就是很公平,在并发环境中,每个线程和在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
非公平锁比较粗鲁,上来就尝试占有锁,如果尝试失败,再采用类似公平锁的那种方式。
Lock lock = new ReentrantLock(true);
ReentrantLock可指定构造函数Boolean参数确定它为公平true or 非公平false,默认非公平锁。非公平锁吞吐量比公平锁大
Synchronized为非公平锁
二、可重入锁(递归锁)
1、理论
可重入锁(又名递归锁): 指在同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码, 在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。即: 线程可以进入任何一个它已经拥有的锁所同步着的代码块
可重入锁最大的作用是避免死锁
2、代码验证:ReentrantLock,Synchronized是可重入锁
/*目的:验证synchronized与ReentrantLock为可重入锁*/
//声明资源类Phone,
class Phone implements Runnable{
@Override
public void run() {
get();
}
// sendMess()与receiveMess()验证synchronized,其中sendMess()调用了receiveMess()
public synchronized void sendMess(){
System.out.println(Thread.currentThread().getName()+"\tsend message!");
receiveMess();
}
public synchronized void receiveMess() {
System.out.println(Thread.currentThread().getName()+"\treceive message!");
}
// get()与set()验证ReentrantLock,其中get()调用了set()
ReentrantLock lock = new ReentrantLock();
public void get(){
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+"\tget!");
set();
}
finally {
lock.unlock();
}
}
public void set(){
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+"\tset!");
}
finally {
lock.unlock();
}
}
}
public class ReentrantLockDemo{
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
//建立两个线程调用sendMess()
for (int i = 0; i < 2; i++) {
new Thread(()->{
phone.sendMess();
},"t"+String.valueOf(i)).start();
}
//建立两个线程调用get()
TimeUnit.SECONDS.sleep(2);
System.out.println();
System.out.println();
System.out.println();
Phone p1 = new Phone();
Phone p2 = new Phone();
Thread thread1 = new Thread(p1,"t3");
Thread thread2 = new Thread(p2,"t4");
thread1.start();
thread2.start();
}
}
结果:
多执行几次:
3、结论
观察结果发现,在synchronized修饰的sendMess()中调用synchronized修饰的receiveMess()竟然是同一个线程,也就说明了synchronized是一个可重入锁;
在get()中调用set()中使用ReentrantLock加锁后竟然是同一个线程调用,也就说明了ReentrantLock也是一个可重入锁。
4、那么Synchronized与ReentrantLock有什么区别?
项目 | synchronized | lock |
---|---|---|
原始结构 | jvm层面关键字 | API层面java.util.concurrent.locks.Lock |
使用方法 | 不需要用户手动释放锁,底层为monitorenter与monitorexit(1:2) | 需要用户手动释放锁,lock()与unlock()在try catch finally中使用 |
等待是否可中断 | 不可中断,除非抛出异常或者正常运行完成 | 可中断,设置超时方法tryLock(Long timeout,TimeUnit unit);lockInterruptibly(),调用interrupt()可中断 |
加锁是否公平 | 非公平锁 | 默认为非公平锁可传入true指定为公平锁 |
有无绑定多个条件的condition | 没有,要么随即唤醒一个线程notify(),要么唤醒全部线程notifyall() | 有,可以精确唤醒需要唤醒的线程condition.await(),condition.singall() |
5、问题:关于ReentrantLock的加锁与释放锁次数不一致
①get()中,次数:lock()=unlock(),结果:程序正常执行完成
结果:程序正常执行
②get()中,次数:lock()大于unlock(),结果:程序通过编译且可以运行,无法结束:由于缺少一次unlock()导致其他线程阻塞
结果:程序通过编译且可以运行,无法结束:由于缺少一次unlock()导致其他线程无法进行
③get()中,次数:lock() 小于unlock(),结果:程序运行完成但抛出异常illegalMonitorStateException
结果:程序运行完成但抛出异常illegalMonitorStateException
6、结论:lock次数需与unlock次数一致
1.次数unlock()==lock(),程序正常执行
2.次数unlock()<lock(),程序可以通过编译,也可以正常运行,但无法结束。由于缺少unlock(),其他线程无法继续执行。
3.次数unlock()>lock(),程序可以通过编译,也可以正常运行,但抛出异常。
三、自旋锁
1、理论
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式
去尝试获取锁。
目的: 减少线程上下文频繁的切换而引起的性能损耗,所以才自旋让当前线程一直占用资源。
好处: 循环比较获取直到成功为止,没有类似wait()的阻塞
缺点: 循环会消耗CPU资源
2、代码验证
①AtomicInteger实现原理CAS
atomicInteger.getAndIncrement();
public final int getAndIncrement() {
return U.getAndAddInt(this, VALUE, 1);
}
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
②自己编写一个自旋锁
/*目的:验证自旋锁的处理机制*/
public class SpinLockDemo {
//实现一个有关于线程的原子类引用
AtomicReference<Thread> atomicReference = new AtomicReference<>();
//线程获取锁
public void mylock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName()+"获得锁");
while(!atomicReference.compareAndSet(null,thread)){
}
}
//线程释放锁
public void myUnlock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName()+"释放锁");
atomicReference.compareAndSet(thread,null);
}
public static void main(String[] args) throws InterruptedException {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(()->{
spinLockDemo.mylock();
try {
TimeUnit.SECONDS.sleep(4);//占用锁4s
spinLockDemo.myUnlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"AAA").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
try {
spinLockDemo.mylock();
TimeUnit.SECONDS.sleep(4);//占用锁4s
spinLockDemo.myUnlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"BBB").start();
}
}
3、结论:自旋锁会使线程通过循环比较方式获取锁直到成功为止
四、读写锁
1、理论
独占锁(写锁):该锁一次只能被一个线程所持有
共享锁(读锁):该锁可被多个线程所持有,
synchronized --> ReentrantLock–> ReentrantReadWriteLock
ReentrantReadWriteLock读共享锁,写独占锁
读-读可共存
读-写不可共存
写-写不可共存
2、代码验证:模拟一个分布式缓存进行多线程读写操作
分布式缓存模拟:map实现
缓存三大基本操作:读,写,清空
业务要求:写操作 原子+独占 ,整个过程必须是一个完整的统一体,不许被分割,被打断
初始代码
/*模拟一个分布式缓存读、写与清空操作*/
class MyCatch {
// 缓存模拟
private volatile Map<String, Object> hashmap = new HashMap<>();
public void put(String key, Object value) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "\t正在写入:" + key);// 主要注重key的写入
TimeUnit.MILLISECONDS.sleep(300);// 模拟网络延迟拥堵等情况
hashmap.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t写入完成");
}
public void get(String key) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "\t正在读取:" + key);// 主要注重key的写入
TimeUnit.MILLISECONDS.sleep(300);// 模拟网络延迟拥堵等情况
Object result = hashmap.get(key);
System.out.println(Thread.currentThread().getName() + "\t读取完成,读取值为:" + result);
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCatch myCatch = new MyCatch();
for (int i = 1; i <= 4; i++) {
final int tempInt = i;
new Thread(() -> {
try {
myCatch.put(tempInt + "", Integer.valueOf(tempInt));
TimeUnit.MILLISECONDS.sleep(300);
myCatch.get(tempInt + "");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
问题:发现不符合写操作的原子+独占
我们可以用Lock,Synchronized与ReentrantReadWriteLock
但是,前两者对于高并发读写操作有很大的性能负荷,不推荐。
代码:
class MyCatch {
// 缓存模拟
private volatile Map<String, Object> hashmap = new HashMap<>();
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public void put(String key, Object value) throws InterruptedException {
try {
rwLock.writeLock().lock();
System.out.println(Thread.currentThread().getName() + "\t正在写入:" + key);// 主要注重key的写入
TimeUnit.MILLISECONDS.sleep(300);// 模拟网络延迟拥堵等情况
hashmap.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t写入完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
rwLock.writeLock().unlock();
}
}
public void get(String key) throws InterruptedException {
try {
rwLock.readLock().lock();
System.out.println(Thread.currentThread().getName() + "\t正在读取:" + key);// 主要注重key的写入
TimeUnit.MILLISECONDS.sleep(300);// 模拟网络延迟拥堵等情况
Object result = hashmap.get(key);
System.out.println(Thread.currentThread().getName() + "\t读取完成,读取值为:" + result);
} catch (Exception e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
}
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCatch myCatch = new MyCatch();
for (int i = 1; i <= 4; i++) {
final int tempInt = i;
new Thread(() -> {
try {
myCatch.put(tempInt + "", Integer.valueOf(tempInt));
TimeUnit.MILLISECONDS.sleep(300);
myCatch.get(tempInt + "");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}