Java开发中几种常见的锁
1.公平锁
是指多个线程按照申请锁的顺序来获取资源,类似于队列,先进先出(FIFO)。
2.非公平锁
是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程先获得锁。在高并发的情况下,有可能造成优先级反转或饥饿现象。
- 源码:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
参数为true为公平,false为非公平,默认参数是非公平锁
- 以上两种锁比较:
非公平锁的优点在于吞吐量比公平锁大,对于synchronized而言,也是一种非公平锁
3.可重入锁(也叫递归锁)
指的是同一线程外层函数获取锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,再进入内层方法会自动获取锁,也即是说,线程可以进入到任何一个它已经拥有的锁所同步着的代码块。
可重入锁的最大作用是避免死锁
验证两种典型得到可重入锁的性质(synchronized和ReentrantLock()):
(1)先验证synchronized,创建一个QuestionAndAnswer类,question() 方法中嵌入了answer()方法
public synchronized void question() {
System.out.println(Thread.currentThread().getName() + "\t魔镜魔镜告诉我,谁是世界上最帅的人?");
answer();
}
public synchronized void answer() {
System.out.println(Thread.currentThread().getName() + "\tbalabalabala.....");
}
结果:
由此可见该线程的锁进入了内层方法,从而验证了可重入锁的性质
(2)验证ReentrantLock()方法,实现Runnable接口,同样在ques()方法中嵌入ans()方法,以下是部分代码:
Lock lock = new ReentrantLock();
@Override
public void run() {
ques();
}
public void ques() {
lock.lock();
//在这个地方可以无限加锁,但是加了多少把锁,必须要对应的解多少把,不然线程会卡死
try {
System.out.println(Thread.currentThread().getName() + "\t魔镜魔镜告诉我,谁是世界上最帅的人?");
ans();
} finally {
lock.unlock();
}
}
public void ans() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\tbalabalabala.....");
} finally {
lock.unlock();
}
}
结果:
由此可见结果和上面相同,从而也验证了可重入锁的性质
4.自旋锁
是指尝试获取的线程不会立即阻塞,而是采用循环的方式去尝试和获取锁,这样做的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
例:读写锁是一种特殊的自旋锁,它把对共享资源对访问者划分成了读者和写者,读者只对共享资源进行访问,写者则是对共享资源进行写操作。读写锁在ReentrantLock上进行了拓展使得该锁更适合读操作远远大于写操作对场景。一个读写锁同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁。
如果读写锁当前没有读者,也没有写者,那么写者可以立刻获的读写锁,否则必须自旋,直到没有任何的写锁或者读锁存在。如果读写锁没有写锁,那么读锁可以立马获取,否则必须等待写锁释放。(但是有一个例外,就是读写锁中的锁降级操作,当同一个线程获取写锁后,在写锁没有释放的情况下可以获取读锁再释放读锁这就是锁降级的一个过程)
- 代码验证性质
(1)结合CAS验证自旋锁,先创建一个线程类型的原子引用,默认值为null,加锁的方法是先创建一个线程thread,当运行到while语句时compareAndSet方法返回值为true,然后将atomicReference对象的值替换为null,整体返回false。而解锁的方法是将atomicReference对象的thread值换为null来跳出while循环,以下是部分代码:
// 添加一个线程类型的原子引用,默认值为null
AtomicReference<Thread> atomicReference = new AtomicReference<Thread>();
// 加锁的方法
public void myLock() {
// 创建一个线程为当前线程
Thread thread = Thread.currentThread();
// 标识线程已进入原子引用
System.out.println(Thread.currentThread().getName() + "\tcome in");
// 如果当前原子引用值为空就将其换为thread,然后返回true,整体返回false
while (!atomicReference.compareAndSet(null, thread)) {
}
// 解锁的方法
public void unLock() {
// 创建一个线程为当前线程
Thread thread = Thread.currentThread();
// 如果当前原子引用对象为thread,则将其换为null
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName() + "\tinvoked unLock()");
}
(2)主函数代码
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(() -> {
spinLockDemo.myLock();
// 等待5秒
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.unLock();
}).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.unLock();
}).start();
当主函数中Thread-0没有运行unLock()方法时结果:
当主函数中Thread-0运行了unLock()方法时结果:
以上结果说明当Thread-0运行了myLock()方法后再启动Thread-1线程时,Thread-1将会一直被卡在while循环中,只能等到Thread-0运行了unLock()方法后将atomicReference对象值变为null,Thread-1才跳出了while循环得以继续接下来的方法
(3)验证读写锁,通过模仿一个缓存机制来实现。刚开始没有对方法加任何锁,为了和加上锁之后形成对比
- 先创建一个map容器,加上volatile是为了保证内存可见性,一有修改立马可见
private volatile Map<String, String> map = new HashMap<String, String>();
- 创建写方法
// 写操作
public void set(String key, String value) {
try {
System.out.println(Thread.currentThread().getName() + "\t正在写入数据...");
// 暂停一段时间模拟网络延迟
TimeUnit.SECONDS.sleep(2);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t数据写入成功");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 创建读方法
// 读操作
public void get(String key) {
try {
System.out.println(Thread.currentThread().getName() + "\t正在读取数据...");
// 暂停一段时间模拟网络延迟
TimeUnit.SECONDS.sleep(2);
String value = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t数据读取为:" + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 运行结果:
由此可见根本无法正确地实现读写操作,接下来对方法加入读写锁处理 - 写操作
public void set(String key, String value) {
//进行写操作加上写锁
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t正在写入数据...");
// 暂停一段时间模拟网络延迟
TimeUnit.SECONDS.sleep(2);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t数据写入成功");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwLock.writeLock().unlock();
}
}
- 读操作
public void get(String key) {
//进行读操作加上读锁
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t正在读取数据...");
// 暂停一段时间模拟网络延迟
TimeUnit.SECONDS.sleep(2);
String value = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t数据读取为:" + value);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
}
}
- 运行结果
由图可见运行结果和理想中一样,缓存机制可正确进行读写操作
5.独占锁
指该锁一次只能被一个线程所持有
6.共享锁
指该锁可被多个先线程所持有