Synchronized
同步代码块
使用 monitorenter 和 moniterexit 指令实现, monitorenter指令插入到同步代码块的开始位置, moniterexit 指令插入到同步代码块的结束位置, jvm 需要保证每一个 monitorenter 都有一个 moniterexit 与之对应。
任何对象都有一个 monitor 与之相关联,当且一个 monitor 被持有之后,他将处于锁定状态。
当执行 monitorenter 指令时,当前线程将试图获取对象锁所对应的 monitor 的持有权,当对象锁的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。
如果当前线程已经拥有对象锁的 monitor 的持有权,那它可以重入这个 monitor,重入时计数器的值会加 1。
倘若其他线程已经拥有对象锁的 monitor 的所有权,那当前线程将被阻塞,直到正在执行的线程执行完毕,即 monitorexit 指令被执行,执行线程将释放 monitor 并设置计数器值为 0,其他线程将有机会持有 monitor。
为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器可处理所有的异常,它的目的就是用来执行 monitorexit 指令。
从字节码中也可以看出多了一个 monitorexit 指令。
同步方法
synchronized 方法会被翻译成普通的方法调用。在 JVM 字节码层面并没有任何特别的指令来实现被 synchronized 修饰的方法。
在 Class 文件的方法表中将该方法的 access_flags 字段中的 synchronized 标志位置 1,表示该方法是同步方法并使用调用该方法的对象(对象锁)或该方法所属的 Class(类锁) 做为锁对象。
Lock
ReentrantLock:
使用lockInterruptibly()获得锁,可以响应中断
使用tryLock()尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
tryLock(long,TimeUnit):与tryLock类似,只不过是有等待时间,在等待时间内获取到锁返回true,超时返回false。
类ReentrantLock实现等待/同步功能,需要借助于condition对象。
Condition类具有很好的灵活性,可以实现多路通知功能:在一个Lock对象中创建多个Condition(对象监视器)实例,线程对象可以注册在指定的Condition中,从而有选择性地进行线程通知,在调度线程上更灵活。
在使用notify()/notifyAll()方法进行通知时,被通知的线程是由JVM随机选择的,但使用ReentrantLock结合Condition类是可以实现前面介绍过的“选择性通知”。
volatile boolean isProcess = false; ReentrantLock lock = new ReentrantLock(); Condtion processReady = lock.newCondtion(); thread: run() { lock.lock(); isProcess = true; try { while(!isProcessReady) { //isProcessReady 是另外一个线程的控制变量 processReady.await();//释放了lock,在此等待signal }catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { lock.unlock(); isProcess = false; } } } }
ReentrantReadWriteLock:
在没有线程进行写操作时,多个进行读操作的线程都可以获取读锁,而进行写入操作的Thread只有在获取写锁后才能进行写入操作。
多个线程可以同时进行读操作,但同一时刻只允许一个Thread进行写入操作。
lock.readLock().lock():获得读锁
lock.writeLock().lock():获得写锁
公平锁与非公平锁:
公平锁:标识线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。
非公平锁(默认):一种线程抢占机制,是随机获取锁的,和公平锁不一样的就是先来的不一定先得到锁,这个方法可能会导致某些线程一直拿不到锁,自然不公平。
synchronized
是非公平锁,它无法保证等待的线程获取锁的顺序。
对于ReentrantLock
和ReentrantReadWriteLock
,默认情况下是非公平锁,但是可以设置为公平锁。
synchronized和lock的区别
Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
Lock可以有公平锁和非公平锁,默认非公平锁,synchronized只有非公平锁。
Lock可以知道有没有成功获取锁,而synchronized却无法办到。
Lock可以提高多个线程进行读操作的效率。(读写锁)
synchronized是悲观锁,Lock是乐观锁
synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。
独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。
而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
对于线程的阻塞或唤醒,需要用户态和核心态切换,状态切换需要很多处理器时间。
而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
乐观锁实现的机制就是CAS操作(Compare and Swap)。
JDK 1.6 后对 synchronized 的锁优化:
自旋锁与自适应自旋:线程挂起和恢复的操作都需要转入内核态中完成,这些操作给系统的并发性能带来了很大的压力,在许多应用中,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得,可以让后请求锁的线程等待一会儿,但不放弃处理器的执行时间,让线程执行一个忙循环(自旋)。
自旋锁默认的自旋次数值是10次,可以使用参数-XX:PreBlockSpin更改。
自适应自旋意味着自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
锁消除:虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持。
锁粗化:如果虚拟机探测到有一系列连续操作都对同一个对象反复加锁和解锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。
轻量级锁:使用对象头的Mark Word中锁标志位代替操作系统互斥量实现的锁。轻量级锁并不是用来代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
轻量级锁是在无竞争的情况下使用CAS(Compare-and-Swap)操作去消除同步使用的互斥量。
偏向锁:和轻量级锁原理基本一致,但偏向锁在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。
java中每个对象都可作为锁,锁有四种级别:
按照量级从轻到重分为:无锁、偏向锁、轻量级锁、重量级锁。
每个对象一开始都是无锁的,随着线程间争夺锁,越激烈,锁的级别越高,并且锁只能升级不能降级。
java对象头:
偏向锁:
大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
当一个线程访问同步块并获取锁时,会在对象头记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
如果测试成功,表示线程已经获得了锁,
如果测试失败,则需要再测试下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),
如果对象的偏向锁标志位为0(当前不是偏向锁),说明发生了竞争,已经膨胀为轻量级锁。这时使用CAS操作尝试获得锁。
如果为1,说明还是偏向锁不过请求的线程不是原来那个了。这时只需要使用CAS尝试把对象头偏向锁从原来那个线程指向目前求锁的线程。
轻量级锁:
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。
然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。