Synchronized的理解
前言
重新看了Synchronized发现之前的理解有不够全面,今天重新梳理下。
Synchronize是我们实现线程互斥同步的常用手法,Synchronize本身是一个悲观机制的独占锁,并且可重入、非公平。对这些锁的分类不明白的可以看锁的分类
Synchronized使用
-
synchronized(.class)只要是访问这个类的方法,就会同步,不管用这个类创建了几个对象,一般单列模式常用
-
synchronized(Object x),通过对象同步,注意必须是同一个对象
一般在多线程中访问同一个对象时,在run方法中用到 -
synchronized(this) 指的是对象本身同步,一般在定义对象的方法时可以用,当只有访问同一对象,才会同步,和synchronized(Object x)功能类似
-
synchronized方法与synchronized代码快的区别 synchronized methods(){} 与synchronized(this){}之间没有什么区别,只是synchronized methods(){} 便于阅读理解,而synchronized(this){}可以更精确的控制冲突限制访问区域,有时候表现更高效率
下面代码结构主要如下
public synchronized void eat()
public synchronized void run()
//同步代码块,同步对象this
public void sleeps() {
synchronized (this)}
//同步代码块,同步对象Animal.class
public static void readBook() {
synchronized (Animal.class) {}
public class Animal {
//同步方法
public synchronized void eat() {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName() + "eat:" + i);
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
//同步方法
public synchronized void run() {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName() + "run:" + i);
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
//同步代码块,同步对象this
public void sleeps() {
synchronized (this) {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName() + "sleeps:" + i);
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
//同步代码块,同步对象Animal.class
public static void readBook() {
synchronized (Animal.class) {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName() + "readBook:" + i);
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public void shout() {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName() + "shout:" + i);
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
Synchronized修饰方法和代码块
使用Synchronized修饰方法
和代码块
,当线程A访问“某对象”的Synchronized方法,那么其他线程访问“该对象”的该Synchronized方法或Synchronized代码块将被阻塞;并且访问该对象的其他Synchronized方法或Synchronized代码块也将阻塞;访问“该对象”的非同步代码块将被允许
public class SyncTest {
public static void main(String[] args) {
Animal animal = new Animal();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
animal.eat();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
animal.sleeps();
}
});
t.start();
t2.start();
}
}
打印如下:当线程t调用同步方法eat()时,另一个线程t2同步方法阻塞;当线程t调用同步方法animal.eat()方法时,另一个线程t2同步方法animal.sleeps()阻塞
Synchronized修饰方法
当线程A访问“某对象”的Synchronized方法,访问“该对象”的非同步代码块将被允许。
public class SyncTest {
public static void main(String[] args) {
Animal animal = new Animal();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
animal.eat();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
animal.shout();;
}
});
t.start();
t2.start();
}
}
打印如下:线程调用交替进行,当一个对象的同步方法被调用的时候,其他线程调用当前对象的其他非同步方法将不被阻塞
上述特点都是实例锁的相关特点,在Synchronized中如果将锁使用在非静态方法或类上那么这就是一个实例锁,如果将锁使用在静态方法上或者类上,那这就是全局锁
。
实例锁和全局锁
实例锁是一个对象锁,如果该对象是单例模式,那么其具有全局锁的效果
全局锁是锁在类上或者静态方法上,无论拥类有多少个对象,线程都共享该锁
网上看到一个很好解释实例锁和全局锁例子:
加入有Something类的两个实例a与b,那么下列组方法可以被1个以上线程同时访问呢
pulbic class Something(){
public synchronizedvoid isSyncA(){}
public synchronizedvoid isSyncB(){}
public static synchronized void cSyncA(){}
public static synchronized void cSyncB(){}
}
a. x.isSyncA()与x.isSyncB()
b. x.isSyncA()与y.isSyncA()
c. x.cSyncA()与y.cSyncB()
d. x.isSyncA()与Something.cSyncA()
a:都是对同一个实例的synchronized域访问,因此不能被同时访问
b:是针对不同实例的,因此可以同时被访问
c:因为是static synchronized,所以不同实例之间仍然会被限制,相当于Something.isSyncA()与 Something.isSyncB()了,因此不能被同时访问。 d: 答案是可以被同时访问的,答案理由是synchronzied的是实例方法与synchronzied的类方法由于锁定(lock)不同的原因。 个人分析也就是synchronized 与static synchronized 相当于两帮派,各自管各自,相互之间就无约束了,可以被同时访问
死锁
我们知道Synchronized可以修饰方法和代码块,在使用Synchronized修饰代码块
的时候得避免死锁的产生,死锁的产生主要是线程1获得了同步锁A,并且在A中要获取同步锁B,但在同步锁A的时候,有另一个线程2已经获取了同步锁B,并在B中获取同步锁A
, 那么线程A会等待线程B解锁,但线程B又等待线程A解锁,这时候就产生了死锁。Synchronized的阻塞过程不能手动结束
public class syncTest2 {
private static Object o1;
private static Object o2;
public static void main(String[] args) {
new syncTest2().deadLock();
}
public void deadLock() {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (o1) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println(Thread.currentThread().getName() );
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (o2) {
synchronized (o1) {
System.out.println(Thread.currentThread().getName() );
}
}
}
});
t1.start();
t2.start();
}
}
避免死锁:
1、使用Lock接口的tryLock(long timeout, TimeUnit unit)方法,设置超时时间,超时可以退出防止死锁
2、降低锁的使用粒度,只加锁共享变量,这样既增加效率,同时尽量避免死锁
3、减少或替代Synchronized的使用
Synchronized原理
理解Synchronized首先得理解两个东西
- monitor:管程,英文是 Monitor,也常被翻译为“监视器”
- Java对象头
monitor
在JVM里,monitor就是实现lock的方式。
- 反编译同步代码块
在反编译同步代码块代码,发现里面有monitorenter 和 monitorexit 指令;monitorenter就是获得某个对象的lock(owner是当前线程)monitorexit就是释放某个对象的lock,多了一个monitorexit指令用于执行异常
同步方法 并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的 - 反编译同步方法
在同步方法反编译代码中synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法
monitor 的重要特点是,同一个时刻,只有一个 进程/线程 能进入 monitor 中定义的临界区,这使得 monitor 能够达到互斥的效果。
为了做到能够阻塞无法进入临界区的 进程/线程,还需要一个 monitor object 来协助,这个 monitor object 内部会有相应的数据结构,例如列表,来保存被阻塞的线程。
不过,由于 monitor object 内部采用了数据结构来保存被阻塞的队列
,因此它也必须对外提供两个 API 来让线程进入阻塞状态以及之后被唤醒,分别是 wait 和 notify。
monitor 中维持着一个列表存放着当前访问该同步方法的所有线程。这个列表就是ObjectMonitor 。ObjectMonitor 里面存放的是
ObjectWaiter(等待锁的线程封装成的ObjectWaiter对象),而monitor 就存放在 monitor object(对象锁) 的对象头中。对象锁的指针指向monitor
monitor object
synchronized 关键字在使用的时候,往往需要指定一个对象与之关联,例如 synchronized(this),或者 synchronized(ANOTHER_LOCK),synchronized 如果修饰的是实例方法,那么其关联的对象实际上是 this,如果修饰的是类方法,那么其关联的对象是 this.class。总之,synchronzied 需要关联一个对象,而这个对象就是 monitor object。
Java 语言中的 java.lang.Object 类,便是满足这个要求的对象,任何一个 Java 对象都可以作为 monitor 机制的 monitor object
对象头
Java 对象存储在内存中,分别分为三个部分,即对象头、实例数据和对齐填充。
-
对象头主要是由MarkWord和Klass Point(类型指针)组成,其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word在默认情况下存储着对象的HashCode、分代年龄
-
实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
-
填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。
而在其对象头中,保存了锁标识;同时,java.lang.Object 类定义了 wait(),notify(),notifyAll() 方法,这些方法的具体实现,依赖于一个叫 ObjectMonitor 模式的实现,这是 JVM 内部基于 C++ 实现的一套机制,基本原理如下所示:
ObjectWaiter 对象里存放thread(线程对象) 和 ParkEvent(线程的unpark), 每一个等待锁的线程都会有一个ObjectWaiter对象.而objectwaiter是个双向链表结构的对象。
ObjectMonitor 中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)
Synchronized锁的优化
Synchronized
在Java中Synchronized
是我们常用的同步互斥手段
,它是一个悲观锁
设计下的独占锁
,并且可重入
。Synchronized在获取到同步锁后,其他线程将会阻塞,对于阻塞或者唤醒一个线程都需要操作系统来完成,这就需要从用户态切换到核心态,这样的操作就需要消耗很多处理器时间,具有很强的性能损耗
,因此Synchronized在1.6中做了很多的优化,减少频繁的切换到核心态。
在1.6之中为了提高Synchronized的性能,增加了自适应自旋、锁消除、锁粗化、轻量级锁、偏向锁等来高效地共享数据,解决竞争问题
。
自旋锁
:在多线程操作中共享数据的锁定可能是和短暂的,避免线程在很短的时间内做用户态的切换,那么在允许多线程并行的基础上,让后面请求锁的线程“等待一下”,等待过程不放弃处理器的执行时间,为了让线程等待只需要让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
自适应自旋
是对自旋锁的优化,自旋锁的缺点就是如果锁占用的时间很长,那么就会浪费处理器资源,并且还对处理器有要求。加入自适应自旋就是自旋的时间不再固定,而是由前一个在同一个锁上的自旋时间以及锁的拥有者的状态决定,如果在同一个锁对象上,自旋等待刚刚成功获取过锁,并且持有锁的线程正在运行中,那么虚拟机会认为这次自旋很可能再次成功,应此可能允许等待时间更长;如果某一个锁上,很少自旋成功,那么将跳过自旋过程,避免资源浪费。虚拟机使用这一的策略和技术完成对Synchronized的一种优化
锁消除
是指在虚拟机及时编译在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除如:消除锁的判断主要来于逃逸分析的数据支持,如果判断在一段代码中,堆上的数据都不会逃逸出去从而被其他线程访问到,那么就可以把他们当做栈上数据对待,认为他们是线程私有的,同步加锁就自热无效
public String concatString(String s1,String s2,String s3){
StringBuffer sb=new SrringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
对于上述代码,StringBuffer.append()方法都具有一个同步块,锁就是对象sb,但sb变量永远不会被其他线程所访问,其一直动态作用域被限制在concatString方法中,其他线程并不能访问。因此此处的锁可以被安全的消除掉。
锁粗化
一段代码里面对同一个对象进行反复的加锁和解锁,那么也会带来性能的损耗,应该将锁同步范围扩展(粗化)到整个操作序列的外部。如上面代码就是扩展到第一个append()操作之前直至追后一个append()操作之后,这样就只需要加一次锁
轻量级锁
轻量级锁的作用是在没有多线程的前提下,减少重量级锁在使用操作系统互斥量产生的新能消耗
。在无竞争的情况下获取锁使用的是CAS
操作。CAS操作主要是用于更新对象头中thread ID, 要理解轻量级锁,得先了解HotPost虚拟机的对象头部分,对象头中一部分保存这GC年龄代,哈希码等信息,官方成为Mark Work,在32位的HotPost下,PostMark Work的32bit存储空间中,有25bit用于存储对象哈希码,4bit用于存储对象的分代年龄,2bit用于存储锁标志位,1bit固定为0;
存储内容 | 标志位 | 状态 |
---|---|---|
对象哈希码、对象分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
空、不需要记录信息 | 11 | GC标记 |
偏向锁ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
偏向锁
偏向锁
是在无竞争的情况下把整个同步都消除掉
,偏向锁的意思是同步锁会偏向第一次获得它的线程,如果在接下来的执行中,该锁没有其他线程获取,则持有偏向锁的线程将永远不需要再进行同步
。使用偏向锁需要虚拟机默认支持,如果在没有设置的情况下将不支持偏向锁。接下来用一张图讲解轻量级锁和偏向锁的转换关系
轻量级锁、偏向锁的状态准换
首先根据偏向锁可用
和不可用
分为左右两种逻辑
可偏向左边上面部分
:如果偏向锁可用,初始化的的对象标志位为01,那么这是一个未锁定、未偏向但是可偏向的对象
可偏向左边下面部分
:当第一次线程获取到锁的时候,那么虚拟机将头中的标志位设置为01
,同时使用CAS操作将线程的thread ID写入到Mark Work中
,成功后,持有偏向锁的线程再进入这个锁的相关同步块时,都不用做同步操作。
对象Mark Work中写入了thread ID状态下,对象可能处于锁定状态或未锁定状态。如果是在锁定
情况下,有另一个线程去获取这个锁时,偏向锁模式将结束,将回到轻量级锁定。
如果是在没有锁定
状态下,有另一个线程去获取这个锁时,偏向锁模式将结束,将回到未锁定、未偏向但是可偏向状态,如果撤销偏向,将回到未锁定、不可偏向对象
不可偏向右上部分
:如果偏向锁不可用,初始化的的对象标志位为01
,那么这是一个未锁定、不可偏向对象
不可偏向中间部分
:当有一个线程获取同步对象时,如果同步对象没有被锁定,那么在当前线程的栈帧中开辟一个叫“Lock Record”的空间保存一份同步对象的Mark Word部分的拷贝,并且使用CAS操作将这个对象的Mark Word更新指向栈帧的“Lock Record”
,如果成功,那么将标志位改为00
;如果更新失败,但当前线程已经拥有对象锁,那就直接运行同步块代码;否则说明有线程在争夺锁,那么轻量级锁就不再有效,要膨胀成重量级锁
不可偏向下面部分
:当同步对象已经是轻量级锁,但有另一个线程在竞争锁资源时,轻量级锁将膨胀成重量级锁,锁的标志位状态值变为“10”
,Mark Word中存储的指向重量级锁的指针,后面等待锁的线程也将进入阻塞状态。
Synchronized实现生产者消费者模式
//消费
public class ConsumptionFactory implements Runnable {
private Warehouse warehouse;
public ConsumptionFactory(Warehouse warehouse) {
this.warehouse = warehouse;
}
@Override
public void run() {
// TODO Auto-generated method stub
onConsumption();
}
private void onConsumption() {
while (true) {
synchronized (warehouse) {
// 如果数量够,就消费
if (warehouse.getCommodityCount() > 0) {
warehouse.delete();
System.out.println(Thread.currentThread().getName() + "====消费后剩余:" + warehouse.getCommodityCount());
warehouse.notifyAll();
} else {
try {
warehouse.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
}
//生产
public class ProductionFactiry implements Runnable {
private Warehouse warehouse;
public ProductionFactiry(Warehouse warehouse) {
this.warehouse = warehouse;
}
@Override
public void run() {
// TODO Auto-generated method stub
onProduction();
}
private void onProduction() {
while (true) {
synchronized (warehouse) {
warehouse.add();
System.out.println(Thread.currentThread().getName() + "---生产后剩余" + warehouse.getCommodityCount());
warehouse.notifyAll();
try {
warehouse.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
public class Warehouse {
private int commodityCount = 0;
public synchronized void add() {
commodityCount++;
}
public synchronized void delete() {
commodityCount--;
}
public synchronized int getCommodityCount() {
return commodityCount;
}
}
//生产消费
public class ThreadTest {
public static void main(String[] args) {
Warehouse warehouse = new Warehouse();
ConsumptionFactory consumptionFactory = new ConsumptionFactory(warehouse);
ProductionFactiry productionFactiry = new ProductionFactiry(warehouse);
Thread c1 = new Thread(consumptionFactory);
Thread c2 = new Thread(consumptionFactory);
Thread p1 = new Thread(productionFactiry);
Thread p2 = new Thread(productionFactiry);
c1.start();
c2.start();
p1.start();
p2.start();
}
}
Java – 偏向锁、轻量级锁、自旋锁、重量级锁
对象锁 全局锁
深入理解Java并发之synchronized实现原理
ObjectMonitor,ObjectWaiter 实现wait(),notify()
Monitor(管程)是什么意思?Java中Monitor(管程)的介绍