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

来自Wikioe
跳到导航 跳到搜索
第228行: 第228行:


== 同步 ==
== 同步 ==
多个线程共享同一数据时,线程发出一个方法调用,在没有得到结果之前,这个调用就不返回,同时其它的线程也不能调用这个方法。
[[File:非同步线程与同步线程的比较.png|400px]]
=== 锁对象 ===
有两种机制防止'''代码块'''受并发访问的干扰:
# Java 语言提供一个'''synchronized''' 关键字
# 并且Java SE 5.0 引入了'''ReentrantLock''' 类。
* 把解锁操作括在finally 子句之内是至关重要的。如果在临界区的代码抛出异常,锁必须被释放。否则,其他线程将永远阻塞。
* 如果使用锁, 就不能使用带资源的try 语句。
* 锁是可重入的, 因为线程可以重复地获得已经持有的锁。锁保持一个持有计数( holdcount ) 来跟踪对lock 方法的嵌套调用。线程在每一次调用lock 都要调用unlock 来释放锁。由于这一特性, 被一个锁保护的代码可以调用另一个使用相同的锁的方法。
==== 示例: ====
<syntaxhighlight lang="java">
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;
}
}
}
</syntaxhighlight>
如上:
# transfer 方法调用getTotalBalance 方法, 这也会封锁bankLock 对象,此时bankLock对象的持有计数为2。
# 当getTotalBalance 方法退出的时候, 持有计数变回1。
# 当transfer 方法退出的时候, 持有计数变为0。线程释放锁。
==== 相关方法 ====
===== java.util.concurrent.locks.Lock =====
* void lock( )
*: 获取这个锁;如果锁同时被另一个线程拥有则发生阻塞。
*  void unlock( )
*: 释放这个锁。
===== java,util.concurrent.locks.ReentrantLock =====
* ReentrantLock( )
*: 构建一个可以被用来保护临界区的可重入锁。
* ReentrantLock(boo1ean fair )
*: 构建一个带有公平策略的锁。一个公平锁偏爱等待时间最长的线程。但是,这一公平的保证将大大降低性能。所以, 默认情况下, 锁没有被强制为公平的。
=== 条件对象(Condition) ===
条件对象用于管理那些已经获得了一个锁但是却不能做有用工作的线程。
==== 使用 ===
# 一个锁对象可以有一个或多个相关的条件对象,用 '''newCondition''' 方法获得一个条件对象。
#: <syntaxhighlight lang="java">
class Bank
{
  private Condition sufficientFunds;
  public BankO
  {
      sufficientFunds = bankLock.newConditionO;
  }
}
</syntaxhighlight>
# 当条件不满足时,调用'''await''':
#: <syntaxhighlight lang="java">
sufficientFunds.await();
</syntaxhighlight>
#: 当前线程现在被阻塞了,并放弃了锁。
#: 一旦一个线程调用await方法, 它进人该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一条件上的signalAll 方法时为止。
# 重新激活因为这一条件而等待的所有线程'''signalAll''':
#: <syntaxhighlight lang="java">
sufficientFunds,signalAll();
</syntaxhighlight>
* 至关重要的是最终需要某个其他线程调用signalAll 方法
* 调用signalAll 不会立即激活一个等待线程。它仅仅解除等待线程的阻塞, 以便这些线程可以在当前线程退出同步方法之后, 通过竞争实现对对象的访问。
==== 示例: ====
<syntaxhighlight lang="java">
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;
  }
}
</syntaxhighlight>
== 阻塞队列 ==
== 阻塞队列 ==
== 线程安全的集合 ==
== 线程安全的集合 ==

2020年10月20日 (二) 00:47的版本


什么是线程

一个程序同时执行多个任务,每一个任务称为一个线程(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;
   }
}

阻塞队列

线程安全的集合

Callable 与 Future

执行器

同步器

线程与Swing