在学习多线程的时候,我们经常会听到可重入锁/不可重入锁、公平锁/非公平锁、读写锁现在我们就逐一它们的神秘面纱。
1.可重入锁/非重入锁:大部分jdk提供的都是可重入锁,如syncronized,reentrantLock 都是可重入,代表单个(也可以说同一个)线程可以多次获得该锁,如果单个线程拿到锁没有释放,你再去拿,拿到则是重入锁 ,拿不到则是非重入锁。
public static void main(String[] args) {
//KodyLock lock = new KodyLock ();
Lock lock = new ReentrantLock ();
lock.lock ();
System.out.println ("获得锁");
lock.lock ();
System.out.println ("再次获得锁");
}
如上,可以发现ReentranLock是可重入的,那非重入锁呢?下面是一个简单的非重入锁的实现
public class NCrLock {
boolean isLock = false;
public synchronized void lock() throws InterruptedException{
while (isLock){
wait ();
}
isLock = true;
}
public synchronized void unlock(){
isLock = false;
notify ();
}
public static void main(String[] args) throws InterruptedException{
NCrLock nCrLock = new NCrLock ();
nCrLock.lock ();
System.out.println ("获得锁");
nCrLock.lock ();
System.out.println ("再次获得锁");
}
}
2.读写锁:我们知道ReentrantLock是一种排它锁,同一时间内只允许一个线程访问,而ReentrantReadWriteLock允许多个读线程同时访问,但不允许读线程和写线程、写线程和写线程同时访问,在实际应用中,大部分共享数据(缓存)的访问都是读操作远多于写操作这时候ReentrantReadWrite就比排它锁提供了更好的并发性和吞吐量。
public static void main(String[] args) {
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock ();
new Thread (() -> {
reentrantReadWriteLock.readLock ().lock ();
long startTime = System.currentTimeMillis ();
while (System.currentTimeMillis () - startTime < 5){
//记录5ms内 读线程做的事情
System.out.println (Thread.currentThread ().getName () + ":正在进行读操作" );
}
reentrantReadWriteLock.readLock ().unlock ();
},"读锁线程").start ();
new Thread (() -> {
reentrantReadWriteLock.readLock ().lock ();
long startTime = System.currentTimeMillis ();
while (System.currentTimeMillis () - startTime < 5){
//记录5ms内 读线程做的事情
System.out.println (Thread.currentThread ().getName () + ":正在进行读操作" );
}
reentrantReadWriteLock.readLock ().unlock ();
},"读锁线程2").start ();
}
两个读线程控制台打印信息如下:多试几次因为可能cpu调度切换这其中时间差导致先执行某个线程
读锁线程2:正在进行读操作
读锁线程:正在进行读操作
读锁线程2:正在进行读操作
读锁线程2:正在进行读操作
........
改成读线程与写线程控制台打印信息如下:
读锁线程:正在进行读操作
读锁线程:正在进行读操作
读锁线程:正在进行读操作
写锁线程2:正在进行读操作
写锁线程2:正在进行读操作
写锁线程2:正在进行读操作
写锁线程2:正在进行读操作
.......
3.公平锁与非公平锁:java中自带的关键字syncronized和ReentrantLock都能事先对方法或者代码块进行加锁,前者只能是非公平锁后者默认是非公平锁,公平锁和非公平锁都是基于锁内部维护的一个双向链表,表节点Node的值就是每一个请求当前锁的线程。公平锁则是每次依次从队首取值,所以能保证获取锁的顺序性,而非公平锁则是有机会去抢锁,可能会导致线程抢锁求而不得。
所以总结下里就是:
- 公平锁获取锁的顺序是按照加锁的顺序来分配的,即FIFO先进先出,非公平锁与公平锁不一样的就是它是随机获取锁,先来的不一定得到锁,这个方式可能造成线程拿不到锁。
非公平锁:基于CAS尝试将state(锁数量)从0改为1,如果设置成功则设置当前线程为独占锁线程,如果设置失败还会在获取一次锁的数量:
- 如果锁的数量为0在基于CAS尝试将state从0设置为1,如果设置成功则设置当前线程为独占锁线程
- 如果锁的数量不为0或者上面尝试失败了,则查看当前锁是不是独占锁线程,如果是则将当前线程锁的数量+1,如果不是则将线程封装在一个Node内部,并加入到等待队列,等待被其前一个线程节点唤醒
公平锁:获取一次锁的数量
- 如果锁的数量为0,如果当前线程是等待队列的头节点,基于CAS尝试将state从哪个0设置为1一次,设置成功则将当前线程设置为独占锁线程
- 如果锁的数量不为0或者当前线程不是等待队列中的头节点或者上面的尝试失败了,查看当前线程是不是已经是独占锁线程,如果是则将当前锁的数量+1,如果不是则将该线程封装在一个Node内,并加入到等待队列,等待被其前面一个线程唤醒。
public static void main(String[] args) throws InterruptedException{
Lock lock = new ReentrantLock (false);
//分别依次启动5个线程,观察它的执行情况
IntStream.range (0,5).forEach (value -> {
new Thread (() -> {
System.out.println (Thread.currentThread ().getName () + "线程开始运行");
lock.lock ();
System.out.println (Thread.currentThread ().getName () + "拿到锁");
lock.unlock ();
},String.valueOf (value)).start ();
});
}
可以看出线程拿到锁的顺序为0,1,4,3,2线程执行的顺序为0,1,3,4,2明显是无序的
0线程开始运行
1线程开始运行
0拿到锁
3线程开始运行
1拿到锁
4线程开始运行
4拿到锁
2线程开始运行
3拿到锁
2拿到锁
改为公平锁的执行情况,线程拿到锁的顺序为0,1,2,3,4程执行的顺序为0,1,2,3,4线程拿到锁顺序与线程执行的顺序一致,当然这里拿到锁的顺序也并非全是0到4顺序来的,这个得看cpu调度切换到谁。
public static void main(String[] args) throws InterruptedException{
Lock lock = new ReentrantLock (true);
//分别依次启动5个线程,观察它的执行情况
IntStream.range (0,5).forEach (value -> {
new Thread (() -> {
System.out.println (Thread.currentThread ().getName () + "线程开始运行");
lock.lock ();
System.out.println (Thread.currentThread ().getName () + "拿到锁");
lock.unlock ();
},String.valueOf (value)).start ();
});
}
0线程开始运行
0拿到锁
1线程开始运行
1拿到锁
2线程开始运行
2拿到锁
3线程开始运行
3拿到锁
4线程开始运行
4拿到锁