Java并发--多线程(To be Cont...)

      JDk1.5中增加了新的支持多线程的包 java.util.concurrent。

关键字---并发(Concurrency)

      并发或者并行,对有操作系统基础知识的人并不陌生,并发可以是系统级的,如今的大多数操作系统都是多任务系统,多个任务或者是抢占式的,或者是通过轮循CPU时间片的方式运行,使得用户看上去好象是多个任务在同时执行。

关键字--任务(Task)进程(Process)线程(Thread)

      任务是一个一般性的术语,指由软件完成的一个活动。一个任务既可以是一个进程,也可以是一个线程。简而言之,它指的是一系列共同达到某一目的的操作。例如,读取数据并将数据放入内存中。

      进程常常被定义为程序的执行。可以把一个进程看成是一个独立的程序,在内存中有其完备的数据空间和代码空间。一个进程所拥有的数据和变量只属于它自己。

      线程是某一进程中一路单独运行的程序。也就是说,线程存在于进程之中。一个进程由一个或多个线程构成,各线程共享相同的代码和全局数据,但各有其自己的堆栈。线程是比进程更小的单位。

      同时运行的各个进程都有独立的内存空间,多个进程之间不会互相影响,也就是我们为什么可以在Windows上开多个相同的程序而不互相影响的原因。线程则不同,多个线程可以在一个进程中共享数据。

 

Many face of concurrency

      “并发有很多的面孔”,这是Bruce Eckel 说的,也就是说它是嬗变的,因为它要解决很多的问题。所以在使用多线程的时候,我们必须知道到底都发生了些什么,因为有时候程序的运行结果会出人意料。处理线程多问题,主要处理两方面的问题,速度与设计可管理性。

      运行速度

      假如只有一个处理器,是让多个任务按顺序连续执行快呢,还是让多个任务轮循这个处理器快呢?表面上看来是顺序执行快,因为轮循的话任务之间的转换是需要时间开销的 ,那为什么还要使用多线程呢?让我们找一个没人的角落,脱下鞋子,用脚趾头想一想吧。( 参考以下关键词:上下文转换(Context switch), 阻塞(Blocking), 如果还想不通,请参考计算机组成原理与体系结构方面的书籍。)

 

      Why Tread?

      既然多任务或多进程系统可以实现并发,为什么还要线程呢?因为有些操作系统本身不是多任务系统,而JAVA的跨平台性必须为在所有的平台在提供兵法方案提供实现,因此JAVA采用多线程的方式实现并发。

 

      JAVA的多线程是抢占式(preemptive)的,意思是调度机制会为每个线程提供时间片,并且通过强制中断来转换到下一个线程。抢占式的实现方式对线程的个数有一个限制。与其相对的是协作式(cooperative)的,协作式的多任务系统对任务的数量是没有限制的,因为任务是自动让出资源的,并且上下文的转换成本较小。

 

      多线程的JAVA实现

      Runable  Thread

      JDK1.5提供了新的多线程实现, java.util.concurrent

      Executor CachedThreadPool  FixedThreadPool  SingleTreadExecutors

      Runable接口的Run()方法没有返回值,如果需要返回值,可以用 java.util.concurrent中的  Callable接口代替Runable接口, Executor.submit(Callable instance) 将返回一个Futrue<?>实例.

      几个重要方法:

      sleep()   wait()  yield()  jion() notify()  notifyAll()

      wait() 与 sleep() 的区别

      priority: 对于不同的操作系统而言,线程的权限可能不同。线程的权限越高,其被执行的频率越高,但并不意味权限越高的线程越先执行,否则会造成线程死锁,即权限低的线程永远不会被执行。

      守护线程(Deamon Thread):一个线程调用setDaemon(true)方法可以被设置为守护线程。所谓守护线程即当用户线程执行的时候,它会在后台执行,当没有用户线程活动的时候它自动停止。JVM的垃圾挥回收线程就是一个典型的守护线程,当我们的程序中不再有任何运行的Tread的时候,程序就不会产生垃圾,垃圾回收器就无事可做,所以当垃圾回收线程是java虚拟机上仅剩的线程的时候,JVM会自动离开。

 

      使用内部类

      有时候你不希望别人从外部访问你的多线程代码,这时你可以使用内部类把你的多线程代码隐藏起来。

 

public class TestThread {

	private Thread t;
	private int count = 5;

	TestThread(String threadName) {
		t = new Thread(threadName) {
			public void run() {
				while (true) {
					try {
						System.out.println(this);
						if (--count == 0) {
							return;
						}
						TimeUnit.MILLISECONDS.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}

			public String toString() {
				return Thread.currentThread().getName();
			}
		};

		t.start();
	}
}

 

 

      t.join() 主线程等待线程 t 执行完成后再执行。

 

      捕捉 run() 的异常:

      在使用 Executor 执行线程的时候,run() 方法抛出的异常是捕捉不到的,

		try {
			ExecutorService exec = Executors.newCachedThreadPool();
			exec.execute(new Thread());
		} catch (Exception e) {
			System.out.print("This can not be catched!");
		}

catch 块中的语句是永远不会被执行的,因为线程的一些特性,我们无法捕捉线程run() 方法中抛出的异常,如果必须处理这些异常,可以使用 Thread.UncaughtExceptionHandler 这个接口的特性,通过实现这个接口,并通过 java.util.TreadFactory 把它与一个线程绑在一起,在这个接口的实现类中处理这些无法捕捉的异常。(参考JDK1.5 API)

 

      资源同步(synchronized 与 同步锁 Lock):

      synchronized 关键字,可以用在方法中,可以用在同步块中。每一个对象都有一个锁,不管是同步方法还是同步块,都是给对象加锁。同一对象的同步方法正在执行时,线程不能进入这一对象的其他同步方法,直到上一个同步方法返回。JDK1.5 提供了一个更容易理解的同步机制, java.util.concurrent.locks.Lock. Lock接口的使用很简单,就象事务处理一样,在对关键资源处理之前加锁, 处理完后解锁。

		Lock lock = new ReentrantLock();
		
		public void method() {
			lock.lock();
			try {
				// To do resource
			} catch (Exception e) {
				// deal with exception
			} finally {
				// continue processing after exception captured
				// unlock
				lock.unlock();
			}
			
		}

 

      原子性(Atomicity), 可见性(Visibilaty) 与 挥发性(Volatility):

      所谓原子操作atomic,是指在一个时钟频率内完成的,不可被打断/中断的简单操作,例如对原始类型变量的赋值(long 和double除外,因为JVM允许对64位的变量赋值分为两才操作,一次写32位)。

      可见性(visibilaty)与挥发性(volatility)的概念有些相近。一般的CPU都是有Cache的,即高速缓存,即使是原子操作,也不一顶能保证当写操作完成后,变量的值对应用程序是可见的,因为写完的值可能被暂存在Cache中,不一定马上被flush到主存。为了保证可见性(visibilaty),java提供了volatile就关键字,被声明为volatile的变量,当有有写操作发生时,写完后的值会马上被flush到主存中,使得其他任务或程序对其可见.

 

      线程中断(Interruption):

      线程有以下几种状态:初始态(New), 可运行(Runnable), 阻塞(Blocked), 终止(Dead)。线程被创建的即刻为初始态,系统为线程分配必要的资源,所有的资源就位后,万事具备,只欠东风,此刻进入可运行状态,调度器可随时使线程运行或继续等待(Blocked);

      处于阻塞状态的线程不能获得CPU 时间,以下事件可以使一个线程进入阻塞状态:

      a) 调用了sleep() 方法(jion()方法等同)

      b) 调用了wait() 方法

      c) 等待同步锁,比如进入synchronized 方法

      d) 等待 I/O

 

      所谓中断是指CPU终止当前运行的任务,使其让出资源让其他任务操作。java多线程中通常情况下所说的中断,就是从线程的run() 方法跳出来. 当然最直接的方法就是等待 run()方法执行完毕自动退出。但是多数情况下我们的线程都是长任务线程,比如守护线程(伺服线程),需要长时间不间断运行。一种可选方法是通过控制变量(这个控制变量,通常是全局变量,以便程序在任何时候任何地点都可以访问到,并且对控制变量的操作应当是同步的,在java中我们可以选择使用volatile 变量)。

      通常“长线程”的run() 方法都会通过一个while循环来控制.

	 	while (true) {
					try {
						// do stm
						if (done) {
							return;
						}
					} catch (InterruptedException e) {
	
					} 
				}

 这样可以通过done的值来判断何时退出。但是控制量并不总是ok的,如果run()方法正常执行我们可以判断控制变量的值,但是会存在一些情况,线程因为调用了wait()方法,或者其他原因而进入了阻塞状态,那么就可能检查不到控制变量。

 

      中断与中断检查 Thread.interrupt() and  Thread.interruptded():

      interrrupt()方法并不能纯粹的中断run()方法,而是通过改变线程的中断状态而抛出 InterruptedException 异常来实现中断,与一般的方法中断是相当类似的。当线程因为调用了sleep(), wait() 方法或因为其他原因进入阻塞状态(Blocked),此时调用 Thread.interrupt() 便会抛出 InterruptedException 异常,没有进入阻塞状态的线程是不会抛出异常的。

      不管有没有抛出异常,Thread.interrupt() 都会改变中断状态,既然Thread.interrupt()改变了中断状态,我们就可以在 while 循环中通过中断状态检查来返回,从而退出while循环。但最好是同时检查中断状态和捕捉InterruptedException异常。

	public void run() {
		// TODO Auto-generated method stub
		try {
			while (!Thread.interrupted()) {
				++count;
				System.out.println("Thread running" + count);
				
				// Interruption here if sleep time in main < sleep time in run
				TimeUnit.MILLISECONDS.sleep(100);
				
				// Interruption here if sleep time in main > sleep time in run
				
				System.out.println("after sleep ...");
			}
		} catch (InterruptedException e) {
			// TODO Auto-generated catch blockt
			System.out
					.println("Thread interrtptd and Interrupted Exception catched");
		}
		System.out.println("exit from run() now..");
	}

  可以通过下面的测试来检验:

	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub

		Thread t = new Thread(new TestThread());
		t.start();
		// This sleep time is important, compare with sleep time in run()
		TimeUnit.MILLISECONDS.sleep(500);
		t.interrupt();
	}

 

      死锁(DeadLock):

      死锁是一个非常有趣的话题。有一个笑话,说一个人回家,发现钥匙被反锁在房间里了,于是他打电话叫来了开锁公司的人。开锁的人很快来了,看了看锁说,门问题,可以开,请出示身份证户口本等有效证件,主人说了,我的证件都在房间里,你打开门我就给你看,开锁师傅说,不行,你先给我看证件我才给你开锁!于是僵持不下。这就是死锁。

      可能会有误解,认为对同一资源的竞争是造成死锁的原因。对同一资源的操作进行同步,就可以解决竞争问题,不会死锁。

      死锁 DeadLock 这个词其实是由一位伟大的计算机科学的先驱--Dijkstra 提出的。最初来源与一个数学问题,5哲学家就餐问题。问题可以简单描述为:有5位哲学家坐在圆桌上,他们思考问题和吃饭,只有5只筷子,每2位哲学家中间各一只。哲学家吃饭的时候需要同时拿起左手边和有手边的筷子;思考的时候放下筷子。由于每只筷子有可能被2位哲学家中的一位拿起,所以会有资源竞争。可能会出现这种情况,每位科学家都拿起了一只筷子,当他们拿另一只筷子的时候发现没有筷子可用了,因为他们右手边或左手边的筷子都被人拿了,于是没有人能够吃饭,必须有等有人放下筷子释放资源系统才能进行下去。于是每个人都在等左右两边的人放下筷子。

      出现死锁的情况必须同时满足以下几个条件:

      (1) 互斥  系统中至少有一个资源不能被共享

      (2) 至少有一项任务正在占有某个资源并同时需要其他资源才能进行下去(开锁的例子)

      (3) 资源被某一任务占有时是不可抢夺的

      (4) 有循环等待的可能

      (5) 任务必须完成操作才能释放资源

防止死锁的办法就是尽可能的打破以上几项,至少是其中一项条件。例如哲学家就餐问题,如果不是必须拿左手边和右手边的筷子,而是任意两只筷子,那么循环等待的条件就可以被打破。

 

      线程间通信:wait(), notify(), notifyAll()

      对于多任务并行的并发系统,多个任务之间有时候有一定的依赖性。比如任务A和B,A先运行,运行一段时间后需要等待B运行,B或者返回了A需要的结果,或者改变了某个外部条件,这个条件正 是A需要的;A接着运行。

      A等待外部条件的改变。A运行一段时间后等待B,那么B是如何告诉A,我运行好了,你开始吧? notify()/notifyAll() 就是做这个事情的。

      wiat(): A运行一段时间后,发现需要等待某种外部条件的改变,于是需要将A阻塞,让其他线程--B运行,这时需要调用 A.wait()将A阻塞。为什么不用 A.sleep()?在一个长任务的 while 循环中让A sleep也可以将A阻塞,但是,这种阻塞不会释放同步锁,是一种“忙等待”,而 wait() 可以让出同步锁。因此,调用wait() 方法必须在synchronized 的作用范围内。notify()/notifyAll() 也如此。

      wait() notify()/notifyAll()方法是在 Object 对象内的,而不是作为线程类中的方法实现。这么做是有道理的,因为同步锁是作用在对象上的。

      通常wait()方法的调用要放在while(condition)循环内,这样做也是有道理的,因为 wait() 是在等待某个特定的外部条件,为了排除其他原因引起的阻塞或唤醒,需要检查条件condition, 如果条件不对,则继续等待。

     

      信号量 Semphore:

      利用信号量机制解决进程同步问题是由Dijkstra提出的(又是这个人!),信号量正式成为有效的进程同步工具,现在信号量机制被广泛的用于单处理机和多处理机系统以及计算机网络中。

  信号量S是一个整数,S大于等于零时代表可供并发进程使用的资源实体数,但S小于零时则表示正在等待使用临界区的进程数。

 

  Dijkstra同时提出了对信号量操作的PV原语 (原语是由系统内核提供的指令级的操作, 具有原子性atomicity, 所以PV操作都是不可中断的原子操作atomic. 参考 《操作系统概论》)

 

  P原语操作的动作是:

  (1)S减1;

  (2)若S减1后仍大于或等于零,则进程继续执行;

  (3)若S减1后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转进程调度。

  V原语操作的动作是:

  (1)S加1;

  (2)若相加结果大于零,则进程继续执行;

  (3)若相加结果小于或等于零,则从该信号的等待队列中唤醒一等待进程,然后再返回原进程继续执行或转进程调度。

  PV操作对于每一个进程来说,都只能进行一次,而且必须成对使用。在PV原语执行期间不允许有中断的发生。对于进程间同步,

              临界区C1;
              P(S);
              V(S);
             临界区C2;

 

      JDK1.5中提供的 java.util.concurrent.Semaphore.java 信号量类可用于多线程的同步操作控制。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值