lin20080410的专栏

从站在巨人的肩上,向成为巨人迈进... 互相学习!

多线程编程-synchronized语句块(二)

2.2 synchronized 同步语句块

2.2.1 使用synchronized同步方法时,如果一个线程调用同步方法执行一个长时间的任务,那么其他线程必须等待很长时间,这时用同步语句块可提高执行效率。

同步方法耗时测试代码:

public class Task {
	private String getData1;
	private String getData2;
	
	public synchronized void doLongTimeTask(){
		System.out.println("begin task!");
		try {
			Thread.sleep(2000);
			getData1="耗时任务,1,threadName="+Thread.currentThread().getName();
			getData2="耗时任务,2,threadName="+Thread.currentThread().getName();
			
			System.out.println(getData1);
			System.out.println(getData2);
			System.out.println("end task!");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
public class CommonUtils {
	public static long beginTime1;
	public static long endTime1;
	public static long beginTime2;
	public static long endTime2;
}
public class Thread1 extends Thread {
	private Task task;
	
	public Thread1(Task task){
		super();
		this.task = task;
	}
	
	@Override
	public void run(){
		super.run();
		CommonUtils.beginTime1 = System.currentTimeMillis();
		task.doLongTimeTask();
		CommonUtils.endTime1 = System.currentTimeMillis();
	}
}
public class Thread2 extends Thread {
	private Task task;
	
	public Thread2(Task task){
		super();
		this.task = task;
	}
	
	@Override
	public void run(){
		super.run();
		CommonUtils.beginTime2 = System.currentTimeMillis();
		task.doLongTimeTask();
		CommonUtils.endTime2 = System.currentTimeMillis();
	}
}
public class RunDemo {
	public static void main(String[] args) {
		Task task = new Task();
		Thread1 thread1= new Thread1(task);
		thread1.start();
		Thread2 thread2= new Thread2(task);
		thread2.start();		
		try{
			Thread.sleep(6000);
		}catch(InterruptedException e){
			e.printStackTrace();
		}
		
		long beginTime = (CommonUtils.beginTime1 > CommonUtils.beginTime2)? 
				CommonUtils.beginTime2 :CommonUtils.beginTime1 ;
		long endTime =  (CommonUtils.endTime1 > CommonUtils.endTime2)? 
				CommonUtils.endTime1 :CommonUtils.endTime2 ;
		
		System.out.println("耗时:"+(endTime - beginTime)/1000);
	}
}

运行结果:

begin task!

耗时任务,1threadName=Thread-0

耗时任务,2threadName=Thread-0

end task!

begin task!

耗时任务,1threadName=Thread-1

耗时任务,2threadName=Thread-1

end task!

耗时:4

从耗时看,synchronized同步方法,弊端很明显。再看synchronized同步块,当两个线程访问同一个object对象中的synchronized同步块时,一段时间内只能有一个线程被执行,另一个线程必须等待。

测试代码,沿用上面2.2.1的代码,修改Task.java

public class Task {
	private String getData1;
	private String getData2;
	
	public void doLongTimeTask(){
		System.out.println("begin task!");
		try {
			Thread.sleep(2000);
			String getDataTmp1="耗时任务,1,threadName="+Thread.currentThread().getName();
			String getDataTmp2="耗时任务,2,threadName="+Thread.currentThread().getName();
			synchronized(this){
				getData1 = getDataTmp1;
				getData2 = getDataTmp2;
			}
			System.out.println(getData1);
			System.out.println(getData2);
			System.out.println("end task!");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

 

运行结果:

begin task!

begin task!

耗时任务,1threadName=a

耗时任务,1threadName=b

耗时任务,2threadName=a

end task!

耗时任务,2threadName=a

end task!

耗时:2

 

耗时减少为2s

2.2.2 当一个线程访问object的一个synchronized(this)同步块时,其他线程对同一个object中所有其他synchronized(this)同步块的访问将被阻塞,也就是说synchronized(this)使用的“对象监视器”是同一个。同synchronized方法一样,这里的this是锁定的当前对象。

使用synchronized(this)同步块时,Java支持将“任意对象”作为“对象监视器”来实现同步功能。这个“任意对象”多数是实例变量或方法参数,即:synchronized(任意对象)

使用任意对象,而不是this对象做为锁,有一个有点就是可以把锁粒度变的很小,从而提高效率。

 

结论:

Synchronized(非this对象x)代码块的写法是将x对象本身作为“对象监视器”,得出下面三个结论(都是用了同一个对象监视器):

1) 多个线程同时执行synchronized(x)同步代码块时呈同步效果。

2) 当其他线程执行x对象中的synchronized同步方法时呈同步效果。

3) 当其他线程执行x对象方法里面的synchronized(this)代码块时也呈同步效果。

2.2.3 静态同步synchronized方法,synchronized(class)代码块

关键字synchronized还可以应用在static静态方法上,这样写,是对当前的*.java文件对应的Class类进行持锁。

在静态static方法上给Class类上锁,和在非static方法上给对象上锁,本质上是不同的,他们并不是同一个锁,一个是Class锁,一个是对象锁。

Class锁可以对类的所有实例起作用。

测试代码:

public class Task {
	synchronized public static void staticMethodA(){
		try{
			System.out.println("线程为:"+Thread.currentThread().getName()
					+"在 "+System.currentTimeMillis()+"进入方法MethodA");
			Thread.sleep(2000);
			System.out.println("线程为:"+Thread.currentThread().getName()
					+"在 "+System.currentTimeMillis()+"离开方法MethodA");	
		}catch(InterruptedException e){
			e.printStackTrace();
		}
	}
	
	synchronized public static void staticMethodB(){
		try{
			System.out.println("线程为:"+Thread.currentThread().getName()
					+"在 "+System.currentTimeMillis()+"进入方法MethodB");
			Thread.sleep(2000);
			System.out.println("线程为:"+Thread.currentThread().getName()
					+"在 "+System.currentTimeMillis()+"离开方法MethodB");	
		}catch(InterruptedException e){
			e.printStackTrace();
		}
	}	
}
public class Thread1 extends Thread {
	private Task task;
	
	public Thread1(Task task){
		super();
		this.task = task;
	}
	
	@Override
	public void run(){
		task.staticMethodA();
	}
}
public class Thread2 extends Thread {
	private Task task;
	
	public Thread2(Task task){
		super();
		this.task = task;
	}
	
	@Override
	public void run(){
		task.staticMethodB();
	}
}

public class RunDemo {
	public static void main(String[] args) {
		Task task1 = new Task();
		Task task2 = new Task();
		Thread1 thread1= new Thread1(task1);
		thread1.setName("a");
		thread1.start();
		Thread2 thread2= new Thread2(task2);
		thread2.setName("b");
		thread2.start();	
		
	}
}
运行结果:

线程为:a在 1515737051093进入方法MethodA
线程为:a在 1515737053093离开方法MethodA
线程为:b在 1515737053093进入方法MethodB
线程为:b在 1515737055093离开方法MethodB

虽然两个线程绑定的是不同的对象,但是静态的同步方法还是同步运行的,如果非static的同步方法,这个测试中肯定是异步执行的。

修改Task.javastaticMethodA方法中使用同步块synchronized(Task.class)):

public class Task {
	public static void staticMethodA(){
		synchronized(Task.class){
			try{
				System.out.println("线程为:"+Thread.currentThread().getName()
						+"在 "+System.currentTimeMillis()+"进入方法MethodA");
				Thread.sleep(2000);
				System.out.println("线程为:"+Thread.currentThread().getName()
						+"在 "+System.currentTimeMillis()+"离开方法MethodA");	
			}catch(InterruptedException e){
				e.printStackTrace();
			}
		}
	}
	
	synchronized public static void staticMethodB(){
		try{
			System.out.println("线程为:"+Thread.currentThread().getName()
					+"在 "+System.currentTimeMillis()+"进入方法MethodB");
			Thread.sleep(2000);
			System.out.println("线程为:"+Thread.currentThread().getName()
					+"在 "+System.currentTimeMillis()+"离开方法MethodB");	
		}catch(InterruptedException e){
			e.printStackTrace();
		}
	}
}

这个测试中无论staticMethodA方法是否是static的,只要同步块是synchronized(Task.class),运行结果都是同步的,这说明synchronized(class)同步块跟synchronized static方法的作用是一样的。

通常情况下,synchronized同步块不建议使用String作为对象锁,而是建议使用new object()实例化的对象的。因为String常量池缓存的特性使得:

String a = “AA”;

String b = “AA”;

a和b 是相等的,虽然a、b不是同一个对象,但是如果把他们作为锁,却是相同的锁。

只要对象不变,即使对象的属性被改变,也不会影响锁的同步。


在Java中,最基本的互斥同步手段就是synchronized关键字,synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。如果Java程序中的synchronized明确指定了对象参数,那就是这个对象的reference;如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象。

 

根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。

 

在虚拟机规范对monitorenter和monitorexit的行为描述中,有两点是需要特别注意的。首先,synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。其次,同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间。对于代码简单的同步块(如被synchronized修饰的getter()或setter()方法),状态转换消耗的时间有可能比用户代码执行的时间还要长。所以synchronized是Java语言中一个重量级(Heavyweight)的操作,在确实必要的情况下才使用这种操作。而虚拟机本身也会进行一些优化,譬如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁地切入到核心态之中。



阅读更多
版权声明:笔记记录,互相学习,不足之处,欢迎指正! https://blog.csdn.net/lin20044140410/article/details/79040452
个人分类: 多线程编程
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭