【校招VIP】Java实现锁的几种方式

05月12日 收藏 0 评论 3 java开发

【校招VIP】Java实现锁的几种方式

转载声明:文章来源https://blog.csdn.net/qq_32099833/article/details/103149102

锁和同步,学习多线程避不开的两个问题,Java提供了synchronized关键字来同步方法和代码块,还提供了很多方便易用的并发工具类,例如:LockSupport、CyclicBarrier、CountDownLatch、Semaphore…

有没有想过自己实现一个锁呢?

笔者通过一个“抢票”的程序,分别用几种不同的方式来实现方法的同步和加锁,并分析它们的优劣。

自旋

就是让加锁失败的线程死循环,不要去执行逻辑代码。

/**
* @author 潘
* @Description 抢票-自旋锁
*/
public class Ticket {
//加锁标记
private AtomicBoolean isLock = new AtomicBoolean(false);
//票库存
private int ticketCount = 10;

//抢票
public void bye(){
while (!lock()) {
//加锁失败,自旋
}
String name = Thread.currentThread().getName();
//加锁成功,执行业务逻辑
System.out.println(name + ":加锁成功...");
System.out.println(name + ":开始抢票...");
//SleepUtil.sleep(1000);
ticketCount--;
System.out.println(name + ":抢到了,库存:" + ticketCount);
System.out.println(name + ":释放锁.");
unlock();
}

//加锁的过程必须是原子操作,否则会导致多个线程同时加锁成功。
public boolean lock(){
return isLock.compareAndSet(false, true);
}

//释放锁
public void unlock() {
isLock.set(false);
}

public static void main(String[] args) {
Ticket lock = new Ticket();
//开启10个线程去抢票
for (int i = 0; i < 10; i++) {
new Thread(() -> lock.bye()).start();
}
}
}

输出如下:

Thread-0:加锁成功...
Thread-0:开始抢票...
Thread-0:抢到了,库存:9
Thread-0:释放锁.
Thread-3:加锁成功...
Thread-3:开始抢票...
Thread-3:抢到了,库存:8
Thread-3:释放锁.
Thread-4:加锁成功...
Thread-4:开始抢票...
Thread-4:抢到了,库存:7
Thread-4:释放锁.
......

加锁的过程必须是原子操作,否则会导致多个线程同时加锁成功。

自旋是实现加锁最简单的方式,但是缺点也很明显:

1、自旋时CPU空转,浪费CPU资源。
2、如果使用不当,线程一直获取不到锁,会造成CPU使用率极高,甚至系统崩溃。

yield+自旋

要解决自旋锁的性能问题,首先就是尽可能的防止CPU空转,让获取不到锁的线程主动让出CPU资源。

获取不到锁的线程主动让出CPU资源,可以通过Thread.yield()实现。

bye()可以做如下优化:

public void bye(){
while (!lock()) {
//获取不到锁,主动让出CPU资源
Thread.yield();
}
String name = Thread.currentThread().getName();
//加锁成功,执行业务逻辑
System.out.println(name + ":加锁成功...");
System.out.println(name + ":开始抢票...");
//SleepUtil.sleep(1000);
ticketCount--;
System.out.println(name + ":抢到了,库存:" + ticketCount);
System.out.println(name + ":释放锁.");
unlock();
}

Thread.yield()虽然让出了CPU资源,但还是会继续争夺,很可能CPU下次还会继续分配时间片给该线程。

yield+自旋适用于两个线程竞争的情况,如果线程太多,频繁的yield也会增加CPU的调度开销。

Sleep+自旋

除了使用yield让出CPU资源外,还可以使用Sleep将获取不到锁的线程暂时休眠,不占用CPU的资源。

bye()可以做如下优化:

public void bye(){
while (!lock()) {
//获取不到锁的线程,暂时休眠1ms,释放CPU资源
SleepUtil.sleep(1);
}
String name = Thread.currentThread().getName();
//加锁成功,执行业务逻辑
System.out.println(name + ":加锁成功...");
System.out.println(name + ":开始抢票...");
//SleepUtil.sleep(1000);
ticketCount--;
System.out.println(name + ":抢到了,库存:" + ticketCount);
System.out.println(name + ":释放锁.");
unlock();
}

使用Sleep可以减轻CPU的压力,但是缺点也很明显:

sleep时间不可控

使用多线程的目的就是为了提升性能,减少响应时间,我们无法预估线程运行结束的时间,sleep的时间是不可控的,在高并发的场景下,哪怕1毫秒、1纳秒都应该分秒必争。

性能测试

笔者进行了简单的测试,抢夺一亿张票,结果如下:
(1)自旋:耗时21806ms。
(2)yield+自旋:耗时2543ms。
(3)sleep+自旋:耗时1593ms。
测试结果仅供参考。

park+自旋

相较于前几种,是比较好的一种实现方式,需要借助于LockSupport来完成。

/**
* @author 潘
* @Description 抢票-park+自旋
*/
public class TicketPark {
//加锁标记
private AtomicBoolean isLock = new AtomicBoolean(false);
//票库存
private int ticketCount = 10;
//等待线程队列
private final Queue<Thread> WAIT_THREAD_QUEUE = new LinkedBlockingQueue<>();

//抢票
public void bye(){
while (!lock()) {
//获取不到锁的线程,添加到队列,并休眠
lockWait();
}
String name = Thread.currentThread().getName();
//加锁成功,执行业务逻辑
System.out.println(name + ":加锁成功...");
System.out.println(name + ":开始抢票...");
ticketCount--;
System.out.println(name + ":抢到了,库存:" + ticketCount);
System.out.println(name + ":释放锁.");
unlock();
}

//加锁的过程必须是原子操作,否则会导致多个线程同时加锁成功。
public boolean lock(){
return isLock.compareAndSet(false, true);
}

//释放锁
public void unlock() {
isLock.set(false);
//唤醒队列中的第一个线程
LockSupport.unpark(WAIT_THREAD_QUEUE.poll());
}

public void lockWait(){
//将获取不到锁的线程添加到队列
WAIT_THREAD_QUEUE.add(Thread.currentThread());
//并休眠
LockSupport.park();
}
}

java.util.concurrent包下很多类都是采用park+自旋来实现同步的,ReentrantLock也不例外!

尾巴

Java实现锁大致分为这么几种方式,感兴趣的同学也可以自己动手写一个Lock。

C 3条回复 评论
奕杉

有知道笔记在哪下载的吗,跪求老师笔记

发表于 2022-11-12 22:00:00
0 0
SLawliet

太强了,学完框架再回来看

发表于 2022-08-12 23:00:00
0 0
箱湘香樟树

内容再全面一些就好了。

发表于 2022-06-18 22:00:00
0 0