4种Java线程锁你了解多少?

看球不费电
发布于 2023-5-12 11:57
浏览
0收藏

对于多线程,在面试中是经常会被问到的一个内容,而锁,也是会被面试官经常提到的,比如你了解 Java 中的锁么?锁的实现原理,如何加锁,如何解锁,以及不同锁的应用场景是什么样子的,都是经常会提到的,今天了不起就来给大家说说关于线程锁的相关知识。

多线程

说到锁,那么我们绕不开的就是这个多线程,在出现了进程之后,操作系统的性能得到了⼤⼤的提升。虽然进程的出现解决了操作系统的并发问题,但是⼈们仍然不满⾜,⼈们逐渐对实时性有了要求。

使⽤多线程的理由之⼀是和进程相⽐,它是⼀种⾮常花销⼩,切换快,更”节俭”的多任务操作⽅式。

在 ​​Linux​​ 系统下,启动⼀个新的进程必须分配给它独⽴的地址空间,建⽴众多的数据表来维护它的代码段、堆栈段和数据段,这是⼀种”昂贵”的多任务⼯作⽅式。

⽽在进程中的同时运⾏多个线程,它们彼此 之间使⽤相同的地址空间,共享⼤部分数据,启动⼀个线程所花费的空间远远⼩于启动⼀个进程所花费 的空间,⽽且,线程间彼此切换所需的时间也远远⼩于进程间切换所需要的时间。

也就是说,使用多线程,更多的是直接来优化我们的业务逻辑,就是是提升代码的质量和可用性,并且能解决一些问题,所以就会有这个多线程的使用了。

多线程的问题

既然多线程能够帮我们解决一些问题,那么势必也会有一些小小的缺点,而这些缺点也是我们应该去解决的内容。

比如多线程的并发问题;

由于多个线程是共同占有所属进程的资源和地址空间的,那么就会存在⼀个问题:

如果多个线程要同时访问某个资源,怎么处理?

在 ​​Java​​​ 并发编程中,经常遇到多个线程访问同⼀个 共享资源 ,这时候作为开发者必须考虑如何维护数据⼀致性,这就是 ​​Java​​ 锁机制(同步问题)的来源。

Java提供了多种多线程锁机制的实现⽅式,常⻅的有:

  • synchronized
  • ReentrantLock
  • Semaphore
  • AtomicInteger

每种机制都有优缺点与各⾃的适⽤场景,必须熟练掌握他们的特点才能在Java多线程应⽤开发时得⼼应 ⼿。

接下来我们就分开来说一下四个机制的优缺点:

synchronized

Java开发⼈员都认识 ​​synchronized​​ ,使⽤它来实现多线程的同步操作是⾮常简单的,只要在需要同步的对⽅的⽅法、类或代码块中加⼊该关键字,它能够保证在同⼀个时刻最多只有⼀个线程执⾏同⼀个对象 的同步代码,可保证修饰的代码在执⾏过程中不会被其他线程⼲扰。

使⽤ ​​synchronized​​ 修饰的代码具有原⼦性和可⻅性,在需要进程同步的程序中使⽤的频率⾮常⾼,可以满⾜⼀般的进程同步要求。

synchronized (obj) {
//⽅法
…….
}

到了​​Java1.6,synchronized​​进⾏了很多的优化,有适应⾃旋、锁消除、锁粗化、轻量级锁及偏向锁等,

效率有了本质上的提⾼。在之后推出的Java1.7与1.8中,均对该关键字的实现机理做了优化。

需要说明的是,当线程通过 ​​synchronized​​​ 等待锁时是不能被 ​​Thread.interrupt()​​ 中断的,因此程序设计时必须检查确保合理,否则可能会造成线程死锁的尴尬境地。

强烈推荐在多线程应⽤程序中使⽤该关键字,因为实现⽅便,后续⼯作由 ​​JVM​​​ 来完成,可靠性⾼。只有在确定锁机制是当前多线程程序的性能瓶颈时,才考虑使⽤其他机制,如 ​​ReentrantLock​​ 等

ReentrantLock

可重⼊锁,顾名思义,这个锁可以被线程多次重复进⼊进⾏获取操作。

​ReentantLock​​​ 继承接⼝ ​​Lock​​​ 并实现了接⼝中定义的⽅法,除了能完成 ​​synchronized​​ 所能完成的所有⼯作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的⽅法。

​Lock​​​ 实现的机理依赖于特殊的 ​​CPU​​​ 指定,可以认为不受 ​​JVM​​​ 的约束,并可以通过其他语⾔平台来完成底层的实现。在并发量较⼩的多线程应⽤程序中,​​ReentrantLock​​​ 与 ​​synchronized​​​ 性能相差⽆⼏,但在⾼ 并发量的条件下,​​synchronized​​​ 性能会迅速下降⼏⼗倍,⽽ ​​ReentrantLock​​ 的性能却能依然维持⼀个⽔准。

因此我们建议在⾼并发量情况下使⽤ ​​ReentrantLock​​。

一般 ReentrantLock 是分为公平锁和非公平锁。

公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁。

反之,JVM按随机、就近原则分配锁的机制则称为不公平锁。

Lock lock = new ReentrantLock();
try {
lock.lock();
//…进⾏任务操作5 }
finally {
lock.unlock();
}

上面的是伪代码,但是需要注意一个比较有意思的地方,那么就是

使⽤ReentrantLock必须在finally控制块中进⾏解锁操作

Semaphore

Semaphore基本能完成ReentrantLock的所有⼯作,使⽤⽅法也与之类似,通过acquire()与release()⽅法来获得和释放临界资源。

其实 ReentrantLock 和 synchronized 都是互斥锁,也就是说相当于只存在⼀个临界资源,因此同时最多只能给⼀个线程提供服务。

调用Semaphore.acquire() 方法, 它本质上是调用的acquireSharedInterruptibly(int), 参数为1.

// arg 等于 1
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 关于tryAcquireShared,Semaphore有两种实现
    // 一种是公平锁,另一种是非公平锁. 这分析非公平锁.
    if (tryAcquireShared(arg) < 0)
        // 调用 AQS#doAcquireSharedInterruptibly(1) 方法
        doAcquireSharedInterruptibly(arg);
}

而他的释放锁,调用的是 release() 方法,其中调用的则是下面:

// 释放共享锁
public final boolean releaseShared(int arg) {
    // 调用Semaphore#tryReleaseShared方法.
    if (tryReleaseShared(arg)) {
        // tryReleaseShared释放成功, 则释放双向链表中head的后继
        doReleaseShared();
        return true;
    }
    return false;
}

AtomicInteger

我们知道,在多线程程序中,诸如 ++i 或 i++ 等运算不具有原⼦性,是不安全的线程操作之⼀。通常我们会使⽤ synchronized 将该操作变成⼀个原⼦操作,但 JVM 为此类操作特意提供了⼀些同步类,使得使⽤更⽅便,且使程序运⾏效率变得更⾼。通过相关资料显示,通常 AtomicInteger 的性能是 ReentantLock 的好⼏倍。

而 AtomicInteger 用于多线程下线程安全的数据读写操作,避免使用锁同步,底层采用CAS实现,内部的存储值使用 volatile 修饰,因此多线程之间是修改可见的。

用法如下:

private AtomicInteger counter = new AtomicInteger (0);//初始计数为0
// dosomething,执行操作之后,计数即可
int count = counter .incrementAndget () ;

关于这四种 Java 的线程锁,你了解了么?



文章转载自公众号:  Java极客技术

标签
已于2023-5-12 11:57:39修改
收藏
回复
举报
回复
    相关推荐