在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种状态。当线程start后,它不能一直"独占"着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。按自己的理解整理出一张线程生命周期图,参照生命周期图更直观的了解线程。下面着重分析这五种状态:
public class XThread extends Thread{
@Override
public void run() {
}
}
//新建就是new出对象
XThread thread = new XThread();
当程序使用new关键字创建了一个线程之后,该线程就处于一个新建状态(初始状态),此时它和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,并初始化了其成员变量值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。
当线程对象调用了Thread.start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有开始运行,它只是表示该线程可以运行了。从start()源码中看出,start后添加到了线程列表中,接着在native层添加到VM中,至于该线程何时开始运行,取决于JVM里线程调度器的调度(如果OS调度选中了,就会进入到运行状态)。
如果处于就绪状态的线程获得了CPU资源,就开始执行run方法的线程执行体,则该线程处于运行状态。run方法的那里呢?其实run也是在native线程中。源码如下:
status_t Thread::run(const char* name, int32_t priority, size_t stack)
{
Mutex::Autolock _l(mLock);
//保证只会启动一次
if (mRunning) {
return INVALID_OPERATION;
}
...
mRunning = true;
bool res;
if (mCanCallJava) {
//还能调用Java代码的Native线程
res = createThreadEtc(_threadLoop,
this, name, priority, stack, &mThread);
} else {
//只能调用C/C++代码的Native线程
res = androidCreateRawThreadEtc(_threadLoop,
this, name, priority, stack, &mThread);
}
if (res == false) {
...//清理
return UNKNOWN_ERROR;
}
return NO_ERROR;
}
mCanCallJava在Thread对象创建时,在构造函数中默认设置mCanCallJava=true.
阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况大概三种:
线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。唤醒线程后,就转为就绪(Runnable)状态。
线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
线程I/O:线程执行某些IO操作,因为等待相关的资源而进入了阻塞状态。比如说监听system.in,但是尚且没有收到键盘的输入,则进入阻塞状态。
线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意性的,并在对实现做出决定时发生。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。
附:线程睡眠(sleep)和线程等待(wait)都会阻塞线程,它们的区别是什么?
sleep():
sleep 是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复,调用sleep 不会释放对象锁。由于没有释放对象锁,所以不能调用里面的同步方法。
sleep()使当前线程进入停滞状态(阻塞当前线程),让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;
sleep()是Thread类的Static(静态)的方法;因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象的机锁并木有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。
在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。
其次sleep()必须捕获异常;在sleep的过程中过程中有可能被其他对象调用它的interrupt(),产生InterruptedException异常,如果你的程序不捕获这个异常,线程就会异常终止,进入TERMINATED状态,如果你的程序捕获了这个异常,那么程序就会继续执行catch语句块(可能还有finally语句块)以及以后的代码。
wait():
wait()方法是Object类里的方法;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁);可以调用里面的同步方法,其他线程可以访问;
wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。
wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常。
一旦一个对象调用了wait方法,必须要采用notify()和notifyAll()方法唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了wait()后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了wait()方法的对象。
线程会以以下三种方式之一结束,结束后就处于死亡状态:
网上很多资料都把ThreadLocal定义为解决多线程同步问题的一个方案,但是ThreadLocal并不是为了解决多线程间共享变量的问题。举个例子:在一个电商系统中,用一个Long型变量表示某个商品的库存量,多个线程需要访问库存量进行销售,并减去销售数量,以更新库存量。在这个场景中,是不能使用ThreadLocal类的。
ThreadLocal适用的场景是,多个线程都需要使用一个变量,但这个变量的值不需要在各个线程间共享,各个线程都只使用自己的这个变量的值。
此外,我们使用ThreadLocal还能解决一个参数过多的问题。例如一个线程内的某个方法f1有10个参数,而f1调用f2时,f2又有10个参数,这么多的参数传递十分繁琐。那么,我们可以使用ThreadLocal来减少参数的传递,用ThreadLocal定义全局变量,各个线程需要参数时,去全局变量去取就可以了。
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Blank blank = new Blank();
MyRunnable myRunnable = new MyRunnable(blank);
Thread thread1= new Thread(myRunnable,"线程1");
thread1.start();
Thread thread2= new Thread(myRunnable,"线程2");
thread2.start();
}
class Blank{
private int account = 100;
public int getAccount() {
return account;
}
/**
* 存钱同步方法
* */
public synchronized void save1(int money) {
account = account + money;
}
/**
* 存钱同步代码快
* */
public void save2(int money) {
synchronized (this){
account = account + money;
}
}
/**
* 存钱非同步方法
* */
public void save(int money) {
account = account + money;
}
}
class MyRunnable implements Runnable {
private Blank blank;
public MyRunnable(Blank blank){
this.blank = blank;
}
@Override
public void run() {
for (int i = 1 ; i < 10 ;i++){
blank.save2(10);
System.out.println(Thread.currentThread().getName() + ":" + i + "账户余额为:" + blank.getAccount());
}
}
}
}
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和块具有相同的基本行为和语义,并且扩展了其能力
ReentrantLock类的常用方法有:
什么是可重入锁?
当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。(自己请求自己的锁是可以的,不会阻塞)
原理:
重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。
使用:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Blank blank = new Blank();
MyRunnable myRunnable = new MyRunnable(blank);
Thread thread1 = new Thread(myRunnable, "线程1");
thread1.start();
Thread thread2 = new Thread(myRunnable, "线程2");
thread2.start();
}
class Blank {
private int account = 100;
private ReentrantLock lock = new ReentrantLock();
public int getAccount() {
return account;
}
public void save(int money) {
lock.lock();
try {
account = account + money;
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
class MyRunnable implements Runnable {
private Blank blank;
public MyRunnable(Blank blank) {
this.blank = blank;
}
@Override
public void run() {
for (int i = 1; i < 10; i++) {
blank.save(10);
System.out.println(Thread.currentThread().getName() + ":" + i + "账户余额为:" + blank.getAccount());
}
}
}
}
常用于保持内存可见性和防止指令重排序。
内存可见性(Memory Visibility)是指所有线程都能看到共享内存的最新状态。
volatile关键字通过“内存屏障”来防止指令被重排序。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Blank blank = new Blank();
MyRunnable myRunnable = new MyRunnable(blank);
Thread thread1 = new Thread(myRunnable, "线程1");
thread1.start();
Thread thread2 = new Thread(myRunnable, "线程2");
thread2.start();
}
class Blank {
private volatile int account = 100;
public int getAccount() {
return account;
}
public void save(int money) {
account = account + money;
}
}
class MyRunnable implements Runnable {
private Blank blank;
public MyRunnable(Blank blank) {
this.blank = blank;
}
@Override
public void run() {
for (int i = 1; i < 10; i++) {
blank.save(10);
System.out.println(Thread.currentThread().getName() + ":" + i + "账户余额为:" + blank.getAccount());
}
}
}
}
需要使用线程同步的根本原因在于对普通变量的操作不是原子的。
什么是原子操作呢?
原子操作即是进行过程中不能被中断的操作,针对某个值的原子操作在被进行的过程中,CPU绝不会再去进行其他的针对该值的操作。为了实现这样的严谨性,原子操作仅会由一个独立的CPU指令代表和完成。原子操作是无锁的,常常直接通过CPU指令直接实现。
使用:
在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,
使用该类可以简化线程同步。小工具包,支持在单个变量上解除锁的线程安全编程。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Blank blank = new Blank();
MyRunnable myRunnable = new MyRunnable(blank);
Thread thread1 = new Thread(myRunnable, "线程1");
thread1.start();
Thread thread2 = new Thread(myRunnable, "线程2");
thread2.start();
}
class Blank {
private AtomicInteger account = new AtomicInteger(100);
public int getAccount() {
return account.get();
}
public void save(int money) {
account.addAndGet(money);
}
}
class MyRunnable implements Runnable {
private Blank blank;
public MyRunnable(Blank blank) {
this.blank = blank;
}
@Override
public void run() {
for (int i = 1; i < 10; i++) {
blank.save(10);
System.out.println(Thread.currentThread().getName() + ":" + i + "账户余额为:" + blank.getAccount());
}
}
}
}