重入锁

​ 重入锁顾名思义就是支持重进入的锁,表示该锁能够支持一个线程对资源的重复加锁。Synchronized和ReentrantLock就是典型的重入锁,只不过前者支持隐式的重进入,而后者需要调用lock方法。谈到这里就需要普及一个知识点:

公平锁与非公平锁

公平锁/非公平锁

​ 公平锁:锁的获取顺序应该符合请求得绝对时间的顺序,意思也就是说,谁等的久谁先获得锁

​ 非公平锁: 只要通过CAS操作设置锁的同步状态成功,就表示当前线程获取到了锁,可以想象成插队打饭,虽然不遵守规则,但效率很高,不需要CPU上下文过多的切换,而公平锁的公平机制需要CPU上下文不停的切换来满足,因此效率很低。

实现重进入

重进入就是指任意线程在获取到锁之后能够在次获取该锁,而不会被锁所阻塞。想要获得锁重入的效果就要满足以下两个问题

线程再次获取锁

​ 锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是则再次成功获取

锁的最终释放

​ 当线程重复n次获取锁后,需要低n次释放锁后,其他线程能够获取到该锁。这种机制是靠锁内部的计数器实现的,获得一次锁+1,释放锁-1。当计数器为0时表示锁已经成功释放。

代码示例

/**
* 可重入锁(也叫递归锁)
* 指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取到该锁的代码,在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
*
* 也就是说:`线程可以进入任何一个它已经拥有的锁所同步的代码块`
* @author: Alan_
* @create: 2020-11-17-13:11
*/

/**
* 资源类
*/
class Phone {

/**
* 发送短信
* @throws Exception
*/
public synchronized void sendSMS() throws Exception{
System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS()");

// 在同步方法中,调用另外一个同步方法
sendEmail();
}

/**
* 发邮件
* @throws Exception
*/
public synchronized void sendEmail() throws Exception{
System.out.println(Thread.currentThread().getId() + "\t invoked sendEmail()");
}
}
public class ReenterLockDemo {


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();
}
}

控制台打印信息

t1	 invoked sendSMS()      t1线程在外层方法获取锁的时候
t1 invoked sendEmail() t1在进入内层方法会自动获取锁

t2 invoked sendSMS() t2线程在外层方法获取锁的时候
t2 invoked sendEmail() t2在进入内层方法会自动获取锁

读写锁

​ 之前所谈到的锁都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许有多个线程访问,但是在写线程访问时,所有的读线程和写线程军均被阻塞,目的就是保证其他线程对写线程正在写的共享变量具有可见性。读写锁的并发性比其他的排他锁来说有很大提升。

读写锁的实现

​ 读写锁同样依赖AQS来实现同步功能,只不过读写锁的状态位status需要维护多个读线程和一个写线程的状态。

读写状态的设计

​ 如果在一个整型变量上维护多种状态,就需要”按位切割使用”这个变量,读写锁将变量切分成两个部分,高16位表示读,低16位表示写。

​ 如果当前同步状态表示一个线程已经获取了写锁,且重进入了两次,也连续获取了两次读锁。读写状态的确定是靠位运算确定的。

​ 当写状态=0时,读状态大于0表示读写已获取。

写锁的获取与释放

​ 写锁是一个支持重进入的排他锁,如果当前线程获取到了写锁,就增加写状态,如果当前线程正在获取写锁时,读锁已经获取或者该线程不是已经获取写锁的线程,则该线程将进入等待状态。

​ 写锁的释放与ReentrantLock的释放过程相似,都是通过每次减少写状态的计数器实现,当写状态为0时表示写锁已经释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。

读锁的获取与释放

​ 读锁是一个支持重进入的共享锁,他能够被多个线程同时获取,在没有其他写线程访问时,读锁总会被成功的获取。如果当前线程已经获取到了读锁,那么就增加读状态,如果在获取读锁的过程中,写锁被其他线程获取,则进入等待状态。

​ 读锁的释放(线程安全,可以有多个读线程同时释放读锁)均减少都状态

代码示例

实现一个读写缓存的操作

/**
* 读写锁
* 多个线程 同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行
* 但是,如果一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写
*
* @author: Alan_
* @create: 2020-11-17-13.36
*/

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
* 资源类
*/
class MyCache {

/**
* 缓存中的东西,必须保持可见性,因此使用volatile修饰
*/
private volatile Map<String, Object> map = new HashMap<>();

/**
* 创建一个读写锁
* 它是一个读写融为一体的锁,在使用的时候,需要转换
*/
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

/**
* 定义写操作
* 满足:原子 + 独占
* @param key
* @param value
*/
public void put(String key, Object value) {

// 创建一个写锁
rwLock.writeLock().lock();

try {

System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);

try {
// 模拟网络拥堵,延迟0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}

map.put(key, value);

System.out.println(Thread.currentThread().getName() + "\t 写入完成");

} catch (Exception e) {
e.printStackTrace();
} finally {
// 写锁 释放
rwLock.writeLock().unlock();
}
}

/**
* 获取
* @param key
*/
public void get(String key) {

// 读锁
rwLock.readLock().lock();
try {

System.out.println(Thread.currentThread().getName() + "\t 正在读取:");

try {
// 模拟网络拥堵,延迟0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}

Object value = map.get(key);

System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);

} catch (Exception e) {
e.printStackTrace();
} finally {
// 读锁释放
rwLock.readLock().unlock();
}
}

/**
* 清空缓存
*/
public void clean() {
map.clear();
}


}
public class ReadWriteLockDemo {

public static void main(String[] args) {

MyCache myCache = new MyCache();

// 线程操作资源类,5个线程写
for (int i = 1; i <= 5; i++) {
// lambda表达式内部必须是final
final int tempInt = i;
new Thread(() -> {
myCache.put(tempInt + "", tempInt + "");
}, String.valueOf(i)).start();
}

// 线程操作资源类, 5个线程读
for (int i = 1; i <= 5; i++) {
// lambda表达式内部必须是final
final int tempInt = i;
new Thread(() -> {
myCache.get(tempInt + "");
}, String.valueOf(i)).start();
}
}
}

控制台打印信息

1	 正在写入:1
1 写入完成
2 正在写入:2
2 写入完成
3 正在写入:3
3 写入完成
4 正在写入:4
4 写入完成
5 正在写入:5
5 写入完成
2 正在读取:
3 正在读取:
1 正在读取:
4 正在读取:
5 正在读取:
2 读取完成:2
1 读取完成:1
4 读取完成:4
3 读取完成:3
5 读取完成:5