Java多线程(二)
一、线程的七种状态
1、初始状态(New):新创建了一个线程对象。2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:1)等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)2)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。3)其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。下面为线程中的 7 中非常重要的状态:(有的书上也只有认为前五种状态,而将“锁池”和“等待池”都看成是“阻塞”状态的特殊情况,这种认识也是正确的,但是将“锁池”和“等待池”单独分离出来有利于对程序的理解)
二、线程的优先级
线程的优先级代表该线程的重要程度,当有多个线程同时处于可执行状态并等待获得 CPU 时间时,线程调度系统根据各个线程的优先级来决定给谁分配 CPU 时间,优先级高的线程有更大的机会获得 CPU 时间,优先级低的线程也不是没有机会,只是机会要小一些罢了。你可以调用 Thread 类的方法 getPriority() 和 setPriority()来存取线程的优先级,线程的优先级界于1(MIN_PRIORITY)和10(MAX_PRIORITY)之间,缺省是5(NORM_PRIORITY)。
1 public class ThreadPriority { 2 public static void main(String[] args) { 3 Thread t1 = new MyThread1(); 4 Thread t2 = new Thread(new MyThread2()); 5 t1.setPriority(10); 6 t2.setPriority(1); 7 t1.start(); 8 t2.start(); 9 } 10 } 11 12 class MyThread1 extends Thread { 13 @Override 14 public void run() { 15 for (int i = 0; i < 10; i++) { 16 System.out.println("线程 1 第 " + i + "次执行!"); 17 try { 18 Thread.sleep(100); 19 } catch (InterruptedException e) { 20 e.printStackTrace(); 21 } 22 } 23 } 24 } 25 26 class MyThread2 implements Runnable { 27 @Override 28 public void run() { 29 for (int i = 0; i < 10; i++) { 30 System.out.println("线程 2 第 " + i + "次执行!"); 31 try { 32 Thread.sleep(100); 33 } catch (InterruptedException e) { 34 e.printStackTrace(); 35 } 36 } 37 } 38 }
三、线程的同步
由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题。Java 语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问。 由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是 synchronized 关键字,它包括两种用法:synchronized 方法和 synchronized 块。
1、synchronized 方法:通过在方法声明中加入 synchronized 关键字来声明 synchronized 方法。 如:public synchronized void synMethod() { //方法体 }2、synchronized 块:通过 synchronized 方法来声明 synchronized 块 。如:
synchronized(syncObject) { //允许访问控制的代码 }
下面是例子:
1 public class Synchronized { 2 3 public static void main(String[] args) { 4 SyncThread t = new SyncThread(); 5 new Thread(t).start(); 6 new Thread(t).start(); 7 new Thread(t).start(); 8 new Thread(t).start(); 9 } 10 11 } 12 13 class SyncThread implements Runnable { 14 15 int num = 10; 16 17 @Override 18 public void run() { 19 synchronized(this) { 20 while(num > 0) { 21 try { 22 Thread.sleep(100); 23 } catch (InterruptedException e) { 24 e.printStackTrace(); 25 } 26 System.out.println(Thread.currentThread().getName() + " : this is " + num --); 27 } 28 } 29 } 30 31 }
输出结果:
Thread-0 : this is 10 Thread-0 : this is 9 Thread-0 : this is 8 Thread-0 : this is 7 Thread-0 : this is 6 Thread-0 : this is 5 Thread-0 : this is 4 Thread-0 : this is 3 Thread-0 : this is 2 Thread-0 : this is 1
四、线程的阻塞
为了解决对共享存储区的访问冲突,Java 引入了同步机制,现在让我们来考察多个线程对共享资源的访问,显然同步机制已经不够了,因为在任意时刻所要求的资源不一定已经准备好了被访问,反过来,同一时刻准备好了的资源也可能不止一个。为了解决这种情况下的访问控制问题,Java 引入了对阻塞机制的支持。
阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪),学过操作系统的同学对它一定已经很熟悉了。Java 提供了大量方法来支持阻塞,下面让我们逐一分析:
1、sleep() 方法
sleep() 允许 指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到 CPU 时间,指定的时间一过,线程重新进入可执行状态。
典型地,sleep() 被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段时间后重新测试,直到条件满足为止。
2、suspend() 和 resume() 方法
两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的 resume() 被调用,才能使得线程重新进入可执行状态。
典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。
3、yield() 方法
yield() 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。
4、wait() 和 notify() 方法
两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种允许 指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的notify() 被调用 。
关于 wait() 和 notify() 方法最后再说明两点:
1、调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。
2、除了 notify(),还有一个方法 notifyAll() 也可起到类似作用,唯一的区别在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。