5、线程同步和通信
当多个线程要访问同一个资源时,有必要使这个资源一次只能让一个线程访问,这就是下面要讲到的线程同步。线程通信是指线程之间通过一些方法进行访问。
1)、线程同步的必要性
经常有这个么一种情况,当网上书店库存量还有10本书时,俩了两个人分别买7本和8本,不管哪个人先买另一个都不可能完全满足要求,因为它的库存都是不够的,下面的程序以买书为例介绍多线程同步的必要性:
public class test{ public static void main(String[] str) { ShangDian sd = new ShangDian(10); GouMai gm1 = new GouMai(7, sd, "一号"); GouMai gm2 = new GouMai(8, sd, "二号"); gm1.start(); gm2.start(); } } class ShangDian{ int kucun = 0; public ShangDian(int kucun) { this.kucun = kucun; } public void goumai(int i){ if (i < kucun) { try { Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } kucun -= i; System.out.println(Thread.currentThread().getName() + "购买" + i + "本"); System.out.println("商店剩余" + kucun +"本"); } else{ System.out.println("库存不够"); } } } class GouMai extends Thread{ int i; ShangDian sd; public GouMai(int i , ShangDian sd , Stringname) { this.i = i; this.sd = sd; this.setName(name); } @Override public void run() { sd.goumai(i); } }
运行结果有多种情况,其中一种为:
一号购买7本
商店剩余3本
二号购买8本
商店剩余-5本
可以看到这里会造成库存量小于0,从而造成后面的人购买出现负数。在这种情况下就需要应用同步方法顺利实现购物流程。
2)、实现同步
同步的工作原理是通过锁定来完成工作的。规定Java中的每一个对象都有一个锁,当对象进入同步方法时,对象锁就会起作用。线程获得锁之后,其他的线程就不可能再进入同步代码中。当线程释放锁之后,其他的线程才有机会进入。同步方法是使用关键字synchronized来标识的。下面的实例讲解了用synchronized实现多线程的同步。
public class test{ public static void main(String[] str) { ShangDian sd = new ShangDian(10); GouMai gm1 = new GouMai(7, sd, "一号"); GouMai gm2 = new GouMai(8, sd, "二号"); gm1.start(); gm2.start(); } } class ShangDian{ int kucun = 0; public ShangDian(int kucun) { this.kucun = kucun; } public synchronized void goumai(int i){ System.out.println(Thread.currentThread().getName() + "开始购买"); if (i < kucun) { try { Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } kucun -= i; System.out.println(Thread.currentThread().getName() + "购买" + i + "本"); System.out.println("商店剩余" + kucun +"本"); } else{ System.out.println("库存不够"); } } } class GouMai extends Thread{ int i; ShangDian sd; public GouMai(int i , ShangDian sd , Stringname) { this.i = i; this.sd = sd; this.setName(name); } @Override public void run() { sd.goumai(i); } }
运行结果为:
一号开始购买
一号购买7本
商店剩余3本
二号开始购买
库存不够
其中一个细节大家要注意,在本程序中将sleep()方法去掉了,因为线程进入睡眠一样会带着锁,这样是浪费资源的。
3)、同步代码块和死锁
不但可以同步方法,还可以同步代码块。同步代码块和同步方法一样,当一个线程进入该代码块后,其他的线程将不能再进入。下面是一个关于同步代码块的实例:
public class JavaApplication1 { public static void main(String[] args){ MyThread t1 = new MyThread("●"); MyThread t2 = new MyThread("▲"); MyThread t3 = new MyThread("■"); t1.start(); t2.start(); t3.start(); } } class MyThread extendsThread{ String name; public MyThread(String name) { this.name = name; } @Override public void run() { synchronized(name){ for (int i = 0; i < 20; i++) { System.out.println(name); } } } }
同步代码是最容易产生死锁的。程序中一旦有死锁的问题,那将会毁掉整个程序,尤其是在比较大的程序中,死锁是很难发现的。下面列举一个很简单的死锁问题,平常的开发中一旦出现死锁就不可能会出现这么简单的问题,它会隐藏的很深。在同步代码块中出现死锁的原因,至少有一个进程必须持有一个资源且正在等候获取一个当前被别的进程持有的资源,也就是说,要发生死锁,至少有一个线程必须执行并且等待下一个进程。下面是一个死锁的例子:
public class test{ public static void main(String[] str) { String s1 = "s1"; String s2 = "s2"; MyThread t1 = new MyThread(s1, s2,"一号"); MyThread t2 = new MyThread(s2, s1,"二号"); t1.start(); t2.start(); } } class MyThread extendsThread{ String s1, s2; public MyThread(String s1 , String s2 ,String name) { this.s1 = s1; this.s2 = s2; this.setName(name); } @Override public void run() { synchronized(s1){ System.out.println(this.getName() + "锁住" + s1); try { Thread.sleep(10); } catch (Exception e) { e.printStackTrace(); } synchronized(s2){ System.out.println(this.getName() + "锁住" + s2); System.out.println("线程结束"); } } } }
运行结果为:
一号锁住s1
二号锁住s2
一定要注意synchronized锁的永远是对象,而不是代码块。synchronized作为方法修饰符时锁的对象是调用者。不同的对象仍然访问此方法不会构成同步。
4)、线程间通信
线程间通信需要Object类中的如下3个方法:
>wait():让当前线程进入等待状态,知道其他线程调用notify()方法
>notify():恢复一个等待状态中的线程
>notifyAll():恢复所有等待状态中的线程
这3和方法都是用final修饰的,声明的方法如下:
>final void wait() throwsInterruptedException
>final void notify()
>final void notifyAll()
其中wait()方法存在重载方法,可以添加参数,表示时间。下面通过程序来说明这些方法,以网上书店为例,为了更好地表示线程间的通信,在程序中键入管理员往库存中加入书的数量,增加数的数量。
public class test{ public static void main(String[] str) { ShangDian sd = new ShangDian(10); PiFa pf = new PiFa(16, sd, "批发人"); GouMai gm1 = new GouMai(8, sd, "一号"); GouMai gm2 = new GouMai(7, sd, "二号"); GouMai gm3 = new GouMai(6, sd, "三号"); gm1.start(); gm2.start(); gm3.start(); pf.start(); } } class ShangDian{ int kucun = 0; public ShangDian(int kucun) { this.kucun = kucun; } public synchronized void PiFa(int i){ kucun += i; System.out.println(Thread.currentThread().getName() + "批发" + i + "本"); notifyAll(); } public synchronized void GouMai(int i){ while (i > kucun) { try { System.out.println(Thread.currentThread().getName() + "等待"); wait(); } catch (Exception e) { e.printStackTrace(); } } kucun -= i; System.out.println(Thread.currentThread().getName() + "购买" + i + "本"); } } class PiFa extends Thread{ int i; ShangDian sd; public PiFa(int i , ShangDian sd , Stringname) { this.i = i; this.sd = sd; this.setName(name); } @Override public void run() { sd.PiFa(i); } } class GouMai extends Thread{ int i; ShangDian sd; public GouMai(int i , ShangDian sd , Stringname) { this.i = i; this.sd = sd; this.setName(name); } @Override public void run() { sd.GouMai(i); } }
其中一种运行结果为:
一号购买8本
二号等待
三号等待
批发人批发16本
三号购买6本
二号购买7本
本程序中,其实库存有10本,一号运行后买了8本。库存还有2本,这时二号三号分别购买7本和6本,库存不够,系统会调用wait()方法,使它们处于等待状态。然后调用管理员线程,批发16本,这时库存有18本,然后管理员线程调用notifyAll()方法通知处于等待状态中的二号三号。线程二号三号接到通知后进入运行状态,分别购买。