线程状态

线程状态图
线程状态图

新创建

刚new出来的Thread还没有被运行,创建线程的三种方式

可运行

一旦调用start 方法,线程处于runnable状态。一个可运行的线桿可能正在运行也可能没有运行, 这取决于操作系统给线程提供运行的时间。

被阻塞线程和等待线程

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

  • 当一个线程试图获取一个内部的对象锁(而不是javiutiUoncurrent 库中的锁),而该锁被其他线程持有, 则该线程进人阻塞状态。当所有其他线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程将变成非阻塞状态。
  • 当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。在调用Object.wait方法或Thread.join方法,或者是等待java,util.concurrent 库中的Lock 或Condition 时,就会出现这种情况。实际上,被阻塞状态与等待状态是有很大不同的。
  • 有几个方法有一个超时参数。调用它们导致线程进人计时等待(timed waiting) 状态。这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有Thread.sleep 和Object.wait、Thread.join、Lock,tryLock以及Condition.await的计时版。

被终止的线程

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

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

同一个线程被 start() 两次(2019-7-13更新)

Java 的线程是不允许启动两次的,第二次调用必然会抛出 IllegalThreadStateException,这是
一种运行时异常,多次调用 start 被认为是编程错误。在第二次调用 start() 方法的时候,线程可能处于终止或者其他(非 NEW)状态,但是不论如何,都是不可以再次启动的。

线程状态之间相互转化
线程状态之间相互转化

创建一个新线程的三种方法

通过Runnable接口创建线程类

  1. 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 调用线程对象的start()方法来启动该线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class RunnableThreadTest implements Runnable {
private int i;
public void run() {
for (i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+ " " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+ " " + i);
if (i == 20) {
RunnableThreadTest rtt = new RunnableThreadTest();
new Thread(rtt, "新线程1").start();
new Thread(rtt, "新线程2").start();
}
}
}
}

继承Thread类创建线程类

  1. 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
  2. 创建Thread子类的实例,即创建了线程对象。
  3. 调用线程对象的start()方法来启动该线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class FirstThreadTest extends Thread {
int i = 0;
//重写run方法,run方法的方法体就是现场执行体
public void run() {
for (; i < 100; i++) {
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+ " : " + i);
if (i == 20) {
new FirstThreadTest().start();
new FirstThreadTest().start();
}
}
}
}

Callable 接口

Callable 与 Runable 有两点不同:

  • 可以通过 call() 获得返回值。
  • call() 可以抛出异常

Thread 和 Runnable 的区别

如果一个类继承 Thread, 则不适合资源共享。但是如果实现了 Runnable 接口的话,则很容易实现资源共享。

总结:
实现 Runnable 接口比继承 Thread 类具有的优势:

  1. 适合多个和相同的程序代码的线程去共享同一个资源。
  2. 可以避免 java 中的单继承的局限性。
  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
  4. 线程池只能放入实现 Runnable或 Callable 类线程,不能直接放入继承Thread 的类。

中断线程

没有任何语言方面的需求要求一个被中断的线程应该终止。中断一个线程不过是引起它的注意。被中断的线程可以决定如何响应中断。某些线程是如此重要以至于应该处理完异常后, 继续执行, 而不理会中断。但是,更普遍的情况是,线程将简单地将中断作为一个终止的请求。

线程属性

线程优先级

每当线程调度器有机会选择新线程时,它首先选择具有较高优先级的线程。但是,线程优先级是高度依赖于系统的。当虚拟机依赖于宿主机平台的线程实现机制时,Java 线程的优先级被映射到宿主机平台的优先级上,优先级个数也许更多,也许更少。

static void yield( )
导致当前执行线程处于让步状态。如果有其他的可运行线程具有至少与此线程同样高的优先级,那么这些线程接下来会被调度。注意,这是一个静态方法。

守护线程(2019-7-13更新)

有的时候应用中需要一个长期驻留的服务程序,但是不希望其影响应用退出,就可以将其设置为守护线程,如果 JVM 发现只有守护线程存在时,将结束进程,具体可以参考下面代码段。注意,必须在线程启动之前设置。

1
2
3
Thread daemonThread = new Thread();
daemonThread.setDaemon(true);
daemonThread.start();

同步

为了避免多线程引起的对共享数据的讹误,必须学习如何同步存取。

竞争条件详解

当两个线程试图同时更新同一个账户的时候,这个问题就出现了。假定两个线程同时执行指令accounts[to] += amount;问题在于这不是原子操作。该指令可能被处理如下:

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

现在,假定第1个线程执行步骤1和2, 然后,它被剥夺了运行权。假定第2个线程被唤醒并修改了accounts 数组中的同一项。然后,第1个线程被唤醒并完成其第3步。
这样,这一动作擦去了第二个线程所做的更新。于是,总金额不再正确。
因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。

锁同步

锁Lock

有两种机制防止代码块受并发访问的干扰。Java语言提供一个 synchronized 关键字达到这一目的,并且Java SE 5.0引入了ReentrantLock 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Bank{
private Lock bankLock = new ReentrantLock0 ;// ReentrantLock implements the Lock interface
public void transfer(int from, intto, int amount){
bankLock.lock();
try
{
System.out.print(Thread.currentThread0);
accounts[from] -= amount;
System.out.printf(" %10.2f from %A to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n",getTotalBalance());
}
finally
{
banklock.unlockO;
}
}
}

重入Lock是一个更强大的工具,他有一个重入功能————当一个线程得到一个对象后,再次请求该对象锁时是可以再次得到该对象的锁的。
具体概念就是:自己可以再次获取自己的内部锁。因为线程可以重复地获得已经持有的锁。锁保持一个持有计数(holdcount) 来跟踪对lock 方法的嵌套调用。线程在每一次调用lock 都要调用unlock 来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。
假定一个线程调用transfer, 在执行结束前被剥夺了运行权。假定第二个线程也调用transfer, 由于第二个线程不能获得锁,将在调用lock 方法时被阻塞。它必须等待第一个线程完成transfer 方法的执行之后才能再度被激活。当第一个线程释放锁时,那么第二个线程才能开始运行

公平锁CPU在调度线程的时候是在等待队列里随机挑选一个线程,由于这种随机性所以是无法保证线程先到先得的(synchronized控制的锁就是这种非公平锁)。但这样就会产生饥饿现象,即有些线程(优先级较低的线程)可能永远也无法获取CPU的执行权,优先级高的线程会不断的强制它的资源。那么如何解决饥饿问题呢,这就需要公平锁了。公平锁可以保证线程按照时间的先后顺序执行,避免饥饿现象的产生。但公平锁的效率比较低,因为要实现顺序执行,需要维护一个有序队列。

条件对象Condition
假定一个线程已经获得锁,将要执行,但是他所需要的条件还没有满足(例如在余额不足的情况下取钱),便会造成有锁却不执行,其他能够提供满足条件的线程(例如存钱)却只能等待,陷入僵局。
一个锁对象可以有一个或多个相关的条件对象。你可以用newCondition 方法获得一个条件对象。习惯上给每一个条件对象命名为可以反映它所表达的条件的名字。例如,在此设置一个条件对象来表达“ 余额充足”条件。

1
2
3
4
5
6
7
8
class Bank{
private Condition sufficientFunds;
···
public Bank(){
···
sufficientFunds=bankLock.newCondition();
}
}

如果transfer方法发现余额不足,它调用下面这个方法
sufficientFunds.await();
当前线程现在被阻塞了,并放弃了锁。我们希望这样可以使得另一个线程可以进行增加账户余额的操作。等待获得锁的线程和调用await 方法的线程存在本质上的不同。一旦一个线程调用await方法,它进人该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一条件上的signalAll 方法时为止。

synchronized关键字

在前面一节中,介绍了如何使用 Lock 和 Condition 对象。在进一步深人之前,总结一下有关锁和条件的关键之处:

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

Lock 和 Condition 接口为程序设计人员提供了高度的锁定控制。然而,大多数情况下,并不需要那样的控制,并且可以使用一种嵌人到Java 语言内部的机制。

如果一个方法用synchronized 关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。
换句话说

1
2
3
4
public synchronized void method()
{
method body
}

其实方法锁其实锁的是实例对象,可以等价于

1
2
3
4
5
6
public void method(){
//相当于锁实例
synchronized(this){
//需要同步的代码块
}
}

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

1
2
3
4
5
6
public static void method(){
//相当于锁的整个类
synchronized(xxx.class){
//需要同步的代码块
}
}

静态方法同步与非静态方法同步区别:

  • 静态同步:因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。
  • 非静态同步:锁住的是该对象,类的其中一个实例,当该对象(仅仅是这一个对象)在不同线程中执行这个同步方法时,线程之间会形成互斥。达到同步效果,但如果不同线程同时对该类的不同对象执行这个同步方法时,则线程之间不会形成互斥,因为他们拥有的是不同的锁。

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

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

synchronized和ReentrantLock的比较

区别:

  1. Lock是一个接口,是通过 JDK 来实现的,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现,是 JVM 实现的;
  2. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock() 去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;
  3. Lock可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断;
  4. 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
  5. Lock可以提高多个线程进行读操作的效率。

两者在锁的相关概念上区别:

  1. 可中断锁
    顾名思义,就是可以响应中断的锁。
    在Java中,synchronized就不是可中断锁,而Lock是可中断锁。如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
    lockInterruptibly()的用法体现了Lock的可中断性。
  2. 公平锁
    公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁(并不是绝对的,大体上是这种顺序),这种就是公平锁。
    非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
    在Java中,synchronized 就是非公平锁,它无法保证等待的线程获取锁的顺序。ReentrantLock可以设置成公平锁。
  3. 读写锁
    读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。
    正因为有了读写锁,才使得多个线程之间的读操作可以并发进行,不需要同步,而写操作需要同步进行,提高了效率。
    ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。
    可以通过readLock()获取读锁,通过writeLock()获取写锁。
  4. 绑定多个条件
    一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多余一个条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这么做,只需要多次调用new Condition()方法即可。

在新版的 JDK 中, synchronize 也逐渐有了很多优化,除非我们需要用到 ReentrantLock 的高级功能(比如上述几个锁),我们尽量选用 synchronize 关键词。

final

还有一种情况可以安全地访问一个共享域,即这个域声明为final 时。考虑以下声明:

1
final Map<String, Double〉accounts = new HashMap<>() ;

其他线程会在构造函数完成构造之后才看到这个accounts变量。

线程间协作

join

在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。

wait、notify、notifyall

调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。

它们都属于 Object 的一部分,而不属于 Thread。

只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateException。

使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。

死锁

产生条件

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

有3种典型的死锁类型:

静态的锁顺序死锁

a和b两个方法都需要获得A锁和B锁。一个线程执行a方法且已经获得了A锁,在等待B锁;另一个线程执行了b方法且已经获得了B锁,在等待A锁。这种状态,就是发生了静态的锁顺序死锁。

经典面试问题:写一个死锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class StaticLockOrderDeadLock{
private final Object lockA = new Object();
private final Object lockB = new Object();

public void a(){
synchronized(lockA){
synchronized(lockB){
System.out.println("function a");
}
}
}

public void b(){
synchronized(lockB){
synchronized(lockA){
System.out.println("function b");
}
}
}
}

解决静态的锁顺序死锁的方法就是:所有需要多个锁的线程,都要以相同的顺序来获得锁。

动态的锁顺序死锁

动态的锁顺序死锁是指两个线程调用同一个方法时,传入的参数颠倒造成的死锁。
如下代码,一个线程调用了transferMoney方法并传入参数accountA,accountB;另一个线程调用了transferMoney方法并传入参数accountB,accountA。此时就可能发生在静态的锁顺序死锁中存在的问题,即:第一个线程获得了accountA锁并等待accountB锁,第二个线程获得了accountB锁并等待accountA锁。

动态的锁顺序死锁解决方案如下:使用System.identifyHashCode来定义锁的顺序。确保所有的线程都以相同的顺序获得锁。

协作对象之间发生的死锁

有时,死锁并不会那么明显,比如两个相互协作的类之间的死锁,比如下面的代码:一个线程调用了Taxi对象的setLocation方法,另一个线程调用了Dispatcher对象的getImage方法。此时可能会发生,第一个线程持有Taxi对象锁并等待Dispatcher对象锁,另一个线程持有Dispatcher对象锁并等待Taxi对象锁。

上面的代码中,我们在持有锁的情况下调用了外部的方法,这是非常危险的(可能发生死锁)。为了避免这种危险的情况发生,我们使用开放调用。如果调用某个外部方法时不需要持有锁,我们称之为开放调用。解决协作对象之间发生的死锁:需要使用开放调用,即避免在持有锁的情况下调用外部的方法

锁优化

多线程

更多关于Java并发多线程请点击Java进阶学习多线程