总结一下java各种锁。
一.公平锁
公平锁:是指多个线程按照申请锁的顺序来获取锁;每个线程在获取锁时,先查看此锁维护的等待队列,如果为空或当前线程是队列中的第一个,就获取锁,否则就会加如到等待队列中。类似食堂打饭,先来后到。
二.非公平锁
非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请锁的线程先获取锁;线程上来就尝试占有锁,如果尝试失败,才会采用类似公平锁的那种方式。在高并发的场景,可能造成优先级反转或饥饿现象(某个线程一直获取不到锁)。
三.可重入锁
可重入锁(递归锁):指的是同一个线程,外层函数获得锁之后,内层递归函数仍然能获取该锁的代码;同一个线程,在外层函数获得锁之后,在进入内层方法时,会自动获取锁;
也就是说,线程可以进入任何一个,它已经拥有的锁(lock),所同步着的代码块。
1.synchronized
synchronized是一个典型的可重入锁;
public synchronized void sendSMS() throws Exception {
System.out.println(Thread.currentThread().getId() + "\t invoked sendSMS");
sendEmail();
}
public synchronized void sendEmail() throws Exception {
System.out.println(Thread.currentThread().getId() + "\t #####invoked sendEmail");
}
说明:一个线程在调用sendSMS方法时,此时已经获取到锁,当在sendSMS中继续调用sendEmail时,会直接获取锁;虽然此方法已经加了synchronized,但是线程在sendSMS已经获取到锁,并且synchronized时可重入的;
2.ReentrantLock
ReentrantLock也是一个典型的可重入锁;默认是非公平锁。
//声明一个可重入锁
Lock lock = new ReentrantLock();
public void get() {
lock.lock();
try {
System.out.println(Thread.currentThread().getId() + "\t invoked set()");
//调用下一个加锁的方法
set();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void set() {
lock.lock();
try {
System.out.println(Thread.currentThread().getId() + "\t ######invoked set()");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
说明:一个线程在进入get方法后(说明已获取lock),可直接继续调用set方法;虽然set方法是被lock锁着的,但是在调用get时,已经获取到锁,并且set方法也是用同一把lock锁着的。
ReentrantLock的详细原理分析,可参考链接:ReentrantLock原理&源码解析
四.自旋锁
自旋是指,尝试获取锁的线程,不会立即阻塞,而是采用循环的方式去尝试获取锁,
优点:减少线程上下文切换的消耗;
缺点:循环会消耗cpu。
原理:利用Unsafe类的cas原理实现。java中提供了一些原子类,比如AtomicReference,可以用它手动实现自旋锁。
手写一个自旋锁:
public class SpinLockDemo {
/**
* 原子引用线程
*/
private AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void myLock() {
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "\t come in");
while (!atomicReference.compareAndSet(null, thread)) {
}
}
public void myUnlock() {
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName() + "\t invocked myUnlock");
System.out.println();
}
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();
}
}
五.读写锁
可实现读写分离,提高程序的并发性能。主要是因为多个线程可以同时读取。
- 读-读:能共存
- 读-写:不能共存
- 写-写:不能共存
读写锁其实可细分为其他几种名字的锁:
独占锁(写锁):一次只能被一个线程所持有,ReentrantLock和Synchronized都是独占锁;
共享锁(读锁):指该锁可以被多个线程所持有;ReentrantReadWriteLock的读锁是共享锁,写锁是独占锁;
互斥锁:不能同时进行,比如写-写,读-写。
ReentrantReadWriteLock的使用demo:
class Mycache {
private volatile Map<String, Object> map = new HashMap<>();
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();
}
}
}