一、线程基本概念
1. 线程的五种状态
- 新建状态(new): 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
- 就绪状态(runnable): "可执行状态",线程对象对创建后,其他线程调用了该线程的start()方法来启动该线程。处于就绪状态的线程,随时可能被CPU调度执行。
- 运行状态(running): 线程获取cpu权限进行执行。
- 阻塞状态(blocked): 线程由于某种原因放弃cpu使用权,暂时进入等待状态。分为三种情况:
- 等待阻塞 - 调用线程的wait方法,等待某项工作的完成。
- 同步阻塞 - 获取锁失败进入同步阻塞状态。
- 其他阻塞 - 调用线程的sleep、join或者发出I/O请求,线程进入阻塞状态。
- 终止状态(dead): 线程执行完毕或者因异常退出运行,该线程结束生命周期。
2. 线程状态图:
3. 线程使用方式:
通过Thread使用多线程
class MyThread extends Thread {
public void run(){
//my task
}
}
public class ThreadTest{
public static void main(String[] args){
MyThread thread1 = new Mythread();
MyThread thread2 = new Mythread();
thread1.start();
thread2.start();
}
}
通过Runnable使用线程
class MyTask implements Runnable{
public void run(){
//my task
}
}
public class ThreadTest{
public static void main(String[] args){
MyTask task = new MyTask();
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
}
}
Thread方法start()和run()的区别
start(): 启动一个新线程并且在新线程中调用run方法执行任务
public synchronized void start() {
//判断是否为新建状态(new)
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
started = false;
try {
//调用本地方法启动新线程
nativeCreate(this, stackSize, daemon);
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
run(): 直接调用Runnable对象的run()方法,在当前线程中执行任务
public void run() {
if (target != null) {
target.run();
}
}
二、同步基础
1. 互斥锁
概念
在Java中,所有对象都能够被作为"监视器monitor"——指一个拥有一个独占锁,一个入口队列和一个等待队列的实体entity。
对于对象的非同步方法来说,在任意时刻都能被任意线程调用,不需要考虑加锁的问题。
对于对象的同步方法来说,在任意时刻有且仅有一个拥有该对象独占锁的线程能够调用它们。例如,一个同步方法是独占的。如果在线程调用某一对象的同步方法时,对象的独占锁被其他线程拥有,那么当前线程将处于阻塞状态,并添加到对象的入口队列中。
分类
同步分为类级别和对象级别,分别对应着类锁和对象锁。每个类只有一个类锁,每一个对象有且仅有一个对象锁。也可以说互斥锁依赖于对象/类存在。
类锁:
pulbic class Something {
public static synchronized void cSyncA(){}
public static synchronized void cSyncB(){}
}
对象锁:
pulbic class Something {
public synchronized void isSyncA(){}
public synchronized void isSyncB(){}
}
2. synchronized关键字
当我们调用某对象的synchronized方法时,就获取了该对象的互斥锁。例如,synchronized(obj)就获取了“obj这个对象”的互斥锁。
不同线程对互斥锁的访问是互斥的。也就是说,某时间点,对象的互斥锁只能被一个线程获取到。通过互斥锁,我们就能在多线程中,实现对“对象/方法”的互斥访问。 例如,现在有两个线程A和线程B,它们都会访问“对象obj的互斥锁”。假设,在某一时刻,线程A获取到“obj的互斥锁”并在执行一些操作;而此时,线程B也企图获取“obj的互斥锁” —— 线程B会获取失败,它必须等待,直到线程A释放了“该对象的互斥锁”之后线程B才能获取到“obj的互斥锁”从而才可以运行。
3. 线程等待与唤醒
wait与notify是java同步机制中重要的组成部分。结合与synchronized关键字使用,可以建立很多优秀的同步模型。
wait()允许我们将线程置入“睡眠”状态,同时又“积极”地等待条件发生改变.而且只有在一个notify()或notifyAll()发生变化的时候,线程才会被唤醒,并检查条件是否有变.
wait()
在调用wait的时候,线程自动释放其占有的对象锁,同时不会去申请对象锁。当线程被唤醒的时候,它才再次获得了去获得对象锁的权利。
notify()/notifyAll()
notify仅唤醒一个线程并允许它去获得锁,notifyAll是唤醒所有等待这个对象的线程并允许它们去获得对象锁,只要是在synchronied块中的代码,没有对象锁是寸步难行的。其实唤醒一个线程就是重新允许这个线程去获得对象锁并向下运行。
顺便说一下notifyall,虽然是对每个wait的对象都调用一次notify,但是这个还是有顺序的,每个对象都保存这一个等待对象链,调用的顺序就是这个链的顺序。其实启动等待对象链中各个线程的也是一个线程。
注意点
- Object.wait()的作用是让“当前线程”等待,即正在cpu上运行的线程。
- 必须要在synchronized(obj){......}的内部才能够去调用obj的wait、notify、notifyAll等同步方法,即只有在已经获取到对象的互斥锁的情况下,才能执行该对象的同步相关方法。否则会抛出 java.lang.IllegalMonitorStateException
- 在同步块中调用了notify()/notifyAll()方法,会在当前同步块执行完成后释放锁并且唤醒其他等待线程。
4.使用
public class ThreadTest{
//需要独占的对象
private static final Object lock = new Object();
public static void main(String[] args){
MyThread myThread = new MyThread();
myThread.start();
//获取myThread的互斥锁
synchronized(lock){
try{
//放弃锁,进入等待状态,即当前线程进入阻塞状态
lock.wait();
}catch(InterruptedException e){
//do nothing
}
}
}
private static class MyThread extends Thread{
public void run(){
//获取this的互斥锁,即myThread的互斥锁
synchronized(lock){
//do something
//执行完毕,通知等待线程
lock.notify();
}
}
}
}
synchronized(lock){...};的意思是定义一个同步块,使用lock作为互斥锁。lock.wait()的意思是临时释放锁,并阻塞当前线程,好让其他使用同一把锁的线程有机会执行,在这里要用同一把锁的就是lock对象.这个线程在执行到一定地方后用notify()通知wait的线程,锁已经用完,待notify()所在的同步块运行完之后,wait所在的线程就可以继续执行。
三、线程常用操作
1. sleep()
Thread类静态方法,让当前线程进入休眠,从运行状态进入阻塞状态。可以指定休眠时间,线程休眠的时间会大于/等于该休眠时间;在线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”,等待cpu的调度执行。
public static void sleep(long millis, int nanos)
throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("millis < 0: " + millis);
}
if (nanos < 0) {
throw new IllegalArgumentException("nanos < 0: " + nanos);
}
if (nanos > 999999) {
throw new IllegalArgumentException("nanos > 999999: " + nanos);
}
//时间=0时不需要进入阻塞状态
if (millis == 0 && nanos == 0) {
//检查线程是否被请求中断,如果被请求中断,抛出InterruptedException
if (Thread.interrupted()) {
throw new InterruptedException();
}
return;
}
long start = System.nanoTime();
long duration = (millis * NANOS_PER_MILLI) + nanos;
//获取当前线程的锁
Object lock = currentThread().lock;
//在同步块中调用sleep本地方法进入阻塞状态
synchronized (lock) {
while (true) {
sleep(lock, millis, nanos);
long now = System.nanoTime();
long elapsed = now - start;
if (elapsed >= duration) {
break;
}
duration -= elapsed;
start = now;
millis = duration / NANOS_PER_MILLI;
nanos = (int) (duration % NANOS_PER_MILLI);
}
}
}
2. yield()
Thread类静态方法,使当前线程由运行状态进入就绪状态,从而让其它具有相同优先级的等待线程获取执行权,但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权。
3. join()
Thread实例方法,让调用线程等待目标线程结束之后才能继续运行。
public final void join(long millis) throws InterruptedException {
//获取目标线程的锁
synchronized(lock) {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
//wait方法进入休眠,若目标线程依然在运行,则继续休眠
if (millis == 0) {
while (isAlive()) {
lock.wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
lock.wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
}
四、线程中断
1. 中断原理
一个线程在未正常结束之前, 被强制终止是很危险的事情. 可能带来预料不到的严重后果。比如会带着自己所持有的锁而永远的休眠,迟迟不归还锁等。 那么不能直接把一个线程搞挂掉, 但有时候又有必要让一个线程关闭, 或者让它结束某种等待的状态,一个比较优雅而安全的做法是:
使用等待/通知机制或者给那个线程一个中断信号, 让线程自己决定该怎么办。
2. 中断实现
对于不同状态下的线程有着不同的中断实现方式:
线程处于运行状态中
对于运行状态的线程,一般需要在运行代码中设置中断检查,可以使用线程提供的interupt相关方法和变量,也可以是自己定义的检查方法和变量,当检查到状态被置为中断时程序停止运行或进行后续一些工作,当然可以出不做任何处理继续保持运行。
class MyThread1 extends Thread{
public void run(){
while(!isInterrupted()){
//do task
}
//do something after interrupted
System.out.println("thread is interrupted.");
}
}
线程处于阻塞状态中
对于阻塞状态的线程,无法用代码检查来实现中断,为此java系统提供了InterruptedException。在调用wait()、sleep()等方法后进入阻塞状态后,被调用Thread.interrupt()会抛出InterruptedException,可捕获InterruptedException实现中断处理。
可中断的阻塞:阻塞库方法(例如Thread.sleep、Thread.join和Object.wait)
不可中断的阻塞:同步Socked I/O、同步I/O、Selector的异步I/O、获取内部锁
class MyThread2 extends Thread{
public void run(){
try{
while(!isInterrupted()){
//do task
synchronized(this){
wait();
}
}
}catch(InterruptedException e){
//do something after interrupted
System.out.println("thread is interrupted.");
}
}
}
3. java中断相关方法
class Thread{
public void interrupt(); //请求中断线程
public boolean isInterrupted(); //返回线程是否被中断
public static boolean interrupted(); //返回当前调用线程的中断状态,并清除中断状态
}
interrupt()
Thread实例方法,调用线程的interrupt()会将线程置为中断状态。
当线程在运行状态时,isInterrupted()将返回true。
当线程在阻塞状态时(可中断的阻塞状态),抛出InterruptedException异常,并清除中断状态,即isInterrupted()返回false。
isInterrupted()
Thread实例方法,返回当前实例线程是否处于中断状态。
interrupted()
Thread静态方法,返回调用线程的中断状态并清除中断状态,即在调用interrupted()后,isInterrupted()返回false。
五、常见问题
1. 为什么wait/notify/notifyAll要在同步块中调用
wait/notify是线程之间的通信,他们之间存在竞态,因此需要强制wait/notify在synchronized中调用。
假设要自定义一个blocking queue,如果不使用synchronized的话可以这样写(假设是可以运行的):
class BlockingQueue {
Queue<String> buffer = new LinkedList<String>();
public void give(String data){
buffer.add(data);
notify();
}
public String take(){
while(buffer.isEmpty()){
wait();
}
return buffer.remove();
}
}
这段代码可能会导致如下问题:
- 一个消费者调用take,发现buffer.isEmpty
- 在消费者调用wait之前,由于cpu的调度,消费者线程被挂起,生产者调用give,然后notify
- 然后消费者调用wait
- 如果很不幸的话,生产者产生了一条消息后就不再生产消息了,那么消费者就会一直挂起,无法消费,造成死锁。
2. sleep方法和wait方法的异同
相同点:
- 都可以使线程进入阻塞状态
- 都可中断,抛出InterruptedException异常
不同点:
- 所属类不同。sleep方法属于Thread类静态方法,wait属于Object类实例方法
对锁处理不同。sleep方法不会释放已有的锁,wait方法会释放锁。
-
调用范围不同。sleep方法可以在任何地方调用,wait方法需要在同步块中调用。
3. 为什么notify(), wait()等函数定义在Object中,而不是Thread中
Object中的wait(), notify()等函数,和synchronized一样,会对“对象的互斥锁”进行操作。
wait()会使“当前线程”等待,因为线程进入等待状态,所以线程应该释放它锁持有的“互斥锁”,否则其它线程获取不到该“互斥锁”而无法运行!
OK,线程调用wait()之后,会释放它锁持有的“互斥锁”;而且,根据前面的介绍,我们知道:等待线程可以被notify()或notifyAll()唤醒。现在,请思考一个问题:notify()是依据什么唤醒等待线程的?或者说,wait()等待线程和notify()之间是通过什么关联起来的?答案是:依据“对象的互斥锁”。
负责唤醒等待线程的那个线程(我们称为“唤醒线程”),它只有在获取“该对象的互斥锁”(这里的互斥锁必须和等待线程的互斥锁是同一个),并且调用notify()或notifyAll()方法之后,才能唤醒等待线程。虽然,等待线程被唤醒;但是,它不能立刻执行,因为唤醒线程还持有“该对象的互斥锁”。必须等到唤醒线程释放了“对象的互斥锁”之后,等待线程才能获取到“对象的互斥锁”进而继续运行。
总之,notify(), wait()依赖于“互斥锁”,而“互斥锁”是对象所持有,并且每个对象有且仅有一个。这就是为什么notify(), wait()等函数定义在Object类,而不是Thread类中的原因。