1.Lock接口
1.1简介、地位、作用
◆锁是一种工具,用于控制对共享资源的访问。
◆Lock和 synchronized,这两个是最常见的锁,它们都可以达到线程安全的目的,但是在使用上和功能上又有较大的不同
◆Lock并不是用来代替 synchronized的,而是当使用synchronized不合适或不足以满足要求的时候,来提供高级功能的
◆Lock接口最常见的实现类是 Reentrantlock
◆通常情况下,Lock只允许—个线程来访问这个共享资源。不过有的时候,一些特殊的实现也可允许并发访问,比如ReadWritelock里面的 Readlock。
1.2为什么 synchronzed不够用?为什么需要Lock?
1)效率低:锁的释放情况少、试图获得锁时不能没定超时、不能中断一个正在试图获得锁的线程
2)不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的
3)无法知道是否成功获取到锁
1.3方法介绍
在Lock中声明了四个方法来获取锁
lock()
◆lock就是最普通的获取锁。如果锁已被其他线程获取,则进行等待
◆Lock不会像 synchronized-样在异常时自动释放锁
◆因此最佳实践是,在 finally中释放锁,以保证发生异常时锁一定被释放
◆lock()方法不能被中断,这会带来很大的隐患:一旦陷入死锁lock()就会陷入永久等待
代码:
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
//业务代码
} finally {
lock.unlock();
}
}
-
tryLock():
尝试获得锁,如果锁空闲就能够获得,否则直接返回FALSE/** * Acquires the lock only if it is free at the time of invocation. * * <p>Acquires the lock if it is available and returns immediately * with the value {@code true}. * If the lock is not available then this method will return * immediately with the value {@code false}. * * <p>A typical usage idiom for this method would be: * <pre> {@code * Lock lock = ...; * if (lock.tryLock()) { * try { * // manipulate protected state * } finally { * lock.unlock(); * } * } else { * // perform alternative actions * }}</pre> * * This usage ensures that the lock is unlocked if it was acquired, and * doesn't try to unlock if the lock was not acquired. * * @return {@code true} if the lock was acquired and * {@code false} otherwise */ boolean tryLock();
-
tryLock( long time,TimeUnit unit)
:超时就放弃。
public class TryLockDeadLock implements Runnable{
int flag = 1;
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
if (flag == 1) {
try {
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("线程" + Thread.currentThread().getName()+"获取到了锁1");
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("线程" + Thread.currentThread().getName()+"获取到了锁2");
System.out.println("线程" + Thread.currentThread().getName()+"获取到了锁12了");
}finally {
lock2.unlock();
}
} else {
System.out.println("线程" + Thread.currentThread().getName()+"获取锁2失败,已经重试");
}
} finally {
lock1.unlock();
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("线程" + Thread.currentThread().getName()+"获取锁1失败,已经重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
try {
if (lock2.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
System.out.println("线程" + Thread.currentThread().getName()+"获取到了锁2");
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
if (lock1.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
System.out.println("线程" + Thread.currentThread().getName()+"获取到了锁1");
System.out.println("线程" + Thread.currentThread().getName()+"获取到了锁21了");
}finally {
lock1.unlock();
}
} else {
System.out.println("线程" + Thread.currentThread().getName()+"获取锁1失败,已经重试");
}
} finally {
lock2.unlock();
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("线程" + Thread.currentThread().getName()+"获取锁2失败,已经重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
TryLockDeadLock tryLockDeadLock1 = new TryLockDeadLock();
TryLockDeadLock tryLockDeadLock2 = new TryLockDeadLock();
tryLockDeadLock1.flag = 1;
tryLockDeadLock2.flag = 0;
new Thread(tryLockDeadLock1).start();
new Thread(tryLockDeadLock2).start();
}
}
-
lockinterruptibly():
相当于tryLock(long time, TimeUnit unit)
把超时时间设置为无限。在等待锁的过程中,线程可以被中断。public class LockInterruptibly implements Runnable{ private Lock lock = new ReentrantLock(); @Override public void run() { System.out.println(Thread.currentThread().getName() + "尝试获取锁"); try { lock.lockInterruptibly(); try { System.out.println(Thread.currentThread().getName() + "拿到了锁"); TimeUnit.SECONDS.sleep(5); } 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() + "获得锁期间被中断"); } } public static void main(String[] args) throws InterruptedException { LockInterruptibly lockInterruptibly = new LockInterruptibly(); Thread thread1 = new Thread(lockInterruptibly); Thread thread2 = new Thread(lockInterruptibly); thread1.start(); thread2.start(); TimeUnit.SECONDS.sleep(2); // thread1.interrupt(); thread2.interrupt(); } }
-
unlock():
释放锁,在使用的时候先try{}finally{lock.unlock();}
确保释放锁
1.4可见性保证
2.锁的分类
线性要不要锁住同步资源
锁住
悲观锁
不锁住
乐观锁多线程能否共享一把锁
可以
共享锁
不可以
独占锁多线程竞争时,是否排队
排队
公平锁
先尝试插队,插队失败再排队
非公平锁同一个线程是否可以重复获取同一把锁
可以
可重入
不可以
不可重入锁是否可以中断
可以
可中断锁
不可以
非可重入锁等待锁的过程
自旋
自旋锁
阻塞
非自旋锁
3.乐观锁和悲观锁
1.为什么会诞生非互斥同步锁: 互斥同步锁的劣势
◆阻塞和唤醒带来的性能劣势
◆永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无限循环、死锁等活跃性问题,那么等待该线程释放锁的那几个悲催的线程,将 永远也得不到执行
◆优先级反转😥 低优先级的拿到了锁,如果一直不释放,那么当等待优先级比较高的想拿的时候却无法拿到
2.什么是乐观锁和悲观锁
乐观锁:
◆认为自己在处理操作的时候不会有其他线程来干扰,所以并不会锁住被操作对象
◆在更新的时候,去对比在我修改的期间数据有没有被其他人改变过,如果没被改变过,就说明真的是只有我自己在操作,那我就正常去修改数据
◆如果数据和我开始拿到的不一样了,说明其他人在这段时间内改过数据,那我就不能继续刚才的更新数据过程了,我会选择放弃、报错、重试等策略
◆乐观锁的实现一般都是利用CAS
算法来实现的
悲观锁:
◆如果我不锁住这个资源,别人就会来争抢,就会造成数据结果错误,所以每次悲观锁为了确保结果的正确性,会在每次获取并修改数据时,把数据锁住,让别人无法访问该数据,这样就可以确保数据内容万无一失
◆Java中悲观锁的实现就是 synchronized
和Lock
相关类
3.典型例子
◆悲观锁: synchronized和lock接口
◆乐观锁的典型例子就是原子类
、并发容器等
◆代码演示
public class PessimismOptimismLock {
public static void main(String[] args) {
//乐观锁
AtomicInteger a = new AtomicInteger();
a.incrementAndGet();
}
//悲观锁
int a;
public synchronized void test() {
a++;
}
}
◆Git:Git就是乐观锁的典型例子,当我们往远端仓库push的时候,git会检查远端仓库的版本是不是领先于我们现在的版本,如果远程仓库的版本号和本地的不一样,就表示有其他人修改了远端代码了,我们的这次提交就失败;如果远端和本地版本号一致,我们就可以利提交版本到远端仓库
◆数据库
◆ select for update就是悲观锁
◆用 version控制数据库就是乐观锁
4.开销对比
◆悲观锁的原始开销要高于乐观锁,但是特点是一劳永逸,临界区持锁时间就算越来越差,也不会对互斥锁的开销造成影响
◆相反,虽然乐观锁—开始的开销比悲观锁小,但是如果自旋时间很长或者不停重试,那么消耗的资源也会越来越多
5.两种锁各自的使用场景
◆悲观锁:适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗,典型情况
1.临界区有IO操作
2.临界区代码复杂或者循环量大
3.临界区竞争非常激烈
◆乐观锁:适合并发写入少,大部分是读取的场景,不加锁的能让读取性能大幅提高。
4.可重入锁和非重入锁,以ReentrantLock为例
什么是可重入锁呢?
当一个线程获取了某个对象锁以后,还可以再次获得该对象锁。
public class Demo1 {
public synchronized void functionA(){
System.out.println("iAmFunctionA");
functionB();
}
public synchronized void functionB(){
System.out.println("iAmFunctionB");
}
private static ReentrantLock lock = new ReentrantLock();
public void f1() {
lock.lock();
try {
f2();
} finally {
lock.unlock();
}
}
public void f2() {
lock.lock();
try {
} finally {
lock.unlock();
}
}
}
functionA()
和functionB()
都是同步方法,当线程进入funcitonA()
会获得该类的对象锁,这个锁"new Demo1()"
,在functionA()
对方法functionB()
做了调用,但是functionB()
也是同步的,因此该线程需要再次获得该对象锁(new Demo1()
),其他线程是无法获该对象锁的。
这就是可重入锁。可重入锁的作用就是为了避免死锁,java中synchronized
和ReentrantLock
都是可重入锁。
重入锁的实现原理
通过为每个锁关联一个请求计数器和一个获得该锁的线程。当计数器为0时,认为锁是未被占用的。线程请求一个未被占用的锁时,JVM将记录该线程并将请求计数器设置为1,此时该线程就获得了锁,当该线程再次请求这个锁,计数器将递增,当线程退出同步方法或者同步代码块时,计数器将递减,当计数器为0时,线程就释放了该对象,其他线程才能获取该锁
ReentrantLock
源码:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
5.公平锁和非公平锁
公平指的是按照线程请求的顺序,来分配锁;非公平指的是不完全按照请求的顺序,在一定情况下,可以插队。
注意:非公平也同样不提倡“插队”行为,这里的非公平,指的是“在合适的时机”插队,而不是盲目插队。
优势 | 劣势 | |
---|---|---|
公平锁 | 各线程公平平等,每个线程在等待一段时间后,总有执行的机会 | 更慢,吞吐量更小 |
非公平锁 | 更快,吞吐量更大 | 有可能产生线程饥饿,也就是某些线程在长时间内始终得不到执行 |
代码演示公平锁和非公平锁的效果:
下面的代码使用的是ReentrantLock
,默认是非公平锁,效果是 printJob(Object document)
中的会连续打印两次,因为非公平锁,当第一次锁释放后,然后第二次会直接插队,因为其他线程还在等待,唤醒需要时间,为了减少空档期,提高速度,会直接插队拿到锁,连续打印;设置new ReentrantLock(true)
, 这时是公平锁,那么会按照排队顺序抢锁,所以printJob(Object document)
中打印第一次后,不会接着打印第二次,因为这个时候再去抢锁会发现,前面还有很多人在排队。
public class FairLock {
public static void main(String[] args) throws InterruptedException {
PrintQueue printQueue = new PrintQueue();
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(new Job(printQueue));
}
for (int i = 0; i < 10; i++) {
threads[i].start();
Thread.sleep(100L);
}
}
}
class Job implements Runnable {
private 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();
public void printJob(Object document) {
queueLock.lock();
try {
int duration = new Random().nextInt(10) + 1;
System.out.println(Thread.currentThread().getName() + " 正在打印, 需要 " + duration);
TimeUnit.SECONDS.sleep(duration);
} 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);
TimeUnit.SECONDS.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
}
}
公平锁源码:
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
非公平锁:
@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
可以看到,公平锁中会通过hasQueuedPredecessors()
判断是否有人在队列等待。
6.共享锁和排它锁:以ReentranReadWriteLock读写锁为例
◆什么是共享锁和排它锁
- 排他锁,又称为独占锁、独享锁
- 共享锁,又称为读锁,获得共享锁之后,可以查看但无法修改和删除数据,其他线程此时也可以获取到共享锁,也可以査看但无法修改和删除数据
- 共享锁和排它锁的典型是读写锁
ReentrantReadWritelock
,其中读锁是共享锁,写锁是独享锁
◆读写锁的作用
- 在没有读写锁之前,我们假设使用
ReentrantLock
,那么虽然我们保证了线程安全,但是也浪费了一定的资源:多个读操作同时进行,并没有线程安全问题 - 在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的提高了程序的执行效率
◆读写锁的规则
- 多个线程只申请读锁,都可以申请到
- 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
- 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
- —句话总结:要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现(要么多读,要多一写)
换—种思路更容易理解:读写锁只是一把锁,可以通过两种方式锁定读锁定和写锁定。读写锁可以同时被一个或多个线程读锁定,也可以被单—线程写锁定。但是永远不能同时对这把锁进行读锁定和写锁定。
这里是把“获取写锁”理解为“把读写锁进行写锁定”,相当于是换了一种思路,不过原则是不变的,就是要么是一个或多个线程同时有读锁(同时读锁定),要么是一个线程有写锁(进行写锁定),但是两者不会同时出现
◆ ReentrantReadwritelock
具体用法
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 read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放了读锁");
readLock.unlock();
}
}
public static void write() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
Thread.sleep(1000L);
} 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();
}
}
◆读锁和写锁的交互方式
选择规则
读线程插队(比喻:男女共用厕所,男生可以插队吗?
**策略一:**线程4正在同时读取,线程3想要写入,拿不到锁,于是进入等待队列,线程6不在队列里,现在过来想要读取。
- 读可以插队,效率高
- 容易造成饥饿,就是等待的写线程一直等待,因为一直有读线程不断插队,读锁被一直持有
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6C0nhYbl-1636097380233)(https://i.loli.net/2021/10/24/O4E1sPm9kpwFlNL.png)]
策略二:避免饥饿。策略的选择取决于具体锁的实现,ReentrantReadWriteLock
实现是选择了策略二。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mpQbm5P2-1636097380235)(https://i.loli.net/2021/10/24/jTv6hLfRgGcnlNs.png)]
◆公平和非公平源码
-
公平锁:不允许插队
static final class FairSync extends Sync { private static final long serialVersionUID = -2274990926593161451L; final boolean writerShouldBlock() { return hasQueuedPredecessors(); } final boolean readerShouldBlock() { return hasQueuedPredecessors(); } }
-
非公平锁:
1)写锁可以随时插队
2)读锁仅在等待队列头不是想获取写锁的线程的时候可以插队,写锁线程在头不能插
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* block if there is a waiting writer behind other enabled
* readers that have not yet drained from the queue.
*/
return apparentlyFirstQueuedIsExclusive();
}
}
◆锁的升降级
为什么需要升级
支持锁的降级,不支持升级:
public class Upgrading {
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
public static void read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");
writeLock.lock();
System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放了读锁");
System.out.println(Thread.currentThread().getName() + "释放了写锁");
writeLock.unlock();
readLock.unlock();
}
}
public static void write() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
readLock.lock();
System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放了写锁");
System.out.println(Thread.currentThread().getName() + "释放了读锁");
readLock.unlock();
writeLock.unlock();
}
}
public static void main(String[] args) {
new Thread(()->write()).start();
new Thread(()->read()).start();
}
}
为什么不支持锁的升级?死锁,假设存在AB线程,A线程升级,那么B线程必须释放读锁,同理,B升级,那么A线程必须释放读锁。
总结:
-
ReentrantReadWriteLock
实现了ReadWriteLock
接口,最主要的有两个方法:readLock()
和writeLock()
用来获取读锁和写锁 -
锁申请和释放策略
-
多个线程只申请读锁,都可以申请到
-
如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
-
如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
要么多读,要么一写
-
-
插队策略:为了防止饥饿,读锁不能插队(等待队列头是写线程时)
-
升降级策略:只能降级,不能升级
-
适用场合∶相比于
ReentrantLock
适用于一般场合,ReentrantReadWriteLock
适用于读多写少的情况,合理使用可以进—步提高并发效率。
7.自旋锁和阻塞锁
◆阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间
◆如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长
◆在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失
◆如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁
◆而为了让当前线程“稍等一下”,我们需让当前线程进行自旋, 如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁
◆阻塞锁和自旋锁相反,阻塞锁如果遇到没拿到锁的情况,会直接把线程阻塞,直到被唤醒
自旋锁的缺点:
◆如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源
◆在自旋的过程中,一直消耗cρu,所以虽然自旋锁的起始开销低于悲观锁,但是随着自旋时间的增长,开销也是线性增长的
源码分析:
◆在java1.5
版本及以上的并发框架java.util.concurrent
的 atmoic
包下的类基本都是自旋锁的实现
◆ AtomicInteger
的实现:自旋锁的实现原理是CAS
,AtomicInteger
中调用 unsafe
进行自增操作的源码中的 do-while
循环就是一个自旋操作,如果修改过程中遇到其他线程竞争导致没修改成功,就在 while里死循环,直至修改成功
public final int getAndIncrement() {
return U.getAndAddInt(this, VALUE, 1);
}
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
自定义自旋锁:
public class SpinLock {
private AtomicReference<Thread> sign = new AtomicReference<>();
public void lock() {
Thread current = Thread.currentThread();
while (!sign.compareAndSet(null, current)) {
}
}
public void unlock() {
Thread current = Thread.currentThread();
sign.compareAndSet(current, null);
}
public static void main(String[] args) {
SpinLock spinLock = new SpinLock();
Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");
spinLock.lock();
System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放自旋锁");
spinLock.unlock();
}
};
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
}
}
自旋锁的适用场景:
◆自旋锁一般用于多核的服务器,在并发度不是特别高的情况下,比阻塞锁的效率高
◆另外,自旋锁适用于临界区比较短小的情况,否则如果临界区很大(线程一旦拿到锁,很久以后才会释放),那也是不合适的
8.可中断锁:顾名思义,就是可以响应中断的锁
◆在java
中, synchronized
就不是可中断锁,而Lock
是可中断锁,因为 tryLock(time)
和 lockInterruptibly
都能响应中断。
◆如果某一线程A正在执行锁中的代码,另一线程B在等待获取该锁可能由于等待时间过长,线程B不想等待了,想先处理其他事情我们可以中断它,这种就是可中断锁
9.锁优化
Java虚拟机对锁的优化
- 自旋锁和自适应
- 锁消除
- 锁粗化
我们在写代码时如何优化锁和提高并发性能
- 缩小同步代码块
- 尽量不要锁住方法
- 减少请求锁的次数
- 避免人为制造“热点”
- 锁中尽量不要再包含锁
- 选择合适的锁类型或合适的工具类