一、公平锁和非公平锁
1.1 公平锁和非公平锁分别是什么
公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。
非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。在高并发的情况下,有可能会造成优先级反转或者饥饿现象。
1.2 区别
并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认是非公平锁。
关于两者的区别:
公平锁
就是很公平,在并发环境中,每个线程在获取锁时,会先查看此锁维护的等待队列。如果为空或者当前线程是等待队列的第一个,就占有锁,否则就加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
非公平锁
非公平锁比较粗鲁,上来就尝试占有锁,如果尝试失败,就再采取类似公平锁的那种方式。
非公平锁的优点在于吞吐量比公平锁大。
synchronized也是一种非公平锁。
二、可重入锁(又名递归锁)
2.1 可重入锁是什么
可重入锁,也叫做递归锁。指的是同一线程外层函数获得锁之后,内层递归函数仍然能够获得该锁的代码。
在同一个线程,在外层方法获取锁的时候,进入内层方法时会自动获取锁。
也就是,线程可以进入任何一个它已经拥有的锁 所同步着的代码块。
ReentrantLock/Synchrnoized就是一个典型的可重入锁。
可重入锁最大的作用就是避免死锁。
2.2 synchronized
package com.yuxx.lock;
import java.util.concurrent.TimeUnit;
class Phone{
public synchronized void sendSMS() throws Exception{
System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS()");
TimeUnit.SECONDS.sleep(2);
sendEmail();
}
public synchronized void sendEmail() throws Exception{
System.out.println(Thread.currentThread().getName() + "\t invoked sendEmail()");
}
}
/**
case one synchronized典型的可重入锁
t1 invoked sendSMS() t1在外层方法获取锁的时候
t1 invoked sendEmail() t1在进入内层方法会自动获取锁
t2 invoked sendSMS()
t2 invoked sendEmail()
*/
public class ReentrantLockDemo {
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();
}
}
2.3 ReentrantLock
package com.yuxx.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Phone implements Runnable{
Lock lock = new ReentrantLock();
@Override
public void run() {
get();
}
public void get() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t invoked get()");
set();
} finally {
lock.unlock();
}
}
public void set() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t invoked set()");
} finally {
lock.unlock();
}
}
}
/**
case two ReentrantLock
t3 invoked get()
t3 invoked set()
t4 invoked get()
t4 invoked set()
*/
public class ReentrantLockDemo {
public static void main(String[] args) {
Phone phone = new Phone();
Thread t3 = new Thread(phone,"t3");
Thread t4 = new Thread(phone,"t4");
t3.start();
t4.start();
}
}
三、自旋锁
3.1 什么是自旋锁
自旋锁(SpinLock)是指:尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁。
这样的好处是:减少线程上下文切换的消耗,缺点是循环会消耗CPU。
CAS算法就采用了自旋锁的方式,Unsafe类的getAndAddInt方法就是一个典型的自旋锁。
3.2 实现一个自旋锁
package com.yuxx.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* 题目:实现一个自旋锁
* 自旋锁好处:循环比较获取直至成功为止,没有类似wait的阻塞。
*
* 通过CAS操作完成自旋锁,A线程先进来调用myLock方法,自己持有5秒钟,
* B随后进来后发现,当前线程持有锁,不是null,
* 所以只能通过自旋等待,直到A释放锁后B随后抢到。
*/
public class SpinLockDemo {
AtomicReference atomicReference = new AtomicReference<>();
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "\t come in!");
while(!atomicReference.compareAndSet(null,thread)){
}
}
public void myUnlock(){
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println(thread.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();
new Thread(()->{
spinLockDemo.myLock();
spinLockDemo.myUnlock();
},"BB").start();
}
}
Demo说明:AA和BB线程,先后进入myLock方法。AA进入myLock方法,打印“AA come in!”,然后进入while循环,通过自旋锁,将atomicReference的引用指向thread,随后AA线程阻塞5秒。B进入后,打印“BB come in!”,然后进入while循环,第一次比较并判断,atomicReference指向的是thread,并不是null,所以继续比较并判断。直到AA线程阻塞完毕,调用了myUnlock方法,将atomicReference引用再指回null,并打印“AA invoked myUnlock()”。如此,BB线程的CAS,检测到atomicReference引用指向了null,会再将atomicReference比较并替换,指向thread。这样,BB线程的myLock方法调用完毕,开始调用myUnlock方法,将atomicReference引用比较并替换,指向null,再打印出“BB invoked myUnlock()”。至此,程序执行完毕。
四、独占锁(写)/共享锁(读)/互斥锁
4.1 介绍
独占锁:指该锁一次只能被一个线程所持有。ReentrantLock和synchronized都是独占锁。
共享锁:指该锁可被多个线程所持有。
ReentrantReadWriteLock的读锁是共享锁,写锁时是独占锁。
读锁的共享锁可以保证并发读是非常高效的,读写、写读、写写的过程都是互斥的。
4.2 ReadWriteLockDemo
package com.yuxx.lock;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class MyCache{
private volatile Map map = new HashMap<>();
//实现ReadWriteLock接口(不是Lock的实现类)
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public void put(String key,Object value){
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t正在写入:" + key);
//模拟网络延迟
try {
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();
}
}
public void get(String key){
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t正在读取:");
//模拟网络延迟
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object result = map.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) {
MyCache myCache = new MyCache();
for (int i = 1; i <= 5; i++) {
System.out.println("W-"+i);
final int tempInt = i;
new Thread(()->{
myCache.put(String.valueOf(tempInt),String.valueOf(tempInt));
},"W-"+String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
final int tempInt = i;
new Thread(()->{
myCache.get(String.valueOf(tempInt));
},"R-"+String.valueOf(i)).start();
}
}
}
打印结果: