1. Synchronized的作用
-
synchronized是Java提供的一个并发控制的关键字,主要有两种用法,分别是同步方法块和同步代码块;
-
synchronized关键字可以给类或者对象进行加锁操作,保证共享资源在同一时间只会被一个线程访问到,这里要注意,对象锁针对的目标是实例化的对象,可以通过创建多个实例而实现多把锁,而类锁针对的目标是类文件,即Class本身,只存在唯一一个类锁;
-
synchronized保证原子性,在同步代码块或者同步代码方法中,即在锁未释放前,共享资源无法被其他线程访问到,通过这种方式实现了synchronized代码的原子性;
-
synchronized保证可见性,Java内存模型中规定了所有的变量都存储在主存,而每个线程又拥有自己的工作内存,线程对变量的所有操作都是基于工作内存的,而不能直接操作主存,synchronized保证,在解锁之前,必须把工作内存中的内存同步回主存中,通过这种方式保证了可见性;
-
synchronized保证有序性,这里的有序性并不是指synchronized杜绝了指令重排的情况,而是说目标代码在单线程的情况下不管如何指令重排,结果都是一致的,而synchronized实现了加锁条件下其他线程的不可访问,即实现了单线程的环境,在这种情况下synchronized代码是有序的。
2. Synchronized和Lock的区别
-
synchronized是一个关键字,而Lock是一个接口;
-
synchronized可以自动获取锁自动释放锁,即使线程执行捕获异常也会正常释放,而Lock需要自己加锁自己解锁,如果遇到异常的时候可能会造成死锁问题,所以需要在finally代码块中释放锁保证一定释放;
-
synchronized获取锁的过程是阻塞式的且锁的状态无法确定,而Lock则有多种方式获取锁,同时锁的状态是可以确定的;对于是否可判定锁的状态,我是这么理解的,synchronized在获得锁的时候,如果获取不到就会被阻塞,无法判断锁目前的状态;而Lock是依托AcstractQueueSynchronizer实现的,提供了tryAcquire()、tryAcquireShared()之类的模板方法,返回值是boolean类型的,可以依据返回类型判断锁当前的状态;(也可能是因为synchronized是依托JVM实现的而Lock是依托JDK实现的)
-
synchronized是可重入的,不可中断的非公平锁,Lock是可重入的,可判断且可中断的,默认非公平锁;
-
在资源竞争激烈的情况下,使用ReentrantLock的性能优于synchronized,反过来在资源竞争不是那么激烈的情况下,synchronized的性能则要更好一些。synchronized适合少量同步代码,而Lock适合大量同步代码;
-
synchronized底层维护两个指令,加锁指令给锁计数器+1,解锁指令给锁计数器-1,通过0/1判断当前锁的状态;Lock底层是CAS(Compare And Swap)乐观锁,依赖AbstractQueueSynchronizer,把所有请求线程构成一个FIFO双向队列,对该队列进行操作。
3. Synchronized的实现原理
Java虚拟机中的同步是基于进入和退出管程(monitor)对象来实现的,因为每个对象都持有唯一的一把锁,所以每个对象也都绑定唯一的monitor对象,在Java虚拟机中,monitor是由ObjectMonitor来实现的。
ObjectMonitor实现了Java多线程之间的同步。事实上在ObjectMonitor中有两个队列,_WaitSet和_EntryList,用来保存ObjectMonitor对象列表(封装过的线程对象),而Owner中的ObjectMonitor包装了持有该对象锁的线程。
当多线程访问时,会优先进入_EntryList,然后有一个线程获取到目标对象的锁,即_Owner指向该线程同时计数器count(应该是ObjectMonitor中的一个字段,用于协助实现重入)置为1,每次重入计数器都会加1;如果调用wait()方法,当前线程会释放锁,_Owner指向空,计数器归0,同时进入_WaitSet等待唤醒。
4. 从字节码的角度理解Synchronized
synchronized关键字可以修饰方法和代码块,而且在不同的修饰中起到了不同的作用:
- 在同步代码块中,编译器识别到synchronized会在相应的位置上将其编译成字节码指令
monitorenter,同时在同步代码块结束处添加字节码指令monitorexit; - 同步方法并不是通过monitorenter和monitorexit来实现同步的,在同步方法中,会在方法的
flags字段中设置ACC_SYNCHRONIZED访问标志,执行方法时,如果发现有该标识,会先去获得目标对象的锁,再去执行方法中的代码体;
5. Synchronized的优化
synchronized是重量级锁,效率低下,因为监视器锁是依赖于操作系统底层的Mutex Lock(互斥锁)来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此在JDK1.6之后引入了锁升级。
Java锁的相关信息是存放在对象的对象头中的,Java对象在内存中由以下三部分组成:对象头、实例数据、对齐填充字节(满足8的倍数),而对象头又是由MarkWord、Klass指针、数组长度(如果有的话)组成,其中的MarkWord存放了Java对象锁的相关信息,在32位JVM中MarkWord结构如下:
锁升级的的过程为:无锁->偏向锁->轻量级锁->重量级锁。
-
无锁
JVM启动的前4000ms创建的对象,处于无锁状态,锁标志位为01,是否偏向锁为0,此时如果出现线程竞争对象锁的情况,会直接进入轻量级锁的状态;
在4000ms之后创建的对象则是处于匿名偏向状态(是否偏向锁为1,线程ID为0);
-
偏向锁
设计者认为,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得,因此为了减少同一线程获取锁的代价而引入了偏向锁。
偏向锁的工作逻辑是:
-
如果一个线程在匿名偏向状态下CAS获得了锁,那么锁就进入偏向模式,更改MarkWord中的相关信息,当线程再次请求对象锁的时候,会对照线程ID进行比较,如果ID一致的话,该线程进入同步块时,不需要CAS进行加锁,只会往当前线程的栈中添加一条Displaced Mark Word为空的锁记录(用来计算重入次数),退出同步块释放偏向锁时,则依次删除对应锁记录,但是不会修改对象头中的线程ID;
-
如果不一致,则会进行偏向锁撤销,这也是偏向锁的特点,一旦出现线程竞争,就会撤销偏向锁。偏向锁的撤销需要等待全局安全点(safe point,代表了一个状态,在该状态下所有线程都是暂停的),遍历当前JVM的所有线程,查看偏向锁持有线程的状态,如果该线程仍然存活且在执行同步代码块,那么升级为轻量级锁,CAS竞争锁;
-
如果偏向锁持有线程已死亡或者并没有执行同步代码块,检验是否允许重偏向,如果允许则偏向锁退回到匿名偏向状态,如果不允许重偏向,则撤销偏向锁,更新为无锁状态,然后升级为轻量级锁,进行CAS竞争锁;
-
-
轻量级锁
升级为轻量级锁后,锁标志位为00,同时Mark Word指向锁持有线程的栈帧中的锁记录,相反,轻量级锁的锁记录不同于偏向锁,需要向锁记录中填充数据,一个锁记录中的Displaced Mark Word设置为无锁状态下的Mark Word,另外一个是Owner字段指向锁的对象,即轻量级锁中,锁记录和目标对象的Mark Word互相指向(在原线程持有锁的情况下升级为轻量级锁则将空锁记录填充内容即可,如果是重新CAS竞争获得锁则是新建);轻量级锁重入会新增将Displaced Mark Word为null的锁记录,记录重入次数。
轻量级锁在释放的时候,即当线程栈帧只剩下最后一个锁记录时,将Displaced Mark Word设置为对象的Mark Word,即对象进入无锁状态,等待下一次CAS获得锁更改为轻量级锁状态。
轻量级锁在多线程竞争锁资源时会通过自旋的方式避免阻塞线程,但自旋的次数是有限的,一旦达到极限还无法获得锁,那么会触发锁升级成为重量级锁。
-
默认情况下,自旋锁的自旋次数为10次,如果自旋10次后仍然无法获得锁则升级为重量级锁;
-
自适应自旋锁,线程空循环等待的自旋次数并非是固定的,而是会动态着根据实际情况来改变自旋等待的次数。
假如线程1刚刚成功获得一个锁,当它把锁释放了之后,线程2获得该锁,并且线程2在运行的过程中,此时线程1又想来获得该锁了,但线程2还没有释放该锁,所以线程1只能自旋等待,但是虚拟机认为,由于线程1刚刚获得过该锁,那么虚拟机觉得线程1这次自旋也是很有可能能够再次成功获得该锁的,所以会延长线程1自旋的次数。
另外,如果对于某一个锁,一个线程自旋之后,很少成功获得该锁,那么以后这个线程要获取该锁时,是有可能直接忽略掉自旋过程,直接升级为重量级锁的,以免空循环等待浪费资源。
-
-
重量级锁
重量级锁是依赖对象内部的monitor来实现的,也就是前面提到的MonitorObject。