一 什么是进程和线程?进程和线程的区别?
- 进程是资源分配的最小单元,线程是程序执行的最小单元(进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位)
- 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。
而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。 - 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
- 但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间
二 线程的生命周期
在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种状态。当线程start后,它不能一直"独占"着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。按自己的理解整理出一张线程生命周期图,参照生命周期图更直观的了解线程。下面着重分析这五种状态:
- 新建 New
- 就绪 Runnable
- 运行 Running
- 阻塞 Blocked
- 死亡 Dead
2.1 新建(New)
public class XThread extends Thread{
@Override
public void run() {
}
}
XThread thread = new XThread();
当程序使用new关键字创建了一个线程之后,该线程就处于一个新建状态(初始状态),此时它和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,并初始化了其成员变量值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。
2.2 就绪(Runnable)
当线程对象调用了Thread.start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有开始运行,它只是表示该线程可以运行了。从start()源码中看出,start后添加到了线程列表中,接着在native层添加到VM中,至于该线程何时开始运行,取决于JVM里线程调度器的调度(如果OS调度选中了,就会进入到运行状态)。
2.3 运行 (Running)
如果处于就绪状态的线程获得了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) {
res = createThreadEtc(_threadLoop,
this, name, priority, stack, &mThread);
} else {
res = androidCreateRawThreadEtc(_threadLoop,
this, name, priority, stack, &mThread);
}
if (res == false) {
...
return UNKNOWN_ERROR;
}
return NO_ERROR;
}
mCanCallJava在Thread对象创建时,在构造函数中默认设置mCanCallJava=true.
- 当mCanCallJava=true,则代表创建的是不仅能调用C/C++代码,还能能调用Java代码的Native线程
- 当mCanCallJava=false,则代表创建的是只能调用C/C++代码的Native线程。
2.4 阻塞(Blocked)
阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况大概三种:
- 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
- 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
线程睡眠: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()方法的对象。
2.4 死亡 (Dead)
线程会以以下三种方式之一结束,结束后就处于死亡状态:
- run()方法执行完成,线程正常结束。
- 线程抛出一个未捕获的Exception或Error。
- 直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用。
三 实现线程同步的方法
3.1 ThreadLocal
网上很多资料都把ThreadLocal定义为解决多线程同步问题的一个方案,但是ThreadLocal并不是为了解决多线程间共享变量的问题。举个例子:在一个电商系统中,用一个Long型变量表示某个商品的库存量,多个线程需要访问库存量进行销售,并减去销售数量,以更新库存量。在这个场景中,是不能使用ThreadLocal类的。 ThreadLocal适用的场景是,多个线程都需要使用一个变量,但这个变量的值不需要在各个线程间共享,各个线程都只使用自己的这个变量的值。
此外,我们使用ThreadLocal还能解决一个参数过多的问题。例如一个线程内的某个方法f1有10个参数,而f1调用f2时,f2又有10个参数,这么多的参数传递十分繁琐。那么,我们可以使用ThreadLocal来减少参数的传递,用ThreadLocal定义全局变量,各个线程需要参数时,去全局变量去取就可以了。
3.2 synchronized关键字
- synchronized修饰方法(同步方法)
- synchronized修饰代码块(同步代码块)
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用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());
}
}
}
}
3.3 可重入锁ReentrantLock
在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());
}
}
}
}
3.4 使用特殊域变量(volatile)实现线程同步
常用于保持内存可见性和防止指令重排序。 内存可见性(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());
}
}
}
}
3.4 使用原子变量实现线程同步
需要使用线程同步的根本原因在于对普通变量的操作不是原子的。 什么是原子操作呢? 原子操作即是进行过程中不能被中断的操作,针对某个值的原子操作在被进行的过程中,CPU绝不会再去进行其他的针对该值的操作。为了实现这样的严谨性,原子操作仅会由一个独立的CPU指令代表和完成。原子操作是无锁的,常常直接通过CPU指令直接实现。
使用: 在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类, 使用该类可以简化线程同步。小工具包,支持在单个变量上解除锁的线程安全编程。
其中AtomicInteger(乐观锁)为例 :
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());
}
}
}
}
|