Java并发与同步(上)

并发

本文档建立在之前博客基础上。

  1. 直接调用Thread类或Runnable对象的run()方法,只会执行统一线程中的任务,而不会启动新线程。调用start()方法,此方法将创建一个执行run方法的新线程。

中断线程

  1. 调用interrupt()方法,可以请求终止线程。调用Thread.currentThread()得到当前线程,再使用isInterrupt()来判断线程是否被终止。

  2. 若线程被阻塞(调用sleepwait),则无法检测中断状态。

    void interrupt()
    向线程发送中断请求。线程的中断状态将被设置为true。如果目前该线程被一个sleep调用阻塞,那么,InterruptedException 异常被抛出。
    • static boolean interrupted()
    测试当前线程(即正在执行这一命令的线程)是否被中断。注意,这是一个静态方法。这一调用会产生副作用—它将当前线程的中断状态重置为 false。
    • boolean islnterrupted()
    测试线程是否被终止。不像静态的中断方法,这一调用不改变线程的中断状态。
    • static Thread currentThread()
    返回代表当前执行线程的 Thread 对象。
    

线程状态

  1. 线程可以有如下 6 种状态:
  • New (新创建)

  • Runnable (可运行)

  • Blocked (被阻塞)

  • Waiting (等待)

  • Timed waiting (计时等待)

  • Terminated (被终止)

  1. 要确定一个线程的当前状态,调用getState()方法。

抢占式调度和协作式调度

  1. 抢占式调度系 统给每一个可运行线程一个时间片来执行任务。当时间片用完,操作系统剥夺该线程的运行 权, 并给另一个线程运行机会当选择下一个线程时, 操作系统考虑线程的优先级。 现在所有的桌面以及服务器操作系统都使用抢占式调度 。
  2. 像手机这样的小型设备 可能使用协作式调度。在这样的设备中,一个线程只有在调用 yield 方法、 或者被阻塞或等 待时,线程才失去控制权。

在任何给定时刻,一个可运行的线程可能正在运行也可能没有运行(这就是为什 么将这个状态称为可运行而不是运行

被阻塞线程和等待线程

  1. 当线程处于被阻塞或等待状态时,它暂时不活动。它不运行任何代码且消耗最少的资源。直到线程调度器重新激活它。细节取决于它是怎样达到非活动状态的。

Java并发与同步(上)_第1张图片

线程属性

线程优先级、守护线程、 线程组以及处理未捕 获异常的处理器。

  1. 线程优先级

    1. 在 Java 程序设计语言中,每一个线程有一个优先级。默认情况下, 线程继承它的父 线程的优先级。可以用setPriority方法提高或降低任何一个线程的优先级。
    2. 每当线程调度器有机会选择新线程时, 它首先选择具有较高优先级的线程。但是,线程 优先级是高度依赖于系统的。
    3. 不要将程序构建 为功能的正确性依赖于优先级。
  2. 守护线程

    1. 可以通过调用 t.setDaemon(true); 将线程转换为守护线程。 这一方法必须在线程启动之前调用。 守护线程的唯一用途 是为其他线程提供服务。 当只剩下守护线程时, 虚拟机就退出了 。
    2. 守护线程应该永远不去访问固有资源, 如文件、 数据库,因为它会在任何时 候甚至在一个操作的中间发生中断。
  3. 未捕获异常处理器

    1. 线程的 run方法不能抛出任何受查异常(必须在内部使用try...catch来解决发生的任何必须处理异常), 但是,非受査异常(典型代表:RuntimeException)会导致线程终止。在这种情 况下,线程就死亡了。

    2. 在线程死亡之前, 异常被传递到一个用于未捕获异常的处理器。 该处理器必须属于一个实现 Thread.UncaughtExceptionHandler接口的类。这个接口只有一个方法:

      void uncaughtException(Thread t, Throwable e)
      
    3. 可以用 setUncaughtExceptionHandler方法为任何线程安装一个处理器。也可以用 Thread 类的静态方法 setDefaultUncaughtExceptionHandler为所有线程安装一个默认的处理器。

    4. 如果不安装默认的处理器, 默认的处理器为空。但是, 如果不为独立的线程安装处理 器,此时的处理器就是该线程的 ThreadGroup对象。

      Thread aThread = new Thread(() -> {
          System.out.println("执行开始");
          int a = 10 / 0;
          System.out.println("执行结束");
      }, "A");
      aThread.start();
      
      // 默认线程组处理结果
      /*
      执行开始
      Exception in thread "A" java.lang.ArithmeticException: / by zero
      	at BaseLearn.multithreadingTest.Test6.Main.lambda$main$0(Main.java:11)
      	at java.lang.Thread.run(Thread.java:748)
      */
      
      Thread aThread = new Thread(() -> {
          System.out.println("执行开始");
          int a = 10 / 0;
          System.out.println("执行结束");
      }, "A");
      aThread.setUncaughtExceptionHandler((Thread t, Throwable e) -> {
          System.out.println("发现错误了");
          System.out.println(e.toString());
      });
      aThread.start();
      
      // 添加自定义异常处理
      Thread aThread = new Thread(() -> {
          System.out.println("执行开始");
          int a = 10 / 0;
          System.out.println("执行结束");
      }, "A");
      aThread.setUncaughtExceptionHandler((Thread t, Throwable e) -> {
          System.out.println("发现错误了");
          e.printStackTrace();
      });
      aThread.start();
      
      /*
      执行开始
      java.lang.ArithmeticException: / by zero
      发现错误了
      	at BaseLearn.multithreadingTest.Test6.Main.lambda$main$0(Main.java:11)
      	at java.lang.Thread.run(Thread.java:748)
      */
      
    5. 线程组是一个可以统一管理的线程集合。默认情况下,创建的所有线程属于相同的线程组, 但是, 也可能会建立其他的组。现在引入了更好的特性用于线程集合的操作, 所以建议不要在自己的程序中使用线程组。

同步

  1. 在大多数实际的多线程应用中, 两个或两个以上的线程需要共享对同一数据的存取。若线程调用了一个修改该对象状态的方法,根据各线程访问数据的次序,可能会产生讹误的对象。这样一个情况通常称为竞争条件。

    竞争条件例子:(示例来自Java核心技术卷14.5.1,643页)

    模拟一个有若干账户的银行。随机地生成在这些账户之间转移钱 款的交易。每一个账户有一个线程。每一笔交易中, 会从线程所服务的账户中随机转移一定 数目的钱款到另一个随机账户。

    // 第一部分截取
    public void transfer(int from, int to, double amount { 
        System.out.print(Thread,currentThread0);
        // 从accounts的第from账户中扣钱
        accounts[from] -= amount; 
        System.out.printff" X10.2f from Xd to Xd", amount, from, to); 
        // 加给to账户
        accounts[to] += amount; 
        System.out.printf("Total Balance: X10.2fXn", getTotalBalance())} 
       
    // 第二部分截取
    Runnable r = () -> {
        try { 
            while (true) { 
                // 转义账户随机生成
                int toAccount = (int) (bank.sizeO * Math.random()); 
                // 转义钱数
                double amount = MAX_AMOUNT * Math.random();
                // 转移
                bank.transfer(fromAccount, toAccount, amount); 
                // 暂停一会
                Thread,sleep((int) (DELAY * Math.randomO)); 
            } 
        } catch (InterruptedExeeption e){
            
        }
    }

    示例程序源代码存放在个人码云示例中点击访问

    执行结果:

    Thread[Thread-26,5,main]      82.57 from 26 to 35 Total Balance:  100000.00
    Thread[Thread-44,5,main]     819.26 from 44 to 49 Total Balance:  100000.00
    Thread[Thread-37,5,main]     966.78 from 37 to 40 Total Balance:  100000.00
    Thread[Thread-5,5,main]     811.87 from 5 to 89 Total Balance:  100000.00
    Thread[Thread-81,5,main]     344.01 from 81 to 3 Total Balance:  100000.00
    Thread[Thread-14,5,main]     730.87 from 14 to 17 Total Balance:  100000.00
    Thread[Thread-87,5,main]     965.71 from 87 to 97 Total Balance:  100000.00
    Thread[Thread-6,5,main]Thread[Thread-62,5,main]Thread[Thread-39,5,main]     313.93 from 39 to 56 Total Balance:   99331.28
    Thread[Thread-57,5,main]      31.16 from 57 to 36 Total Balance:   99331.28
    Thread[Thread-77,5,main]Thread[Thread-57,5,main]     523.69 from 57 to 19 Total Balance:   98750.24
    Thread[Thread-17,5,main]     537.68 from 17 to 22 Total Balance:   98750.24
         581.04 from 77 to 13 Total Balance:   99331.28
    Thread[Thread-13,5,main]     212.00 from 13 to 46 Total Balance:   99331.28
    Thread[Thread-91,5,main]      98.60 from 91 to 34 Total Balance:   99331.28
    Thread[Thread-20,5,main]     898.50 from 20 to 95 Total Balance:   99331.28
          15.54 from 62 to 70 Total Balance:   99346.82
         653.18 from 6 to 34 Total Balance:  100000.00
    Thread[Thread-20,5,main]     777.97 from 20 to 14 Total Balance:  100000.00
    Thread[Thread-13,5,main]     194.07 from 13 to 40 Total Balance:  100000.00
    Thread[Thread-74,5,main]     815.60 from 74 to 58 Total Balance:  100000.00
    Thread[Thread-70,5,main]     844.52 from 70 to 73 Total Balance:  100000.00
    
    

    由结果可以看出。一段时间之后, 错误不知不觉地出现了,总额要么增加, 要么变少。当两个线程试图同时更新同一个账户的时候, 这个问题就出现了。

    1. 个人分析:因为线程执行时有抢占资源的问题,假定现在Thread-1线程为A向B转钱,A账户钱数减少时,Thread-2线程此时抢占到了资源开始运行,此时A的钱数少了,所以银行总钱数减少,但是后续线程A又拿到了运行权,此时才会继续执行下面的增加B账户钱数的运算。所以后面钱数又回归正常了。

    2. 官方分析:假定两个线程同时执行指令

      accounts[to] += amount;

      问题在于这不是原子操作。该指令可能被处理如下:

      1. 将 accounts[to] 加载到寄存器。
      2. 增加 amount。
      3. 将结果写回 accounts[to]。

      现在,假定第 1 个线程执行步骤 1 和 2, 然后, 它被剥夺了运行权。假定第 2 个线程被 唤醒并修改了 accounts 数组中的同一项。然后,第 1 个线程被唤醒并完成其第 3 步。 这样, 这一动作擦去了第二个线程所做的更新。于是, 总金额不再正确。(见图 14-4J 我们的测试程序检测到这一i化误。(当然, 如果线程在运行这一测试时被中断,也有可能 会出现失败警告!)

    锁对象

    1. 有两种机制防止代码块受并发访问的干扰。Java语言提供一个 synchronized关键字达 到这一目的,并且 Java SE 5.0引入了 ReentrantLock 类。synchronized 关键字自动提供一个 锁以及相关的“ 条件”, 对于大多数需要显式锁的情况, 这是很便利的。

    2. 用 ReentrantLock 保护代码块的基本结构如下:

       myLock.lock(); 
       try { 
           critical section 
       } finally { 
           myLock.unlockO;
       } 
    
    1. 这一结构确保任何时刻只有一个线程进人临界区。一旦一个线程封锁了锁对象, 其他任 何线程都无法通过 lock语句。当其他线程调用 lock 时,它们被阻塞,直到第一个线程释放 锁对象。

    2. 把解锁操作括在 finally 子句之内是至关重要的。如果在临界区的代码抛出异常, 锁必须被释放。否则, 其他线程将永远阻塞。

    3. 如果使用锁, 就不能使用带资源的 try语句。

    修改示例:

    public class Bank { 
        private Lock bankLock = new ReentrantLock0public void transfer(int from, int to, int amount) t bankLock.lock(); 
        try { 
        System.out.print(Thread.currentThread0);     accounts[from] -= amount;     				System.out.printf(" X10.2f from %A to Xd", amount, from, to); 
        accounts[to] += amount; 					System.out.printf(" Total Balance: X10.2fXn", getTotalBalanceO)} finally {
            banklock.unlock();
        }
    } 
    

    结果:

    101.10 from 93 to 68 Total Balance:  100000.00
    Thread[Thread-66,5,main]      20.39 from 66 to 41 Total Balance:  100000.00
    Thread[Thread-5,5,main]     905.92 from 5 to 89 Total Balance:  100000.00
    Thread[Thread-87,5,main]     608.07 from 87 to 41 Total Balance:  100000.00
    Thread[Thread-38,5,main]     942.93 from 38 to 84 Total Balance:  100000.00
    Thread[Thread-39,5,main]     757.74 from 39 to 32 Total Balance:  100000.00
    Thread[Thread-62,5,main]     503.86 from 62 to 16 Total Balance:  100000.00
    Thread[Thread-4,5,main]     417.09 from 4 to 51 Total Balance:  100000.00
    Thread[Thread-67,5,main]     781.40 from 67 to 67 Total Balance:  100000.00
    Thread[Thread-56,5,main]     972.45 from 56 to 74 Total Balance:  100000.00
    Thread[Thread-91,5,main]     102.22 from 91 to 65 Total Balance:  100000.00
    Thread[Thread-58,5,main]     112.92 from 58 to 51 Total Balance:  100000.00
    Thread[Thread-61,5,main]     375.14 from 61 to 64 Total Balance:  100000.00
    Thread[Thread-1,5,main]     275.49 from 1 to 40 Total Balance:  100000.00
    Thread[Thread-33,5,main]     368.64 from 33 to 53 Total Balance:  100000.00
    Thread[Thread-25,5,main]     875.35 from 25 to 86 Total Balance:  100000.00
    Thread[Thread-30,5,main]     104.90 from 30 to 21 Total Balance:  100000.00
    

你可能感兴趣的:(Java基础)