java同步锁有哪几种_Java实现锁的几种方式

锁和同步,学习多线程避不开的两个问题,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:释放锁.

......

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

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

自旋时CPU空转,浪费CPU资源。

如果使用不当,线程一直获取不到锁,会造成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纳秒都应该分秒必争。

性能测试

笔者进行了简单的测试,抢夺一亿张票,结果如下:

自旋:耗时21806ms。

yield+自旋:耗时2543ms。

sleep+自旋:耗时1593ms。

测试结果仅供参考。

park+自旋

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

/**

* @author 潘

* @Description 抢票-park+自旋

*/

public class TicketPark {

//加锁标记

private AtomicBoolean isLock = new AtomicBoolean(false);

//票库存

private int ticketCount = 10;

//等待线程队列

private final Queue 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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值