什么是线程
线程5种状态
- 新建状态(New): 当用new操作符创建一个线程时, 例如new Thread(r),线程还没有开始运行,此时线程处在新建状态。 当一个线程处于新生状态时,程序还没有开始运行线程中的代码
- 就绪状态(Runnable) 一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。 处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度的。
- 运行状态(Running) 当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法.
- 阻塞状态(Blocked)
线程运行过程中,可能由于各种原因进入阻塞状态:
1>线程通过调用sleep方法进入睡眠状态;
2>线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
3>线程试图得到一个锁,而该锁正被其他线程持有;
4>线程在等待某个触发条件;
所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。 - 死亡状态(Dead) 有两个原因会导致线程死亡: 1) run方法正常退出而自然死亡, 2) 一个未捕获的异常终止了run方法而使线程猝死。 为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false.
线程同步的7种方式
java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查), 将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。
方式1:同步方法
即有synchronized关键字修饰的方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。 代码如:
1 | public synchronized void save(){} |
注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类
方式2:同步代码块
即有synchronized关键字修饰的语句块。 被该关键字修饰的语句块会自动被加上内置锁,从而实现同步 代码如:
1 | synchronized(object){ |
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
方式3:使用特殊域变量(volatile)实现线程同步
- volatile关键字为域变量的访问提供了一种免锁机制,
- 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,
- 因此每次使用该域就要重新计算,而不是使用寄存器中的值
- volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
1
2
3
4
5
6
7
8
9
10
11
12class Bank {
//需要同步的变量加上volatile
private volatile int account = 100;
public int getAccount() {
return account;
}
//这里不再需要synchronized
public void save(int money) {
account += money;
}
}
注:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。 用final域,有锁保护的域和volatile域可以避免非同步的问题。
方式4:使用重入锁实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。 ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力 ReenreantLock类的常用方法有:
- ReentrantLock() : 创建一个ReentrantLock实例
- lock() : 获得锁
- unlock() : 释放锁
注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用
1 | class Bank { |
注:关于Lock对象和synchronized关键字的选择:
- 最好两个都不用,使用一种java.util.concurrent包提供的机制,能够帮助用户处理所有与锁相关的代码。
- 如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码
- 如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁
方式5:使用局部变量实现线程同步
如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本, 副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
ThreadLocal 类的常用方法
- ThreadLocal() : 创建一个线程本地变量
- get() : 返回此线程局部变量的当前线程副本中的值
- initialValue() : 返回此线程局部变量的当前线程的”初始值”
- set(T value) : 将此线程局部变量的当前线程副本中的值设置为value
首先定义一个ThreadLocal对象,这里选择Boolean类型的,如下所示:
private ThreadLocal
然后分别在主线程、子线程1和子线程2中设置和访问它的值,代码如下所示:
1 | mBooleanThreadLocal.set(true); |
上面的代码中,在主线程中设置mBooleanThreadLocal的值为true,在子线程1中设置mBooleanThreadLocal的值为false,在子线程2中不设置mBooleanThreadLocal的值,然后分别在3个线程中通过get方法去mBooleanThreadLocal的值,根据前面对ThreadLocal的描述,这个时候,主线程中应该是true,子线程1中应该是false,而子线程2中由于没有设置值,所以应该是null,安装并运行程序,日志如下所示:
1 | D/TestActivity(8676):[Thread#main]mBooleanThreadLocal=true |
注:ThreadLocal与同步机制
- ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。
- 前者采用以”空间换时间”的方法,后者采用以”时间换空间”的方式
方式6:使用阻塞队列实现线程同步
前面5种同步方式都是在底层实现的线程同步,但是我们在实际开发当中,应当尽量远离底层结构。使用javaSE5.0版本中新增的java.util.concurrent包将有助于简化开发。 这里主要是使用LinkedBlockingQueue
- LinkedBlockingQueue() : 创建一个容量为Integer.MAX_VALUE的LinkedBlockingQueue
- put(E e) : 在队尾添加一个元素,如果队列满则阻塞
- size() : 返回队列中的元素个数
- take() : 移除并返回队头元素,如果队列空则阻塞
1 | package com.xhj.thread; |
注:BlockingQueue
- add()方法会抛出异常
- offer()方法返回false
- put()方法会阻塞
方式7:使用原子变量实现线程同步
需要使用线程同步的根本原因在于对普通变量的操作不是原子的。
那么什么是原子操作呢?原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作,即-这几种行为要么同时完成,要么都不完成。在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。
其中AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器), 但不能用于替换Integer;可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。
AtomicInteger类常用方法:
- AtomicInteger(int initialValue) : 创建具有给定初始值的新的AtomicInteger
- addAddGet(int dalta) : 以原子方式将给定值与当前值相加
- get() : 获取当前值
1 | class Bank { |
补充–原子操作主要有: 对于引用变量和大多数原始变量(long和double除外)的读写操作; 对于所有使用volatile修饰的变量(包括long和double)的读写操作。
线程局部存储ThreadLocal
分析Java线程中断机制stop和interrupted的用法
当我们点击某个杀毒软件的取消按钮来停止查杀病毒时,当我们在控制台敲入quit命令以结束某个后台服务时……都需要通过一个线程去取消另一个线程正在执行的任务。Java没有提供一种安全直接的方法来停止某个线程,但是Java提供了中断机制。如果对Java中断没有一个全面的了解,可能会误以为被中断的线程将立马退出运行,但事实并非如此。中断机制是如何工作的?捕获或检测到中断后,是抛出InterruptedException还是重设中断状态以及在方法中吞掉中断状态会有什么后果?Thread.stop与中断相比又有哪些异同?什么情况下需要使用中断?中断的原理Java中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理中断。这好比是家里的父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则完全取决于自己。Java中断模型也是这么简单,每个线程对象里都有一个boolean类型的标识(不一定就要是Thread类的字段,实际上也的确不是,这几个方法最终都是通过native方法来完成的),代表着是否有中断请求(该请求可以来自所有线程,包括被中断的线程本身)。例如,当线程t1想中断线程t2,只需要在线程t1中将线程t2对象的中断标识置为true,然后线程2可以选择在合适的时候处理该中断请求,甚至可以不理会该请求,就像这个线程没有被中断一样。java.lang.Thread类提供了几个方法来操作这个中断状态,这些方法包括:
- public static boolean interrupted测试当前线程是否已经中断。线程的中断状态 由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。
- public boolean isInterrupted()测试线程是否已经中断。线程的中断状态不受该方法的影响。
- public void interrupt()中断线程。其中,interrupt方法是唯一能将中断状态设置为true的方法。静态方法interrupted会将当前线程的中断状态清除,但这个方法的命名极不直观,很容易造成误解,需要特别注意。
上面的例子中,线程t1通过调用interrupt方法将线程t2的中断状态置为true,t2可以在合适的时候调用interrupted或isInterrupted来检测状态并做相应的处理。此外,类库中的有些类的方法也可能会调用中断,如FutureTask中的cancel方法,如果传入的参数为true,它将会在正在运行异步任务的线程上调用interrupt方法,如果正在执行的异步任务中的代码没有对中断做出响应,那么cancel方法中的参数将不会起到什么效果;又如ThreadPoolExecutor中的shutdownNow方法会遍历线程池中的工作线程并调用线程的interrupt方法来中断线程,所以如果工作线程中正在执行的任务没有对中断做出响应,任务将一直执行直到正常结束。
既然Java中断机制只是设置被中断线程的中断状态,那么被中断线程该做些什么?显然,作为一种协作机制,不会强求被中断线程一定要在某个点进行处理。实际上,被中断线程只需在合适的时候处理即可,如果没有合适的时间点,甚至可以不处理,这时候在任务处理层面,就跟没有调用中断方法一样。“合适的时候”与线程正在处理的业务逻辑紧密相关,例如,每次迭代的时候,进入一个可能阻塞且无法中断的方法之前等,但多半不会出现在某个临界区更新另一个对象状态的时候,因为这可能会导致对象处于不一致状态。 处理时机决定着程序的效率与中断响应的灵敏性。频繁的检查中断状态可能会使程序执行效率下降,相反,检查的较少可能使中断请求得不到及时响应。如果发出中断请求之后,被中断的线程继续执行一段时间不会给系统带来灾难,那么就可以将中断处理放到方便检查中断,同时又能从一定程度上保证响应灵敏度的地方。当程序的性能指标比较关键时,可能需要建立一个测试模型来分析最佳的中断检测点,以平衡性能和响应灵敏性。
一般说来,当可能阻塞的方法声明中有抛出InterruptedException则暗示该方法是可中断的,如BlockingQueue#put、BlockingQueue#take、Object#wait、Thread#sleep等,如果程序捕获到这些可中断的阻塞方法抛出的InterruptedException或检测到中断后,这些中断信息该如何处理?一般有以下两个通用原则:
- 如果遇到的是可中断的阻塞方法抛出InterruptedException,可以继续向方法调用栈的上层抛出该异常,如果是检测到中断,则可清除中断状态并抛出InterruptedException,使当前方法也成为一个可中断的方法。 若有时候不太方便在方法上抛出InterruptedException,比如要实现的某个接口中的方法签名上没有throws InterruptedException,这时就可以捕获可中断方法的InterruptedException并通过Thread.currentThread.interrupt()来重新设置中断状态。如果是检测并清除了中断状态,亦是如此。 一般的代码中,尤其是作为一个基础类库时,绝不应当吞掉中断,即捕获到InterruptedException后在catch里什么也不做,清除中断状态后又不重设中断状态也不抛出InterruptedException等。因为吞掉中断状态会导致方法调用栈的上层得不到这些信息。当然,凡事总有例外的时候,当你完全清楚自己的方法会被谁调用,而调用者也不会因为中断被吞掉了而遇到麻烦,就可以这么做。总得来说,就是要让方法调用栈的上层获知中断的发生。假设你写了一个类库,类库里有个方法amethod,在amethod中检测并清除了中断状态,而没有抛出InterruptedException,作为amethod的用户来说,他并不知道里面的细节,如果用户在调用amethod后也要使用中断来做些事情,那么在调用amethod之后他将永远也检测不到中断了,因为中断信息已经被amethod清除掉了。如果作为用户,遇到这样有问题的类库,又不能修改代码,那该怎么处理?只好在自己的类里设置一个自己的中断状态,在调用interrupt方法的时候,同时设置该状态,这实在是无路可走时才使用的方法。
- 中断的响应 程序里发现中断后该怎么响应?这就得视实际情况而定了。有些程序可能一检测到中断就立马将线程终止,有些可能是退出当前执行的任务,继续执行下一个任务……作为一种协作机制,这要与中断方协商好,当调用interrupt会发生些什么都是事先知道的,如做一些事务回滚操作,一些清理工作,一些补偿操作等。若不确定调用某个线程的interrupt后该线程会做出什么样的响应,那就不应当中断该线程。
Thread.interrupt VS Thread.stopThread.stop方法已经不推荐使用了。而在某些方面Thread.stop与中断机制有着相似之处。如当线程在等待内置锁或IO时,stop跟interrupt一样,不会中止这些操作;当catch住stop导致的异常时,程序也可以继续执行,虽然stop本意是要停止线程,这么做会让程序行为变得更加混乱。那么它们的区别在哪里?最重要的就是中断需要程序自己去检测然后做相应的处理,而Thread.stop会直接在代码执行过程中抛出ThreadDeath错误,这是一个java.lang.Error的子类。
1 | package com.ticmy.interrupt; |
这个例子很简单,线程t里面做了一个非常耗时的排序操作,排序方法中,只有简单的加、减、赋值、比较等操作,一个可能的执行结果如下:
1 | go to stop thread t |
这里sort方法是个非常耗时的操作,也就是说主线程休眠一秒钟后调用stop的时候,线程t还在执行sort方法。就是这样一个简单的方法,也会抛出错误!换一句话说,调用stop后,大部分Java字节码都有可能抛出错误,哪怕是简单的加法! 如果线程当前正持有锁,stop之后则会释放该锁。由于此错误可能出现在很多地方,那么这就让编程人员防不胜防,极易造成对象状态的不一致。例如,对象obj中存放着一个范围值:最小值low,最大值high,且low不得大于high,这种关系由锁lock保护,以避免并发时产生竞态条件而导致该关系失效。假设当前low值是5,high值是10,当线程t获取lock后,将low值更新为了15,此时被stop了,真是糟糕,如果没有捕获住stop导致的Error,low的值就为15,high还是10,这导致它们之间的小于关系得不到保证,也就是对象状态被破坏了!如果在给low赋值的时候catch住stop导致的Error则可能使后面high变量的赋值继续,但是谁也不知道Error会在哪条语句抛出,如果对象状态之间的关系更复杂呢?这种方式几乎是无法维护的,太复杂了!如果是中断操作,它决计不会在执行low赋值的时候抛出错误,这样程序对于对象状态一致性就是可控的。 正是因为可能导致对象状态不一致,stop才被禁用。
中断的使用通常,中断的使用场景有以下几个:
- 点击某个桌面应用中的取消按钮时;
- 某个操作超过了一定的执行时间限制需要中止时;
- 多个线程做相同的事情,只要一个线程成功其它线程都可以取消时;
- 一组线程中的一个或多个出现错误导致整组都无法继续时;
- 当一个应用或服务需要停止时。
下面来看一个具体的例子。这个例子里,本打算采用GUI形式,但考虑到GUI代码会使程序复杂化,就使用控制台来模拟下核心的逻辑。这里新建了一个磁盘文件扫描的任务,扫描某个目录下的所有文件并将文件路径打印到控制台,扫描的过程可能会很长。若需要中止该任务,只需在控制台键入quit并回车即可。
1 | package com.ticmy.interrupt; |
在扫描文件的过程中,对于中断的检测这里采用的策略是,如果碰到的是文件就不检测中断,是目录才检测中断,因为文件可能是非常多的,每次遇到文件都检测一次会降低程序执行效率。此外,在fileIteratorThread线程中,仅是捕获了InterruptedException,没有重设中断状态也没有继续抛出异常,因为我非常清楚它的使用环境,run方法的调用栈上层已经没有可能需要检测中断状态的方法了。 在这个程序中,输入quit完全可以执行System.exit(0)操作来退出程序,但正如前面提到的,这是个GUI程序核心逻辑的模拟,在GUI中,执行System.exit(0)会使得整个程序退出。
线程stop方法源码:
1 | /** |
上面注释,第9行到第16行表明,stop()方法可以停止“其他线程”。执行thread.stop()方法这条语句的线程称为当前线程,而“其他线程”则是 调用thread.stop()方法的对象thread所代表的线程。 第21行至23行表明,可以停止一个尚未started(启动)的线程。它的效果是:当该线程启动后,就立马结束了。
第48行以后的注释,则深刻表明了为什么stop()方法被弃用!为什么它是不安全的。比如说,threadA线程拥有了监视器,这些监视器负责保护某些临界资源,比如说银行的转账的金额。当正在转账过程中,main线程调用 threadA.stop()方法。结果导致监视器被释放,其保护的资源(转账金额)很可能出现不一致性。比如,A账户减少了100,而B账户却没有增加100