Synchronized 的特点
-
内存可见性: 当线程释放锁时,JMM(Java Memory Model) 会把该线程对应的本地内存中的共享变量刷新到主内存中;当线程获取锁时,JMM 会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量(这个过程实质上是线程 A 通过主内存向线程 B 发送消息)
-
操作原子性: JMM 提供保证了访问基本数据类型的原子性(在写一个工作内存变量到主内存主要分两步:store、write),但是实际业务处理场景往往是需要更大的范围的原子性保证,所以模型也提供了
synchronized块来保证 -
有序性: 这个概念是相对而言的,如果在本线程内,所有的操作都是有序的,如果在一个线程观察另一个线程,所有的操作都是无序的,前句是 "线程内表现为串行行为",后句是 "指令的重排序" 和 "工作内存和主内存同步延迟" 现象,JMM 提供了
volatile和synchronized来保证线程之间操作的有序性
Synchronized 底层原理
JVM 是基于进入和退出 Monitor(监视器锁) 对象来实现方法同步和代码块同步的
synchronized修饰的方法在字节码中添加了一个ACC_SYNCHRONIZED的flag,当线程执行到某个方法时,JVM 会去检查该方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了那线程会去获取这个对象所对应的monitor对象(每一个对象都有且仅有一个与之对应的monitor对象),获取成功后才执行方法体,方法执行完再释放monitor对象,在这一期间,任何其他线程都无法获得这个monitor对象- 同步代码块则是在同步代码块前插入
monitorenter,在同步代码块结束后插入monitorexit。根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁(monitor),如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加 1;相应地,在执行monitorexit指令时会将锁计数器减 1,当计数器被减到 0 时,锁就释放了。如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止
注意:
synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题- 同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入
Mutex Lock
监视器锁(Monitor)本质是依赖于底层操作系统的 Mutex Lock(互斥锁)来实现的。每个对象都对应于一个可称为 "互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象
互斥锁: 用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁;在完成了对共享资源的访问后,要对互斥量进行解锁
Mutex 的工作方式:
- 申请 Mutex
- 如果成功,则持有该 Mutex
- 如果失败,则进行
spin自旋。spin的过程就是在线等待Mutex,不断发起 Mutex gets,直到获得 Mutex 或者达到spin_count限制为止 - 依据工作模式的不同选择
yiled或者sleep - 若达到
sleep限制或者被主动唤醒或者完成yield, 则重复 1~4 步,直到获得为止
由于 Java 的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一条线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间。所以 synchronized 是 Java 语言中的一个重量级操作
JDK 1.6 改进: synchronized 与 java.util.concurrent 包中的 ReentrantLock 相比,由于 JDK1.6 中加入了针对锁的优化措施(引入 "偏向锁" 和 "轻量级锁"),使得 synchronized 与 ReentrantLock 的性能基本持平。ReentrantLock 只是提供了比 synchronized 更丰富的功能,而不一定有更优的性能,所以在 synchronized 能实现需求的情况下,优先考虑使用 synchronized 来进行同步
锁优化具体见 Java Synchronized(二)锁的优化
小结
- synchronized 的特点: 保证内存可见性、操作原子性、代码执行有序性
- synchronized 影响性能的原因:
- 加锁解锁操作需要额外操作
- 互斥同步对性能最大的影响是阻塞的实现,因为阻塞涉及到的线程挂起和恢复操作都需要转入内核态中完成(用户态与内核态的切换的性能代价是比较大的)
参考资料: