昨天重新学习了java多线程的使用,多线程的难点就在线程之间的协调。在《操作系统》一课中,我们学习了进程,其实多线程和多进程一样,都会涉及到多个进程或者线程对某一资源共享访问的问题,当多个线程都需要修改这个资源的时候就会出现线程安全问题。
比如说在银行开个账户会有一个存折和一张卡,如果某一天同一时间丈夫拿着存折去柜台取钱,而妻子拿着银行卡去ATM取钱。当丈夫查询余额里面有3000元,正准备取2000元,这时候妻子也到ATM里面查询也有3000,也取2000元。其实银行不可能让我们这么做,如果这样的话那我们天天取钱去了,还搞什么工作啊。其实在丈夫查询的时候已经对该账号上了锁,另外的银行卡要取钱的话必须等待该锁被释放。下面用一个程序模拟这个例子:
1 package com.sync; 2 3 public class TestSync2 implements Runnable{ 4 public BankCard bc = new BankCard(); 5 public static void main(String[] args) { 6 TestSync2 test = new TestSync2(); 7 Thread wife = new Thread(test); 8 Thread husband = new Thread(test); 9 wife.setName("wife"); 10 husband.setName("husband"); 11 wife.start(); 12 husband.start(); 13 } 14 public void run() { 15 bc.getMoney(Thread.currentThread().getName(), 2000); 16 } 17 } 18 class BankCard{ 19 private static int money = 3000;//模拟账户余额 20 public synchronized void getMoney(String name,int m){ 21 //synchronized(this){ 22 try { 23 Thread.sleep(1); 24 } catch (InterruptedException e) { 25 e.printStackTrace(); 26 } 27 if(money > m){ 28 System.out.println(name+"取走了"+m+"元"); 29 money = money - m; 30 }else{ 31 System.out.println("对不起,您的余额不足!"); 32 } 33 //} 34 } 35 }
上面的例子如果在getMoney()方法上面不加synchronized关键字的话,输出结果为:
wife取走了2000元
husband取走了2000元
而加上synchronized后,输出结果为:
wife取走了2000元
对不起,您的余额不足!
上面两种情况说明,如果多个线程同时访问某个资源,而不给该资源枷锁的话,就会出现问题。而加上synchronized关键字后就可以避免这种错误发生了。它能够保证只有一个线程能够访问getMoney()这个方法,其他药访问该方法的线程必须等待。
锁住某个资源可以用synchronized关键字来修饰一个方法或者同步代码块,这样能保证同一时间只能由一个线程访问该资源。
①、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
②、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
③、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
我们都知道,操作系统中多个进程之间如果不进行协调就很容易出现死锁的情况,死锁的四个条件:互斥、占有等待、非剥夺、循环等待。我们只要破坏其中一个条件就能避免死锁发生。线程之间也容易出现死锁,下面这个例子就演示了死锁的情况:
1 package com.sync; 2 3 import com.thread.SleepTest; 4 5 6 public class TestDeadLock implements Runnable{ 7 int flag = 1; 8 static Object o1 = new Object(); 9 static Object o2 = new Object(); 10 public void run() { 11 System.out.println(flag); 12 if(flag == 1){ 13 synchronized (o1) { 14 try { 15 Thread.sleep(1000); 16 } catch (InterruptedException e) { 17 e.printStackTrace(); 18 } 19 synchronized (o2) { 20 System.out.println("1"); 21 } 22 } 23 } 24 if(flag == 0){ 25 synchronized (o2) {//锁住某个对象,相当于占有该对象不让其他人使用 26 try { 27 Thread.sleep(1000); 28 } catch (InterruptedException e) { 29 e.printStackTrace(); 30 } 31 synchronized (o1) { 32 System.out.println("0"); 33 } 34 } 35 } 36 } 37 public static void main(String[] args) { 38 TestDeadLock t1 = new TestDeadLock(); 39 TestDeadLock t2 = new TestDeadLock(); 40 t1.flag = 1; 41 t2.flag = 0; 42 new Thread(t1).start(); 43 new Thread(t2).start(); 44 } 45 }
运行程序输出1 0后就进入死锁状态,该程序永远也不会停止,因为两个线程同时处于等待状态。线程t1锁住了o1对象,等待o2对象,而线程t2锁住o2等待o2对象,谁也不让谁,这就进入了一个循环占有等待的情况了,死锁也就出现了。
所以,如果多个线程如果不进行协调的话很容易出现死锁的问题。操作系统中使用进程调度来协调各个进程,那么java重如何对各个线程进行协调呢?
java中主要使用Object类中的wait()、notify()、notifyAll()方法来协调各个线程。典型的例子有哲学家吃饭问题、生产者和消费者问题、理发师问题。下面一个用一个例子来演示生产者和消费者问题。
问题描述:生产者负责做馒头,做好馒头后放进指定的篓子里面,消费者消费该篓子里面的馒头。篓子里只能装一定量的馒头,满了以后生产者必须进入等待状态,消费者吃完馒头后也必须进入等待状态。
1 package com.sync; 2 3 public class ProductAndConsumer { 4 public static void main(String[] args) { 5 Basket b = new Basket(); 6 Product p = new Product(b); 7 Consumer c = new Consumer(b); 8 new Thread(p).start(); 9 new Thread(c).start(); 10 } 11 } 12 13 class ManTou{ 14 int id; 15 public ManTou(int id) { 16 this.id = id; 17 } 18 @Override 19 public String toString() { 20 return "ManTou"+id; 21 } 22 } 23 24 //装馒头的篮子 25 class Basket{ 26 int index = 0; //相当于栈顶指针 27 ManTou[] manTous = new ManTou[6]; 28 //往篮子里面放馒头 29 public synchronized void push(ManTou m){ 30 while(index == manTous.length){ 31 try { 32 this.wait(); 33 } catch (InterruptedException e) { 34 e.printStackTrace(); 35 } 36 } 37 this.notify(); 38 manTous[index] = m; 39 index++; 40 } 41 //往篮子里面取馒头 42 public synchronized ManTou pop(){ 43 while(index == 0){ 44 try { 45 this.wait(); 46 } catch (InterruptedException e) { 47 e.printStackTrace(); 48 } 49 } 50 this.notify(); 51 index--; 52 return manTous[index]; 53 } 54 } 55 //生产者 56 class Product implements Runnable{ 57 Basket basket; 58 public Product(Basket basket) { 59 this.basket = basket; 60 } 61 public void run() { 62 for (int i = 0; i < 20; i++) { 63 ManTou m = new ManTou(i); 64 basket.push(m); 65 System.out.println("生产了"+m); 66 try { 67 Thread.sleep(1); 68 } catch (InterruptedException e) { 69 e.printStackTrace(); 70 } 71 72 } 73 } 74 } 75 76 //消费者 77 class Consumer implements Runnable{ 78 Basket basket; 79 public Consumer(Basket basket) { 80 this.basket = basket; 81 } 82 public void run() { 83 for (int i = 0; i < 20; i++) { 84 ManTou m = basket.pop(); 85 System.out.println("消费了"+m); 86 try { 87 Thread.sleep((int)(Math.random()*1000)); 88 } catch (InterruptedException e) { 89 e.printStackTrace(); 90 } 91 } 92 } 93 }
wait()、notify()、notifyAll()方法的作用:
wait():导致当前的线程等待,直到其他线程调用此对象的 notify()
方法或 notifyAll()
方法。
notify():唤醒在此对象监视器上等待的单个线程。
notifyAll():唤醒在此对象监视器上等待的所有线程。
wait()与sleep()的区别:
两个方法的共同点就是让当前线程进入等待状态。
不同点:
wait()之后,锁就不归我所有了,必须等醒过来后才能拥有该锁,并且必须要有人唤醒它才会醒过来
sleep()不同,锁还是归我所有,一段时间后会自动醒过来