在java多线程——线程同步问题中,对于多线程下程序启动时出现的线程安全问题的背景和初步解决方案已经有了详细的介绍。本文将再度深入解析对线程代码块和方法的同步控制和多线程间通信的实例。
一、再现多线程下安全问题
先看开启两条线程,分别按序打印字符串的实验
1、创建一个Output内部类,并给出根据参数name,循环打印出传入字符串
2、new 两个线程,分别传入zhongailing、max两个name,调用同一个output对象的output方法
public static void main(String[] args) throws InterruptedException { Output output=new Output(); new Thread(new Runnable(){ @Override public void run() { while(true) { try { Thread.sleep(1000);//休息1s } catch (InterruptedException e) { e.printStackTrace(); } output.output("zhongailing"); } } }).start(); new Thread(new Runnable(){ @Override public void run() { while(true) { try { Thread.sleep(1000);//休息1s } catch (InterruptedException e) { e.printStackTrace(); } output.output("max"); } } }).start(); } //内部类 class Output { public void output(String name) { for(int i=0;i<name.length();i++) synchronized (name) { System.out.print(name.charAt(i)); } System.out.println(); } }摘取部分输入结果如下:
zhonmaxgailing max zhongailing zhongailing max mazhongax iling max zhongailing zhonmgax ailing mzhax ongailing zmhaoxngailing max zhongailing可以看出,打印出来的name名除了zhongailing 和max之外,还出现iling、mzhax之类字样,这是肿么回事?
这是因为两条线程同时调用同一个output对象的output方法,当第一个线程的打印循环还未走完时,下一个线程又进入传入另一个参数继续循环,使得打印出的结果出现iling、mzhax这些非正常字样。
二、解决线程安全问题——设置线程互斥
在java多线程——线程同步问题中已经介绍了使用synchronized关键字对代码块或者整个方法体进行设置,使得该段代码享有排他性,独立占用资源,完成打印循环之后,下个线程再进入。
class Output { public void output(String name) { for(int i=0;i<name.length();i++) synchronized (this) //代码块同步,指定加锁对象为this,指向当前传入的output { System.out.print(name.charAt(i)); } System.out.println(); } public synchronized void output2(String name) //在方法体上执行synchronized,把整个方法都保护起来。 { for(int i=0;i<name.length();i++) { System.out.print(name.charAt(i)); } System.out.println(); }
题外话:客户端使用main方法进行调用时:
public static void main(String[] args) throws InterruptedException { Output output=new Output(); new Thread(new Runnable(){ @Override public void run() { while(true) { try { Thread.sleep(1000);//休息1s } catch (InterruptedException e) { e.printStackTrace(); } output.output("zhongailing");//内部类不能访问局部变量?? } } }).start(); }在new 内部类 Output是报错,这是因为在静态方法中不能实例化内部类。因为内部类的特点就是可以访问外部类的成员变量,又因为对象创建完了才为成员变量分配空间,所以在使用变量之前,这个类已经实例化了。而静态方法执行时,可以不用创建这个对象就使用而矛盾。并且main这个静态方法在运行时没有使用任何外部类的成员变量,所以要创建内部类的实例对象,必须已经存在外部类的实例对象来创建。代码改造为:
public static void main(String[] args) throws InterruptedException { new SynchronizationApp().init(); } public void init()//初始化-调用这个非静态方法时,一定是某个类已经被实例化了,该方法才能被调用。 { Output output=new Output(); new Thread(new Runnable(){//线程0 @Override public void run() { while(true) { try { Thread.sleep(1000);//休息1s } catch (InterruptedException e) { e.printStackTrace(); } output.output("zhongailing"); } } }).start(); new Thread(new Runnable(){//线程1 @Override public void run() { while(true) { try { Thread.sleep(1000);//休息1s } catch (InterruptedException e) { e.printStackTrace(); } output.output("max"); } } }).start(); } //内部类,用于循环字母,挨个输出传入的name字符。 class Output { public void output(String name) { for(int i=0;i<name.length();i++) synchronized (this) //或者在方法上加同步关键字 { System.out.print(name.charAt(i)); } System.out.println(); } }}使用synchronized需谨慎:
1、对于多个线程的执行对象一定是同一个。例如这里new 同一个Output对象,执行方法,只是方法传入的参数不同。这个应该比较好理解,如果你为当前线程加上互斥方案,但多个线程启用的并不是同一个对象,那无论做多少不同步方案都是无用功,因为压根就没在一个对象中进行多线程处理。
2、该关键字在一段代码里一般只使用一次。打个比方,如果将方法和方法内部的代码块同时加上synchronized关键字,就类似于原本只有一把锁,钥匙给了方法,而代码块也需要这把钥匙开锁执行自己的代码,双方谁也不释放,容易产生死锁问题。
三、多线程间通信
一个实例:两条线程-主线程、子线程,要求子线程先执行10次,主线程再执行100次,整体反复执行50次。
代码思路:
1、首先两条线程互斥,各不影响
2、创建标识,让子线程先执行,主线程等待;当子线程执行完第一个10次循环,改变标识,唤醒主线程执行。
package it.synchronization; public class ThreadCommunication { public static void main(String[] args) { new ThreadCommunication().init(); } public void init() { innerBusiness inner=new innerBusiness(); new Thread(new Runnable(){ //线程1 @Override public void run() { for(int i=1;i<=50;i++) //再循环50次 { try { inner.sub(i); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }).start(); new Thread(new Runnable(){//线程2 @Override public void run() { for(int i=1;i<=50;i++) { /*synchronized (ThreadCommunication.class) {//多组线程呢,这个范围有点太大。如果该类中有多个线程组,这种方式就会出问题。 }*/ try { inner.main(i); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }).start(); } class innerBusiness //创建内部类 { private boolean bShouldSub=true;//约定sub线程先执行 public synchronized void sub(int i) throws InterruptedException { if(!bShouldSub)//如果没到子线程执行,子线程wait { this.wait(); } for(int j=1;j<=10;j++) { System.out.println("子线程运行第"+j+"次"); } bShouldSub=false; this.notify();//唤醒等待线程-main } public synchronized void main(int i) throws InterruptedException { if(bShouldSub)//如果该子线程执行,主线程wait { this.wait(); } for(int j=0;j<100;j++) { System.out.println("主线程运行第"+j+"次"); } bShouldSub=true; this.notify();//唤醒等待线程-sub } } }亮点:
将互斥约束到线程资源上,而非线程代码块中:将两条线程代码写到一个innerclass中,在方法上进行同步约束和线程执行等待逻辑操作,而不是直接在main方法new Thread时,对代码块进行互斥。这样无论谁调用线程逻辑,直接调用innerclass(资源的)的sub 和main方法就可满足线程安全要求,更符合面向对象的思想。同时免去了对同对象参数的考虑。
四、总结
一)synchronized是java语言级别内置的同步机制,根据作用的对象分为:类锁、实例锁;
对于类锁的应用:只能应用到类的静态方法上,形如:staticsynchronized method();
对于实例锁应用:
1、非静态方法:synchronized method();
2、代码块:synchronized{}或synchronized(this){}
3、指定实例:synchronized(Object obj);
非静态方法和代码块使用synchronized的实质都一样,对当前所属类的实例枷锁。而指定实例是对括号里传入的obj对象加锁。如同上面的例子,使用的就是代码块和指定实例对象加锁方式。另外synchronized不能被直接继承。
笔者体会而言, 对于多线程中synchronized的使用,除了它的作用范围定在类上、方法上、对象、代码块这些point之外,还需要注意加锁的对象是否是同一个。例如上面例子如果不加this,哪怕使用了synchronized对于线程安全还是无法保障的。因为线程调用同一对象的output方法,琐是加了,but并未作用于该对象上。
二)多线程间通信wait,notify方法的合理使用,也是线程的重要部分。本文仅提供了一个线程间通信的实例,在接下来的博文将详细介绍线程间通信应用。