Java-Note-Concurrency-多线程

  1. 多线程基本概念
    • 进程: 进程是程序(任务)的执行过程,它持有资源(共享内存,共享文件)和线程。
    • 线程: 线程是系统中最小的执行单元;同一进程中可以有多个线程;线程共享进程的资源。
      • 线程的生命周期:
        ![生命周期图片](https://note.youdao.com/yws/public/resource/340ba176925ee287a13ec8321f3181f3/xmlnote/23C64264F6504C689D8D3BE00867A5B8/17114)
        1. New: 新建一个线程对象
        2. Runnable: 线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
        3. Running: 就绪状态的线程获取了CPU,执行程序代码。
        4. Blocked: 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
          • 分类:`
            1. 等待阻塞: 运行的线程执行wait()方法,JVM会把该线程放入等待池中。
            2. 同步阻塞: 运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
            3. 其它阻塞: 运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
        5. Dead: 线程执行完了或者因异常退出了run()方法,该线程结束生命周期
      • 线程的调度
        1. 调整优先级
          • setPriority()
          • getPriority()
        2. 线程睡眠
          • Thread.sleep(long mills)
        3. 线程等待
          • Object类的wait()方法, 导致当前线程等待, 直到其它线程调用此对象的notify()或notifyAll()
        4. 线程让步
          • Thread.yield(): 暂停当前线程执行的对象, 把机会让给相同或更高优先级的线程
        5. 线程加入
          • join()方法, 等待其它线程终止. 在当前线程中调用另一个线程的join()方法, 则当前线程转入阻塞状态, 直到另一个线程运行结束, 当前线程再由阻塞转为就绪状态
        6. 线程唤醒: Object()类中的notify()方法, 唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生
      • 名词解释
        • 主线程: JVM调用程序main()所产生的线程。
        • 当前线程: 通过Thread.currentThread()来获取的线程
        • 后台线程: 为其它线程提供服务的线程, 如垃圾回收线程, 通过isDaemon()和setDaemon()来判断和设置后台线程
        • 前台线程: 接受后台线程服务的线程, 其实前台后台线程是联系在一起的
      • 常见问题
        • 尽管叫线程队列, 但并不是一个队列, 队列形式是指当一个线程完成“一轮”时,它移到可运行队列的尾部等待,直到它最终排队到该队列的前端为止,它才能被再次选中。事实上,我们把它称为可运行池而不是一个可运行队列,目的是帮助认识线程并不都是以某种有保障的顺序排列唱呢个一个队列的事实。
  2. Java线程的正确停止
    • 正确方法: 设置退出标志
      • 规则: 使用volatile定义boolea running = true, 通过设置标志变量running来结束线程
    • 错误方法: interrupt方法
      • 该方法只是在目标线程中设置一个标志变量, 表示它已经被中断, 并需要立即返回. 实际上, 如果没有判断isInterrupt()的话, 线程不会终止, 而是继续往下执行
        • 本质上是上一种方法, 只是退出的标志是一个特殊的标志isInterrupt()
      • 注意: 在线程阻塞情况下,标志位会被清除,更无法设置中断标志位
  3. 线程交互
    • 线程的同步: 线程的运行有相互的通信控制,运行完一个再正确的运行另一个, 如: 线程的等待和唤醒:wait()+notifyAll()
    • 线程的互斥: 线程的运行隔离开来,互不影响,使用synchronized关键字实现互斥行为,此关键字即可以出现在方法体之上也可以出现在方法体内,以一种块的形式出现,在此代码块中有线程的等待和唤醒动作,用于支持线程的同步控制
  4. 同步和锁定

    • 在使用同步代码块时, 应该指定在哪个对象上同步, 也就是要获取哪个对象的锁

      1
      2
      3
      4
      5
      6
      public int fix(int y) {
      synchronized (this) {
      x = x - y;
      }
      return x;
      }
    • 同步静态方法,需要一个用于整个类对象的锁,这个对象是就是这个类(XXX.class)

      1
      2
      3
      public static synchronized int setName(String name){
      Xxx.name = name;
      }

      等价于

      1
      2
      3
      4
      5
      public static int setName(String name){
      synchronized(Xxx.class){
      Xxx.name = name;
      }
      }
  5. 线程同步总结

    • 线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他同步方法。
    • 对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。
    • 静态和非静态方法的锁互不干预。
    • 一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
    • 死锁是线程间相互等待锁锁造成的

      • 例子:

        1
        进程A占有资源R1,等待进程B占有的资源Rr;进程B占有资源Rr,等待进程A占有的资源R1。而且资源R1和Rr只允许一个进程占用,即:不允许两个进程同时占用。结果,两个进程都不能继续执行,若不采取其它措施,这种循环等待状况会无限期持续下去,就发生了进程死锁。
      • 死锁的必要条件:

        • 互斥条件。即某个资源在一段时间内只能由一个进程占有,不能同时被两个或两个以上的进程占有。
        • 不可抢占条件。进程所获得的资源在未使用完毕之前,资源申请者不能强行地从资源占有者手中夺取资源,而只能由该资源的占有者进程自行释放。
        • 占有且申请条件。进程至少已经占有一个资源,但又申请新的资源;由于该资源已被另外进程占有,此时该进程阻塞;但是,它在等待新资源之时,仍继续占用已占有的资源。
        • 循环等待条件。存在一个进程等待序列{P1,P2,…,Pn},其中P1等待P2所占有的某一资源,P2等待P3所占有的某一源,……,而Pn等待P1所占有的的某一资源,形成一个进程循环等待环。
      • 死锁的预防
        • 打破互斥条: 即允许进程同时访问某些资源。但是,有的资源是不允许被同时访问的,像打印机等等,这是由资源本身的属性所决定的。所以,这种办法并无实用价值。
        • 打破不可抢占条件: 即允许进程强行从占有者那里夺取某些资源。就是说,当一个进程已占有了某些资源,它又申请新的资源,但不能立即被满足时,它必须释放所占有的全部资源,以后再重新申请。它所释放的资源可以分配给其它进程。这就相当于该进程占有的资源被隐蔽地强占了。这种预防死锁的方法实现起来困难,会降低系统性能.
        • 打破占有且申请条件: 可以实行资源预先分配策略。即进程在运行前一次性地向系统申请它所需要的全部资源。如果某个进程所需的全部资源得不到满足,则不分配任何资源,此进程暂不运行。只有当系统能够满足当前进程的全部资源需求时,才一次性地将所申请的资源全部分配给该进程。由于运行的进程已占有了它所需的全部资源,所以不会发生占有资源又申请资源的现象,因此不会发生死锁。
          • 缺点:
            • 在许多情况下,一个进程在执行之前不可能知道它所需要的全部资源。这是由于进程在执行时是动态的,不可预测的
            • 资源利用率低。无论所分资源何时用到,一个进程只有在占有所需的全部资源后才能执行。即使有些资源最后才被该进程用到一次,但该进程在生存期间却一直占有它们,造成长期占着不用的状况。这显然是一种极大的资源浪费;
            • 降低了进程的并发性。因为资源有限,又加上存在浪费,能分配到所需全部资源的进程个数就必然少了。
        • 打破循环等待条件: 实行资源有序分配策略。采用这种策略,即把资源事先分类编号,按号分配,使进程在申请,占用资源时不会形成环路。所有进程对资源的请求必须严格按资源序号递增的顺序提出。进程占用了小号资源,才能申请大号资源,就不会产生环路,从而预防了死锁。这种策略与前面的策略相比,资源的利用率和系统吞吐量都有很大提高.
          • 缺点:
            • 限制了进程对资源的请求,同时给系统中所有资源合理编号也是件困难事,并增加了系统开销;
            • 为了遵循按编号申请的次序,暂不使用的资源也需要提前申请,从而增加了进程对资源的占用时间。