每一天的每一分每一秒都不晚!       2021年3月16日 23:18:07       

桐君过客

大三

Java学习_多线程

  • 多线程基础

    • 多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。
    • 和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。
  • 创建新线程
    • 要创建一个新线程非常容易,我们需要实例化一个Thread实例,然后调用它的start()方法
    • 希望新线程能执行指定的代码,有以下几种方法
      • 方法一:从Thread派生一个自定义类,然后覆写run()方法,start()方法会在内部自动调用实例的run()方法。
      • 方法二:创建Thread实例时,传入一个Runnable实例(即匿名内部类)  
      • 在线程中调用Thread.sleep(),强迫当前线程暂停一段时间
      • 调用Thread实例的start()方法才能启动新线程 
    • 线程的优先级

  • 线程的状态

    • Java线程对象Thread的状态包括:NewRunnableBlockedWaitingTimed WaitingTerminated
    • 当线程启动后,它可以在RunnableBlockedWaitingTimed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。
    • 一个线程还可以等待另一个线程直到其运行结束。例如,main线程在启动t线程后,可以通过t.join()等待t线程结束后再继续运行
  • 中断线程
    • 如果线程处于等待状态,例如,t.join()会让main线程进入等待状态,此时,如果对main线程调用interrupt()join()方法会立刻抛出InterruptedException,因此,目标线程只要捕获到join()方法抛出的InterruptedException,就说明有其他线程对其调用了interrupt()方法,通常情况下该线程应该立刻结束运行。
    • 另一个常用的中断线程的方法是设置标志位。我们通常会用一个running标志位来标识线程是否应该继续运行,在外部线程中,通过把HelloThread.running置为false,就可以让线程结束
    • 对线程间共享的变量用关键字volatile声明
      • 每次访问变量时,总是获取主内存的最新值;
      • 每次修改变量后,立刻回写到主内存。
  • 守护线程(Daemon Thread)
    • 守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
    • 如何创建守护线程呢?方法和普通线程一样,只是在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程:

      Thread t = new MyThread();
      t.setDaemon(true);
      t.start();
    • 在守护线程中,编写代码要注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。
  • 线程同步

    • 多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待
    • 通过加锁和解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。  
    • synchronized(Counter.lock) { // 获取锁
          ...
      } // 释放锁
    • 在使用synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁
    • JVM只保证同一个锁在任意时刻只能被一个线程获取,但两个不同的锁在同一时刻可以被两个线程分别获取。
    • 不需要synchronized的操作

      • 单条原子操作的语句不需要同步。即基本类型的变量(不包括float和double)和引用类型的变量的赋值 
  • 同步方法
    • 让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把synchronized逻辑封装起来。
    • synchronized修饰的方法就是同步方法,它表示整个方法都必须用this实例加锁。 
      public void add(int n) {
          synchronized(this) { // 锁住this
              count += n;
          } // 解锁
      }
      
      
      public synchronized void add(int n) { // 锁住this
          count += n;
      } // 解锁
    • 对于static方法,是没有this实例的,因为static方法是针对类而不是实例。但是我们注意到任何一个类都有一个由JVM自动创建的Class实例,因此,对static方法添加synchronized,锁住的是该类的Class实例。上述synchronized static方法实际上相当于:

      public class Counter {
          public static void test(int n) {
              synchronized(Counter.class) {
                  ...
              }
          }
      }
    • 一个类没有特殊说明,默认不是thread-safe
  • 死锁

    • JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。
    • 避免死锁的方法:线程获取锁的顺序要一致。假设有两个锁,则不同的方法严格按照相同的获取锁的顺序
  • 使用wait和notigy
    • wait()方法必须在当前获取的锁对象上调用,这里获取的是this锁,因此调用this.wait()
    • wait()方法的执行机制非常复杂。首先,它不是一个普通的Java方法,而是定义在Object类的一个native方法,也就是由JVM的C代码实现的。其次,必须在synchronized块中才能调用wait()方法,因为wait()方法调用时,会释放线程获得的锁,wait()方法返回后,线程又会重新试图获得锁。
    • 如何让等待的线程被重新唤醒,然后从wait()方法返回?答案是在相同的锁对象上调用notify()方法。
  • 使用ReentrantLock

    • java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁
  • 使用Condition

    • 使用Condition对象来实现waitnotify的功能。
    • 使用Condition时,引用的Condition对象必须从Lock实例的newCondition()返回,这样才能获得一个绑定了Lock实例的Condition实例。
  • ReadWriteLock
    • 只允许一个线程写入(其他线程既不能写入也不能读取);
    • 没有写入时,多个线程允许同时读(提高性能)。
  • 使用StampedLock

    • StampedLockReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁后写入!这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。
    • ReadWriteLock相比,写入的加锁是完全一样的,不同的是读取。注意到首先我们通过tryOptimisticRead()获取一个乐观读锁,并返回版本号。接着进行读取,读取完成后,我们通过validate()去验证版本号,如果在读取过程中没有写入,版本号不变,验证成功,我们就可以放心地继续后续操作。如果在读取过程中有写入,版本号会发生变化,验证将失败。在失败的时候,我们再通过获取悲观读锁再次读取。由于写入的概率不高,程序在绝大部分情况下可以通过乐观读锁获取数据,极少数情况下使用悲观读锁获取数据。
  • 使用Concurrent集合

  • 使用Atomic

  • 使用线程池

    • 把很多小任务让一组线程来执行,而不是一个任务对应一个新线程。这种能接收大量小任务并进行分发处理的就是线程池。
    • ExecutorService只是接口,Java标准库提供的几个常用实现类有:
      • FixedThreadPool:线程数固定的线程池;
      • CachedThreadPool:线程数根据任务动态调整的线程池;
      • SingleThreadExecutor:仅单线程执行的线程池。
  • 使用Future

  • 使用CompletableFuture

  • 使用fForkJoin
  • 使用ThreadLocal

 

posted @ 2021-04-06 20:38  桐君过客  阅读(66)  评论(0)    收藏  举报