Java的Thread机制可以类比进程,可让几个操作同时执行,详情googl:time sharing。
线程架构图:代表一个线程持有CPU资源,代码资源和数据资源
Java中想让某种操作具有线程能力有两种方式:
extends Thread和implements Runnable,重载run 方法,在里面实现想要的操作:
请看码:
public class TestThread { public static void main(String[] args) { new TestThread().testThreadRun(); } /** * extends Thread 方式 */ class T1 extends Thread{ private String name; public T1(String name) { this.name = name; } @Override public void run() { for (int i =0; i<10; i++) { System.out.println(this.name + " print " + i); } } } /** * implements Runnable 方式 */ class T2 implements Runnable { private String name; public T2(String name) { this.name = name; } @Override public void run() { for (int i =0; i<10; i++) { System.out.println(this.name + " print " + i); } } } public void testThreadRun() { T1 t1 = new T1("Oham"); T2 t2 = new T2("Lulu"); // 继承Thread的直接调用start, 线程进入Runnable状态 t1.start(); // 实现Runnable的对象需要作为new出的Thread对象的构造,由Thread对象start, 线程进入Runnable状态 new Thread(t2).start(); }
调用start后线程状态如下:
调用start后线程并不是立刻进入Running状态,而是可执行状态,等待CPU的调度,当得到CPU分配的时间片线程才能进入Running状态,这个过程是系统自身的过程,我们无法干涉(控制CPU分配时间片到某个线程)。
进入Running状态后线程执行run中的操作,当run执行完毕后线程进入死亡状态,无法再回到其他状态了,线程在非死亡状态时只能调用一次start方法。
-------------------------------------------------------------------------------------------------------------------------------------
暂停线程执行
1.sleep方法
正如其名,就是让Thread对象睡睡觉,此方法需要传入一个时长参数,单位为毫秒,表示过一段时间再恢复可执行状态,注意是可执行状态(Runnable),若指定时长为1秒,是指暂停时长为1秒,暂停完毕后进入可执行状态,等待CPU的调度,所以到执行状态(Running)其实已经超过一秒。
class TestSleep implements Runnable { @Override public void run() { while (true) { try { System.out.print("**"); //调用sleep方法将使Thread进入“暂停”(blocked)状态 Thread.sleep(3000); System.out.println("暂停3秒"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
2.yield方法,让某一执行当中的线程交出CPU时间片,进入可执行状态,不过可能又立刻被分配到CPU时间片。。。
修改前面的T1run方法:
class T1 extends Thread{ private String name; public T1(String name) { this.name = name; } @Override public void run() { for (int i =0; i<10; i++) { System.out.println(this.name + " print " + i); //让出当前执行线程的时间片,进入Runnable状态 Thread.yield(); } } }
试重新运行结果。。。不好评价。。。,可以把for的循环设置为10000试试,效果更佳。
3.join 有甲乙两个线程,要求甲线程必须等待乙线程执行玩后才能执行,这时我们用到join。
借书上一个例子:时间不早妈妈开始煮饭,于是进厨房开始准备,去发现米酒用完了,所以叫儿子去巷口士多买瓶米酒回来,等到买回来了妈妈才又开始煮饭,直至饭煮好。
这里的例子用到两个线程,mother和son,中间儿子去买酒,mother线程必须等待son线程执行完毕后才往下执行,这里就用到join,从方法字面理解,join——参与,也就是说son线程参与到mother线程当中,相当于把son线程的run操作放到mother线程的run操作中去,合并为一个线程操作(是相当于,其实里头还是两个线程,只是效果如是而已)。
package test; public class MotherThread implements Runnable { class SonThread implements Runnable { @Override public void run() { System.out.println("儿子出门买米酒"); System.out.println("儿子出门需要5分钟"); try { for(int i=1; i<=5; i++) { Thread.sleep(1000); System.out.print(i + "分钟 "); } } catch (InterruptedException e) { System.err.println("儿子粗大事了"); } System.out.println("\n儿子买酒回来了"); } } @Override public void run() { System.out.println("妈妈准备煮饭"); System.out.println("妈妈发现米酒用完"); System.out.println("妈妈叫儿子去买米酒"); Thread son = new Thread(new SonThread()); son.start(); System.out.println("妈妈等待儿子把米酒买回来"); try { //son线程调用join是mother线程挂起进入Blocked状态,等待son线程执行run操作完毕 son.join(); } catch (InterruptedException e) { System.err.println("发生异常"); System.err.println("妈妈中断煮饭"); System.exit(1); } System.out.println("妈妈开始煮饭"); System.out.println("饭煮好了"); } public static void main(String[] args) { new Thread(new MotherThread()).start(); } }
状态图:
------------------------------------------------------------------------------------------------------------------------------------
同步处理
不同的线程之间,线程支配的资源:CPU资源,代码资源和数据资源,除了CPU外其他的都可以共用,代码是写好的,共用没问题,但数据共用就有问题了,因为不同的线程获得CPU时间片不可预计,当其中涉及到对数据操作时,对单个线程而言其数据已经造成混乱。
首先给共享代码,但没有共享数据的两个线程:
package test; public class TestShare { class ShareData implements Runnable { int i; @Override public void run() { while(i < 10) { i++; for(int j=0;j<10000000; j++); System.out.println(Thread.currentThread().getName() + ":" + i); } } } public void goTest() { //共享代码,没有共享数据 ShareData s1 = new ShareData(); ShareData s2 = new ShareData(); Thread t1 = new Thread(s1); Thread t2 = new Thread(s2); t1.setName("Oham"); t2.setName("Lulu"); t1.start(); t2.start(); } public static void main(String[] args) { new TestShare().goTest(); } }
运行结果是对于单个线程而言依次打印出i 1至10。若将goTest改为:
public void goTest() { //共享代码,共享数据 ShareData s = new ShareData(); Thread t1 = new Thread(s); Thread t2 = new Thread(s); t1.setName("Oham"); t2.setName("Lulu"); t1.start(); t2.start(); }
结果是对整个结果而言,i没有依次输出1至10。原因是两个线程争夺CPU时间片的结果把i在打印出之前或之时就已i++。就是因竞争CPU时间片说代码段被分割执行了。
为了防止多线程情形下代码被分割执行,这里需要锁机制——synchronized:
语法:
sychronized(要取得锁的对象)——问谁取得锁
{要锁定保护的代码}
修改上述run方法:
public void run() { while(i < 10) { //未取得锁的线程进入锁定池等待 synchronized (this) { i++; for(int j=0;j<10000000; j++); System.out.println(Thread.currentThread().getName() + ":" + i); } } }
从结果可以看出对于整体结果而言,i是依次输出1至10了,只是两个线程交互执行。
一点细节:受synchronized保护的代码段中,要访问的对象应该设置为private,否则可以通过其他的方式访问该对象了,这样synchronized好像失去了实质意义。
状态图:
如果一个线程要执行某段锁定的程序代码,但它没有取得指定的锁,那么该线程就会从执行状态进入锁定池中等待,当获取到锁后,它会转移到可执行状态(Runnable)。
Java的锁定机制若是滥用,很可能会造成系统进入死锁状态,如下图情况是死锁的一种:
预防死锁状态,Java有种Monitor Model的机制,除了预防死锁,还保证共用数据的一致状态。
看一个经典的生产者与消费者问题:
生产者生产东西,不可能无节制地生产下去,因为库存量的限制,所以未到达一定的库存量时,生产者会继续生产;当达到生产量时库存量时,生产者就等待消费者用掉库存,而消费者看到有库存的时候才会进行消费;如果库存量没有了,消费者就等待生产者生产出东西来。
Storage.java
package test; public class Storage { private int count; //记录库存量 private int size; //库存量上限 public Storage(int s) { size = s; } /** *供生产者调用,生产被执行时累加库存量 */ public synchronized void addData(String producer) { //检查库存量是否达到上限,如果是则此对象调用wait方法, //持有该对象锁的Running状态的线程就进入等待状态(Wait pool) while(count == size) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //通知其他正在等待的消费者,在wait pool中挑选任一线程,使其进入Lock Pool当中 this.notify(); count++; System.out.println(producer + " make data count: " + count); } public synchronized void delData(String producer) { //与addData相对,检查库存量是否为零,如果是则 //持有该对象锁的Running状态的线程就进入等待状态, //顺便一提,此处用while是因为调用notify唤醒wait pool中的线程 //时是任意挑选的,且或是生产者,或是消费者被调醒,醒后又 //不会立刻执行。有可能别的线程先执行了,所以有必要再检查一次。 //(某一线程被wait,它的执行流程停在wait方法处,当被唤醒重新进入 //Running状态时,执行流程从wait调用之后开始,而不是重新来过一遍) while(count == 0) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } this.notify(); count--; System.out.println(producer + " use data count: " + count); } }
Producer.java
package test; public class Producer extends Thread { private String name; private Storage storage; public Producer(String name, Storage storage) { this.name = name; this.storage = storage; } @Override public void run() { while(true) { storage.addData(name); try { Thread.sleep((int)Math.random()*3000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
Consumer.java
package test; public class Consumer extends Thread { private String name; private Storage storage; public Consumer(String name, Storage storage) { this.name = name; this.storage = storage; } @Override public void run() { while(true) { storage.delData(name); try { Thread.sleep((int)Math.random()*3000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
执行测试代码:
Storage storage = new Storage(5); Producer p1 = new Producer("Oham", storage); Producer p2 = new Producer("Lulu", storage); Consumer c = new Consumer("Cancan", storage); p1.start(); p2.start(); c.start();
注意:wait与notify必须在sychronized的代码块内调用,否则运行时抛java.lang.IllegalMonitorStateException。
wait是使得持有特定锁A的当前Running的线程进入wait pool,而notify是针对该特定锁A去notify,若是针对另外的锁B去notify,那么将不会去唤醒针对特定锁A的在wait pool当中的线程,而是唤醒另外的锁B对应的wait pool。
wait 与 notify 要用于相对而言的业务逻辑当中才能发挥Monitor Model的机制的作用,即一方wait的条件成立,另一方的notify条件必须成立,一方notify条件成立,另一方wait条件同时成立。如此才能预防死锁状态和保证共用数据的一致状态。
状态图: