Java锁机制 浅析(一)

1.相关概念

  • CAS:比较和交换(Conmpare And Swap)用于实现多线程同步的原子指令。 它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。 这是作为单个原子操作完成的。 原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。 操作结果必须说明是否进行替换; 这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成

JAVA中的CAS操作是通过sun.misc.Unsafe类的一系列compareAndSwap...()函数实现的。这些都是native函数。最终是由JVM利用CPU级的支持来实现。

  • 自旋锁:循环使用CAS机制对目标进行修改,直到修改成功。
  • 悲观锁/乐观锁:悲观锁认为一定发生冲突,将所有操作排队串行执行。乐观锁则假定数据没有变化,在修改时如果发生变化,读取最新的数据作为更新条件,然后重新尝试修改。
  • 独享锁/共享锁:独享锁主要用在写操作,由单个线程独享。也叫“单写”。共享锁则主要用于读操作,多个线程可以同时加锁,但是只能加读锁,不能加写锁。也叫“多读”。
  • 可重入锁/不可重入锁:当获得外围锁之后,则可访问这把锁内部所有的锁的内容则为可重入锁,反之则为不可重入锁。
  • JIT:当Java虚拟机发现某个方法或代码块运行特别频繁时,就会把这些代码认定为“Hot Spot Code”(热点代码),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,完成这项任务的正是JIT编译器

2. Java锁实现

2.1 synchronized

最基本的线程同步机制, 基于对象监视器实现。java中的每一个对象都有一个与之关联的监视器,可以由线程来锁定或者解锁。

2.1.1 锁的范围

synchronized关键字修饰不同的对象,其所表达的范围有所不同。

类锁
//1,修饰静态函数时
public synchronized static void print()...
//2,直接指定类的class对象。
synchronized(Demo1.class){
   //do somethings...
}
对象锁
//1,修饰动态函数时
public synchronized void print()...
//2,指定this关键字。
synchronized(this){
  //do somethings...
}
//3,直接指定某个对象
Object obj = new Object();
synchronized(obj){
  //do somethings...
}

2.1.2 实现原理

java中每一个对象都有与之关联的monitor,当monitor被占用时就会处于锁定状态。而synchronized在执行阶段是依靠monitorenter指令(获取对象锁),monitorexit指令(释放对象锁)来实现的。

  • monitorenter:尝试获取monitor的所有权,如果成功则对象monitor进入数加1,否则线程将被阻塞。
  • monitorexit:释放monitor所有权,当前线程释放对象锁,monitor进入数减1,其他被阻塞的线程可以尝试获取该对象monitor的所有权。

通过javap 查看以下代码class文件:

public class LockDemo{
	public void print(){
		synchronized (this){
			System.out.println("hello");
		}
	}
}

class文件即可以看到指令在相应位置出现:
class字节码反编译结果

被synchronized修饰的方法在执行时是通过检查ACC_SYNCHRONIZED标记来实现的
当方法调用时,会先检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

2.1.3 synchronized的优化

从JDK 6 开始,synchronized的实现机制进行了较大调整,加入了CAS自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等优化策略,使得synchronized的性能有了较大的提升。

自旋锁

当线程尝试获取锁时,如果已被其他线程占用,就会一直采用玄幻监测是否被释放,而不是进入线程挂起或睡眠状态。
从JDK 6 开始,自旋默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整。

自旋锁适用于缩范围较小的情况,这样锁占用的时间很短。此时通过自旋避免线程切换带来的开销。但是,自旋等待不能替代阻塞,自旋等待期间一直占用CPU时间。如果缩范围较大,会使自旋等待时间较长,这样就会浪费资源。

锁消除

JIT编译器会根据代码上下文来判断一些加锁操作是否必要,并会对一些明显不需要的锁进行消除以提高效率。例如:

public void print(){
	StringBuffer sb = new StringBuffer();
	sb.append("hello").append("world");	
	System.out.println(sb.toString());
}

我们知道StringBufferappend()函数是一个线程安全的方法(由synchronized关键字修饰),但是当其作为函数的局部变量时,它的生命周期始终都是处于线程安全环境中,那么这个加锁的动作完全是没有必要的。在这种情况下,JIT就会将该加锁动作进行消除来提高效率。

锁粗化

JIT编译器在执行动态编译时,若发现前后相邻的synchronized块使用的是同一个锁对象,那么它就会把这几个synchronized块给合并为一个较大的同步块,这样就无需频繁申请与释放锁了通过一次锁操作,就可以执行全部代码块,从而提升了性能。

public class LockDemo{
    private int count = 0;
    public void print(){
        synchronized (this){
            count++;//同步代码1.
        }
        synchronized (this){
            System.out.println(count);//同步代码2.
        }
    }
} 

实际JIT在执行print()函数时被优化一个同步块:

    synchronized (this){
        count++; //同步代码1.
        System.out.println(count);//同步代码2.
    }
偏向锁

偏向锁是在单线程执行同步代码块时所采用的策略,其本质就是没有锁。JVM在执行代码块时,如果发现该代码块只有单个线程访问,此时如果有获取锁的操作,会直接记录下当前线程ID而不获取锁资源。当有其他线程执行该代码块时,则会从偏向锁转向轻量级锁状态。以此来提高单线程下的代码块执行效率。

从JDK 6开始 默认开启偏向锁,可以使用参数-XX:-UseBiasedLocking 关闭偏向锁。

轻量级锁

轻量级锁的主要目的是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
轻量级锁的获取机制如下:
1,进入同步时
未加锁时线程堆栈与对象mark word信息
2, 将对象mark word复制到Lock record并使用CAS机制尝试更新对象mark word指向当前线程。
3,如果更新成功那么线程就获得了对象锁。
加锁后线程堆栈与对象mark word信息
4,如果更新cas更新失败,有可能线程在之前已经获得过该对象的锁,此时可以直接金额如同步块执行。否则说明有其他线程与当前线程在竞争对象锁,此时会进入自旋等待重新尝试获取锁。如果自旋次数达到上限仍然没有获得对象锁,则会进入重量级锁模式。线程不再自旋等待而是释放CPU进入阻塞。

重量级锁

Synchronized是通过对象内部的监视器锁(Monitor)来实现的。其底层是通过操作系统的Mutex Lock来实现,成本非常之高;这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”
重量级锁获取示意
最后附上锁的升级过程(摘自open JDK文档)
锁之间的转换(openjdk)

2.2 j.u.c Lock API

j.u.c的Lock api与synchronized方式比较:

  1. synchronized是java底层就支持的,j.u.c Lock api,由具体的JDK实现。
  2. synchronized不需要用户去手动释放锁,当synchronized块执行完成后,jvm会自动让线程释放对象锁;而j.u.c Lock api则必须由用户手动释放锁(出现死锁的几率更高)。

2.2.1 Lock、ReadWriteLock

Lock接口定义了锁相关的操作,它定义了获取和释放锁相关方法:

  • void lock();:当前线程以阻塞方式尝试获取锁,如果锁被占用线程会被阻塞直到成功获取锁
  • void lockInterruptibly() throws InterruptedException;:可中断获取锁,与lock()不同在于该方法会响应中断,即在锁的获取过程中可以中断当前线程
  • boolean tryLock();:尝试非阻塞的获取锁,调用该方法立即返回,true表示获取到锁
  • boolean tryLock(long time, TimeUnit unit) throws InterruptedException;: 超时获取锁,以下情况会返回:时间内获取到了锁,时间内被中断,时间到了没有获取到锁
  • void unlock();: 释放锁

ReadWriteLock是读写锁的顶层接口,读写锁维护了一对相关联的Lock,定义了返回读锁(共享锁)和写锁(独占锁)的方法:

  • Lock readLock():返回读锁(共享锁)。
  • Lock writeLock():返回写锁(独占锁)。

接下来看下Lock接口和ReadWriteLock的具体实现类。

2.2.1.1 ReentrantLock 可重入锁

ReentrantLock是支持重进入的锁即表示该锁能够支持一个线程对资源的重复加锁。该锁还支持获取锁时的公平与非公平的选择。

public class LockDemo{
	//创建锁,lock对象的位置不同,锁影响的范围不同
	private Lock lock = new ReentrantLock();
	
	public static void main(String args[]){
	   final Demo demo = new LockDemo();
	   new Thread(public void run(){
	   	 	demo.test();
	   }).start();
	  
	   new Thread(public void run(){
	   	 	demo.test();
	   }).start();
	}
	public void test(){
		//考虑在此声明lock对象时,与成员变量的不同。
		//Lock lock = new ReentrantLock();
		try{
			lock.lock();
			System.out.println("获取到锁"+lock.getHoldCount());
			lock.lock() //可重入
			System.out.println("获取到锁"+lock.getHoldCount());
		}finally{
		    //在finally块释放锁,避免因未释放导致死锁
		    System.out.println(Thread.currentThread().getName()+":释放锁");
			lock.unlock();
			//锁定几次,就必须释放几次,否则锁不会释放掉。
			lock.unlock();
		}
	}
}
2.2.1.1 ReentrantReadWriteLock 读写锁

ReentrantReadWriteLockReadWriteLock的实现类,其内部维护一对关联的读写锁,一个用于只读操作,可以由多个读线程同时持有;一个用于写入操作,同时只能有1个线程操作。读写锁适合读取线程比写入线程多的场景,改进互斥锁的性能,例如:缓存组件、集合的并发线程安全性改造。

Demo1:线程安全的Cache
public class ConcurrentCache {
    private static Map<String,Object> cache = new HashMap<>();
    private static ReentrantReadWriteLock rwi = new ReentrantReadWriteLock();

    //读取时,多个线程共享
    public static Object get(String key){
        rwi.readLock().lock();
        try {
            return cache.containsKey(key) ? cache.get(key) : null;
        }finally {
            rwi.readLock().unlock();
        }
    }

	//写入时,单线程独占
    public static void put(String key,Object obj){
        rwi.writeLock().lock();
        try {
            cache.put(key,obj);
        }finally {
            rwi.writeLock().unlock();
        }
    }
}
Demo2:锁降级-缓存未命中时从DB读取
public class DBCache {
    public static Map<String,Object> cache = new HashMap<>();
    public static ReentrantReadWriteLock rwi = new ReentrantReadWriteLock();

    //先从缓存读取,读取不到时,从数据库读取并更新缓存。
    public static Object get(String key){
        rwi.readLock().lock();
        try {
            if(!cache.containsKey(key)){
                //缓存未命中,释放读锁,获取写锁后从数据库加载数据。
                rwi.readLock().unlock();
                rwi.writeLock().lock();
                //双重判断避免有其他线程先行写入
                if(cache.containsKey(key)){
                    Object obj = dbGet(key);
                    cache.put(key,obj);
                }
                //锁降级(持有写锁同时获取读锁,然后释放写锁)保证数据更新的可见。
                rwi.readLock().lock();
                rwi.writeLock().unlock();
   			}
   			return cache.get(key);
        }finally {
            rwi.readLock().unlock();
        }
    }
    //模拟从数据库读取
    public static Object dbGet(String key){
        //从数据库读取
        return "";
    }
}

通过源码发现,不管是ReentrantLock还是ReentrantReadWriteLock其内部其实都是借助组合一个私有的内部类Sync来实现最终的lock或者unlock操作。而Sync则是继承自一个叫做AbstractQueuedSynchronizer的东东。
这个就是Doug Lea大师创作的用来构建锁或者其他同步组件的基础框架类,简称AQS。除了ReentrantLockReentrantReadWriteLock,j.u.c中还有很多其他的并发工具类的实现都依赖于AQS,包括CountDownLatch计数器、CyclicBarrier线程屏障、Semaphore信号量等。


后续通过分析AQS的源码和实现思路来理解j.u.c的内部原理。附上Doug Lea大师关于AQS的相关论文。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值