“核心技术:并发”的版本间差异

来自Wikioe
跳到导航 跳到搜索
第477行: 第477行:
=== 死锁(deadlock) ===
=== 死锁(deadlock) ===


# 互斥访问:一个资源每次只能被一个线程使用。
# 请求与保持:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
# 不可抢占:线程已获得的资源,在未使用完之前,不能强行剥夺。
# 循环等待:若干线程之间形成一种头尾相接的循环等待资源关系。
相关:
* 银行家算法


=== 线程局部变量 ===
=== 线程局部变量 ===

2020年10月20日 (二) 16:58的版本


什么是线程

一个程序同时执行多个任务,每一个任务称为一个线程(thread), 它是线程控制的简称。

  • 进程与线程有的区别,在于每个进程拥有自己的一整套变量, 而线程则共享数据。
    共享变量使线程之间的通信比进程之间的通信更有效、更容易。

使用线程

实现线程的两种方式:

  1. 实现Runnable接口,利用接口构造Thread对象;(Runnable 是一个函数式接口,可以用lambda 表达式)
    Runnable r = () -> { /*task code*/ };
    Thread t = new Thread(r);
    
  2. 继承Thread类,构造一个子类的对象;
    class MyThread extends Thread
    {
       public void run()
       {
          //task code
       }
    }
    


  • 不要调用Thread类或Runnable对象的run方法,而应该调用Thread.start方法。
    直接调用run 方法,只会执行同一个线程中的任务,而不会启动新线程。
  • 如果有很多任务, 要为每个任务创建一个独立的线程所付出的代价太大了。可以使用线程池来解决这个问题


相关方法

java.Iang.Thread
  • Thread( Runnable target )
    构造一个新线程, 用于调用给定目标的nm() 方法。
  • void start( )
    启动这个线程, 将引发调用mn() 方法。这个方法将立即返回, 并且新线程将并发运行。
  • void run( )
    调用关联 Runnable 的run 方法。
  • static void sleep(long minis)
    休眠给定的毫秒数。参数:millis 休眠的毫秒数
java.lang.Runnable
  • void run( )
    必须覆盖这个方法, 并在这个方法中提供所要执行的任务指令。

中断线程

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

  • 在Java 的早期版本中,还有一个stop方法,其他线程可以调用它终止线程。
    因为,stop会瞬间强行停止一个线程,且该线程持有的锁并不能释放;所以,这个方法现在已经被弃用了。



相关方法

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

线程状态

线程状态.png

线程可以有如下6 种状态:

  1. New ( 新创建)
  2. Runnable (可运行)
  3. Blocked ( 被阻塞)
  4. Waiting ( 等待)
  5. Timed waiting (计时等待)
  6. Terminated ( 被终止)
  • 可调用“getState”方法获取一个线程的当前状态;


Java线程状态图.jpeg

新创建线程

用 new 操作符创建一个新线程,未调用 start 方法,该线程还没有开始运行,即 New 状态

可运行线程

一旦调用 start 方法,则线程处于 runnable 状态。

  • (Java 的规范说明没有将它作为一个单独状态:正在运行中的线程仍然处于可运行状态。)


结合线程状态图,理解为:“可运行” = “运行” + “就绪”

被阻塞线程和等待线程

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


阻塞(blocked)

阻塞:当一个线程请求锁,而该资源被其他线程持有时,该线程被置为阻塞状态

  • 请求内部的对象锁(非java.util.concurrent库中的锁)。

等待(waiting)

等待:当线程等待另一个线程通知调度器一个条件时,该线程主动进入等待状态
如:

  1. Object.wait
  2. Thread.join
  3. java.util.concurrent 库中的Lock 或Condition

计时等待(timed waiting)

计时等待状态将一直保持到超时期满或者接收到适当的通知。
如:

  1. Object.wait(long millis)
  2. Thread.join(long millis)
  3. java.util.concurrent 库中的Lock(long millis) 或Condition(long millis)

被终止的线程(Terminated)

线程因如下两个原因之一而被终止:

  1. 因为run 方法正常退出而自然死亡。
  2. 因为一个没有捕获的异常终止了nm 方法而意外死亡。
  • 不应该使用“stop”方法来终止线程!(虽然能做到)

相关方法

java.iang.Thread
  • void join()
    等待指定线程的终止。【在当前线程调用“s.join();”,则必须等待线程s执行完毕,当前线程才能继续执行】
  • void join(long mi11is)
    等待指定的线程死亡或者经过指定的毫秒数。
  • Thread.State getState()
    得到这一线程的状态;NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING 或 TERMINATED 之一。
  • void stop()
    停止该线程。这一方法已过时。
  • void suspend()
    暂停这一线程的执行。这一方法已过时。
  • void resume()
    恢复线程。这一方法仅仅在调用suspend() 之后调用。这一方法已过时。

线程属性

线程的各种属性,如:线程优先级、守护线程、线程组以及处理未捕获异常的处理器。

	private int priority;
	...
	private boolean daemon = false;
	...
	private ThreadGroup group;
	

    public final static int MIN_PRIORITY = 1;

    public final static int NORM_PRIORITY = 5;

    public final static int MAX_PRIORITY = 10;
	
	...
	// null unless explicitly set
    private volatile UncaughtExceptionHandler uncaughtExceptionHandler;

    // null unless explicitly set
    private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;

线程优先级(Priority)

  • 默认情况下,线程会继承它的父线程的优先级;(可以在 init 方法中发现,同时还会继承 “group”、“daemon”)
  • 可以用setPriority 方法提高或降低任何一个线程的优先级。


  • 不要将程序构建为功能的正确性依赖于优先级。
如果确实要使用优先级, 应该避免初学者常犯的一个错误。如果有几个高优先级的线程没有进入非活动状态, 低优先级的线程可能永远也不能执行。每当调度器决定运行一个新线程时, 首先会在具有高优先级的线程中进行选择, 尽管这样会使低优先级的线程完全饿死。

【???不用动态优先级吗,还是和底层实现有关?】

相关方法

java.lang.Thread
  • void setPriority(int newPriority)
    设置线程的优先级。优先级必须在Thread.MIN_PRIORITY 与Thread.MAX_PRIORITY之间。一般使用Thread.NORMJ»RIORITY 优先级。
  • static int MIN_PRIORITY
    线程的最小优先级。最小优先级的值为1。
  • static int N0RM_PRI0RITY
    线程的默认优先级。默认优先级为5。
  • static int MAX—PRIORITY
    线程的最高优先级。最高优先级的值为10。
  • static void yield( )
    导致当前执行线程处于让步状态。如果有其他的可运行线程具有至少与此线程同样高的优先级,那么这些线程接下来会被调度。注意,这是一个静态方法。【让出CPU】

守护线程(Daemon)

  • 守护线程的唯一用途是为其他线程提供服务,当只剩下守护线程时,虚拟机就退出了。
  • 可以通过调用“t .setDaemon(true);”将线程转换为守护线程(daemon thread)。
  • 守护线程应该永远不去访问固有资源, 如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。

相关方法

java.lang.Thread
  • void setDaemon( boolean isDaemon )
    标识该线程为守护线程或用户线程。这一方法必须在线程启动之前调用。

线程组(ThreadGroup)

线程组是一个可以统一管理的线程集合。默认情况下,创建的所有线程属于相同的线程组,但是,也可能会建立其他的组。现在引入了更好的特性用于线程集合的操作,所以建议不要在自己的程序中使用线程组。

相关方法

java.lang.ThreadGroup
  • void UncaughtException( Thread t, Throwable e)
    如果有父线程组,调用父线程组的这一方法; 或者,如果Thread 类有默认处理器,调用该处理器,否则,输出栈轨迹到标准错误流上(但是, 如果e 是一个ThreadDeath对象,栈轨迹是被禁用的。ThreadDeath 对象由stop 方法产生, 而该方法已经过时)。


未捕获异常处理器(uncaughtExceptionHandler)

  • 线程的run 方法不能抛出任何受查异常。非受査异常会导致线程终止,在线程死亡之前,异常被传递到一个用于未捕获异常的处理器。
  • 该处理器必须属于一个实现Thread.UncaughtExceptionHandler接口的类,这个接口只有—个方法“void uncaughtException(Thread t, Throwable e);”

相关方法

java.lang.Thread
  • static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler )
  • static Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler()
    设置或获取未捕获异常的默认处理器。
  • void setUncaughtExceptionHandler( Thread.UncaughtExceptionHandlerhandler )
  • Thread.UncaughtExceptionHandler getUncaughtExceptionHandler( )
    设置或获取未捕获异常的处理器。如果没有安装处理器, 则将线程组对象作为处理器。
java.Iang.Thread.UncaughtExceptionHandler
  • void UncaughtException(Thread t , Throwable e)
    当一个线程因未捕获异常而终止, 按规定要将客户报告记录到日志中。
    参数:t 由于未捕获异常而终止的线程;e 未捕获的异常对象;

同步

多个线程共享同一数据时,线程发出一个方法调用,在没有得到结果之前,这个调用就不返回,同时其它的线程也不能调用这个方法。

非同步线程与同步线程的比较.png

锁对象

有两种机制防止代码块受并发访问的干扰:

  1. Java 语言提供一个synchronized 关键字
  2. 并且Java SE 5.0 引入了ReentrantLock 类。


  • 把解锁操作括在finally 子句之内是至关重要的。如果在临界区的代码抛出异常,锁必须被释放。否则,其他线程将永远阻塞。
  • 如果使用锁, 就不能使用带资源的try 语句。


  • 锁是可重入的, 因为线程可以重复地获得已经持有的锁。锁保持一个持有计数( holdcount ) 来跟踪对lock 方法的嵌套调用。线程在每一次调用lock 都要调用unlock 来释放锁。由于这一特性, 被一个锁保护的代码可以调用另一个使用相同的锁的方法。

示例:

public class Bank {
	private Lock bankLock = new ReentrantLock0// ReentrantLock implements the Lock interface
	...
	public void transfer(int from, int to, int amount) {
		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.unlockO;
		}
	}
}

如上:

  1. transfer 方法调用getTotalBalance 方法, 这也会封锁bankLock 对象,此时bankLock对象的持有计数为2。
  2. 当getTotalBalance 方法退出的时候, 持有计数变回1。
  3. 当transfer 方法退出的时候, 持有计数变为0。线程释放锁。


相关方法

java.util.concurrent.locks.Lock
  • void lock( )
    获取这个锁;如果锁同时被另一个线程拥有则发生阻塞。
  • void unlock( )
    释放这个锁。
java,util.concurrent.locks.ReentrantLock
  • ReentrantLock( )
    构建一个可以被用来保护临界区的可重入锁。
  • ReentrantLock(boo1ean fair )
    构建一个带有公平策略的锁。一个公平锁偏爱等待时间最长的线程。但是,这一公平的保证将大大降低性能。所以, 默认情况下, 锁没有被强制为公平的。

条件对象(Condition)

条件对象用于管理那些已经获得了一个锁但是却不能做有用工作的线程。

使用

  1. 一个锁对象可以有一个或多个相关的条件对象,用 newCondition 方法获得一个条件对象。
    class Bank
    {
       private Condition sufficientFunds;
       public BankO
       { 
          sufficientFunds = bankLock.newConditionO;
       }
    }
    
  2. 当条件不满足时,调用await
    sufficientFunds.await();
    
    当前线程现在被阻塞了,并放弃了锁。
    一旦一个线程调用await方法, 它进人该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一条件上的signalAll 方法时为止。
  3. 重新激活因为这一条件而等待的所有线程signalAll
    sufficientFunds,signalAll();
    


  • 至关重要的是最终需要某个其他线程调用signalAll 方法
  • 调用signalAll 不会立即激活一个等待线程。它仅仅解除等待线程的阻塞, 以便这些线程可以在当前线程退出同步方法之后, 通过竞争实现对对象的访问。

示例:

public void transfer(int from, int to, int amount)
{
   bankLock.lockO
   try
   {
      while (accounts[from] < amount)
         sufficientFunds.awaitO
      
      // transfer funds
      ...
      
      
      sufficientFunds.signalAll()
   }
   finally
   {
      bankLock.unlock0;
   }
}

synchronized 关键字

有关锁和条件的关键之处:

  • 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。
  • 锁可以管理试图进入被保护代码段的线程。
  • 锁可以拥有一个或多个相关的条件对象。
  • 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。

synchronized 与 wait、notifyAll

从1.0 版开始,Java中的每一个对象都有一个内部锁。如果一个方法用 synchronized 关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法, 线程必须获得内部的对象锁。

内部对象锁只有一个相关条件。wait 方法添加一个线程到等待集中,notifyAll /notify 方法解除等待线程的阻塞状态。
  • 每一个对象有一个内部锁, 并且该锁有一个内部条件。

即:

  1. public synchronized void methodO
    {
       method body
    }
    
    等价于
    public void methodQ
    {
       this.intrinsidock.1ock();
       try
       {
          method body
       }
       finally { this.intrinsicLock.unlockO; }
    }
    
  2. wait 、notityAll等价于
    intrinsicCondition.await();
    intrinsicCondition.signalAll();
    
示例
class Bank
{
   private double[] accounts;
   public synchronized void transfer(int fromint to, int amount) throws InterruptedException
   {
      while (accounts[from] < amount)
         wait();      // wait on intrinsic object lock's single condition
      accounts[from] -= amount ;
      accounts[to] += amount ;
      notifyAll();      // notify all threads waiting on the condition
   }
   public synchronized double getTotalBalanceO { . . . }
}

Synchronized 使用

  • 将静态方法声明为 synchronized 时,该方法获得相关的类对象的内部锁,没有其他线程可以调用同一个类的这个或任何其他的同步静态方法。
    例如, 如果Bank 类有一个静态同步的方法,那么当该方法被调用时,Bank.class对象的锁被锁住。


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

  1. 不能中断一个正在试图获得锁的线程。
  2. 试图获得锁时不能设定超时。
  3. 每个锁仅有单一的条件, 可能是不够的。


  • 最好使用JUC框架(java.util.concurrent)提供的锁,而不是“Lock/Condition”和“synchronized”。

相关方法

java.lang.Object
  • void notifyAl1 ()
    解除那些在该对象上调用wait 方法的线程的阻塞状态。该方法只能在同步方法或同步块内部调用。如果当前线程不是对象锁的持有者,该方法拋出一个IllegalMonitorStateException异常。
  • void notify( )
    随机选择一个在该对象上调用wait 方法的线程, 解除其阻塞状态。该方法只能在一个同步方法或同步块中调用。如果当前线程不是对象锁的持有者, 该方法抛出一个IllegalMonitorStateException 异常。
  • void wait( )
    导致线程进人等待状态直到它被通知。该方法只能在一个同步方法中调用。如果当前线程不是对象锁的持有者,该方法拋出一个IllegalMonitorStateException 异常。
  • void wait( long mi11is)
  • void wait( long mi 11Is, int nanos )
    导致线程进入等待状态直到它被通知或者经过指定的时间。这些方法只能在一个同步方法中调用。如果当前线程不是对象锁的持有者该方法拋出一个IllegalMonitorStateException异常。
    参数: millis 毫秒数;nanos 纳秒数,<1 000 000

同步阻塞

除了调用同步方法,还有另一种机制可以获得锁:通过进入一个同步阻塞。


如:

public class Bank
{
   private doublet[] accounts;
   private Object lock = new Object();
   ...
   public void transfer(int from, int to, int amount)
   {
      synchronized (lock) // an ad-hoc lock
      {
         accounts[from] -= amount;
         accounts[to] += amount;
      }
      System.out.print1n( ... );
   }
}
  • lock 对象被创建仅仅是用来使用每个Java 对象持有的锁。

监视器概念(monitor)

监视器可以在不需要程序员考虑如何加锁的情况下,就可以保证多线程的安全性。
监视器具有如下特性:

  1. 监视器是只包含私有域的类。
  2. 每个监视器类的对象有一个相关的锁。
  3. 使用该锁对所有的方法进行加锁。换句话说,如果客户端调用obj.methood() , 那么obj对象的锁是在方法调用开始时自动获得,并且当方法返回时自动释放该锁。因为所有的域是私有的,这样的安排可以确保一个线程在对对象操作时,没有其他线程能访问该域。
  4. 该锁可以有任意多个相关条件。


Java中以不是很精确的方式采用了监视器概念:

  1. 用synchronized 关键字声明方法
  2. 通过调用wait/notifyAU/notify 来访问条件变量


Java 对象不同于监视器, 从而使得线程的安全性下降:

  1. 域不要求必须是private。
  2. 方法不要求必须是synchronized。
  3. 内部锁对客户是可用的。【?】

Volatile 域

volatile 关键字为实例域的同步访问提供了一种免锁机制

如果声明一个域为volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。


  • volatile 不保证原子性

原子性

java.util.concurrent.atomic 包中有很多类使用了很高效的机器级指令(而不是使用锁) 来保证其他操作的原子性(操作不会中断)。

死锁(deadlock)

  1. 互斥访问:一个资源每次只能被一个线程使用。
  2. 请求与保持:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不可抢占:线程已获得的资源,在未使用完之前,不能强行剥夺。
  4. 循环等待:若干线程之间形成一种头尾相接的循环等待资源关系。


相关:

  • 银行家算法

线程局部变量

锁测试域超时

读/写锁(ReenterantReadWriteLock)

为什么弃用 stop 和 suspend 方法

阻塞队列

线程安全的集合

Callable 与 Future

执行器

同步器

线程与Swing