synchronized 所添加的锁有以下几个特点。
互斥性:
同一时间点,只有一个线程可以获得锁,获得锁的线程才可以处理被 synchronized 修饰的代码片段。
阻塞性:
只有获得锁的线程才可以执行被 synchronized 修饰的代码片段,未获得锁的线程只能阻塞,等待锁释放。
可重入性:
如果一个线程已经获得锁,在锁未释放之前,再次请求锁的时候,是必然可以获得锁的。
synchronized 主要可以用来修饰方法和代码块。根据其锁定的对象不同,可以用来定义同步方法和同步代码块。
synchronized分为类锁和对象锁,其中类锁也是通过对象锁实现的,因为Java中万物皆对象。无论是同步方法还是同步代码块,其实现都是依赖对象的监视器。
方法级的同步是隐式的(同步方法)。同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并目方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。
同步代码块使用 monitorenter和monitorexit两个指令实现。可以把执行monitorenter指令理解为加锁,执行 monitorexit 理解为释放锁。每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行monitorenter)后,该计数器自增变为1,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行 monitorexit 指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。
Monitor
为了解决线程安全的问题,Java 提供了同步机制、互斥锁机制,这个机制保证了在同一时刻只有一个线程能访问共享资源。
这个机制的保障来源于监视锁 Monitor,每个对象都拥有自己的监视锁 Monitor。当我们尝试获得对象的锁的时候,其实是对该对象拥有的 Monitor 进行操作。
我们知道了synchronized 对某个对象进行加锁的时候,会调用该对象拥有的objectMonitor 的enter方法,解锁的时候会调用exit 方法。
在 JDK1.6 之前,synchronized的实现才会直接调用 ObjectMonitor 的enter和exit,这种锁被称之为重量级锁。为什么说这种方式操作锁很重呢?
Java 的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到核心态,因此状态转换需要花费很多的处理器时间,对于代码简单的同步块(如被 Synchronized 修饰的get 或 set 方法)状态转换消耗的时间有可能比用户代码执行的时间还要长,所以说synchronized 是java 语言中一个重量级的操作。
所以,在JDK1.6 中出现对锁进行了很多的优化,进而出现轻量级锁,偏向锁,锁消除,适应性自旋锁,锁粗化(自旋锁在1.4就有只不过默认的是关闭的,JDK1.6是默认开启的),这些操作都是为了在线程之间更高效的共享数据,解决竟争问题。
synchronized的锁升级过程是怎样的:
无锁
当一个线程第一次访问一个对象的同步块时,JVM会在对象头中设置该线程的Thread ID,并将对象头的状态位设置为“偏向锁”。这个过程称为“偏向”,表示对象当前偏向于第一个访问它的线程。
偏向锁(Biased Locking)
触发条件:首次进入synchronized块时自动开启,假设JVM启动参数没有禁用偏
向锁
在偏向锁模式下,锁会偏向于第一个获取它的线程,JVM会在对象头中记录该线程的ID作为偏向锁的持有者,并将对象头中的 Mark Word 中的一部分作为偏向锁标识。
在这种情况下,如果其他线程访问该对象,会先检查该对象的偏向锁标识,如果和自己的线程 ID 相同,则直接获取锁。如果不同,则该对象的锁状态就会升级到轻量级锁状态
轻量级锁:
触发条件:当有另一个线程尝试获取已被偏向的锁时,偏向锁会被撤销,锁会升级为轻量级锁。
将对象头中的Mark Word复制到线程栈中的锁记录空间。(保留对象的原始信息,因为锁的获取和释放是成对出现的,所以在释放锁时,JVM需要使用这份复制的原始Mark Word来恢复对象头,确保对象状态的正确性)。
轻量级锁会采用自旋锁的方式去频繁的以CAS的形式获取锁资源(采用的是自适应自旋锁)如果成功获取到,拿着锁资源走,如果自旋了一定次数,没拿到锁资源,锁升级。
重量级锁(Heavyweight Locking)
触发条件:当轻量级锁的CAS操作失败,即出现了实际的竞争,锁会进一步升级为重量级锁。
当锁状态升级到重量级锁状态时,JVM会将该对象的锁变成一个重量级锁,并在对象头中记录指向等待队列的指针。
此时,如果一个线程想要获取该对象的锁,则需要先进入等待队列,等待该锁被释放。当锁被释放时,JVM会从等待队列中选择一个线程唤醒,并将该线程的状态设置为“就绪”状态,然后等待该线程重新获取该对象的锁。
synchronized 的锁能降级吗?
如果是指锁从重量级回退到轻量级,当前的HopSpot虚拟机实现是不支持的。(JRocket是支持的)
锁一旦被升级为重量级锁,就会保持在这个状态直到完全释放。
但是有一种特殊情况的"降级”,就是重量级锁的Monitor对象在不再被任何线程持有时,被清理和回收的过程。这一过程确实可以在Stop-the-World(STW)暂停期间进行,这时所有Java线程都停在安全点(SafePoint)。这个过程会做以下事情:
1.锁状态检查:在STW停顿期间,JVM会检查所有的Monitor对象。
2.确定降级对象:JVM识别出那些没有被任何线程持有的Monitor对象。这通常是通过检查Monitor对象的锁计数器或者所有权信息来实现的。
3."降级"操作:对于那些确定未被使用的Monitor对象,JVM会进行所谓的“deflation”操作,即清理这些对象的状态,使其不再占用系统资源。在某些情况下,这可能涉及到重置Monitor状态,释放与其相关的系统资源等。
synchronized升级过程中有几次自旋?
- 第一次自旋:在尝试获取轻量级锁失败后,线程会进行自旋,使用CAS操作去尝试获取锁。这里的自旋并没有使用while循环,而是使用了C++的inline函数,如ObjectSynchronizer::FastLock()。在JDK 8中,轻量级锁的自旋默认是开启的,最多自旋15次,每次自旋的时间逐渐延长。如果15次自旋后仍然没有获取到锁,就会升级为重量级锁。
- 第二次自旋:在尝试获取重量级锁失败后,线程会进行自旋,等待拥有锁的线程释放锁。这里的自旋同样使用了C++的inline函数,如ObjectSynchronizer::FastUnlock()。
- 自适应自旋(JDK6之后引入的):在尝试获取轻量级锁时,线程会进行自旋,等待拥有锁的线程释放锁。但这里的自旋不是固定次数的,而是根据前一次自旋的时间和成功获取锁的概率进行自适应调整。这里的自旋实现在C++的Thread.inline.hpp中,如Thread::SpinPause()。
synchronized的锁优化是怎样的?
自旋锁
不放弃处理器的执行时间,时刻检查共享资源是否可以被访问
锁消除
在使用synchronized时,如果JIT(编译器)经过逃逸分析(判断同步块所使用的锁对象是否只能被一个线程访问而没有被发布到其他线程)之后发现并无线程安全问题,就会做锁消除。
锁粗化
当JIT发现一系列连续的操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中的时候,会将加锁同步的范围扩散(粗化)到整个操作序列的外部。
synchronized和reentrantLock区别?
二者相同点是,都是可重入锁。二者也有很多不同,如:
synchronized是Java内置特性,而ReentrantLock是通过Java代码实现的。synchronized是可以自动获取/释放锁的,但是ReentrantLock需要手动获取/释放锁。
ReentrantLock还具有响应中断、超时等待等特性。
ReentrantLock可以实现公平锁和非公平锁,而synchronized只是非公平锁。
公平锁和非公平锁:
公平锁:保证了等待获取锁的线程按照请求锁的顺序来获取锁。也就是说,先请求锁的线程会先获得锁。所有的线程都能得到资源,不会饿死在队列中。但是队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:不保证等待获取锁的线程的执行顺序。这意味着即使某个线程最早请求锁,也可能会在其他后来请求锁的线程之后获得锁。非公平锁可能会导致“饥饿”问题,但通常具有更高的吞吐量
ReentrantLock用法:
怎么创建公平锁?
new ReentrantLock() 默认创建的为非公平锁,如果要创建公平锁可以使用new ReentrantLock(true)。
lock()和lockInterruptibly()的区别:
lock() 和 lockInterruptibly() 的区别在于获取锁的途中如果所在的线程中断,lock()会忽略异常继续等待获取锁,而lockInterruptibly()则会抛出InterruptedException 异常:
tryLock():
tryLock(5,TimeUnit.SECONDS) 表示获取锁的最大等待时间为 5 秒,期间会一直尝试获取,而不是等待5 秒之后再去获取锁。
ReentrantLock 如何实现可重入的:
可重入锁指的是同一个线程中可以多次获取同一把锁。比如在JAVA中,当一个线程调用一个对象的加锁的方法后,还可以调用其他加同一把锁的方法,这就是可重入锁。
ReentrantLock 加锁的时候,看下当前持有锁的线程和当前请求的线程是否是同一个,一样就可重入了。只需要简单得将state值加1,记录当前线程的重入次数即可。
同时,在锁进行释放的时候,需要确保state=0的时候才能执行释放资源的动作也就是说,一个可重入锁,重入了多少次,就得解锁多少次。