Java多线程6:Synchronized锁代码块(this和任意对象)
一、Synchronized(this)锁代码块
用关键字synchronized修饰方法在有些情况下是有弊端的,若是执行该方法所需的时间比较长,线程1执行该方法的时候,线程2就必须等待。这种情况下就可以使用synchronized同步该方法中会引起线程安全的那部分代码,其余不会引起线程安全的就不需要同步,这部分代码就可以多线程并发执行,减少时间提高效率。
举例:多线程执行同一个方法时,同步方法和同步代码块花费时间的比较
1、synchronized修饰方法(同步方法)
synchronized修饰longTimeTask方法,其中花费时间比较长的且与线程安全无关的是37-39行代码,会引起线程安全问题的是42-46。
1 public class ThreadSynch { 2 3 private int num; 4 5 public synchronized void longTimeTask(String userName){ 6 //定义各线程的进入时间 7 long thread0StartTime = 0L; 8 long thread1StartTime = 0L; 9 long thread2StartTime = 0L; 10 long thread3StartTime = 0L; 11 long thread4StartTime = 0L; 12 //定义各线程执行该方法所需的时间 13 long thread0LastTime; 14 long thread1LastTime; 15 long thread2LastTime; 16 long thread3LastTime; 17 long thread4LastTime; 18 //显示各线程进入的时间 19 if(Thread.currentThread().getName().contains("-0")){ 20 thread0StartTime = System.currentTimeMillis(); 21 System.out.println(Thread.currentThread().getName() + "进入时间为====" + thread0StartTime); 22 }else if(Thread.currentThread().getName().contains("-1")){ 23 thread1StartTime = System.currentTimeMillis(); 24 System.out.println(Thread.currentThread().getName() + "进入时间为====" + thread1StartTime); 25 }else if(Thread.currentThread().getName().contains("-2")){ 26 thread2StartTime = System.currentTimeMillis(); 27 System.out.println(Thread.currentThread().getName() + "进入时间为====" + thread2StartTime); 28 }else if(Thread.currentThread().getName().contains("-3")){ 29 thread3StartTime = System.currentTimeMillis(); 30 System.out.println(Thread.currentThread().getName() + "进入时间为====" + thread3StartTime); 31 }else if(Thread.currentThread().getName().contains("-4")){ 32 thread4StartTime = System.currentTimeMillis(); 33 System.out.println(Thread.currentThread().getName() + "进入时间为====" + thread4StartTime); 34 } 35 36 //花费时间较长,与线程安全无关的代码 37 for(int i = 200000000; i > 0; i--) { 38 String nameID = Thread.currentThread().getName() + Thread.currentThread().getId(); 39 } 40 41 //与线程安全相关的代码块 42 if("zs".equals(userName)){ 43 num = 100; 44 }else if("ls".equals(userName)){ 45 num = 200; 46 } 47 48 //显示各线程执行该方法的时间 49 if(Thread.currentThread().getName().contains("0")){ 50 thread0LastTime = System.currentTimeMillis() - thread0StartTime; 51 System.out.println(Thread.currentThread().getName() + "执行时间为===" + thread0LastTime + "ms"); 52 }else if(Thread.currentThread().getName().contains("1")){ 53 thread1LastTime = System.currentTimeMillis() - thread1StartTime; 54 System.out.println(Thread.currentThread().getName() + "执行时间为===" + thread1LastTime + "ms"); 55 }else if(Thread.currentThread().getName().contains("2")){ 56 thread2LastTime = System.currentTimeMillis() - thread2StartTime; 57 System.out.println(Thread.currentThread().getName() + "执行时间为===" + thread2LastTime + "ms"); 58 }else if(Thread.currentThread().getName().contains("3")){ 59 thread3LastTime = System.currentTimeMillis() - thread3StartTime; 60 System.out.println(Thread.currentThread().getName() + "执行时间为===" + thread3LastTime + "ms"); 61 }else if(Thread.currentThread().getName().contains("4")){ 62 thread4LastTime = System.currentTimeMillis() - thread4StartTime; 63 System.out.println(Thread.currentThread().getName() + "执行时间为===" + thread4LastTime + "ms"); 64 } 65 66 } 67 }
继承Thread的Thread01类,其run方法调用上述对象的longTimeTask方法
public class Thread01 extends Thread{ private ThreadSynch threadSynch; public Thread01(ThreadSynch threadSynch) { this.threadSynch = threadSynch; } @Override public void run() { threadSynch.longTimeTask("ls"); } }
测试,构建同一对象的多个线程
public class Test { public static void main(String[] args) { ThreadSynch threadSynch = new ThreadSynch(); //五个线程使用同一个对象构建 Thread thread01 = new Thread01(threadSynch); Thread thread02 = new Thread01(threadSynch); Thread thread03 = new Thread01(threadSynch); Thread thread04 = new Thread01(threadSynch); Thread thread05 = new Thread01(threadSynch); //五个线程同时调用该对象中的方法 thread01.start(); thread02.start(); thread03.start(); thread04.start(); thread05.start(); } }
结果:
Thread-0进入时间为====1553150692703 Thread-0执行时间为===8437ms Thread-3进入时间为====1553150701140 Thread-3执行时间为===7014ms Thread-1进入时间为====1553150708154 Thread-1执行时间为===7002ms Thread-4进入时间为====1553150715157 Thread-4执行时间为===7121ms Thread-2进入时间为====1553150722278 Thread-2执行时间为===7147ms
说明:因为synchronized修饰的是整个方法,所以线程Thread-0访问longTimeTask方法的时候,其余四个线程都处于阻塞状态,待其执行结束释放锁的时候,线程Thread-3开始执行,其余三个线程还是处于阻塞状态,所以,这五个线程执行完毕所需的时间是各自执行时间的相加,8.4 + 7.0 + 7.0 + 7.1 + 7.1 = 36.6s。
2、synchronized修饰代码块(同步代码块)
synchronized由同步方法改为同步方法中引起线程安全问题的代码块,其余都不变
public class ThreadSynch { private int num; public void longTimeTask(String userName){ long thread0StartTime = 0L; long thread1StartTime = 0L; long thread2StartTime = 0L; long thread3StartTime = 0L; long thread4StartTime = 0L; long thread0LastTime; long thread1LastTime; long thread2LastTime; long thread3LastTime; long thread4LastTime; if(Thread.currentThread().getName().contains("-0")){ thread0StartTime = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + "进入时间为====" + thread0StartTime); }else if(Thread.currentThread().getName().contains("-1")){ thread1StartTime = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + "进入时间为====" + thread1StartTime); }else if(Thread.currentThread().getName().contains("-2")){ thread2StartTime = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + "进入时间为====" + thread2StartTime); }else if(Thread.currentThread().getName().contains("-3")){ thread3StartTime = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + "进入时间为====" + thread3StartTime); }else if(Thread.currentThread().getName().contains("-4")){ thread4StartTime = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + "进入时间为====" + thread4StartTime); } //花费时间较长,与线程安全无关的代码 for(int i = 200000000; i > 0; i--) { String nameID = Thread.currentThread().getName() + Thread.currentThread().getId(); } //与线程安全相关的代码块用synchronized修饰 synchronized(this){ if("zs".equals(userName)){ num = 100; }else if("ls".equals(userName)){ num = 200; } } if(Thread.currentThread().getName().contains("0")){ thread0LastTime = System.currentTimeMillis() - thread0StartTime; System.out.println(Thread.currentThread().getName() + "执行时间为===" + thread0LastTime + "ms"); }else if(Thread.currentThread().getName().contains("1")){ thread1LastTime = System.currentTimeMillis() - thread1StartTime; System.out.println(Thread.currentThread().getName() + "执行时间为===" + thread1LastTime + "ms"); }else if(Thread.currentThread().getName().contains("2")){ thread2LastTime = System.currentTimeMillis() - thread2StartTime; System.out.println(Thread.currentThread().getName() + "执行时间为===" + thread2LastTime + "ms"); }else if(Thread.currentThread().getName().contains("3")){ thread3LastTime = System.currentTimeMillis() - thread3StartTime; System.out.println(Thread.currentThread().getName() + "执行时间为===" + thread3LastTime + "ms"); }else if(Thread.currentThread().getName().contains("4")){ thread4LastTime = System.currentTimeMillis() - thread4StartTime; System.out.println(Thread.currentThread().getName() + "执行时间为===" + thread4LastTime + "ms"); } } }
同样的五个线程访问,看一下结果:
Thread-0进入时间为====1553151204348 Thread-3进入时间为====1553151204348 Thread-1进入时间为====1553151204348 Thread-2进入时间为====1553151204348 Thread-4进入时间为====1553151204380 Thread-3执行时间为===19330ms Thread-2执行时间为===19383ms Thread-1执行时间为===19854ms Thread-4执行时间为===20498ms Thread-0执行时间为===20782ms
说明:因为synchronized修饰的是方法中会引起线程安全问题的代码块,所以仅仅是这一部分代码无法并发执行。可以看到Thread-0,Thread-1,Thread-2,Thread-3,Thread-4几乎同时进入longTimeTask方法,并发执行for循环中花费时间较长的代码,由结果看,Thread-3最先执行完这部分代码,开始执行synchronized修饰的代码块,其余四个线程随后进入阻塞状态。因为同步代码块中执行时间较短,Thread-3执行完后,Thread-2开始执行,最后是Thread-0执行,至此,五个线程执行完毕,所花费的时间就是Thread-0花费的时间,即20.8s。
可以看到,在longTimeTask方法中,synchronized由修饰方法改为修饰代码块,多线程执行所花费的时间由36.6s变成20.8s,执行时间明显减少,效率提升。
二、任意对象作为对象监视器
2.1 上述同步代码块使用的是synchronized(this)格式,其实Java还支持对“任意对象”作为对象监视器来实现同步的功能。这种任意对象大多是该方法所属类中的实例变量或该方法的参数,不然抛开这个类去使用别的对象作为对象监视器,意义不大。使用的格式是synchronized(非this的任意对象)。
举例:以ThreadSynch类中的变量student作为对象监视器去同步代码块
public class ThreadSynch { private Student student = new Student(); private String schoolName; public void setNameAndPassWord(String name,String age){ synchronized(student){ System.out.println(Thread.currentThread().getName() + "===" + "进入同步代码块"); try { Thread.sleep(3000); this.student.setName(name); this.student.setAge(age); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "===" + "离开同步代码块"); } } }
Thread01的run方法调用setNameAndPassWord方法
public class Thread01 extends Thread{ private ThreadSynch threadSynch; public Thread01(ThreadSynch threadSynch) { this.threadSynch = threadSynch; } @Override public void run() { threadSynch.setNameAndPassWord("ls","11"); } }
测试:
public class Test { public static void main(String[] args) { ThreadSynch threadSynch = new ThreadSynch(); //三个线程使用同一个对象构建 Thread thread01 = new Thread01(threadSynch); Thread thread02 = new Thread01(threadSynch); Thread thread03 = new Thread01(threadSynch); //三个线程同时调用该对象中的方法 thread01.start(); thread02.start(); thread03.start(); } }
结果:
Thread-1===进入同步代码块 Thread-1===离开同步代码块 Thread-2===进入同步代码块 Thread-2===离开同步代码块 Thread-0===进入同步代码块 Thread-0===离开同步代码块
说明:Thread-0,Thread-1,Thread-2执行到同步代码块synchronized(student)时,都会去获取与student对象关联的monitor,判断该monitor是否被别的线程所有,因为三个线程中的student都是同一个对象,所以一个线程执行的时候,与student关联的那个monitor会被当前线程所有,别的线程都会处于阻塞状态。
稍微改一下ThreadSynch类中setNameAndPassWord的方法,添加7-9行的代码
1 public class ThreadSynch { 2 3 private Student student = new Student(); 4 private String schoolName; 5 6 public void setNameAndPassWord(String name,String age){ 7 if(Thread.currentThread().getName().contains("1")){ 8 student = new Student(); 9 } 10 synchronized(student){ 11 System.out.println(Thread.currentThread().getName() + "===" + "进入同步代码块"); 12 try { 13 Thread.sleep(3000); 14 this.student.setName(name); 15 this.student.setAge(age); 16 } catch (InterruptedException e) { 17 e.printStackTrace(); 18 } 19 System.out.println(Thread.currentThread().getName() + "===" + "离开同步代码块"); 20 } 21 } 22 }
其余都不变,看一下结果:
Thread-0===进入同步代码块 Thread-1===进入同步代码块 Thread-0===离开同步代码块 Thread-1===离开同步代码块 Thread-2===进入同步代码块 Thread-2===离开同步代码块
说明:可以看到,Thread-0和Thread-1同时进入同步代码块。分析一下原因,Thread-0执行到synchronized(student)时,会去获取与该student对象关联的monitor的所有权,该monitor没有被别的线程占有,Thread-0进入同步代码块中。Thread-1执行setNameAndPassWord方法的时候,新添加的7-9行的代码将student变量指向了一个新的student对象,此时的student对象和Thread-0时的student对象已经不是同一个了,对应的monitor也不是Thread-0时的那个monitor,所以Thread-1在Thread-0还未离开同步代码块的时候,也可以进入到同步代码块中执行。但Thread-2执行同步代码块时的student还是Thread-1时的那个student,所以Thread-2只能等到Thread-1执行结束,才能进入同步代码块中。
所以,多个线程访问同步代码块时,只要synchronized(this对象/非this对象)中的对象是同一个对象,那么同一时间只能有一个线程可以执行同步代码块中的内容。这里注意一下当任意对象是string类型时,使用不当可能会有一些麻烦。具体就是以下两个例子:
public class Test { public static void main(String[] args) { String str1 = "111"; String str2 = "111"; System.out.println(str1 == str2); String str3 = new String("222"); String str4 = new String("222"); System.out.println(str3 == str4); } }
结果:
true false
多线程并发执行时,当synchronized(str1)由str1变成str2时,其余线程是否还会处于阻塞状态(会)。
多线程并发执行时,当synchronized(str3)由str3变成str4时,其余线程是否还会处于阻塞状态(不会)。
具体的string常量与new String对象的区别,参见这篇文章从为什么String=String谈到StringBuilder和StringBuffer。
参考资料: