Lock

Lock

  • Lock和synchronized,这两个是最常见的锁,它们都可以达到线程安全的目的,但是在使用上和功能上又有较大的不同。
  • Lock并不是用来代替synchronized的,而是当使用synchronized不合适或不足以满足要求的时候,来提供高级功能的。

为什么synchronized不够用

  1. 效率低:锁的释放情况少、试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程
  2. 不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的
  3. 无法知道是否成功获取到锁

Lock接口

  • 通常情况下,Lock只允许一个线程来访问这个共享资源。不过有的时候,一些特殊的实现也可允许并发访问,比如ReadWriteLock里面的ReadLock。

ReentrantLock实现类

既是互斥锁,又是可重入锁。

获取锁方法

lock()
  • lock()就是最普通的获取锁。如果锁已被其他线程获取,则进行等待
  • Lock不会像synchronized一样在异常时自动释放锁,在finallly中释放锁,以保证发生异常时锁一定被释放

coding

/**

* 描述: 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();

}

}

}

  • lock()方法不能被中断,这会带来很大的隐患:一旦陷入死锁,lock()就会陷入永久等待
tryLock()
  • tryLock()用来尝试获取锁,如果当前锁没有被其他线程占用则获取成功,则返回true,否则返回false,代表获取锁失败
  • 相比于lock,这样的方法显然功能更强大了,我们可以根据是否能获取到锁来决定后续程序的行为
  • 该方法会立即返回,即便在拿不到锁时不会一直在那等
tryLock(long time,TimeUnit unit)

超时就放弃,避免死锁

coding

**

* 描述: 用tryLock来避免死锁

*/

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 < 10; 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(3000, 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();

}

}

}

}

}

==============运行结果=============

线程1获取到了锁1

线程2获取到了锁2

线程1获取锁2失败,已重试

线程2获取到了锁1

线程2成功获取到了两把锁

线程1获取到了锁1

线程1获取到了锁2

线程1成功获取到了两把锁

lockInterruptibly()

相当于tryLock(long time,TimeUnit unit)把超时时间设置为无限。在等待锁的过程中,线程可以被中断。

coding

public class LockInterruptibly implements Runnable {

private Lock lock = new ReentrantLock();
public static void main(String[] args) {
LockInterruptibly lockInterruptibly = new LockInterruptibly();
Thread thread0 = new Thread(lockInterruptibly);
Thread thread1 = new Thread(lockInterruptibly);
thread0.start();
thread1.start();

try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread1.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() + "获得锁期间被中断了");
}
}
}

=============运行结果=============

Thread-0尝试获取锁

Thread-1尝试获取锁

Thread-0获取到了锁

Thread-1获得锁期间被中断了

Thread-0释放了锁

isHeldByCurrentThread

锁是否被当前线程持有。

一般调试开发使用

getQueueLength

返回当前正在等待这把锁的队列有多长。

一般调试开发使用

可见性

Lock的加解锁和synchronized有同样的内存语义,也就是说,下一个线程加锁后可以看到所有前一个线程解锁前发生的所有操作

synchronized可见性

lock可见性

锁分类

乐观锁和悲观锁

乐观锁(非互斥同步锁)

  • 认为自己在处理操作的时候不会有其他线程来干扰,所以并不会锁住被操作对象
  • 在更新的时候,去对比在我修改的期间数据有没有被其他人改变过:如果没被改变过,就说明真的是只有我自己在操作,那我就正常去修改数据
  • 如果数据和我一开始拿到的不一样了,说明其他人在这段时间内改过数据,那我就不能继续刚才的更新数据过程了,我会选择放弃、报错、重试等策略
  • 乐观锁的实现一般都是利用CAS算法来实现的
  • 原子类、并发容器属于乐观锁

操作流程

悲观锁(互斥同步锁)

  • 如果我不锁住这个资源,别人就会来争抢,就会造成数据结果错误,所以每次悲观锁为了确保结果的正确性,会在每次获取并修改数据时,把数据锁住,让别人无法访问该数据,这样就可以确保数据内容万无一失
  • Java中悲观锁的实现就是synchronized和Lock相关类
  • 互斥同步锁的劣势
  • 阻塞和唤醒带来的性能劣势
  • 永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无限循环、死锁等活跃性问题,那么等待该线程释放锁的那几个悲催的线程,将永远也得不到执行
  • 优先级反转
  • synchronized和lock接口属于悲观锁

悲观锁流程

开销对比

  • 悲观锁的原始开销要高于乐观锁,但是特点是一劳永逸,l合||四界区持锁时间就算越来越差,也不会对互斥锁的开销造成影响
  • 相反,虽然乐观锁一开始的开销比悲观锁小,但是如果自旋时间很长或者不停重试,那么消耗的资源也会越来越多
  • 悲观锁:适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗,典型情况:
  1. 临界区有IO操作

2. 临界区代码复杂或者循环量大3临界区竞争非常激烈

  • 乐观锁:适合并发写入少,大部分是读取的场景,不加锁的能让读取性能大幅提高。

可重入锁和非可重入锁

可重入?

再次申请锁,无需释放这把锁,继续使用这把锁,也叫递归锁。同一线程可多次获取同一把锁。

优点:

避免死锁。

提升封装性。

coding:

public class GetHoldCount {
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());
}
}

=======输出结果========

0

1

2

3

2

1

0

coding

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();
}
}

==========输出结果============

已经对资源进行了处理

1

已经对资源进行了处理

2

已经对资源进行了处理

3

已经对资源进行了处理

4

已经对资源进行了处理

4

3

2

1

非可重入?

ThreadPoolExecutor的Worker类

公平锁和非公平锁

公平指的是按照线程请求的顺序,来分配锁;非公平指的是,不完全按照请求的顺序,在一定情况下,可以插队。

注意:非公平也同样不提倡“插队”行为,这里的非公平,指的是“在合适的时机”插队,而不是盲目插队。

避免唤醒带来的空档期,提交高效.

ReentrantLock创建公平锁

new ReentrantLock(true);

公平情况

不公平情况

  • 如果在线程1释放锁的时候,线程5恰好去执行lock()
  • 由于ReentrantLock发现此时并没有线程持有lock这把锁(线程2还没来得及获取到,因为获取需要时间)
  • 线程5可以插队,直接拿到这把锁,这也是ReentrantLock默认的公平策略,也就是“不公平”

coding

/**
* 描述: 演示公平和不公平两种情况
*/
public class FairLock {

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));
}
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);//false不公平锁

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+"秒");
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
}
}

==============公平锁输出结果============

Thread-0开始打印

Thread-0正在打印,需要6

Thread-1开始打印

Thread-2开始打印

Thread-1正在打印,需要2

Thread-2正在打印,需要10

Thread-0正在打印,需要6秒

Thread-0打印完毕

Thread-1正在打印,需要3秒

Thread-1打印完毕

Thread-2正在打印,需要3秒

Thread-2打印完毕

说明:公平锁,线程会按顺序排队获取锁

=============不公平锁输出结果=============

Thread-0开始打印

Thread-0正在打印,需要7

Thread-1开始打印

Thread-2开始打印

Thread-0正在打印,需要4秒

Thread-0打印完毕

Thread-1正在打印,需要1

Thread-1正在打印,需要8秒

Thread-1打印完毕

Thread-2正在打印,需要6

Thread-2正在打印,需要4秒

Thread-2打印完毕

特例

  • 针对tryLock()方法,它是很猛的,它不遵守设定的公平的规则
  • 例如,当有线程执行tryLock()的时候,一旦有线程释放了锁,那么这个正在tryLock的线程就能获取到锁,即使在它之前已经有其他现在在等待队列里了

优缺点

共享锁和排它锁

  • 排他锁,又称为独占锁、独享锁。

synchronized属于排它锁

  • 共享锁,又称为读锁,获得共享锁之后,可以查看但无法修改和删除数据,其他线程此时也可以获取到共享锁,也可以查看但无法修改和删除数据
  • 共享锁和排它锁的典型是读写锁ReentrantReadWriteLock,其中读锁是共享锁,写锁是独享锁

作用

  • 在没有读写锁之前,我们假设使用ReentrantLock,那么虽然我们保证了线程安全,但是也浪费了一定的资源:多个读操作同时进行,并没有线程安全问题
  • 在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,提高了程序的执行效率

规则

a)多个线程只申请读锁,都可以申请到

b)如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。

c)如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

d)一句话总结:要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现(要么多读,要多一写)

换一种思路更容易理解:读写锁只是一把锁,可以通过两种方式锁定:读锁定和写锁定。读写锁可以同时被一个或多个线程读锁定,也可以被单一线程写锁定。但是永远不能同时对这把锁进行读锁定和写锁定。这里是把“获取写锁”理解为“把读写锁进行写锁定”相当于是换了一种思路,不过原则是不变的,就是要么是一个或多个线程同时有读锁(同时读锁定),要么是一个线程有写锁(进行写锁定),但是两者不会同时出现

ReentrantReadWriteLock用法

public class CinemaReadWrite {

private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

private static void read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");
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() + "得到了写锁,正在写入");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放写锁");
writeLock.unlock();
}
}

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();
}
}

========输出=======

Thread1得到了读锁,正在读取

Thread2得到了读锁,正在读取

Thread2释放读锁

Thread1释放读锁

Thread3得到了写锁,正在写入

Thread3释放写锁

Thread4得到了写锁,正在写入

Thread4释放写锁

交互规则

不允许读锁插队(公平)

允许写锁降为读锁,不允许读锁升为写锁

非公平和公平的ReentrantReadWriteLock的策略

公平锁:不允许插队

非公平锁:

写锁可以随时插队

读锁仅在等待队列头结点不是想获取写锁的线程时可以插队

非公平:假设线程2和线程4正在同时读取,线程3想要写入拿不到锁,于是进入等待队列,线程5不在队列里,现在过来想要读取

此时有2种策略

public class NonfairBargeDemo {

private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(
true); //false非公平锁

private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

private static void read() {
System.out.println(Thread.currentThread().getName() + "开始尝试获取读锁");
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
try {
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() + "得到写锁,正在写入");
try {
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 < 1000; i++) {
thread[i] = new Thread(() -> read(), "子线程创建的Thread" + i);
}
for (int i = 0; i < 1000; i++) {
thread[i].start();
}
}
}).start();
}
}

锁升降级

写锁可降级读锁

读锁不可升级写锁,否则死锁。

public class Upgrading {

private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(
false);
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

private static void readUpgrading() {
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 {
System.out.println(Thread.currentThread().getName() + "释放读锁");
readLock.unlock();
}
}

private static void writeDowngrading() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
Thread.sleep(1000);
readLock.lock();
System.out.println("在不释放写锁的情况下,直接获取读锁,成功降级");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放写锁");
writeLock.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();
}
}

自旋锁和阻塞锁

  • 阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间
  • 如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长
  • 在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失
  • 如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁
  • 而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
  • 阻塞锁和自旋锁相反,阻塞锁如果遇到没拿到锁的情况,会直接把线程阻塞,直到被唤醒
  • 在java1.5版本及以上的并发框架java.util.concurrent的atmoic包下的类基本都是自旋锁的实现
  • AtomicInteger的实现:自旋锁的实现原理是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改过程中遇到其他线程竞争导致没修改成功,就在while里死循环,直至修改成功

/**
* 描述: 自旋锁
*/
public class SpinLock {

private AtomicReference<Thread> sign = new AtomicReference<>();

public void lock() {
Thread current = Thread.currentThread();
while (!sign.compareAndSet(null, current)) {
System.out.println(current.getName() + "自旋获取失败,再次尝试");
}
}

public void unlock() {
Thread current = Thread.currentThread();
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();
}
}

可中断锁

  • 在Java中,synchronized就不是可中断锁,而Lock是可中断锁,因为tryLock(time)和lockInterruptibly都能响应中断。
  • 如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以中断它,这种就是可中断锁

锁优化

  1. 缩小同步代码块
  2. 尽量不要锁住方法
  3. 减少请求锁的次数
  4. 避免人为制造“热点”
  5. 锁中尽量不要再包含锁
  6. 选择合适的锁类型或合适的工具类
posted @ 2024-01-31 19:04  wangzhilei  阅读(2)  评论(0编辑  收藏  举报