• JAVA多线程编程中有哪些锁?
  • 发布于 2个月前
  • 200 热度
    0 评论
对于多线程,在面试中是经常会被问到的一个内容,而锁,也是会被面试官经常提到的,比如你了解 Java 中的锁吗?锁的实现原理,如何加锁,如何解锁,以及不同锁的应用场景是什么样子的?

多线程
说到锁,那么我们绕不开的就是这个多线程,在出现了进程之后,操作系统的性能得到了⼤⼤的提升。虽然进程的出现解决了操作系统的并发问题,但是⼈们仍然不满⾜,⼈们逐渐对实时性有了要求。使⽤多线程的理由之⼀是和进程相⽐,它是⼀种⾮常花销⼩,切换快,更”节俭”的多任务操作⽅式。在 Linux 系统下,启动⼀个新的进程必须分配给它独⽴的地址空间,建⽴众多的数据表来维护它的代码段、堆栈段和数据段,这是⼀种”昂贵”的多任务⼯作⽅式。

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

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

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

比如多线程的并发问题;
由于多个线程是共同占有所属进程的资源和地址空间的,那么就会存在⼀个问题:
如果多个线程要同时访问某个资源,怎么处理?

在 Java 并发编程中,经常遇到多个线程访问同⼀个 共享资源 ,这时候作为开发者必须考虑如何维护数据⼀致性,这就是 Java 锁机制(同步问题)的来源。
Java提供了多种多线程锁机制的实现⽅式,常⻅的有:
1.synchronized
2.ReentrantLock
3.Semaphore
4.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();
// 堆代码 duidaima.com
//…进⾏任务操作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 () ;

用户评论