c3 Threads - Thread Scheduling

当有多个线程同时可以使用时,那就必须考虑到线程调度问题。需要保证每次线程至少有一点时间可以去run,重要的线程有更多的时间去run。

实际上,避免饿死要比避免错误同步或者死锁要容易得多。


Properties

并非每个创建的线程都是“同等”的,线程有属性,属性值从0-10,vm执行时,当有多个线程可执行时,vm会只执行属性值最高的。默认线程属性值是5,可设置。


一般会把1,5,10赋给下面3个变量

public static final int MIN_PRIORITY  = 1;
public static final int NORM_PRIORITY = 5;
public static final int MAX_PRIORITY  = 10;


并非所有的系统都支持11种不同的properties,比如Windows只支持7种:0,1 2,3 4,5,6 7,8 9,10 。1和2, 3和4,6和7,8和9是equael的。也就是说9并不比8更高!


设置线程的properties

public final void setPriority(int newPriority)

如果newPriority大于最大的,或者负数,报异常IllegalArgumentException。


Preemption

每个vm都有线程调度器以在可以运行线程时决定运行哪个线程。

主要有2种调度器:抢占式(preemptive)和合作式(cooperative)。

抢占式:当需要时暂停正在执行的线程,切换到其他线程。

合作式:当正在执行的线程自己暂停时,才切换到其他线程。

运用合作式的方式更容易导致“饿死”。


所有的jvm都使用“抢占式”来调度,当一个低properties的线程正在运行时,一个高properties的线程ready to run,调度器会“很快”使低properties的线程暂停,而去使高properties的线程运行!


比较棘手的情况是当可以运行的线程的properties是一样的的时候,调度器该怎么办?抢占式的偶尔会让正在运行的线程暂停,让别的同properties的线程跑一会,而合作式的就不同了,它会一直等,直到正在运行的线程自己暂停或者到达某个停止点,如果这些情况没有出现,那别的线程只能都饿死了。所以让你所有的线程都周期性的暂停一下,以让其他线程有机会执行是很有必要的。


note:如果你基于一个使用抢占式调度算法的VM开发程序的话,很难会碰到饿死的现象。但在你的机器上不发生,不代表在客户上的机器上也不发生,比如,客户的VM使用的调度算法是合作式的呢?尽管现在大部分已使用抢占式,但老式的VM还是使用合作式,又或者有特殊目的的VM呢,比如嵌入式设备。


这里有10种方式表明一个可以暂停或者已准备好暂停。

<span style="font-size:18px;">• It can block on I/O.
• It can block on a synchronized object.
• It can yield.
• It can go to sleep.
• It can join another thread.
• It can wait on an object.
• It can finish.
• It can be preempted by a higher-priority thread.
• It can be suspended.
• It can stop.</span>

确保在你写的run()方法里上面的条件会以合理的频率出现。最后2个方法已弃用,因为会导致对象状态前后不一致的情况。


Blocking

blocking会发生在任何一个需要停下来等待它需要的资源的时候。网络程序最常见的被block是 I/O ,因为cpu的速度远比 network 和 disk的速度快。网络程序常在接受数据或发送数据时发送阻塞,即使这个时间只有多少毫秒,那也够其他线程做很显著的工作了。


线程还可能被synchronization代码块或者同步方法阻塞。如果没有拿到lock,那线程就会停止等待,直到拿到lock,永远拿不到就永远等待。


如果线程既没有被 I/O 阻塞,也没有被阻塞在lock上,那它会释放所有已经持有的lock。对 I/O block来说没什么大事,因为要么unblock,线程继续,要么抛出IOException,线程退出同步代码块或方法,释放所有locks。然而如果是阻塞在同步代码块或者方法上,谁也不放弃也有的lock,都在傻等对方有的lock,那结果就是死锁!


Yieling

使线程放弃cpu控制权的另一个方法是显式地调用yield,Thread.yield()!告诉jvm,如果现在有线程可以运行,那就去取运行它们吧,我暂时放弃运行的权利了!但在一些实时运行的jvm上,会忽略该方法。


在Yieling前,线程要确保它或与它相关的Runnable对象有前后一致的状态,因为该状态可能会被其他线程使用。Yielding不会使线程放弃已有的lock,所以,在yield前,要把持有的同步的lock,release掉。如果唯一等待运行的线程被block了,原因是它需要的lock被刚才yield的线程拿着呢,那这个线程就会block在那,而cpu执行权会又回到刚才yield的线程处,那这就违背了yield的目的了。


yield的方式很简单,比如在一个infinite的loop中,调用Thread.yield(),给其他同properties的线程运行的机会。

<span style="font-size:18px;">public void run() {
  while (true) {
    // Do the thread's work...
    Thread.yield();
  }
}</span>


Sleeping

yielding是表明一个线程愿意暂停,以给其他同properties的线程运行的机会。而sleeping则是一个将要暂停,不管有没有其他线程在等着执行。sleeping后,其他同properties或low properties的线程都有机会执行。线程sleep,会保持已经获得的lock,所以避免在同步代码块中sleep,因为其他线程拿到cpu控制权后发现需要的lock被sleep的线程拿着,那它会被block。


要使线程sleep的api是:

<span style="font-size:18px;">public static void sleep(long milliseconds) throws InterruptedException
public static void sleep(long milliseconds, int nanoseconds)
    throws InterruptedException</span>

现代cpu的时钟接近毫秒的准确度,但纳秒的很少。并不能保证在任何vm上都能达到想要的sleep时间。如果本地硬件不支持需要的那个水平的时间准确度,那就用可计算的最接近的时间值。

可能线程在设定的时间没有wake up,只是因为VM在忙其他事,也可能,还没到设定的时间,就被其他线程wake up了。

<span style="font-size:18px;">public void interrupt()</span>

一个线程在sleep,并不表示其他work的线程就不能与其交互了,调用sleep线程的 interrupt()方法,sleep线程会抛出InterruptedException,然后就醒来,和正常线程没有区别了。在一个infinite loop thread 在sleep时,调用sleep线程的interrupt()方法,可以将其唤醒,infinite loop is broken,run method finished ,the thread dies.


如果一个线程在I/O操作上block了,interrupt()这个线程的效果与平台有关。通常情况下,是没有效果的,线程继续被blocked。如果你的程序需要可中断的I/O,那就要仔细考虑,使用非阻塞的I/O,而不是Streams。不像streams,buffere和channel明确被设计来支持中断的,当在read或write被block时。


Joining

一个线程经常需要其他线程的结果。比如,一个web浏览器在主线程加载html时,会spawn一个子线程去下载图片,如果图片的标签中没有heigh和width的话,那主线程需要等待所有的图片下载完成后才能去显示他们。

<span style="font-size:18px;">public final void join() throws InterruptedException
public final void join(long milliseconds) throws InterruptedException
public final void join(long milliseconds, int nanoseconds)
    throws InterruptedException</span>

第一个方法会无限期的等待joined线程完成,其他方法等待指定的时间,时间到,即使joined线程未完成也会接着做自己的事。


the joinging thread(调用join方法的thread)会等待the joined thread(方法join被调用的thread)完成。

下面的例子中,求一个数组中的最大,最小,中间值。main thread是joining thread,SortThread是the joined thread。main thread 会等st执行完了,再执行,然后输出最大,最小,中间值。如果把注释的代码打开,那就像sleep的thread被interrupt一样,the joining thread也会被interrupt,抛出InterruptionException。

<span style="font-size:18px;">	public static void main(String[] args)  {
		
//		final Thread main = Thread.currentThread();
		
		Double[] arrays = new Double[10000];
		for(int i=0;i<arrays.length;i++){
			arrays[i] = Math.random();
		}
		
		SortedThread st = new SortedThread(arrays);
		st.start();
		
//		Thread t = new Thread(new Runnable() {
//			
//			@Override
//			public void run() {
//				try {
//					Thread.sleep(10);
//				} catch (InterruptedException e) {
//					// TODO Auto-generated catch block
//					e.printStackTrace();
//				}
//				
//				main.interrupt();
//			}
//		});
//		t.start();
		
		try {
			st.join();
			System.out.println("min:"+arrays[0]);
			System.out.println("max:"+arrays[arrays.length-1]);
			System.out.println("mid:"+arrays[arrays.length/2]);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		
	}</span>

class SortedThread extends Thread{
	private Double[] arr;
	public SortedThread(Double[] arr){
		this.arr = arr;
	}
	@Override
	public void run() {
		Arrays.sort(arr,new Comparator<Double>(){

			@Override
			public int compare(Double o1, Double o2) {
				// TODO Auto-generated method stub
				if(o1 > o2){
					return 1;
				}else if(o1 < o2){
					return -1;
				}else{
					return 0;
				}
			}
			
		});
	}
}


note:jonin可能不如在jdk 5 之前那么重要了,因为可以用Executor和Future代替了,后者使用起来更方便。


Waiting on an Object

一个线程可以在一个它已经lock住的对象上等待。在等待时,线程会释放该对象的lock然后会一直pause,直到该对象被其他线程notified。其他线程可以改变object,然后notify在该object上等待的线程,notify后自己继续执行。wait不同于join,在等待线程和唤醒线程间没有谁必须等对象结束了才能继续执行,waiting暂停一个线程的执行直到某个对象或资源达到某个状态,joining暂停一个线程的执行直到另一个线程执行完!


在一个对象上Waiting可使一个线程pause,知道这个的人并不多,因为没有调用任何Thread的方法。一个想要通过waiting object pause的线程,先要通过synchronized获得该object的lock,然后调用object的以下方法:

public final void wait() throws InterruptedException
public final void wait(long milliseconds) throws InterruptedException
public final void wait(long milliseconds, int nanoseconds)
    throws InterruptedException

上述方法一被调用,在其上waiting的线程即释放掉该object的lock,进入sleep状态,直到有以下条件之一发生:

• The timeout expires.
• The thread is interrupted.
• The object is notified.

timeout就像sleep,join一样,时间到后,线程恢复执行, 从wait()后开始。但是,如果不能立马获得object的lock的话,会被block,等待获得lock。

interrupt也像sleep、join一样,其他线程调用正pause线程的interrupt()方法,抛出InterruptionException,从Catch处接着执行,但仍需要先获得object的lock,在获得lock前不会抛出异常,所以,可能其他线程调用interrupt()后,waiting线程会被blocked一段时间。

第三种方式,notify,要调用waiting线程所wait的对象的notify,或notifyAll方法,其他线程在调用这2个方法前,要先获得object的lock,并要synchronized,notify会在等待线程中随机选一个,notifyAll会使所有在该对象上等待的线程都恢复工作,waiting线程所等待的对象呗notify后,waiting线程会从wait()后开始执行。


现在有一个这样的例子,一个线程从网络上读取一个文件,这个文件先读到的部分是Manifest文件,而另一个线程对Menifest很感兴趣,它希望先拿到Manifest的数据,等整个文件都接收完成了再读其他数据。

ManifestFile m = new ManifestFile();
JarThread    t = new JarThread(m, in);
synchronized (m) {
  t.start();
  try {
    m.wait();
    // work with the manifest file...
  } catch (InterruptedException ex) {
    // handle exception...
  }
}

JarThread

		ManifestFile theManifest;
		InputStream in;
		public JarThread(Manifest m, InputStream in) {
		  theManifest = m;
		  this.in= in;
		}
		@Override
		public void run() {
			  synchronized (theManifest) {
			    // read the manifest from the stream in...
			    theManifest.notify();
			  }
			  // read the rest of the stream...
		}



在相同对象上的waiting和notification在多线程编程中是很常见的。

现在有有一个这样的例子:多线程处理log,用另一个专门的线程读取log,并放在List中,处理log的线程从List中获取log。

	public static void main(String[] args)  {
		
		List<Log> logList = new LinkedList<Log>();
		
		Thread[] proArr = new Thread[3];
		for(int i=0;i<proArr.length;i++){
			proArr[i] = new ProcessLogThread(logList);
			proArr[i].start();
		}
		
		ReadLogThread rt = new ReadLogThread(logList, proArr);
		rt.start();
		
	}

class ReadLogThread extends Thread{
	private List<Log> logList;
	private Thread[] processArr;
	private int logPoint;
	
	public ReadLogThread(List<Log> logList,Thread[] processArr){
		this.logList = logList;
		this.processArr = processArr;
	}
	
	@Override
	public void run() {
		while(true){
			Log log = getNextLog();
			
			if(null == log){
				while(logList.size() > 0){} //如果此时还有log没有被处理,那就等着处理,尽管这种情况不大可能发生
				for(Thread p : processArr) p.interrupt(); //打断所有pause的线程
				break;
			}
			
			synchronized (logList) {
				logList.add(log);
				logList.notifyAll();
			}
		}
	}

	private Log getNextLog() {
		
		if(logPoint >= 1000) return null;
		
		return new Log("log"+logPoint++);
	}
}

class ProcessLogThread extends Thread{
	
	private List<Log> logList;

	public ProcessLogThread(List<Log> logList){
		this.logList = logList;
	}

	@Override
	public void run() {
		while(true){
			synchronized (logList) {
				while(logList.isEmpty()){ //这里使用while的原因是当线程被notify后,获得logList的lock时可能logList
										//中已经没有log了,被其他线程处理完了,所以判断如果没有了,那就继续wait
					try {
						logList.wait();
					} catch (InterruptedException e) {
						return;
					}
				}
				
				Log log = logList.remove(logList.size()-1);
				System.out.println(Thread.currentThread().getName()+" process log:"+log.getLog());
			}
		}
	}
	
}

class Log {
	private String log;
	
	public Log(String log){
		this.log = log;
	}
	
	public String getLog(){
		return log;
	}
}

这里我曾考虑用“观察者模式”实现当List中添加数据了,就通知处理线程去工作,但观察者的本质是“回调”,实际执行处理的还是通知的线程,所以观察者在这里不合适。


Finish

最后的一个线程放弃cpu执行权的是finishing !当run()方法结束,线程dies,其他线程接管。另外,如果你的线程每次都非常快的都结束了,以致都没有blocking,那有必要spawn一个线程吗,为啥不用个方法呢?vm创建销毁一个线程开销也是很大的!


++++++++++++++++++++++++++++++++++++++++++++++++

现在有一个任务,要求线程B在执行时需要线程A产生的某个结果,线程B只要线程A中间的某个结果,并不需要线程A执行完毕,因此没必要用join(因为the joinging thread(调用join方法的thread)会等待the joined thread(方法join被调用的thread)完成),这里可以在线程B需要资源时调用某个唯一对象的wait,然后等待线程A产生资源后,再调用同一对象的notify方法。

	class TaskA implements Runnable{
		
		private MyData myData;

		public TaskA(MyData myData){
			this.myData = myData;
		}

		@Override
		public void run() {
			// TODO Auto-generated method stub
			synchronized (myData) {
				try {
					System.out.println("TaskA will wait...");
					myData.wait();
					System.out.println("TaskA Inside1 "+myData.data);  //当调用myData.notify()后,执行myData.wait()后面的语句
				} catch (InterruptedException e) {
					//如果执行该Task的线程被interrupt,则执行catch中的内容
					e.printStackTrace();
					System.out.println("TaskA Inside2 "+myData.data);  
				}
			}
		}
		
	}
	
	class MyData{
		public int data = 0;
	}

	public static void main(String[] args) {
		
		MyData myData = new MyData();
		TaskA ta = new TaskA(myData);
		new Thread(ta).start();
		
		System.out.println("main thread is sleeping...");
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		
		myData.data = 3;
		synchronized (myData) {
			myData.notify();
		}
	}


Sleep和Yield比较

同:都是Thread的静态方法,都可以让渡cpu执行权

异:在让渡cpu执行权方面,可以指定一定的时间,sleep更好用,如果调用yield的目的是让渡cpu,完全可以sleep(1)来代替。   sleep不会让当前执行线程在sleeping时放弃很多monitor。   当sleep的线程被interrupt时抛出InterruptedExcepiton ,而yield不会抛任何异常。

所以,可以忘记yield,使用sleep吧。






  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值