并发-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膨胀(重量级锁定)
空、不需要记录信息11GC标记
偏向锁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(管程)的介绍

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值