java学习总结——synchronized关键字的使用及原理分析

一、 synchronized的使用

    1、方法级别的synchronized

public class TargetObject {
	
	private int number = 0;
	
	public void addNumber(){
		for(int i=0; i<10000; i++){
			number++;
		} 
	}
	
	public int getNumber(){
		return number;
	}
}

public class ThreadClass extends Thread{

	
	private TargetObject targetObject;

	public ThreadClass(TargetObject targetObject) {
		this.targetObject = targetObject;
	}
	
	@Override
	public void run() {
		 targetObject.addNumber();
	}
}

public class Main {

	 public static void main(String[] args) throws Exception{
	        TargetObject targetObject = new TargetObject();
	    
	        //创建100个线程
		ThreadClass[] ths = new  ThreadClass[100];
		for(int i=0; i<100; i++){
			ths[i] = new ThreadClass(targetObject);
		}
		
		//启动线程
		for (ThreadClass threadClass : ths) {
			threadClass.start(); 
		}
		
		//等待所有的线程执行完成
		for (ThreadClass threadClass : ths) {
			threadClass.join();
		}
		
		//100个线程,每个线程加10000,number最后的值应该是1000000;
		System.out.println(targetObject.getNumber());
	}
}
//输出结果:925833
最后的输出结果不是1000000,也就是说多个线程对number的计算出错了。如果我们需要保证100个线程正确的计算Number值,这里就可以使用synchronized关键字。如下:

public class TargetObject {
	
	private int number = 0;
	
	public synchronized void addNumber(){
		for(int i=0; i<10000; i++){
			number++;
		} 
	}
	
	public int getNumber(){
		return number;
	}
}
//输出结果:1000000
使用synchronized关键字之后,可以看到Number的结果计算正确。接下来将代码进行修改,所有的代码都不变,只改变Main类中线程的创建方式,如下:

public class Main {

	 public static void main(String[] args) throws Exception{
		//创建两个目标对象
	        TargetObject targetObject1 = new TargetObject();
	        TargetObject targetObject2 = new TargetObject();
	    
	        //创建100个线程
		ThreadClass[] ths = new  ThreadClass[100];
		for(int i=0; i<100; i++){
			if(i <50){ //前50个线程使用targetObjet1
				ths[i] = new ThreadClass(targetObject1);
			}else{     //后50个线程使用targetObjet2
				ths[i] = new ThreadClass(targetObject2);
			}
		}
		
		//启动线程
		for (ThreadClass threadClass : ths) {
			threadClass.start(); 
		}
		
		//等待所有的线程执行完成
		for (ThreadClass threadClass : ths) {
			threadClass.join();
		}
		
		//分别打印两个目标对象的number值
		System.out.println("targetObject1--"+targetObject1.getNumber());
		System.out.println("targetObject2--"+targetObject2.getNumber());
	}
}
//输出结果:
//targetObject1--500000
//targetObject2--500000
接着再进一步修改代码,延续上面的代码,不做修改。这次将TargetObject类的代码修改如下:

public class TargetObject {
	
	private static int number = 0;
	
	public static synchronized void addNumber(){ //加上了static静态修饰符
		for(int i=0; i<10000; i++){
			number++;
		} 
	}
	
	public int getNumber(){
		return number;
	}
}

//输出结果:
//targetObject1--1000000
//targetObject2--1000000
经过上面两次的修改,我们可以得出结论:

synchronized关键字的同步的范围是与方法的范围相匹配的,如果方法是对象的方法(没有static关键字),则synchronized关键字只会同步那些“调用了同一个对象的内的被synchronized修饰的方法”,而对于不是同一个对象内的方法调用,则不同步,这也是为什么第一次修改后的输出结果是targetObject1--500000和targetObject2--500000。

         但是,如果将方法修改为静态方法(static修饰),此时这个方法是属于类级别的。所以,所有调用这个静态方法的线程都会被同步,无论是否在一个对象内部。这就是第二次修改代码后的输出结果是targetObject1--1000000和targetObject2--1000000的原因。

        2、代码块级别的synchronized

示例代码:

public class TargetObject {
	
	private int number = 0;
	
	public  void addNumber(){  
		
		synchronized(this){ //代码块形式的synchronized的使用
			for(int i=0; i<10000; i++){
				number++;
			} 
		}
	}
	
	public int getNumber(){
		return number;
	}
}

public class ThreadClass extends Thread{

	
	private TargetObject targetObject;

	public ThreadClass(TargetObject targetObject) {
		this.targetObject = targetObject;
	}
	
	@Override
	public void run() {
		 targetObject.addNumber();
	}
}

public class Main {

	 public static void main(String[] args) throws Exception{
		//创建两个目标对象
	    TargetObject targetObject = new TargetObject();
	    
	    //创建100个线程
		ThreadClass[] ths = new  ThreadClass[100];
		for(int i=0; i<100; i++){
			ths[i] = new ThreadClass(targetObject);
		}
		
		//启动线程
		for (ThreadClass threadClass : ths) {
			threadClass.start(); 
		}
		
		//等待所有的线程执行完成
		for (ThreadClass threadClass : ths) {
			threadClass.join();
		}
		
		//分别打印两个目标对象的number值
		System.out.println("targetObject--"+targetObject.getNumber());
	}
}	

//输出结果
//targetObject1--1000000
可以看到,处理synchronized的使用方式改变了之外,其他的都没有变。输出结果也是跟方法级别的synchronized是一样的。这至少说明,两种使用方式在形式上是一样的。至于两者的区别,在下面的运行原理分析进行说明,这里不赘述。

二、synchronized的原理分析

       先看一下方法级别synchronized和代码块级别的synchronized的反编译文件:

源代码:

public class TargetObject {
	
	private Object lock = new Object();
	
	public  void  targetMethod1(){  
		synchronized (lock) {  //代码块
			System.out.println("invoke targetMethod-1 ");
		}
	}
	
	public synchronized void targetMethed2(){   //方法synchronized
		System.out.println("invoke targetMethod-2 ");
	}
}
字节码文件,targetMethod1:


字节码文件,targetMethod2:


上面的截图是源代码编译之后的文件,可以看到两个方法编译后的结果差异很大。

       首先解释一个那个flag标志。两个方法都有这个标志,但是两个方法的flag的值却不一样。这个flag表示方法进入权限。acc_public表示是公共方法,acc_synchronized表示是同步方法。很显然,我们的两个方法都是公共方法,所有两个方法都有acc_public值;然而,只有targetMethod2有synchronized关键字,所以只有targetMethod2有acc_synchronized。 在每个方法调用之前,会检查这个标志,如果acc_synchronized被设置了,那么线程就需要先去获得对象想的Monitor(监听者,类似锁),之后才可以进入方法。

       对于targetMethod1,它没有synchronized关键字,但是却在第6行,也就是源代码synchronized代码块的位置出现了一个monitorenter指令,之后又有一个monitorexit指令。接下来看一下官方文档对这两个指令的解释:

Synchronized:  
The Java Virtual Machine supports synchronization of both methods and sequences of instructions within a method by a single synchronization construct: the monitor.  
java虚拟机通过monitor,这种synchronized结构来支持方法的同步和指令的顺序执行。  
  
monitorenter指令:  
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:  
每一个对象都关联着一个monitor,当这个monitor存在一个所有者的时候,monitor就会被锁住。当线程执行monitorenter指令尝试去获取一个对象的monitor时,可能存在以下情况;  
  
If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.  
如果对象的monitor的“进入数量”是0,则线程就进入Monitor,并且将monitor的“进入数量”设置为1。此时,这个线程就是这个monitor得所有者了。  
  
If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.  
如果进入的线程已经持有了当前对象的Monitor,线程可以直接进入monitor,并将“进入数量”加1。(也就说,synchronized可以重入)  
  
If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.  
如果其他线程已经持有了这个对象的monitor,那么当前进入的线程就会被阻塞。当对象的monitor的“进入数量”被设置为0时,当前进入的线程可以尝试再次获取这个monitor的所有权。  
  
monitorexit指令:  
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.  
执行这个指令的线程必须是对象Monitor的所有者。  
  
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.  
执行这个操作,线程会将monitor的"进入次数"减一。如果“进入次数”是0,线程将会退出这个monitor,不再是这个monitor的所有者。此时,其他尝试获取该对象的Monitor的线程,将会被允许获得这个对象的Monitor  

       通过官方文档的介绍,可以了解到,synchronized是通过获取对象的monitor来实现同步的。方法级别的synchronized通过判断acc_synchronized去获取锁,代码块级别的synchronized通过monitorenter指令去获取Monitor。

       这里有几个地方需要留意:

1、synchronized可以重入;这类情形主要出现在递归调用的情况下。就如上面所说的:如果线程已经是monitor的所有者,则可以直接进入monitor,只需要将“进入次数”加一。示例示例代码:

public class TargetObject {
	
	private  int number = 0;
	
	private  Object lock = new Object();
	
	public void addNumber(){  
		synchronized(lock){ //代码块形式的synchronized的使用
			for(int i=0; i<10000; i++){
				number++;
				if(number < 3){
					addNumber(); //递归调用,已经是Monitor的所有者,不需要再获取,直接进入
				}
			} 
		}
	}
	
	public int getNumber(){
		return number;
	}
}
2、每一个对象只有一个monitor,当monitor已经有所有者之后,其他线程将被阻塞。示例代码:

public class TargetObject {
	
	private int num = 0;
	
	private  Object lock1 = new Object();
	private  Object lock2 = new Object();
	
	public void addNumber(){  
		synchronized(lock1){ //synchronized获取的是lock1对象的Monitor
			System.out.println(Thread.currentThread().getName()+" 休眠....");
			try {
				Thread.sleep(4000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	
	public void addNum(){
		synchronized(lock2){ //synchronized获取的是lock2对象的Monitor
			for(int i=0; i<5; i++){
				num++;
				System.out.println(Thread.currentThread().getName()+"---"+num);
			} 
		}
	}
}

public class ThreadClass extends Thread{

	
	private TargetObject targetObject;

	public ThreadClass(TargetObject targetObject) {
		this.targetObject = targetObject;
	}
	
	@Override
	public void run() {
		String name = Thread.currentThread().getName();
		if(name.equals("thread-1")){
			 targetObject.addNumber();
		}else{
			 targetObject.addNum();;
		}
		
	}
}

public class Main {

	 public static void main(String[] args) throws Exception{
		//创建两个目标对象
	    TargetObject targetObject = new TargetObject();
	    ThreadClass t1 = new ThreadClass(targetObject);
	    ThreadClass t2 = new ThreadClass(targetObject);
	    t1.setName("thread-1");
	    t2.setName("thread-2");
	    
	    t1.start();
	    t2.start();
	}
}

//输出结果:
//thread-2---1
//thread-1 休眠....
//thread-2---2
//thread-2---3
//thread-2---4
//thread-2---5
targetObject的两个方法分别获取不同对象的monitor,从输出结果可以看出,thread-1的休眠并不能阻止thread-2的运行。也就是说thread-1和thread-2没有同步。我们把targetObject的代码修改一下,示例代码:

public class TargetObject {
	
	private int num = 0;
	
	private  Object lock1 = new Object();
	private  Object lock2 = new Object();
	
	public synchronized void addNumber(){   //synchronized获取的是当前对象的Monitor
		System.out.println(Thread.currentThread().getName()+" 休眠....");
		try {
			Thread.sleep(4000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	public synchronized  void addNum(){//synchronized获取的是当前对象的Monitor
		for(int i=0; i<5; i++){
			num++;
			System.out.println(Thread.currentThread().getName()+"---"+num);
		} 
	}
}
运行测试类后,就会发现,在thread-1休眠的时候,thread-2并没有执行。也就是说thread-1的休眠阻止了thread-2的执行。

3、monitorenter与monitorexit并不一定是配对的。同一个monitorenter可以对应多个monitorexit。留心的伙伴就会发现,上面的字节码截图中,targetMethod1里面是一个monitorenter对应两个monitorexit。这是因为monitorexit只关心monitor的“进入次数”是否为零,为零表示可以退出,不为零则不退出。与执行了几次monitorenter指令并没有直接的关系。此外,当方法或者代码块抛异常时,也需要monitorexit指令来退出monitor。这也就决定了monitorexit并不会等于monitorenter。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值