共享资源
的访问ReentrantLock
效率低
:锁的释放情况少(只有执行结束或者抛异常才能释放锁)、试图获取锁时不能设定超时、不能中断一个正在试图获取锁的线程
不够灵活
:加锁和释放的时机单一(不像读写锁那样针对不同场景而选择使用读锁或写锁),每个锁仅有单一的条件(某个对象),适用场景可能是不够的
成功获取到锁
特性 | 描述 |
---|---|
尝试非阻塞地获取锁:tryLock() | 当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁,否则立即返回false |
能被中断地获取锁:lockInterruptibly() | 在等待获取锁的线程能够响应中断,等待锁的线程被中断时,中断异常将会被抛出,不再等待 |
超时获取锁:tryLock(long time, TimeUnit unit) | 在指定的截止时间之前获取锁, 超过截止时间后仍旧无法获取则返回 |
方法名称 | 描述 |
---|---|
void lock() | 获得锁。如果锁已经被其他线程获取,则进行等待 |
boolean tryLock() | 只有在调用时才可以获得锁。如果可用,则获取锁定,并立即返回值为true;如果锁不可用,则此方法将立即返回值为false 。 |
boolean tryLock(long time, TimeUnit unit) | 超时获取锁,当前线程在一下三种情况下会返回: 1. 当前线程在超时时间内获得了锁;2.当前线程在超时时间内被中断;3.超时时间结束,返回false. |
void lockInterruptibly() | 获取锁,如果可用并立即返回。如果锁不可用,那么等待,和 tryLock(long time, TimeUnit unit) 方法不同的是等待时间无限长,但是在等待中可以中断当前线程(响应中断)。 |
Condition newCondition() | 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用后,当前线程将释放锁。 |
void unlock() | 释放锁。 |
下面对以上接口的使用进行演示:
代码演示:
/**
* 描述: Lock不会像synchronized一样,异常的时候自动释放锁,所以最佳实践是,finally中释放锁,以便保证发生异常的时候锁一定被释放
*/
public class MustUnlock {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try{
//获取本锁保护的资源
System.out.println(Thread.currentThread().getName()+"开始执行任务");
}finally {
lock.unlock();
}
}
}
尝试获取锁
,如果当前线程没有被其他线程占用,则获取成功,则返回true,否则返回false,代表获取锁失败决定后续程序的行为
立刻返回
,即便在拿不到锁时,不会一直等待可以设定超时时间的尝试获取锁
,一段时间内等待锁,超时就放弃。
使用 tryLock(long time,TimeUnit unit) 来避免死锁的代码演示:
/**
* 〈用trylock避免死锁〉
*
* @author Chkl
* @create 2020/3/11
* @since 1.0.0
*/
public class TryLockDeadLock implements Runnable {
int flag = 1;
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) {
TryLockDeadLock r1 = new TryLockDeadLock();
TryLockDeadLock r2 = new TryLockDeadLock();
r1.flag = 1;
r2.flag = 0;
new Thread(r1).start();
new Thread(r2).start();
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (flag == 1) {
try {
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("线程1获取到了锁1");
Thread.sleep(new Random().nextInt(1000));
if (lock2.tryLock(800,TimeUnit.MILLISECONDS)){
try {
System.out.println("线程1获取到了锁2");
System.out.println("线程1获取到了两把锁");
break;
}finally {
lock2.unlock();
}
}else {
System.out.println("线程1获取锁2失败");
}
} finally {
lock1.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("线程1获取锁1失败,已重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (flag == 0) {
try {
if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("线程2获取到了锁2");
Thread.sleep(new Random().nextInt(1000));
if (lock1.tryLock(800,TimeUnit.MILLISECONDS)){
try {
System.out.println("线程2获取到了锁1");
System.out.println("线程2获取到了两把锁");
break;
}finally {
lock1.unlock();
}
}else {
System.out.println("线程2获取锁1失败,已重试");
}
} finally {
lock2.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("线程2获取锁2失败,已重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
打印结果、
相当于把tryLock(long time,TimeUnit unit)的超时时间设置为无限长,在等待锁的过程中,线程可以中断
public class LockInterruptibly implements Runnable{
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
LockInterruptibly l = new LockInterruptibly();
Thread thread0 = new Thread(l);
Thread thread1 = new Thread(l);
thread0.start();
thread1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread0.interrupt();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "尝试获取锁");
try {
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + "获取到了锁");
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "睡眠期间被中断了");
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了锁");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "等待锁期间被中断了");
}
}
}
打印结果:
根据不同的划分标准,常见的锁的划分如思维导图所示
主要由于互斥同步锁(悲观锁)存在一些劣势,如下:
悲观锁:
乐观锁:
CAS
算法,典型例子是:原子类,并发容器等代码演示:实现累加器
public class PessimismOptimismLock {
int a;
//悲观锁
public synchronized void testMethod(){
a++;
}
public static void main(String[] args) {
//乐观锁
AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();
//悲观锁
new PessimismOptimismLock().testMethod();
}
}
Git:Git是乐观锁的典型应用,当我们向远程仓库push的时候,git会检查远程仓库的版本是不是领先我们现在的版本,
数据库:
update set num = 2 , version = vsersion+1 where version = mversion and id = 5
非可重入锁就是最常见的锁,一旦锁被使用,如果没有释放,就不能再使用这个锁了
可重入锁,是指同一线程获取到一把锁之后,可在不释放该锁的条件下再次获取该锁,以ReentrantLock为例进行演示,如下:
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
}
public class RecursionDemo {
private static ReentrantLock lock = new ReentrantLock();
private static void accessResource(){
lock.lock();
try {
System.out.println("已经对资源进行处理");
if (lock.getHoldCount()<5){
//递归调用
System.out.println(lock.getHoldCount());
accessResource();
System.out.println(lock.getHoldCount());
}
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
accessResource();
}
}
从结果可以看出获得锁之后是可以重复获得锁再最后释放的,这就是可重入锁
可重入锁 & 非可重入锁的获取锁和释放锁的方法源码对比:
提倡"插队"行为
,这里的非公平,指的是在合适的时机
插队,而不是盲目插队演示案例:模拟打印机打印任务,有两个类,一个是打印作业Job类,一个是打印队列PrintQueeue 类,一个打印任务包含两次打印,两次获得锁。在main方法中创建10个线程执行Job,当锁使用公平锁时:
/**
* 〈演示公平锁和不公平锁〉
*
*/
public class FairLock {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
PrintQueue printQueue = new PrintQueue();
Thread thread[] = new Thread[10];
for (int i = 0; i < 10; i++) {
thread[i] = new Thread(new Job(printQueue), "线程"+i);
}
for (int i = 0; i < 10; i++) {
thread[i].start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Job implements Runnable {
PrintQueue printQueue;
public Job(PrintQueue printQueue) {
this.printQueue = printQueue;
}
@Override
public void run() {
System.out.println(
Thread.currentThread().getName() + "开始打印");
printQueue.printJob(new Object());
System.out.println(
Thread.currentThread().getName() + "打印结束");
}
}
class PrintQueue {
//公平锁
private Lock queueLock = new ReentrantLock(true);
//非公平锁
// private Lock queueLock = new ReentrantLock();
public void printJob(Object document) {
queueLock.lock();
try {
int duration = new Random().nextInt(10) + 1;
System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration);
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
queueLock.lock();
try {
int duration = new Random().nextInt(10) + 1;
System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration);
} finally {
queueLock.unlock();
}
}
}
使用公平锁进行打印操作,会按照请求锁的顺序依次执行,按照请求顺序,不会出现插队。一次运行结果如下,因为每次打印后需要休眠n秒模拟打印耗时,休眠时间足够所有的线程依次启动,所以执行顺序一定是线程0-9按顺序请求第一把锁,之后线程0-9再按顺序请求第二把锁,顺序一定不会变
线程0开始打印
线程0正在打印,需要8
线程1开始打印
线程2开始打印
线程3开始打印
线程4开始打印
线程5开始打印
线程6开始打印
线程7开始打印
线程8开始打印
线程9开始打印
线程1正在打印,需要3
线程2正在打印,需要3
线程3正在打印,需要5
线程4正在打印,需要4
线程5正在打印,需要2
线程6正在打印,需要2
线程7正在打印,需要2
线程8正在打印,需要10
线程9正在打印,需要7
线程0正在打印,需要9秒
线程1正在打印,需要5秒
线程0打印完毕
线程2正在打印,需要8秒
线程1打印完毕
线程2打印完毕
线程3正在打印,需要2秒
线程3打印完毕
线程4正在打印,需要8秒
线程4打印完毕
线程5正在打印,需要5秒
线程5打印完毕
线程6正在打印,需要9秒
线程7正在打印,需要1秒
线程6打印完毕
线程7打印完毕
线程8正在打印,需要10秒
线程8打印完毕
线程9正在打印,需要10秒
线程9打印完毕
修改PrintQueue 中的锁为非公平锁
//非公平锁
private Lock queueLock = new ReentrantLock(); // 或者 new ReentrantLock(false)
打印结果:
运行结果如上,从结果可以看到,打印顺序并没有再按照0-9、0-9执行了,线程2的第一次打印结束后马上又开始了第二次打印,这就是非公平锁的好处了,线程2执行完第一个打印之后,线程3准备打印,但是在准备的空窗期,线程2干脆一次性把第二次打印也完成了,不影响线程3打印的正常运行,同理下面的线程56789都是这种情况,提高了效率,充分利用了空窗期
以ReetrantReadWriteLock读写锁为例
要么多读,要么一写
创建4个线程,前两个获取读锁,后两个获取写锁,运行后可以看到读锁可以同时获取,写锁必须等前面的线程释放了才能再获取,写锁获取期间,不允许其它线程的读写操作。
public class CinemaReadWrite {
private static ReentrantReadWriteLock
reentrantReadWriteLock = new ReentrantReadWriteLock();
//读锁
private static ReentrantReadWriteLock.ReadLock
readLock = reentrantReadWriteLock.readLock();
//写锁
private static ReentrantReadWriteLock.WriteLock
writeLock = reentrantReadWriteLock.writeLock();
public static void main(String[] args) {
new Thread(()->read(),"Thread1").start();
new Thread(()->read(),"Thread2").start();
new Thread(()->write(),"Thread3").start();
new Thread(()->write(),"Thread4").start();
}
private static void read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName()
+ "得到了读锁,正在读取ing");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()+"释放读锁");
readLock.unlock();
}
}
private static void write() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName()
+ "得到了写锁,正在读取ing");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()
+"释放读锁");
writeLock.unlock();
}
}
}
运行结果:
public class NonfairBargeDemo {
private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(false);//非公平读写锁
private static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();//读锁
private static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();//写锁
//读操作
private static void read(){
System.out.println(Thread.currentThread().getName()+" : 开始尝试获取读锁");
readLock.lock();
try{
System.out.println(Thread.currentThread().getName()+" : 得到读锁,正在读取");
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()+" : 释放读锁");
readLock.unlock();
}
}
//写操作
private static void write(){
System.out.println(Thread.currentThread().getName()+" : 开始尝试获取写锁");
writeLock.lock();
try{
System.out.println(Thread.currentThread().getName()+" : 得到读锁,正在写入");
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()+" : 释放写锁");
writeLock.unlock();
}
}
//主函数
public static void main(String[] args) {
new Thread(()->write(),"Thread1").start();
new Thread(()->read(),"Thread2").start();
new Thread(()->read(),"Thread3").start();
new Thread(()->write(),"Thread4").start();
new Thread(()->read(),"Thread5").start();
new Thread(new Runnable() {
@Override
public void run() {
Thread thread[] = new Thread[1000];
for (int i = 0; i < thread.length; i++) {
thread[i] = new Thread(()->read(),"子线程创建的Thread"+i);
thread[i].start();
}
}
}).start();
}
}
线程2刚拿到锁时,此时等待队列中的头结点是线程3,是读操作,所以后面的读操作可以进行插队,Thread330就没有排队,直接去抢到了锁;当线程3也获取到锁之后,由于队列的头节点是线程4,写操作,所以后面的读操作都不再插队,按部就班的排队。
提高效率:
某个线程执行过程中不同时间段的操作不同,一开始执行写操作,之后都进行读;一直使用写锁的话,后面的读操作不能和其他线程进行共享,就会浪费资源;如果将写锁释放掉然后去抢占读锁,不一定能抢到。所有就有了写锁降级,然后让其他线程也能获取到读锁。
为什么不支持读锁的升级?
因为读锁升级需要等所有的读锁都释放了才能升级,容易造成死锁,比如两个线程都在等待升级的话,就会互相等待对方释放读锁,就成了死锁
public class NonfairBargeDemo {
private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(false);//公平读写锁
private static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();//读锁
private static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();//写锁
//读升级
private static void readUpgrading(){
System.out.println(Thread.currentThread().getName()+" : 开始尝试获取读锁");
readLock.lock();
try{
System.out.println(Thread.currentThread().getName()+" : 得到读锁,正在读取");
Thread.sleep(1000);
System.out.println("升级会带来阻塞");
writeLock.lock();
System.out.println(Thread.currentThread().getName()+"升级成功");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
System.out.println(Thread.currentThread().getName()+" : 释放读锁");
readLock.unlock();
}
}
//写降级
private static void writeDownGrading(){
System.out.println(Thread.currentThread().getName()+" : 开始尝试获取写锁");
readLock.lock();
try{
System.out.println(Thread.currentThread().getName()+" : 得到读锁,正在写入");
Thread.sleep(1000);
readLock.lock();
System.out.println(Thread.currentThread().getName()+":在不释放写锁的情况下,直接获取读锁,成功降级");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
System.out.println(Thread.currentThread().getName()+" : 释放写锁");
readLock.unlock();
}
}
//主函数
public static void main(String[] args) throws InterruptedException {
System.out.println("降级是可以的");
Thread thread1 =new Thread(()->writeDownGrading(),"thread1");
thread1.start();
thread1.join();
System.out.println("=========================");
System.out.println("升级是不行的");
Thread thread2 = new Thread(() -> readUpgrading(), "thread2");
thread2.start();
}
}
相比于 ReentrantLock 适用于一般场合,ReentrantReadWriteLock 适用于读多写少的情况,合理使用可以进一步提高并发效率。
阻塞或者唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态装换需要耗费处理器时间
如果同步代码块中的内容过于简单,状态转换消耗的时间
可能比用户代码执行的时间
还长
同步资源锁定时间很短的场景,线程挂起和恢复现场的花费可能会让系统得不偿失
如果物理机有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就释放锁
自旋锁:为了让当前线程“稍微等一下”
,需要让当前线程自旋
,如果自旋完成后
前面锁定同步资源的线程已经释放锁了,那么当前线程可以不必阻塞而是直接获取同步资源
,从而避免线程切换的开销,这就是自旋锁
阻塞锁和自旋锁相反,阻塞锁如果没有拿到锁,会直接把线程阻塞,直到被唤醒
如果锁被占用时间很长,那么自旋的线程只会白白浪费CPU资源
自增操作的的do-while循环
就是一个自旋操作,如果修改过程中遇到其他线程竞争导致没修改成功,就在while里死循环,直至修改成功.AtomicInteger的getAndIncrement源码
/**
* 自旋锁演示
*/
public class SpinLock {
private AtomicReference<Thread> sign = new AtomicReference<>();
//加锁操作
public void lock(){
Thread current = Thread.currentThread();
//只有在 null 的时候,current 才能执行通过,否则就是循环
while (!sign.compareAndSet(null,current)){
System.out.println("获取锁失败,已重试");
}
}
//解锁操作
public void unlock(){
Thread current = Thread.currentThread();
// 将 sign 的线程设为null,就意味着其他线程可以再次进行设值,就相当于解锁了
sign.compareAndSet(current,null);
}
public static void main(String[] args) {
SpinLock spinLock = new SpinLock();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":开始尝试获取自旋锁");
spinLock.lock();
System.out.println(Thread.currentThread().getName() + ":获取到了自旋锁");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLock.unlock();
System.out.println(Thread.currentThread().getName() + ":释放了自旋锁");
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}
public class LockInterruptibly implements Runnable{
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
LockInterruptibly l = new LockInterruptibly();
Thread thread0 = new Thread(l);
Thread thread1 = new Thread(l);
thread0.start();
thread1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread0.interrupt();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+": 尝试获取锁");
try {
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName()+": 拿到了锁");
Thread.sleep(5000);
}catch (InterruptedException e){
System.out.println(Thread.currentThread().getName()+"【睡眠期间】被中断");
} finally{
lock.unlock();
System.out.println(Thread.currentThread().getName()+": 释放锁");
}
} catch (InterruptedException e) {
System.out.println("【等锁期间】被中断");
e.printStackTrace();
}
}
}
在一些不会出现线程不安全的地方,jvm 会对这里的锁消除
理论上来说,让同步代码块的范围越小越好;但是如果一系列的操作都是对一个对象进行反复的加锁解锁操作,那他就会优化,只在这一系列操作的开始加锁,在结束时进行解锁,就减少了锁的数量。避免了反复加锁解锁带来的资源浪费。
缩小同步代码块
尽量不要锁住方法
减少请求锁的次数
例如,在日志框架中,多个线程去执行日志记录操作,那可以加一个中间件,将多个操作合成一个操作,然后去用一个线程去执行这合成的一个操作
避免人为制造"热点"
例如,在使用size()方法获取hashmap的大小时,如果遍历整个hashmap来获取大小时,为了准确性就会加锁,从而使得其他线程进入阻塞;为了避免这里成为加锁"热点",我们可以维护一个计数器,每次put()操作就给计数器加1,每次删除操作就减1,就无需遍历获取hashmap的大小,而是直接去取map中的一个元素,大大减小开销。
锁中尽量不要再包含锁
因为容易出现死锁
选择合适锁的类型&合适的工具类