一、线程状态和的生命周期
各种状态一目了然,值得一提的是"blocked"这个状态:
线程在Running的过程中可能会遇到阻塞(Blocked)情况
- 调用join()和sleep()方法,sleep()时间结束或被打断,join()中断,IO完成都会回到Runnable状态,等待JVM的调度。
- 调用wait(),使该线程处于等待池(wait blocked pool),直到notify()/notifyAll(),线程被唤醒被放到锁定池(lock blocked pool ),释放同步锁使线程回到可运行状态(Runnable)
- 对Running状态的线程加同步锁(Synchronized)使其进入(lock blocked pool ),同步锁被释放进入可运行状态(Runnable)。
- 此外,在runnable状态的线程是处于被调度的线程,此时的调度顺序是不一定的。Thread类中的yield方法可以让一个running状态的线程转入runnable。
1、新建状态(new)
用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态(runnable)。
注意:不能对已经启动的线程再次调用start()方法,否则会出现java.lang.IllegalThreadStateException异常。
2、就绪状态(runnable)
处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配CPU。等待状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。
提示:如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一会儿,转去执行子线程。
3、运行状态(running)
处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态。
当发生如下情况是,线程会从运行状态变为阻塞状态:
①、线程调用sleep方法主动放弃所占用的系统资源
②、线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
③、线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有
④、线程在等待某个通知(notify)
⑤、程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量避免使用该方法。
当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态。
4、阻塞状态(blocked)
处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。
在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。有三种方法可以暂停Threads执行:
5、死亡状态(dead)
当线程的run()方法执行完,或者被强制性地终止,就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
二、sleep() 和 wait() 的区别
sleep() | wait() | |
---|---|---|
1 | sleep()方法属于Thread类 | wait()方法属于Object类 |
2 | sleep()有多个重载方法,但是全部都是有参的。意味着必须设定时间,并且会随着睡眠时间结束而重新进入runnable状态抢占cpu资源 | wait()有多个重载方法,可以有参也可以无参。有参时同sleep一样,时间结束后会进入runnable状态。但无参时只有被调用Object.notify()或notifyAll()才会进入runnable状态 |
3 | sleep可以在任何地方使用,不一定非要定义在同步中 | wait只能在同步方法或同步块中使用 |
4 | 在同步中时,调用sleep()方法的过程中,会释放CPU资源 线程不会释放对象锁,到时间后会继续执行 | 在同步中时,调用wait()方法的时候,会释放CPU资源 线程会放弃对象锁,进入等待此对象的等待锁定池(释放锁)。只有针对此对象调用notify()/notifyAll()方法后本线程才进入对象锁定池(获取锁)准备。重新获取到对象锁资源后才能继续执行 |
三、wait、notify机制(重点)
1、概念
- 每个对象从Object继承下来的,都拥有了一把锁(监视器):【wait(),notify(),notifyAll()方法】;【一个等待池】;【一个锁池】;
- 线程可以在synchronized的同步方法,和同步块里面可以使用一个对象锁来锁定资源,也可以调用对象的这几个方法实现wait和notify
- 注意,对象的wait(),notify(),notifyAll()方法只能在自己锁定的同步代码块或同步方法中被调用
2、执行流程
- 如果一个线程尝试想要获取锁时,该锁如果已经被占用,则此线程进入锁池,称为阻塞(blocked)
- 如果线程已经拥有锁,在需要某些条件,暂时不执行了。则可以调用 锁对象.wait(),释放锁,进入等待池。等待条件满足了,别的线程调用 锁对象.notify() 后,线程再次进入锁池。等到线程竞争到锁后,再从 wait() 方法处向后执行。
- 也可以调用 锁对象.wait(时间),如果没有等到 notify(),等待的时间到,则进入锁池
- 【锁对象.notify() 】:从等待池中随机选一个线程,放入锁池
- 【锁对象.notifyAll() 】:将所有等待池中的线程,放入锁池
- 即两个或者多个线程,可以使用同一把锁的 wait() 和 notify() 方法相互协作,实现线程间的通讯
思考1:为什么wait()方法要在同步方法或同步代码块中运行?
如果不知道synchronized请浏览我的另一篇文章Java多线程(二)之Synchronized锁
我们常用wait(),notify()和notifyAll()方法来进行线程间通信。线程检查一个条件后就行进入等待状态,例如,在“生产者-消费者”模型中,生产者线程发现缓冲区满了就等待,消费者线程通过消费一个产品使得缓冲区有空闲并通知生产者线程。notify()或notifyAll()的调用给一个或多个线程发出通知,告诉它(它们)条件已经发生改变,并且,一旦通知线程离开同步块,所有等待这个对象锁的线程将竞争这个对象锁,幸运的线程获得锁后就从wait()方法返回并继续执行。让我们把这整个操作分成几步来看看wait()和notify()方法之间的竞争条件(race condition),我们将使用“生产者-消费者”模型以便更容易理解这个场景:
- 生产者线程测试条件(缓冲区是否已满)并确定必须等待(发现缓冲区满后)
- 消费者线程从缓冲区消费一个产品后设置条件
- 消费者线程调用notify()方法,由于生产者线程此时还没有等待,这个消息将被忽略。
- 生产者线程调用wait()方法并进入等待状态。
因此,由于这里的竞争条件,我们可能在丢失一个通知,如果我们使用缓冲区或者只有一个产品,生产者线程将永远等待,你的程序也就挂起了。
现在我们考虑下这个潜在的竞争条件怎么解决。可以通过使用Java提供的synchronized关键字和锁来解决这个竞争条件。为了调用wait(),notify()和notifyAll()方法,我们必须获取调用这些方法的对象上的锁。由于wait()方法在等待前释放了锁并且在wait()方法返回之前重新获得了锁,我们必须使用这个锁来确保检查条件(缓冲区是否已满)和设置条件(从缓冲区取产品)是原子的,而这可以通过同步块或者同步方法实现。
简而言之,我们从同步块或者同步方法中调用wait(),notify()和notifyAll()方法可以避免一下问题:
- IllegalMonitorStateException,如果我们不通过同步环境(synchronized context)调用这几个方法,系统将抛出此异常
- wait()和notify()之间任何潜在的竞争条件。
案例
开发中,经常会先判断某种状态,如果不符合便调用wait()挂起线程等待别的线程唤醒。但是在状态判定和实际挂起这两个事件之间的时间是十分脆弱的。无法保证在实际挂起线程时,判断状态仍然满足。
让我们用一个具体的例子来说明,如果在同步块之外调用wait(),将会遇到什么问题。
假设我们要实现一个阻塞队列(我知道,API中已经有一个:)
第一次尝试(没有同步)可能会看到以下内容
class BlockingQueue {
Queue<String> buffer = new LinkedList<String>();
public void give(String data) {
buffer.add(data);
notify(); // Since someone may be waiting in take!
}
public String take() throws InterruptedException {
while (buffer.isEmpty()) // don't use "if" due to spurious wakeups.
wait();
return buffer.remove();
}
}
以下是可能发生的问题:
- 消费者线程A调用take()时buffer.isEmpty()为true。
- 在消费者线程A继续调用wait()之前,生产者线程B出现并调用一个完整的give(),即buffer.add(data); notify();
- 消费者线程A现在将调用wait() (错过了刚刚B调用的notify())。
- 如果不幸,生产者线程B不会产生更多的give(),那么消费者线程A永远不会醒来,并且我们有一个死锁。
一旦理解了这个问题,解决方案就很明显了:使用synchronized确保在isEmpty和wait之间不会调用notify。
这就是为什么“only wait inside synchronized”的规则是强制执行的(wait方法要强制在同步代码块中调用)
案例原地址:java - Why must wait() always be in synchronized block - Stack Overflow
思考2:为什么notify(), wait()等函数定义在Object中,而不是Thread中?
Object中的wait(), notify()等函数,和synchronized一样,会对“对象的同步锁”进行操作。wait()会使“当前线程”等待,因为线程进入等待状态,所以线程应该释放它锁持有的“同步锁”,否则其它线程获取不到该“同步锁”而无法运行。
现在,请思考一个问题:wait()等待线程和notify()之间是通过什么关联起来的?答案是:依据“对象的同步锁”。notify(), wait()依赖于“同步锁”,而“同步锁”可以是任意的对象,任意对象调用方法一定在Object类中!
三、yield()与join()
Java线程调度的一点背景
在各种各样的线程中,Java虚拟机必须实现一个有优先权的、基于优先级的调度程序。这意味着Java程序中的每一个线程被分配到一定的优先权,使用定义好的范围内的一个正整数表示。优先级可以被开发者改变。即使线程已经运行了一定时间,Java虚拟机也不会改变其优先级
优先级的值很重要,因为Java虚拟机和下层的操作系统之间的约定是操作系统必须选择有最高优先权的Java线程运行。所以我们说Java实现了一个基于优先权的调度程序。该调度程序使用一种有优先权的方式实现,这意味着当一个有更高优先权的线程到来时,无论低优先级的线程是否在运行,都会中断(抢占)它。这个约定对于操作系统来说并不总是这样,这意味着操作系统有时可能会选择运行一个更低优先级的线程。(我憎恨多线程的这一点,因为这不能保证任何事情)
理解线程的优先权
接下来,理解线程优先级是多线程学习很重要的一步,尤其是了解yield()函数的工作过程。
- 记住当线程的优先级没有指定时,所有线程都携带普通优先级。
- 优先级可以用从1到10的范围指定。10表示最高优先级,1表示最低优先级,5是普通优先级。
- 记住优先级最高的线程在执行时被给予优先。但是不能保证线程在启动时就进入运行状态。
- 与在线程池中等待运行机会的线程相比,当前正在运行的线程可能总是拥有更高的优先级。
- 由调度程序决定哪一个线程被执行。
- setPriority() 用来设定线程的优先级。
- 记住在线程开始方法 start() 被调用之前,线程的优先级应该被设定。
- 你可以使用常量,如MIN_PRIORITY,MAX_PRIORITY,NORM_PRIORITY来设定优先级
现在,当我们对线程调度和线程优先级有一定理解后,让我们进入主题。
1、yield()
理论上,yield意味着放手,放弃,投降。一个调用 yield() 方法的线程告诉虚拟机它乐意让其他线程占用自己的位置。这表明该线程没有在做一些紧急的事情。注意,这仅是向调度程器发起一个示意,表明当前线程乐意去放弃当前使用的处理器. 调度程序可以忽略这一点 (放弃的时间不确定,有可能刚刚放弃,马上又获得cpu的时间片)
让我们列举一下关于以上定义重要的几点:
- Yield是一个静态的原生(native)方法
- Yield告诉当前正在执行的线程把运行机会交给线程池中拥有相同优先级的线程。
- Yield不能保证使得当前正在运行的线程迅速转换到可运行的状态
- 它仅能使一个线程从运行状态转到可运行状态,而不是等待或阻塞状态.
public static native void yield();
yield的方法一般不怎么使用,当前线上程调用了yield方法后只是表示此乐意放弃处理器的使用权,调度器可以先去处理其他的程序.但是因为操作系统的调度室不确定的,并且线程是有优先级的,有可能会A线程调用完yield()以后,等会A线程还是会被执行. 一般用于调试程序和多线程之间调用的问题.
2、join()
线程实例的方法join()方法可以使得一个线程在另一个线程结束后再执行。如果join()方法在一个线程实例上调用,当前运行着的线程将阻塞直到这个线程实例完成了执行。
join() 源码:
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
解释:
从代码中,我们可以发现。当millis==0时,会进入while( isAlive() )循环;即只要子线程是活的,主线程就不停的等待。
我们根据上面解释 join() 作用时的代码来理解 join() 的用法!
wait() 的作用是让“当前线程”等待,而这里的 “当前线程” 是指当前运行的线程。虽然是调用子线程的 wait() 方法,但是它是通过“主线程”去调用的;所以,休眠的是主线程,而不是“子线程”!
这样理解: 例子中的Thread t只是一个对象 , isAlive()判断当前对象(例子中的t对象)是否存活, wait()阻塞的是当前执行的线程(例子中的main方法)
可以看出,Join方法实现是通过 wait()。 当main线程调用t.join时候,main线程会获得线程对象t的锁(wait 意味着拿到该对象的锁),调用该对象的wait(),直到该对象唤醒main线程 ,比如退出后。这就意味着main 线程调用t.join时,必须能够拿到线程t对象的锁。
案例:
package com.otherMethod.demo;
public class TestJoinMethod {
public static void main(String[] args) {
// TODO Auto-generated method stub
Thread t1 = new Thread(new JoinDemo());
for (int i = 0; i < 20; i++) {
System.out.println("主线程第" + i + "次执行!");
if (i == 5)
try {
System.out.println("main state:"+Thread.currentThread().getState());
System.out.println("t1 state of befor :"+t1.getState());
t1.start();
// t1线程合并到主线程中,主线程停止执行过程,转而执行t1线程,直到t1执行完毕后继续。
// join()方法一定要在start后
t1.join();
System.out.println("t1 state of after:"+t1.getState());
System.out.println("main state:"+Thread.currentThread().getState());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class JoinDemo implements Runnable{
@Override
public void run() {
System.out.println("t1 state run:"+Thread.currentThread().getState());
for (int i = 0; i < 10; i++) {
System.out.println("线程1第" + i + "次执行!");
}
}
}
四、volatile关键字的作用
volatile也是变量修饰符,只能用来修饰变量。volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
volatile是轻量级的synchronized,在多处理器(多线程)开发中保证了共享变量的“可见性”。可见性表示当一个线程修改了一个共享变量时,另外一个线程能读到这个修改的值。正确的使用volatile,能比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。使用时只需要把字段声明成volatile即可。
1、保证可见性
2、不保证原子性
3、禁止指令重排
volatile作用
- 在JDK1.2之前,Java的类型模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而随着JVM的成熟和优化,现在在多线程环境下volatile关键字的使用变的非常重要。
- 在当前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另一个线程还在继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
- 要解决这个问题,就需要把变量声明为volatile,这就指示JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。一般来说,多任务环境下,各任务间共享的变量都应该加volatile修饰符。
volatile特性
- volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。
- 而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。
- 这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
- Java语言规范中指出:为了获得最佳速度,允许线程保存成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才将私有拷贝与共享内存中的原始值进行比较。这样当多个线程同时与某个对象交互时,就必须注意到要让线程及时的得到共享成员变量的变化。而volatile关键字就是提示JVM:对于这个成员变量,不能保存它的私有拷贝,而应直接与共享成员变量交互。
volatile和synchronized比较
- volatile是一种稍弱的同步机制,在访问volatile变量时不会执行加锁操作,也就不会执行线程阻塞,因此volatilei变量是一种比synchronized关键字更轻量级的同步机制。
volatile使用建议
- 在两个或者更多的线程需要访问的成员变量上使用volatile。
- 当要访问的变量已在synchronized代码块中,或者为常量时,或者为常量时,没必要使用volatile。
- 由于使用volatile屏蔽掉了JVM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。
- 当该变量的值由自身的上一个决定时,volatile的作用就将失效,这是由volatile关键字的性质所决定的。