JAVA和Nginx 教程大全

网站首页 > 精选教程 正文

Java面试篇基础部分- 锁详解

wys521 2024-11-21 22:23:07 精选教程 44 ℃ 0 评论


可重入锁

可重入锁也叫作递归锁,是指在同一个线程中,在外层函数获取到该锁之后,内存的递归函数还可以获取到该锁。在Java语言环境下,ReentrantLock和Synchroinzed都是可重入锁的代表。

公平锁与非公平锁

  • 公平锁(Fair Lock)是指在分配锁之前检查是否有线程在排队等待获取到锁,优先将锁分配给排队时间最长的线程。
  • 非公平锁(Nonfair Lock)是指在分配锁的时候不考虑线程排队等待的情况,直接尝试获取锁的操作,在获取不到锁的情况下再排到队尾进行等待。如果获取到锁,则执行自己的业务逻辑。

因为公平锁需要在多核的情况下维护一个锁线程的等待队列,基于该队列进行锁的分配,因此效率比非公平锁要低很多。Java中Synchroinzed就是非公平锁,ReentrantLock默认的lock方法采用的也是非公平锁。

读写锁:ReadWriteLock

在Java中通过Lock接口以及其子类对象提供了方便对对象进行加锁和释放锁。但是这种锁是不能区分读写的。所以叫做普通锁。为了提高I/O性能,Java提供了读写锁机制。

读写锁分为读锁和写锁两种,多个读锁之间是不互斥的,读锁与写锁互斥。在读的地方使用读锁,在写的地方使用写锁,在没有写锁的情况下,读锁是无阻塞的。

如果系统的要求是共享数据可以同时支持多线程并发读的操作,但不能支持多线程并发写,那么使用读锁机制可以很大程度上增加读取效率;如果系统对于共享数据在同一时只能由一个线程进行写操作,并且在写的过程中不能进行读取,那么这个时候就需要使用写锁。因为读写锁之间是互斥的,所以说如果有写锁的话,读锁是没有办法进入的。

一般的情况下,分别定义一个读锁,一个写锁,在读取共享数据的时候使用读锁,在使用完成之后释放读锁,在写共享数据的时候使用写锁,在使用完成后释放写锁,在Java中提供了java.util.concurrent.locks.ReadWriteLock的实现类ReentrantReadWriteLock来完成对读写锁的定义和使用。

import java.util.HashMap;  
import java.util.Map;  
import java.util.concurrent.locks.ReadWriteLock;  
import java.util.concurrent.locks.ReentrantReadWriteLock;  
/** 
 * 缓存的实现,每个线程只能获得他自己的缓存,也应该是单例的 
 * 本类没有去实现单例,如果需要的话可以自行去实现 
 * @author scl 
 * 
 */  
public class CacheSystem {  
    private Map<String, Object> cache = new HashMap<String,Object>();  
    private ReadWriteLock rwl = new ReentrantReadWriteLock();  
    public Object getData(String key){  
        //先从缓存中去取数据,先加上读锁  
        rwl.readLock().lock();  
        Object obj = null;  
        try{  
            obj = cache.get(key);  
            if(obj == null){  
                //先解除读锁,在上写锁(必须先解除读锁才能成功上写锁)  
                rwl.readLock().unlock();  
                rwl.writeLock().lock();  
                //去数据库取数据,再判断一次是否为null,因为有可能多个线程获得写锁  
                try{  
                if(obj == null){  
                    obj = new String("obj is get from db");  
                }  
                }finally{  
                    //先上读锁,然后再解除写锁(这样可以成功完成,在解除写锁前获得读锁,写锁被降级--这翻译的api上的)  
                    rwl.readLock().lock();  
                    rwl.writeLock().unlock();//解除写锁,读锁仍然持有  
                }  
            }  
        }finally{  
            rwl.readLock().unlock();  
        }  
        return obj;  
    }  
  
} 

共享锁和独占锁

Java并发包提供的加锁模式分为独占锁和共享锁

  • 独占锁:也叫作互斥锁,每次只允许一个线程持有该锁,ReentrantLock为独占锁的具体实现
  • 共享锁:允许多个线程同时获取该锁,并发访问共享资源。ReentrantReadWriteLock中的读锁为共享锁的实现。

ReentrantReadWriteLock的加锁解锁操作最终调用内部的Sync提供的方法,Sync对象通过继承AQS(Abstract Queued Synchronizer)实现。AQS的内部类Node定义了两个常量SHARED和EXCLUSIVE,分别标识AQS队列中等待线程的锁获取模式。

独占锁是一种悲观锁的策略模式,同一时刻只允许一个线程访问资源,限制了操作的并发性。例如并发读操作,对于读操作其实是不影响数据的一致性的。所以共享锁采用了乐观加锁的策略,允许多个线程同时读取共享资源。

重量级锁和轻量级锁

重量级锁是基于操作系统的互斥量(Mutex Lock)而实现的锁,会导致进程在用户态与内核态之间切换,开销相对较大。

Synchroinzed 在内部基于监视器锁(Monitor)实现,监视器锁基于底层的操作系统的Mutex Lock 实现,所以Synchroinzed属于重量级锁。重量级锁需要在用户态与内核态之间进行转换,所以Synchroinzed的运行效率不高。

在JDK1.6的版本之后,为了减少获取锁和释放锁带来的性能消耗,引入了轻量级锁和偏向锁。

轻量级锁是相对于重量级锁而言的,轻量级锁的核心设计是在没有多线程竞争的前提下,减少重量级锁从而提升系统性能。轻量级锁适用于线程交替执行同步代码块的情况也就是互斥操作,如果同一时刻有多个线程访问同一个锁,则会导致轻量级锁膨胀为重量级锁。

偏向锁

在实际中,除了多线程之间存在竞争锁的情况,还会出现同一个锁被同一个线程多次获取的情况,偏向锁就是用于在某个线程获取某个锁之后,消除这个锁重入的开销。看上去有点像优先获取该锁。

偏向锁的主要目的是在同一个线程多次获取某个锁的情况下尽量减少轻量级锁的执行路径,因为在轻量级锁中,需要多次的执行CAS的操作。也就是说偏向锁只需要切换ThreadID的时候执行一次CAS操作,这样就可以提升锁的获取执行效率。

在多线程竞争锁的时候,JVM会自动的撤销偏向锁,所以偏向锁的撤销的耗时,必须少于CAS的操作的耗时。

综上,轻量级锁用于提高线程交替执行代码块导致的性能问题,偏向锁在某个线程交替执行代码块的性能。注意字眼。

锁升级:无锁、偏向锁、轻量级锁、重量级锁。在Java中只能锁升级,没有锁降级。

分段锁

分段锁并非是一种实际的锁,而是一种思想,用于将数据进行分段处理,在每个分段上都单独进行加锁,把锁进一步细粒度话,从而提升锁的并发执行效率。ConcurrentHashMap内部就是使用了分段锁机制。

同步锁与死锁

在多线程执行过程中,同时被阻塞,他们之间等待相互之间释放锁,如果都不能释放锁就会出现死锁,为了避免出现死锁,可以对锁进行一个加超时时间的操作,或者是在适当的代码中对锁进行释放。

如何进行锁优化

  • 1、减少持有锁的时间
  • 2、减小锁的粒度
  • 3、锁分离,读写锁
  • 4、锁粗化,如果锁分的太细了也会导致系统性能问题。
  • 5、锁消除,在很多场景中,通过业务逻辑操作可以避免一些加锁的操作

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表