多线程(一)synchronized实现原理,volatile实现原理

一. synchronized实现
1.1 synchronized的作用

(1)确保线程互斥的访问同步代码
(2)保证共享变量的修改能够及时可见

原因:被synchronized修饰的代码,在开始执行的时候会加锁,为了保证可见性,有一条规则是这样的,
在对变量解锁的时候,必须先把变量同步回主内存中,保证了可见性。

(3)有效解决重排序问题

(注:synchronized是无法阻止指令重排的,但是由于加了synchronized之后,同一时间只能被
     同一线程访问,也就是单线程执行的,所以可以保证有序性)
1.2 Synchronized三种用法

(1)修饰普通方法
(2)修饰静态方法
(3)修饰代码块

  1. 修饰普通方法
    此作用域中的synchronized锁,可以防止多个线程同时访问这个对象的synchronized方法。
    但是不同对象的synchronized方法是不相干预的,也就是说另一个线程可以同时访问该类的另一个实例的synchronized方法。
public synchronized void method1(){
		System.out.println("method1 start");
		try {
			System.out.println("method1 execute");
			Thread.sleep(3000);
		} catch (Exception e) {
			e.printStackTrace();
		}
		System.out.println("method1 end");
	}
	
	public synchronized void method2(){
		System.out.println("method2 start");
		try {
			System.out.println("method2 execute");
			Thread.sleep(1000);
		} catch (Exception e) {
			e.printStackTrace();
		}
		System.out.println("method2 end");
	}
		
		public static void main(String[] args) {
		final SynchronizedTest test = new SynchronizedTest();
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				test.method1();;
			}
		}).start();
		
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				test.method2();
			}
		}).start();
		
	}
}
  1. 修饰静态方法
    对静态方法的同步本质上是对类的同步,即使是两个不同的实例,但它们都属于同一个类,所以即使调用不同的synchronized方法,也只能顺序的执行。
public static synchronized void method1(){
		System.out.println("method1 start");
		try {
			System.out.println("method1 execute");
			Thread.sleep(3000);
		} catch (Exception e) {
			e.printStackTrace();
		}
		System.out.println("method1 end");
	}

  1. 修饰代码块
public void method3(){
		System.out.println("method3 start");
		try {
			synchronized (this) {
				System.out.println("method3 execute");
				Thread.sleep(3000);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		System.out.println("method3 end");
	}
	
	public void method4(){
		System.out.println("method4 start");
		try {
			synchronized (this) {
				System.out.println("method4 execute");
				Thread.sleep(1000);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		System.out.println("method4 end");
	}
	
	public static void main(String[] args) {
		final SynchronizedTest test = new SynchronizedTest();
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				test.method3();;
			}
		}).start();
		
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				test.method4();
			}
		}).start();
		
	}

输出结果

method3 start
method3 execute
method4 start
method3 end
method4 execute
method4 end

虽然两个线程都正常的开始了,但是线程2在进入同步块之前,还是要等待线程1中同步块执行完毕。

1.3.Synchronized原理:
1.3.1 同步代码块

Synchronized底层是通过一个monitor对象实现的,每个对象有一个监视器锁monitor。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权。

1.如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2.如果线程已经占用该monitor,只是重新进入,则进入monitor的进入数加1
3.如果其他线程占用了该monitor,则线程进入阻塞状态,直到monitor的进入数变为0,
  再重新尝试获取monitor的所有权。
1.3.2 同步方法

Synchronized方法相对应普通方法,其常量池中多了ACC_SYNCHRONIZED标识符。JVM就是根据该标识符来实现方法的同步的,当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED方法是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完之后再释放monitor。在方法执行期间,其他线程都不能同时获取同一个monitor对象。

二.volatile实现原理

首先理解一下

2.1 并发编程中的三个概念:

1.原子性
即一个操作或者多个操作,要么全部被执行,要么就都不执行。

在单线程环境中,我们可以认为整个步骤都是原子性操作,但是在多线程环境下则不一样,Java只保证了基本数据类型的变量和赋值操作才是原子性的。
要想在多线程下保证原子性操作,可以通过锁来实现,volatile无法保证复合操作的原子性。

2.可见性
指当多线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值。

当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后,它会立即更新到主内存中,当其他线程读取共享变量时,会直接从内存中读取。

3.有序性
即程序执行的顺序按照代码的先后顺序执行。

在java内存模型中,为了效率是允许编译器和处理器对指令进行重排序的,当然重排序不会影响单线程的操作,但是对多线程有影响。volatile可以保证有序性。

2.2 volatile的可见性

volatile的第一条语义是要保证线程之间变量的可见性,要符合两个规则:

  • 线程对变量修改后要立即写回主内存
  • 线程对变量进行读取的时候,要从主内存中读取,而不是缓存

Java为了保证其平台性,使Java应用程序与操作系统内存模型分开,需要定义自己的内存模型。在Java内存模型中,内存分为主内存和工作内存两部分

其中主内存是所有线程共享的,而工作内存则是每个线程分配一份,各线程之间的内存独立,互不可见。在线程启动之后,虚拟机为每块内存分配一块工作内存,不仅包含线程内部定义的局部变量,也包含线程之间共享的变量的副本,即为了提高执行效率,读取副本比直接读取内存更快。工作内存和主内存之间的图入下:
在这里插入图片描述
对于普通共享变量来说,如果再工作内存中发生了变化,必须要写回工作内存。但对于volatile变量来说,要求工作内存中发生变化之后要马上写回内存,而读取的时候要从内存中读取,而不是读取本地工作内存中的副本。

2.3 volatile不能保证原子性

比如执行i++操作,线程A和线程B同时执行,i++分为 读取i的值,对i进行操作,把结果赋值给i三部分操作,比如线程在执行i+1操作,还没有进行赋值操作,此时线程B读取i的值,依然为初始值0,线程A执行完操作之后,把i赋值为1,而线程B也赋值为1,最终内存中i的值为1,而不是预期的2。

2.4 volatile可以禁止指令重排

指令重排 是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能的提高并行度。编译器,处理器也遵循这个目标,单线程指令重排序没有问题,但是多线程指令重排序就会带来问题。

参考网址:http://www.importnew.com/23535.html
(1)多线程时,如果线程A指令重排

线程A: context = loadInit();
	   flag = true;  
	   
线程B: while(!flag){
	      sleep(1000)
	   }
	   doSomething(context);

如果线程A指令重排,可能导致context还没有初始化,而线程B就已经使用,从而报错。
(2)指令重排导致单例模式失效
public class Singleton {

private static Singleton singleton = null;
private Singleton(){};
public static Singleton getInstance(){
	if(singleton==null){
		synchronized (Singleton.class) {
			singleton = new Singleton();  //非原子操作
		}
	}
	return singleton;
}

}
由于singleton = new Singleton()非原子操作,可以分为三部分

memory = allocate();  //1.分配对象的内存空间
ctorInstance(memory); //2.初始化对象
instance = memory;    //3.设置instance指向刚分配的内存

这3步操作,2依赖于1,但是3并不依赖于2,多线程操作下,指令重排可能导致2和3互换,instance指向了刚分配的内存,此时对象还未初始化,导致下一个线程进来发现instance不为空,直接返回,导致出错。如果再instance关键字上加volatile修饰就可防止指令重排。

volatile通过内存屏障来防止指令重排:
内存屏障:用来控制和规范CPU对内存操作顺序的CPU指令。

内存屏障列表:

1.loadload:  确保”前者数据装载“先于”后者装载指令“,屏障之前的执行完,屏障后面的才能执行
2.storestore:确保“前者数据”先于“后者数据”刷入系统内存,且“前者刷入系统内存的数据”对后者是可见的
3.loadstore: 确保“前者装载数据”先于“后者刷新数据到内存系统”,必须先从内存中获取数据,
              然后再修改完push回内存,主要防止后面文件修改前面修改的值。
4.storeload:  确保“前者刷入系统内存”的数据对“后者加载数据”是可见的。

volatile的内存语义的实现策略:

1.在每个volatile写操作前,插入一个storestore屏障
2.在每个volatile写操作后,插入一个storeload屏障
3.在每个volatile读操作后,插入一个loadload屏障
4.在每个volatile读操作后,插入一个loadstore屏障
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值