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!
耗时任务,1,threadName=Thread-0
耗时任务,2,threadName=Thread-0
end task!
begin task!
耗时任务,1,threadName=Thread-1
耗时任务,2,threadName=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!
耗时任务,1,threadName=a
耗时任务,1,threadName=b
耗时任务,2,threadName=a
end task!
耗时任务,2,threadName=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.java(staticMethodA方法中使用同步块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)的操作,在确实必要的情况下才使用这种操作。而虚拟机本身也会进行一些优化,譬如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁地切入到核心态之中。