Android Tech And Thoughts.

Java Concurrency - Learning core Java

Word count: 4.2kReading time: 15 min
2020/01/02 Share

中断

中断线程

在Java的早期版本中,stop方法可以终止某个线程,但是现在已被弃用。
没有强制线程终止的办法,然而,interrupt方法可以用来请求终止线程。

中断状态是每一个线程都具有的boolean标志。每个线程都应该不时地检查这个标志,以判断线程是否被中断。而interrupt方法则会将线程的中断状态置位,所以interrupt方法可以用来请求终止线程,注意,是请求,而非要求。

1
while(!Thread.currentThread().isInterrupted() && more work todo){
2
	do more work;
3
}

上面提到,每个线程都应该不时地检查中断状态,但是,如果线程被阻塞,就无法检测中断状态(丧失了CPU控制权,怎么检查)。这也是产生 InterruptedException 异常的地方。当在一个被阻塞(调用sleep或wait)的线程上调用interrupted方法时,阻塞调用将会被 Interrupted Exception 异常中断。

具体来说,当对一个线程,调用 interrupt() 时:

  • 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常 (Dan: 同时清除中断状态) 仅此而已。
  • 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。

如何处理中断请求

请注意,没有任何语言方面的需求要求一个被中断的线程应该终止

中断一个线程不过是引起它的注意,被中断的线程可以决定如何响应中断。你可以继续运行代码,也可以退出线程。

阻塞与中断的关系

阻塞表示线程的一种状态,在这种状态下,线程是不占CPU的,更进一步来说,也就是你的代码在执行过程中,在某个地方暂停了。

当Java的某个线程处于可中断的阻塞状态时,你用另一个线程调用该线程的interrupt()方法时,JVM会使该线程离开阻塞状态,并抛出一个异常InterruptException ,大多数时候阻塞方法在返回的时候,还会清除中断标记(刚刚说的被interrupt置位的中断标志)。说白了就是你不能中断一个阻塞的线程,调用interrupt方法除了唤醒它并抛出一个异常外,没有其它的作用。
这就是Exception的设计哲学了,为什么此刻应该抛出一个异常,阻塞方法直接返回不就行了吗?

线程状态

线程可以有如下6种状态

  • 新创建(New)
  • 可运行(Runnable)
  • 被阻塞(Blocked)
  • 等待(wait)
  • 计时等待(Timed waiting)
  • 被终止(Terminated)

可运行态

一旦调用start方法,线程处于Runnable状态。一个可运行的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间,这也是为什么将其称为可运行态而不是运行态。

被阻塞于等待

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

  • 当线程试图获取一个内部的对象锁(而不是java.util.concurrent库中的锁),而该锁被其它线程持有,则线程进入阻塞状态
  • 当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。

被阻塞与等待是两种不同的状态

被终止

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

  • run方法正常退出
  • 因为一个没有捕获的异常终止了 run 方法而意外死亡

1577968535_1_.png

线程属性

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

线程优先级

每一个线程有一个线程优先级,默认情况下,一个线程继承它的父线程的优先级,可以用setPriority来设置一个线程的优先级(1~10),Normal_PRIORITY=5
每当线程调度器有机会选择新线程时,它首先选择具有较高优先级的线程。但是线程优先级是高度依赖于系统的,当虚拟机依赖主机平台的线程实现机制时,Java线程的优先级被映射到宿主平台的优先级上,优先级个数也许更多,也许更少(可能原本不同的优先级,映射之后就相同了)

守护线程

可以通过 t.setDaemon(true) 将线程转换为守护线程。它没有什么神奇的,它唯一的作用就是为其他线程提供服务,计时线程就是一个例子。当只剩下守护线程时,虚拟机就退出了。守护线程应该永远不去访问固有资源,如文件数据库等,因为它会在任何适合甚至在一个操作的中间发生中断。

未捕获异常处理器

线程的 run 方法不能抛出任何受查异常。
当抛出非受查异常的时候,会导致线程终止。在这种情况下,线程就死亡了。

但是,不需要你来捕获这些异常,就在线程死亡之前,异常被传递到一个用于捕获异常的处理器。该处理器必须属于一个实现了 Thread.UncaughtExceptionHanldler 接口的类,这个接口只有一个方法:

1
void uncaughtException(Thread t,Throwable e)

同步

竞争条件 - race condition

竞争条件的例子:银行取款

锁对象

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

  1. synchronized
  2. ReentrantLock : 可重入的锁,可重入锁

sychronized关键字自动提供一个锁以及相关的”条件”
java.util.concurrent 框架位这些基础机制提供独立的类

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

1
mLock.lock();
2
3
try{
4
	critical section
5
}finally{
6
	mLock.unlock(); // make sure the lock is unlocked even if an exception is thrown
7
}

这一结构确保任何时刻只有一个线程进入临界区,一旦一个线程封锁了锁对象,其它任何线程都无法通过 lock 语句,当其他线程调用 lock 时,它们被阻塞,直到第一个线程释放锁对象。记住,把 unlock 放在 finally 内至关重要,如果在临界区的代码抛出异常,锁必须被释放,否则其它线程将永远阻塞。

注意每一个 Bank 对象有自己的 ReentrantLock 对象。如果两个线程试图访问同一个Bank 对象,那么锁以串行方式提供服务。但是, 如果两个线程访问不同的 Bank 对象, 每一个线程得到不同的锁对象, 两个线程都不会发生阻塞。本该如此,因为线程在操纵不同的Bank 实例的时候, 线程之间不会相互影响。

附上Lock的实现:

1
public class Lock{
2
    private boolean isLocked = false;
3
    public synchronized void lock() throws InterruptedException{
4
        while(isLocked){    //不用if,而用while,是为了防止假唤醒
5
            wait();
6
        }
7
        isLocked = true;
8
    }
9
    public synchronized void unlock(){
10
        isLocked = false;
11
        notify();
12
    }
13
}

锁是可重入的。因为线程可以重复地获得已经持有的锁。锁保持一个持有计数(hold count)来跟踪对lock方法的嵌套调用,对于同一个线程来说,每一次lock,对象的持有计数加1,每一次unlock,计数器-1。通常,可能想要保护若干个操作来更新或检查共享对象的代码块,要确保这些操作完成后,另一个线程才能使用相同对象

1
public class Lock{
2
    boolean isLocked = false;
3
    Thread  lockedBy = null;
4
    int lockedCount = 0;
5
    public synchronized void lock()
6
        throws InterruptedException{
7
        Thread callingThread = Thread.currentThread();
8
        while(isLocked && lockedBy != callingThread){
9
            wait();
10
        }
11
        isLocked = true;
12
        lockedCount++;
13
        lockedBy = callingThread;
14
  }
15
    public synchronized void unlock(){
16
        if(Thread.curentThread() == this.lockedBy){
17
            lockedCount--;
18
            if(lockedCount == 0){
19
                isLocked = false;
20
                notify();
21
            }
22
        }
23
    }
24
}

ReentrantLock有两个构造方法

  • ReentrantLock()

  • ReentrantLock(boolean fair)

    构建一个公平锁。一个公平锁偏爱等待时间最长的线程,但是,这一策略将大大降低性能。所以默认情况下,锁没有被强制为公平

条件对象

当一个线程进入临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。(你需要理解为什么不是进入之前做判断,因为进入之前到进入是有时间差的,有可能条件已经改变)。由于历史原因,条件对象经常被称为条件变量(conditional variable)

Java种监视器的概念

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。本质都是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器

代码块同步是使用monitorenter和monitorexit指令实现的。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

Volatile域

首先了解一下Java并发编程种的三个概念

  • 原子性
  • 可见性
  • 有序性

1.原子性

即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
在Java种,对基本数据类型的变量的读取和赋值操作是原子性的操作,即这些操作是不可被中断的,要么执行,要么不执行。

看看下面四个语句:

1
x = 10;     //原子性
2
y = x;      //非
3
x++;  			//非
4
x = x + 1;	//非

后面三句都需要先读取x=变量的值,然后进行其他操作,那么在读取x变量值后,程序都可能被打断,这时就破坏了原子性

保证原子性的方法: synchronized 和 Lock 。java.util.concurrent.atomic包中有很多类使用了很高效的机器级指令(而不是使用锁)来保证其它操作的原子性。例如 AtomicInterger类提供了方法 increnebrAndGet 和 decrementAndGet,它们分别以源自方式将一个整数自增或自减

2.可见性

首先思考一个问题,只涉及到一个共享变量的修改和读取,需要使用同步么?
由于二者都是原子操作,所以看似不需要同步,但是!这种情况下是可能出错的,而且出错的概率很大!!!

当一个共享变量被voliatile修饰时,它会保证修改的值会被立即更新到主存,当有其它线程需要读取时,它会去内存中读取新值。而普通的共享变量则无法保证可见性,因为变量被修改后,什么时候被写入主存是不确定的,当其它线程尝试读取时,此时内存中可能还是之前的旧值,对于现代处理器,运行在不同处理器上的线程可能在同一个内存位置取到不同的值。
Synchronized和Lock也能够保证可见性,释放锁之前会将对变量的修改刷新到主存当中,因此可以保证可见性。但是synchronized和Lock的开销都很大,我们如果只是涉及到对某个共享变量的修改和读取,则没必要这样做,而是使用 voliatle关键字即可,volatile关键字为实例域的同步访问提供了一种免锁机制,如果声明一个域为volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。

3.有序性

先介绍一下 happens-before 原则(先行发生原则)

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则: 一个unlock操作先行发生于后面对同一个锁的lock操作
  • volatile规则: 对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则: 如果操作A先行发生于操作B,而操作B又先行于C,则A先行于C

需要关注的是 volatile规则,直观的解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。

final变量

除了使用锁或volatile修饰符,还有一种情况可以安全地访问一个共享域,即这个域声明为final时。其它线程会在构造函数完成构造之后才看到这个account变量。
如果不使用final,就不能保证其他线程看到地时account更新后地值,它们可能都只是看到null,而不是新构造地HashMap

死锁

锁和条件不能解决多线程中的所有问题,考虑如下情况:


账户1:200

账户2:300

线程1:从账户1转300给账户2

线程2:从账户2转400给账户1


有可能会因为每一个线程需要等待更多的欠款存入,而导致所有线程都被阻塞,这样的状态称为死锁(deadlock) 。没有任何一个线程能够满足运行下去(解除阻塞)的条件,而导致所有的线程都处于阻塞状态。

线程局部变量 ThreadLocal

前面我们都在讨论的是共享变量的风险,有时可能要避免共享变量,使用 ThreadLocal 辅助类为各个线程提供各自的实例。

锁测试与超时 : 破局死锁

线程在调用 lock 方法来获得另一个线程所持有的锁的时候,很可能发生阻塞,应该更加谨慎地申请锁。tryLock方法试图申请一个锁,在成功后返回true,否则,立即返回false,线程可以立即离开去做其它事情

1
if(mLock.tryLock()){
2
	//now the thread owns the lock
3
	try{
4
		.......
5
	}finally{
6
		mLock.unLock();
7
	}
8
}else{
9
	// do sth
10
}

还可以采用超时策略,像下面这样

1
if(mLock.tryLock(100,TimeUnit.MILLISECONDS))...

lock不响应中断(为什么这么设计,一个可能阻塞的方法为什么不设计成可响应中断呢,我的猜测是同时提供多个方法,以供你选择,以增加程序的可定制性)
tryLock响应中断,lockInterruptily延时为无线久的tryLock。

**为什么需要tryLock和tryInterrruptibly?
为什么只有lockInterruptibly可以被interrupted

首先你得了解为什么Lock要提供tryLock和lockInterruptibly因为它要破坏死锁中的“不可抢占条件”。synchronized没办法破坏“不可抢占条件”,synchronized申请资源的时候,如果申请不到,线程直接进入阻塞状态,无法释放已经占有的锁。但我们希望的是:如果占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占条件被破坏掉了。所以,Lock提供了三种方法,均可以破坏不可抢占条件。这也是为什么有了synchronized还要搞一个Lock的原因之一。tryLock(long time, TimeUint unit): 一段时间内没有获取锁,不是进入阻塞状态,而是返回一个错误;tryLock:立即返回,获得锁返回true,没获得锁返回false;tryInterruptibly:在锁上等待,直到获取锁,但是会响应中断,这个方法优先考虑响应中断,而不是响应锁的普通获取或重入获取。

Thanks:

InterruptedException
知乎Thread.interrupt意味着什么
为什么java里的线程可以在阻塞的时候捕获中断异常并处理?
不可不说的Java”锁”事
Asynchronous vs synchronous execution, what does it really mean?
Lock、tryLock以及lockInterruptibly的区别和联系
Java并发编程:volatile关键字
有了缓存一致性为什么还要多线程同步

CATALOG
  1. 1. 中断
    1. 1.1. 中断线程
      1. 1.1.1. 如何处理中断请求
      2. 1.1.2. 阻塞与中断的关系
  2. 2. 线程状态
    1. 2.1. 可运行态
    2. 2.2. 被阻塞于等待
    3. 2.3. 被终止
  3. 3. 线程属性
    1. 3.1. 线程优先级
    2. 3.2. 守护线程
    3. 3.3. 未捕获异常处理器
  4. 4. 同步
    1. 4.1. 竞争条件 - race condition
    2. 4.2. 锁对象
    3. 4.3. 条件对象
    4. 4.4. Java种监视器的概念
    5. 4.5. Volatile域
      1. 4.5.1. 1.原子性
      2. 4.5.2. 2.可见性
      3. 4.5.3. 3.有序性
    6. 4.6. final变量
    7. 4.7. 死锁
    8. 4.8. 线程局部变量 ThreadLocal
    9. 4.9. 锁测试与超时 : 破局死锁
    10. 4.10. Thanks: