使用
锁定的是对象
1 | public class T { |
1.当一个线程想要去执行这段代码,必须要获得o的锁,当o被其他线程占用时,该线程必须要等其他线程释放o的锁,再去获得o的锁,才能执行。
2.synchronized关键字锁定的是对象不是代码块,demo中锁的是object对象的实例
3.可能锁对象包括: this, 临界资源对象,Class 类对象。
4.关于线程安全:加synchronized关键字之后不一定能实现线程安全,具体还要看锁定的对象是否唯一。
5.synchronized关键字修饰普通方法等同于synchronized(this)
静态方法上锁
1 | /* |
给静态方法上锁,锁定的是类对象,类的.class文件是唯一的,所以说synchronize修饰静态方法或者锁定的对象是类的.class文件的时候在多线程中是可以实现线程安全的.。
需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
同步和非同步方法同时调用
1 | public class T { |
线程t1首先获得了当前对象t的锁,并执行m1。因为m2非同步的,不需要获得锁就可以执行,所以t2不需要获得锁就可以直接执行m2.只有执行synchronized方法才需要申请那把锁。
可重入锁
1 | public class Test1 { |
所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的,synchronized和ReentrantLock都是可重入锁。可重入锁的意义在于防止死锁。实现原理实现是通过为每个锁关联一个请求计数和一个占有它的线程。当计数为0时,认为锁是未被占有的。线程请求一个未被占有的锁时,jvm讲记录锁的占有者,并且讲请求计数器置为1 。如果同一个线程再次请求这个锁,计数将递增;每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。可重入锁锁定的必须得是同一个对象(或者是父类子类对象)。
不要以字符串常量作为锁的对象。
因为锁定的是对象。比如说你用到了一个类库,里边锁定了一个”Hello”,而你在你的代码中也锁定了”Hello”,实际上这锁定的是是同一个对象,容易发生死锁。
原子类
1 | public class Test_11 { |
AtoXXX本身的方法是具有原子性的,但是他比synchronized效率要高。
底层实现
Java对象头
synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成 。Class Metadata Address存储的是该对象属于类的地址,即可以判断这个对象属于哪一个类。
MarkWord有五种类型:

MarkWord:

重量级锁(sychronized):
锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联。只有获取到对象的monitor的线程,才可以执行方法或代码块,其他获取失败的线程会被阻塞,并放入同步队列中,进入BLOCKED状态。
Monitor
当我们使用synchronized修饰方法名时,编译后会在方法名上生成一个ACC_SYNCHRONIZED标识来实现同步;当使用synchronized修饰代码块时,编译后会在代码块的前后生成monitorenter和monitorexit字节码来实现同步。
无论使用哪种方式实现,本质上都是对指定对象相关联的monitor的获取,只有获取到对象的monitor的线程,才可以执行方法或代码块,其他获取失败的线程会被阻塞,并放入同步队列中,进入BLOCKED状态。
为了解决线程安全的问题,Java提供了同步机制、互斥锁机制,这个机制保证了在同一问题内只有一个线程能访问共享资源。这个机制的保障来源于监视锁Monitor,每个对象都拥有自己的监视锁Monitor。
Monitor的实现
数据结构:
1 | ObjectMonitor() { |
关键属性:
_owner:指向持有ObjectMonitor对象的线程
_WaitSet:存放处于wait状态的线程队列
_EntryList:存放处于等待锁block状态的线程队列
_recursions:锁的重入次数
_count:用来记录该线程获取锁的次数、
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。
若执行线程调用 notify/notifyAll 方法,WaitSet 中的线程被唤醒,进入EntryList 中阻塞,等 待获取锁标记。若执行线程的同步代码执行结束,同样会释放锁标记,monitor 中的_Owner 标记赋值为 null,且计数器赋值为 0 计算。

等待唤醒机制与synchronized
所谓等待唤醒机制本篇主要指的是notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或synchronized方法中,否则就会抛出IllegalMonitorStateException异常。
这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor ,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。
锁优化
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。但是不可以降级。
重量级锁
sychronized就是重量级锁。
Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。JDK为了sychronized的优化,引入了轻量级锁和偏向锁。
一个依据:“对于绝大部分的锁,在整个同步周期内都是不存在竞争的。”
这是轻量级锁和偏向锁的依据。
偏向锁
在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。
偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。
所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态偏向锁可以提高有同步但竞争比较少的程序性能。
轻量级锁
轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。
当尝试获取一个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record,然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。
如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。
总结
重量级锁通过Monitor来实现,状态转换效率低。
轻量级锁基于CAS来实现。
偏向锁不需要同步,要是同一个线程申请锁。

乐观锁与悲观锁
- synchronized是悲观锁,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
- CAS操作的就是乐观锁,总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。
CAS
Atomic底层的实现就是CAS。
CAS是一个原子操作。
CAS机制当中使用了3个基本操作数:内存地址V,旧的值A,要修改的新值B。
更新一个变量的时候,只有当变量旧的值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
这样说或许有些抽象,我们来看一个例子:
1.在内存地址V当中,存储着值为10的变量。

2.此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。

3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。

4.线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。

5.线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。

6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。

7.线程1进行SWAP,把地址V的值替换为B,也就是12。

当多个线程同时使用CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会挂起,仅是被告知失败,并且允许再次尝试,当然也允许实现的线程放弃操作。基于这样的原理,CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰。
Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。
CAS缺点:
CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。ABA问题。
假设一个变量 A ,修改为 B之后又修改为 A,CAS 的机制是无法察觉的,但实际上已经被修改过了。如果在基本类型上是没有问题的,但是如果是引用类型呢?这个对象中有多个变量,我怎么知道有没有被改过?加个版本号啊。每次修改就检查版本号,如果版本号变了,说明改过,就算你还是 A,也不行。
AtomicReference就是这样做的。
锁消除
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除.
主要通过逃逸分析来判定。
何为逃逸?
当一个对象在方法中被定义后,如果被外部方法所引用,甚至可能会被外部线程所访问到,称为线程逃逸。
如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除。
因为代码中会有许多隐形的锁,比如String。
锁粗化
如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。
例如在一个for循环里枷锁,就可以把锁提到外面。
自旋锁(空转打圈儿)
适用于共享数据只会锁定很短的一段时间。
当获取锁的过程中,未获取到。为了提高效率,JVM 自动执行若干次空循环(while循环中啥也不做),再次申请 锁,而不是进入阻塞状态的情况。称为自旋锁。自旋锁提高效率就是避免线程状态的变更。避免线程挂起导致的花费。
互斥同步对性能影响最大的是阻塞,即线程的挂起和恢复。许多应用中,共享数据的锁定状态只会持续很短的一段时间。如果有两个以上的处理器,能让两个或者以上的线程并行执行,我们就可以让后面请求锁的线程等待一下,但是并不放弃处理器的执行时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。
自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
自适应的自旋锁:
自适应的自旋锁意味着自旋的时间不在固定了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得锁,并且持有者的线程正在运行中,那么虚拟机认为这次自旋也很有可能再次成功,因此会自旋等待较长的时间。相反的是,假如对于某个锁,自旋等待很少成功,那么以后获取这个锁的时候即有可能省略掉这个过程。
一般自旋锁可以搭配CAS来使用。