并发编程多种锁
公平锁与非公平锁
-
公平锁
指多个线程按照申请锁的顺序来获取锁,按照先来先服务的原则。其落地实现可使用队列。 -
非公平锁
多个线程获取锁的方式不是按照申请锁的顺序,后申请锁的线程可以比先申请锁的线程先获得锁。 -
JUC下的ReentrantLock的构造boolean值参数为锁的种类,true为公平锁,false为非公平锁;
代码如下:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁和非公平锁的区别
1)公平锁在并发环境中获取锁时会查看锁维护的等待队列,如果队列为空,或者当前线程是等待队列的第一个,就占有锁,否则加入等待队列,按照FIFO等待获取锁。
2)非公平锁直接尝试占有锁,如果尝试失败,就在采用类似公平锁的方式获取锁。
注意:synchronized是一种非公平锁
synchronized与ReentrantLock的区别
可重入锁
-
已获取锁的线程可以进入该锁锁住的同步代码;
-
同一线程外层函数获得锁之后,内层递归函数仍然能获取锁的代码。同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
-
可重入锁最大作用就是规避死锁;
代码示例
/*
* 可重入锁(也就是递归锁)
*
* 指的是同一个线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,
* 在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
*
* 也就是说,线程可以进入任何一个它已经拥有的锁所有同步着的代码块。
*
* t1 invoked sendSMS() t1线程在外层方法获取锁的时候
* t1 invoked sendEmail() t1在进入内层方法会自动获取锁
* t2 invoked sendSMS()
* t2 invoked sendEmail()
*
*/
public class RenenterLockDemo {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> {
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
}, "t1").start();
new Thread(() -> {
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
}, "t2").start();
}
}
class Phone {
public synchronized void sendSMS() throws Exception {
System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS()");
sendEmail();
}
public synchronized void sendEmail() throws Exception {
System.out.println(Thread.currentThread().getName() + "\t invoked sendEmail()");
}
}
可以证明synchronized是一种可重入锁;进入sendSMS方法后,还能再访问另外一个同步方法sendEmail;
也证明了sychronized锁的当前实例对象;
package com.juc;
import java.util.concurrent.locks.ReentrantLock;
public class ReentryLock {
public static void main(String[] args) {
Phone phone=new Phone();
new Thread(()->{
phone.set();
},"AAA").start();
new Thread(()->{
phone.set();
},"BBB").start();
}
}
class Phone{
public ReentrantLock lock=new ReentrantLock();
public void set(){
lock.lock();
try{
System.out.println("set.....");
get();
}catch(Exception e){
} finally{
lock.unlock();
}
}
public void get(){
lock.lock();
try{
System.out.println("get....");
}catch(Exception e){
} finally{
lock.unlock();
}
}
}
可以证明ReentrantLock 是一种可重入锁;进入set的同步代码后,还能再访问另外一个get方法里的代码块;
自旋锁
尝试获取锁的线程不会立刻阻塞,而是通过循环的方式等待获取锁。而不用再操作系统层面被挂起(用户态-》内核态),避免了上下文切换。
场景:线程执行任务的时间较短,可以较快的释放锁,在循环等待期间就可以获取到锁了。
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
这里涉及到了CAS;
自旋锁代码示例
使用原子引用封装Thread,通过CAS算法实现线程的自旋锁
/**
* 写一个自旋锁
* 自旋锁的好处:循环比较获取直到成功为止,没有类似wait的阻塞。
*
* 通过CAS操作完成自旋锁:
* A线程先进来调用myLock方法自已持有锁5秒钟
* B随后进来后发现当前有线程持有锁,不是null,
* 所以只能通过自旋等待,直至A释放锁后B随后抢到
*
* @ClassName SpinLockDemo
* @Description TODO
* @Author Heygo
* @Date 2020/8/8 13:37
* @Version 1.0
*/
public class SpinLockDemo {
// 泛型为 Thread
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void myLock() {
// 获取当前线程
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "\t come in ");
/*
自旋:
期望值为 null 表示当前没有线程
新值为 thread ,即 Thread.currentThread()
*/
while (!atomicReference.compareAndSet(null, thread)) {
}
}
public void myUnLock() {
// 获取当前线程
Thread thread = Thread.currentThread();
// 解锁当前线程
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName() + "\t invoked myUnLock()");
}
public static void main(String[] args) {
// 原子引用线程
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(() -> {
spinLockDemo.myLock(); // 加锁
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.myUnLock(); // 解锁
}, "AA").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
spinLockDemo.myLock(); // 加锁
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.myUnLock(); // 解锁
}, "BB").start();
}
}
程序运行结果:核心为 CAS 算法
线程 A 先执行,此时期望值为 null ,线程 A 将获得锁,并将期望值设置为线程 A 自身
线程 B 尝试获取锁,发现期望值并不是 null ,就在那儿原地自旋
线程 A 释放锁之后,将期望值设置为 null ,此时线程 B 获得锁,将期望值设置为线程 B 自身
最后线程 B 释放锁3
独占锁(写锁)、共享锁(读锁)、互斥锁
- 独占锁:指的是该锁一次只能被一个线程持有,对于ReentrantLock和synchronized都是独占锁。
- 共享锁:该锁可以被多个线程共同持有。
- ReentrantWriteReadLock其内部的读锁是共享锁,写锁是独占锁;
public class ReadWriteLockTest {
public static void main(String[] args) {
MyCache myCache=new MyCache();
for (int i = 0; i < 5; i++) {
final int index=i;
new Thread(()->{
myCache.put(String.valueOf(index),index);
},String.valueOf(i)).start();
}
for (int i = 0; i < 5; i++) {
final int index=i;
new Thread(()->{
myCache.get(String.valueOf(index));
},String.valueOf(i)).start();
}
}
}
class MyCache{
public volatile Map<String,Object> map=new HashMap<>();
ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
public void put(String key,Object value){
lock.writeLock().lock();
try{
System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
Thread.sleep(3000);
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "\t 写入完成");
}catch(Exception e){
} finally{
lock.writeLock().unlock();
}
}
public void get(String key){
lock.readLock().lock();
try{
System.out.println(Thread.currentThread().getName()+"读取---");
Object val=map.get(key);
System.out.println(Thread.currentThread().getName()+"读取结束result="+val);
}catch(Exception e){
} finally{
lock.readLock().unlock();
}
}
}
程序运行结果:写操作没有被打断,读操作可以同时进行
1 正在写入:1
1 写入完成
2 正在写入:2
2 写入完成
4 正在写入:4
4 写入完成
3 正在写入:3
3 写入完成
0读取---
0读取结束result=0
1读取---
1读取结束result=1
2读取---
3读取---
3读取结束result=3
4读取---
2读取结束result=2
4读取结束result=4
ReentrantReadWriteLock源码
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
private static final long serialVersionUID = -6992448646407690164L;
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
分别维护了一个读锁,写锁;
另外Synchronized在锁升级的过程会涉及到一些锁、偏向锁、轻量级锁、重量级锁。
可以查看这篇博文
10分钟必懂-深入理解Synchronized关键字