代码改变世界

C\C++ 程序员从零开始学习Android - 个人学习笔记(十) - java基础 - 多线程(待续)

2012-02-13 21:21  CreateLight  阅读(503)  评论(0编辑  收藏  举报

1,多线程和多进程区别

  多进程中的每个进程都有自己独立的进程空间。

  多线程中的每个线程有自己独立的栈空间,但是共享其它数据(堆、方法区等)。

  一般而言,多线程拥有更小的创建开销、更快的速度,更麻烦的编码。

2, 为什么要并发?

  分时系统,增强用户响应。

  因为外设(典型的如IO)阻塞时,可以让CPU处理其他任务。

  充分利用多核或多CPU架构提升性能。

3,创建线程

  class MyRunnable implements Runnable

{

  void run(){ //do sth...}

}

Runnable r = new MyRunnable();

Thread t = new Thread(r);

t.start(); //创建一个新线程,并立刻调用r.run方法运行。



4, 中断线程

  线程执行完run方法体中的最后一条语句后,或者由return语句返回时,或者出现了没有捕获的异常时,线程将终止。

  java中没有强行终止线程的方法,但可以对一个线程调用interrupt方法中断它,一个线程被中断时会置位线程的中断状态(每个线程都有的一个boolean标志)。

  当一个线程处于阻塞状态(调用sleep或wait)时,如果被中断,会清除中断状态,抛出InterruptException异常;当一个线程的中断状态被置位,它调用sleep方法,也会清除中断状态,并抛出InterruptException异常。

  静态方法interrupted判断当前线程是否被中断,并清空中断状态;实例方法isInterrupted单纯的判断线程是否被中断,不会改变中断状态。

5,线程状态

  使用Thread实例方法get_state()获取当前线程的状态。

  5.1 New(新生)

  当new Thread(r)创建一个新的线程对象,这个线程还没有运行,其状态为new,在其运行之前还有一些簿记工作要做。

  5.2 Runnable(可运行)

  一旦调用start方法,线程处于Runnable状态,一个处于Runnable状态的线程可能正在运行,也可能不在运行,取决于线程调度。

  对于单核单CPU体系,某一时刻只有一个线程在运行,线程调度可以是抢占式的(每个线程分派时间片,时间片结束后,进程调度程序选择高优先级线程运行),也可以是协作式的(线程只有在阻塞、等待、调用yield时才会失去控制权)。

  对于多核或者多CPU体系,多个线程可以并发执行。

  5.3 Blocked(阻塞)

  当线程处于阻塞、Waiting、Timed Waiting状态时,它暂时不活动,它不运行任何代码且消耗最少的资源。直到线程调度器重新激活它。

  这些行为可以导致线程阻塞:

  当一个线程试图获取一个内部的对象锁,而改锁被其他线程持有,则该线程进入阻塞状态。当所有其它线程释放锁,并且线程调度器允许该线程持有它时,该线程变成非阻塞状态。

  5.4 Waiting (等待)

  当一个线程等待另一个线程通知线程调度器一个条件时,它自己进入等待状态。调用Object.wait或Thread.join方法,或者是等待java.util.concurrent库中的Lock或Condition时,就会出现这种情况。

  5.5 Timed Waiting (计时等待)

  有一些方法拥有超时参数,调用它们将导致线程进入计时等待状态,这一状态将一直保持到计时期满或接收到合适的通知。包括:Thread.sleep、Object.wait、Thread.join、Lock.tryLock以及Condition.await的计时版。

  5.6 Terminated (被终止)

  线程因为以下两种情况被终止:

  a, 因为run方法正常退出而自然死亡。

  b, 因为一个没有捕获的异常终止了run方法而意外死亡。

  当线程因为一个没有捕获的异常而死亡之前,该异常会先被传递到一个用于未捕获异常的处理器。该处理器必须属于一个实现Thread.UncaughtExceptionHandler接口的类。可以用setUncaughtExceptionHandler方法为任何一个线程安装一个处理器。也可以用Thread的静态方法setDefaultUncaughtExceptionHandler为所有线程安装一个默认的处理器。如果不安装默认的处理器,默认的处理器为空。

6,线程属性

  6.1 ,优先级

       默认情况下一个线程的优先级继承其父线程的优先级。优先级高度依赖于底层系统,不要让功能的正确性依赖于优先级。

       最低优先级为Thread.MIN_PRIORITY,其值为1;默认优先级为Thread.NORM_PRIORITY,值为5;最高优先级为Thread.MAX_PRIORITY,值为10。

     int getPriority() - 获取当前线程的优先级;

      void setPriority(int) - 设置当前线程的优先级;

      static void yield() - 导致当前执行线程处于让步状态,若有其他可运行的线程和该线程优先级相同,则优先调度其他线程。 

  6.2 守护线程

    void setDaemon(boolean isDaemon) -  设置当前线程为守护线程或用户线程。

    当只有守护线程运行时,虚拟机就会退出,守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。

7 同步

  当多个线程对公共资源进行访问的时候,如果访问操作不是原子的,就可能导致公共资源处于不一致的状态,从而导致错误的结果。

  一种简单的解决办法是加锁,当第一个线程访问公共资源时,加锁,然后访问,访问完成后解锁,执行访问动作的代码称之为临界区代码,只能同时被一个任务(进程、线程)执行;在此期间内,如果其它线程试图访问这个资源,会发现资源已经上锁,不能访问,它可以做两种选择:

  1,简单循环判断是否解锁,直到时间片耗完交出控制权。

  2,立刻交出控制权,同时进入休眠状态,直到被别的事件唤醒(通常是加锁的线程访问完毕,解锁)。

  7.1 ReentrantLock

  java提供了Lock接口,ReentrantLock是实现了这个接口的一个类,提供了可重入的锁操作:

  ReentrantLock myLock = new ReentrantLock();

  myLock.lock(); // 加锁

  try

  {

    // 临界区代码

  }

  finally

  {

    myLock.unlock(); // 解锁,finally块防止临界区代码抛出异常导致的死锁。

  }



  当第一个线程加锁后,第二个线程调用lock方法时,会被阻塞(进入Blocked状态),直到第一个线程释放锁,并且线程调度器允许其持有锁时才会被激活(重新进入Runnable状态).

  锁是可重入的,同一个线程可多次持有一个锁,java使用持有计数(hold count)来跟踪对lock方法的嵌套调用,线程在每次调用lock后都必须要调用unlock来解锁。因此被一个锁保护的代码可以调用另一个使用同一个锁对象的方法:

  

void f1()

  {

    myLock.lock(); //holdCount = 1;

    
//do sth

    f2(); //调用另一个使用同样锁对象的方法

    mylock.unlock(); // holdCount == 0;

  }

  void f2()

  {

    myLock.lock(); // holdCount +=1; holdCount == 2;

    
//do sth

    myLock.unlock(); // holdCount -=1; holdCout == 1;

  }

 

  注意:如果Thread_1调用lock,还没有unlock;此时Thread_2调用了lock,并不会增加holdCount,Thread_2.getHoldCount()返回的值是0,而不是1.

  ReentrantLock(boolean fair)

  构建一个带有公平策略的锁,一个公平锁偏爱等待时间最长的线程,但是公平锁的性能会大大降低。

7.2 条件对象condition

  如果一个线程获得了锁,然后执行临界区代码,发现某个条件暂时不满足,等其它线程先处理后条件可能才会满足;而对于其它线程来说,这个条件可能是满足的;在这种情况下,需要某种机制来让获得锁的线程阻塞,放弃锁,让别的线程获取锁,运行,直到条件满足后再运行因条件不满足而阻塞的线程(从阻塞处开始运行)。这就是条件对象condition.

  一个锁对象可以有一个或多个条件对象,使用Condition myCondition = Lock.newCondition()获取一个和myLock锁对象相关的条件对象。使用myCondition.await()来放弃锁、阻塞当前线程(进入waiting状态)、并等待其它线程的唤醒。

  当一个线程调用condition.await()方法阻塞时,它会被加入到该条件的等待集,直到别的线程调用调用同一个条件对象的signalAll方法,并且锁可用(没有任何线程持有),它才会被移出等待集,解除Waitting状态,进入Runnable状态。然后这些线程重新被调度,其中一个线程将获取到锁,这个线程将从await()返回,继续运行。只有获取了锁的线程才能调用signalAll方法,否则会抛出一个IllegalMonitiorStateException异常。

  示例代码:

  myLock.lock();  // 加锁

  try

  {

    while(/*条件不满足*/)

    {

      /**

        条件不满足,阻塞,交出锁,让其他线程运行,本线程进入Waitting状态;

        当别的线程signalAll并且释放所获获得的锁后,本线程恢复Runnable状态;

        当本线程获取到锁后,从awati()返回

      
*/

      myCondition.await();

    }

    // do sth;

    
// 激活条件等待集中的线程,让其有再次判断条件的权利; 注意,在此步之后,被激活的线程并不马上进入Runnable状态,

    
// 而是依然被阻塞,需要等锁被释放后才会进入Runnable状态,被线程管理器调度。

    myCondition.signalAll();

  }

  finally

  {

    myLock.unlock();
  }


  至关重要的是最终需要有一个线程来调用signalAll方法,当一个线程调用await()后,它没办法激活自身,只能寄希望于其它线程,如果没有其它线程来重新激活等待的线程,它就永远不会再运行了,这将导致死锁现象。另一个方法signal随机激活一个等待集中的线程,要小心使用,如果被激活的线程条件依然无法满足,则再次被阻塞,如果没有其他线程再次调用signal,则会死锁。

  7.3 synchronized

  java提供了synchronized关键字来代替显式的锁,java中每个对象都有一个内部锁,当使用synchronized关键字修饰一个方法或一个块时,会自动对这个方法或块内的代码上锁,执行完之后自动解锁,异常发生时也会自动解锁。内部锁同样是可重入的,如同ReentrantLock对象。内部锁的对象就是this自己。例:

  public synchronized void f() 

  {

  }

  等价于:

  public void f()

  {

    this.intrinsicLock.lock();

    try

    {

      //method body  

    }

    finally

    {

      this. intrinsicLock.unlock();

    }

  }

  内部对象锁只有一个相关内部条件,wait方法添加线程到等待集中,notifyAll/notify方法接触等待线程的阻塞状态,调用wait或notifyAll方法类似于调用:

    intrinsicCondition.await();

    intrinsicCondition.signalAll();
  将静态方法声明为synchronized也是合法的,如果调用这种方法,该方法将获得对应类对象的内部锁,比如Bank类有一个静态同步方法,当该方法被调用时,Bank.class类的对象(类加载时由jvm在堆上创建)的锁被锁住。

  内部锁和条件存在一些局限,包括:

    不能中断一个正在试图获取锁的线程。

    试图获取锁时不能设定超时。

    每个锁仅有单一的条件,可能是不够用的。

  对于Lock/Condition和synchronized的使用建议:

    a, 最好不要使用这两种机制。

    b, 优先使用synchronized,可以写更少的代码,降低出错机率。

    c, 特别需要Lock/Condition时再使用。

  7.4 同步阻塞

  线程可以通过调用同步方法获取对象的内部锁,还有一种机制也可以获得锁,通过进入一个同步阻塞:

    synchronized (obj)

    {

      // 临界区代码

    }

  以上代码将会获得obj对象的内部锁。

  7.5 监视器

  锁和条件是线程同步的强大的工具,但严格的来讲它们不是面向对象的。人们一直在找这样一种方案:无需程序考虑如何加锁,就可以保证多线程的安全性。最成功的解决方案之一是监视器。

  用java术语来描述,监视器有如下特征:

    a, 监视器是只有私有域的类。

    b, 每个监视器对象都有一个相关的锁。

    c, 使用该锁对所有方法加锁,每个方法都是同步方法,也就是每个方法的开始都会自动上锁,退出都会自动解锁。

    d, 该锁可以有任意多个相关条件。

  因为监视器对象的所有域都是私有的,并且所有方法调用自动上(同一个)锁,所以在一个线程对其进行操作时,可以确保其它线程无法访问对象域,从而保证了对象状态的一致性。

  java用不是很精确的方式采用了监视器概念,java中每个对象都有一个内部锁和内部的条件,如果一个方法使用synchronized关键字声明,它就表现的好像一个监视器方法,但是在这些方面java对象不同于监视器,从而使得线程安全性下降:

    a, 域不要求必须是private。

    b, 方法不要求必须是synchronized。

    c, 内部锁对用户是可用的。

  7.6 Volatile域

  在多处理器的机器上,计算机能够暂时在寄存器或者本地内存缓冲区中保存内存的值,对内存的操作可以表现为对这些区域的操作,只有在特定的时刻才会更新(写回)内存;这样会导致不期望的结果:运行在不同处理器上的线程,对同一内存的访问可能导致不同的结果(取到不同的值)。

  编译器可以改变指令执行的顺序以使得吞吐量最大化,这种顺序上的变化不会改变代码语义,但是编译器假定内存的值仅仅在代码中有显式的修改指令时才会被改变,并据此生成中间代码(字节码),然而,内存的值可以被另一个线程所改变,这就会导致编译器根据假定生成的中间代码可能产生一个错误的结果。

  如果使用锁来保护可以被多个线程访问的代码,不会出现上述问题;编译器被要求在必要的时候刷新本地缓存来保持锁的效应,并且不能不正当地改变指令的执行顺序。

  由于使用锁的(性能)代价高昂,可以使用Volatile修饰域,这样编译器和虚拟机就知道这个域是可能被另一个线程并发更新的,从而生成相应的中间代码保证一致性。Volatile不能保证原子性,可以使用AtomicBoolean,这个类有get方法和set方法,其实现使用有效的机器指令,在不使用锁的情况下确保原子性。

  如果满足以下条件之一,则域的并发访问是安全的:

  a, 域是final,并且在构造器调用完成之后被访问。

  b, 对域的访问由公有的锁进行保护。

  c, 域是volatile的。

  7.7 锁测试与超时

  线程在调用lock方法获取另一个线程持有的锁时,很可能阻塞,应该更谨慎的申请锁。tryLock方法试图申请一个锁,如果成功,返回true,如果失败,立刻返回false,线程可以去做别的事情。tryLock方法立刻抢夺一个锁,即使是公平锁,即使其他线程等待很久也是如此。

  可以在调用tryLock方法时设定超时参数:myLock.tryLock(100, TimeUnit.MILLISECONDS);  如果获取lock失败,就阻塞100 ms,然后再次进入Runnable状态,被调度后从tryLock返回

  Lock方法不能被中断,当一个线程调用lock方法而阻塞时,如果被中断,在获得锁之前会一直处于阻塞状态,直到获得锁之后才会运行,进行必要的中断处理。如果出现死锁,lock方法就一直不会终止。

  如果调用带有超时参数的tryLock方法,则可以被中断,中断发生时会抛出一个InterruptedException异常,这是一个非常有用的特性,因为允许程序打破死锁。也可以调用lockInterruptibly方法,等价于一个超时设为无限的tryLock方法。 

  在等待一个条件时,也可以设置超时: myCondition.await(100, TimeUnit.SECONDS); 如果一个线程被另一个线程调用signalAll或signal激活,或超时时限已到,或线程被中断,那么await方法将返回。如果等待的线程被中断,await方法将抛出一个InterruptedException异常。如果等待时间到了就返回false,其他条件则返回true;如果你希望出现这种情况时不抛出异常,而是继续等待,可以使用awaitUninterruptibly方法代替await方法。

  7.8 读/写 锁