JAVA API备忘----Thread

JAVA的线程是一个既简单又复杂的概念。说它简单,是因为JAVA在语言级就内置了线程,使得线程的编程相对其它语言来说要简单得多;另一方面,线程编程本身就很复杂,无论再怎么去简化,总离不开并发、通信等概念,而这些东西就有复杂性。
    JAVA线程编程涉及的变化比较多,但是总结下来,基本上都是对基础知识的深入应用。本文不打算介绍每本书都必写的东西,而是试图强调一些比较重要而又需要深入理解的基础概念。对于线程的高级应用,为免混乱,将在今后陆续指出。
 

 
    1.最基本的知识。
        每本书都提过了为何要使用多线程、以及Thread类的一些基本方法,比如start、run、sleep。另外,每本书都提过编写多线程的两种方法:继 承Thread、实现Runnable。这里稍微提一下区别,继承Thread你就能很快得到一个线程,只需要覆盖run方法,而实现Runnable则 仅仅完成了一个方法,等到建立线程时就必须以Runnable为参数传给Thread从而构造一个新的线程,可以这样理解,这种方法就是将线程的run方 法和其它方法分离开,在需要时再合起来成为想要的线程。这种分离的好处带来了极大的便利,面向接口编程给程序带来了相当的灵活性,这里就不再赘述了。
          以上就是比较基本的知识,之后的概念将建立在这些知识之上。
 

 
    2.synchronized。
          这个关键字也是每本书必讲的概念,但是大多书都没讲很细致,所以程序分析完之后常常还不知所云。synchronized有两种常见的写法,首先来看比较难理解的一种:

 ......

 synchronized(obj){  -----------//begin

     ......

                   -----------//end

 ......

        上面这段的语法是:将obj这个对象 所有加锁的方法锁定住,并且这个锁的生存期是从begin到end,意思就是从begin到end的程序段中,obj的所有synchronized都将被锁住而不能被另外的线程访问。
        明白了这种写法的意义就能轻松掌握第二种写法了。
另一种写法:

 ......

 synchronized void method(){

     ......

 }

 ......

     一看也能马上明白锁的生存期是从方法开始到方法结束,但是却不明白第一种写法中的obj在后面方法中的什么地方,难道没有这个obj?答案自然是否定的。后面的写法等价于:

 ......

 void method(){

     synchronized(this){

         ......

     }

 }

 ......

     实际上是对当前对象加锁。必须得记住,首先得知道synchronized加锁的对象是谁。

     关于synchronized概念已经澄清。现在来看一段程序(引自thinking in java 3rd edition)

 public class AlwaysEven {
 private int i;
 public void next() {i++; i++;}
 public int getValue() {return i;}
 public static void main(String [] args){
  final AlwaysEven ae = new AlwaysEven();
   new Thread("Watcher"){
    public void run(){
     while(true){
      int val = ae.getValue();
      if(val % 2 != 0){
       System.out.println(val);
       System.exit(0);
      }
     }
    }
   }.start();
  while(true)
   ae.next();
 }
}

    这段短小精干的代码却不能正常运行,它不总是偶数,马上就想到是next没加锁,那加上之后呢?再运行试试,结果仍然如此,这时就很奇怪了,到底是什么回事。

    这就涉及到接下来要讲的synchronized的几个比较重要的概念了。

    1).使用synchronized,首先得明白它是在保护什么。既然加锁,表示一定有什么想保护。操作系统课有对临界区的解释:访问临界资源的那段代码称为临界区。上例,synchronized锁定的对象是this,更进一步,是想保护实例变量i。

    明白了这点就理解为什么修改之后没成功,因为我们需要保护i,就必须对所有将i暴露出来的方法锁住,否则等于没有保护,上例中,将getValue()上 锁之后就不会出现同样的问题了。可能很难想清楚,还记得synchronized的定义吗?将对象中所有加锁的方法锁定,那意味着没锁定的方法是可以任意 被访问的,getValue()没锁定,所以它能够访问i。就像把家门锁了,却把窗户打开了一样。如图:

   

    通过这么一个简单的例子,或许会觉得该留心些类似的陷阱。的确,陷阱很多。这里再列举几个:

    2).上锁的粒度。这涉及到原子性操作的问题,比如每次需要修改两个字段,身份证号和名字。这是两个字段一起作为一个原子操作,要么全做要么全不做。因此应该对同时修改这两个字段的方法加锁,如果分别对它们改变并加锁又是什么结果呢?比如我写了这么两个方法:

 public void setName(){ ...... }

 public void setID(){ ...... }

    结果是又将不安全,因为对它们的操作是不能分散的,这点是需要特别注意的。

    3).原子操作。对于多线程而言,区别哪些是原子操作,哪些不是很重要。java内置的基本类型中除了long, double外赋值和引用操作都是原子的。long和double不是原子的,操作时需要使用synchronized或者将字段声明为 volatile。++和--操作不是原子操作,虽然很容易被误认为一步就能完成,在java中它们的操作是分两步完成的,如果没上任何锁,那有可能在没 完成整个操作前访问了它的值--这意味着我们得到了操作前的值,关于这点可以自己写一个程序来证明。

    4).集合的线程安全。java.util.Collections包中提供了许多方法能让集合安全的运行在多线程环境中。它们 是:synchronizedCollection、synchronizedList、synchronizedMap、 synchronizedSet、synchronizedSortedMap、synchronizedSortedSet。需要时应该查阅JDK的 API文档。

 


 

   3.wait, notify, notifyAll

   关于wait,其实也有许多值得提的地方。首先还是来回顾一下它们的用法定义吧。

   wait,notify和notifyAll是Object上的方法而不是Thread的,这点不能搞混。至于为什么这样设置,稍后会讲。wait的语法很简单:

 ......

 obj.wait();

 ......

   但却有很多必须注意的地方。上例意思是当前线程在对象obj上等待,这里可以认为每个对象提供了一个等待的队列,专门供线程等待,照此理解就是将当前线程 挂在obj的等待队列上(当然实际可能并非是一个队列)。而每次进入队列前需要以synchronized上锁,而一旦进入了便会解除锁定,这点是由 wait()来做的,但却必须得知道。

   wait能够被以下事件唤醒:有其它线程调用notify,notifyAll,interrupt或wait到时间。wait可以传一个毫秒参数进去, 作用是在等待的时间到期还在等待时自动唤醒,类似sleep(),稍微有些不同的是如果调用wait(0)则等价于wait(),表示无限期。

   notify和notifyAll跟wait一样会在唤醒前一刻上锁,唤醒后解锁。而唤醒后究竟谁被调度选中,那就是JVM的事,不同的环境和JVM有不同的算法。

   


 

    4.interrupt

    interrupt常常不是当前线程调用来中断自己,相反,是由其它线程调用来中断自己的, 与wait和notify,notifyAll不同的是它并不需要获得对象锁,任何线程在任何时候都能用interrupt去中断别的线程。这时被中断的 线程就会抛出InterruptException异常,在catch块中被捕获。Interrupt常常被写在循环中,并且不抛出异常,事实上一个线程 在wait,sleep或join时被打断并不是一个异常,只能算是一种特殊的分支,正因如此,当执行完之后接着执行catch之后的第一条语句,如果说 有如下一段程序:

 public void run(){

     while(true){

         System.out.println("1");

         try{

              sleep(100000000);

              System.out.println("2");

         }

         catch(InterruptException ie){

              System.out.println("3");

         }

     }

 }

    那么当main调用了interrupt之后将会输出1 2 1。奇怪的结果,仔细分析,开始输出1都能想通,然后它睡了,中途被main给打断,直接跳到catch里输出3,但catch之后并不像其它异常一样中止整个程序,而是继续循环输出1。

 

    5.volatile

    关于volatile的说明可能需要先了解JAVA的内存模型,简单点说就是JVM有个主存,各个线程再有各自的工作内存,两个存放的地方带来的问题就是不一致。volatile就是为了解决这个不一致出现的。

    使用volatile会每次修改后及时的将工作内存的内容同步回主存。

    另外,volatile还能修饰long和double,作用是令这两个操作原子化。

 


 

    5.关于线程这部分的异常: IllegalMonitorStateException, IllegalThreadStateException,InterruptException

    IllegalMonitorStateException常发生的情况:没有获得synchronized锁定时便去调用wait,notify,notifyAll时抛出

    IllegalThreadStateException常发生的情况:一个线程已经调用了start()方法在运行中,另一个线程对该线程重复调用start()时抛出

    InterruptException在线程wait,join,sleep被中断时抛出

 


 

    6.关于线程的编程,可以归结为一些问题,以下列举了几个,算是比较常用的问题,今后将会对这几个列举的问题一一讲解,请关注。

    1.线程编程的一些特殊技巧。

    2.生产者-消费者问题。

    3.读者-写者问题。

    4.异步线程问题。

    5.线程池问题。


转自 http://blog.sina.com.cn/s/blog_58adc9e7010009ru.html


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值