首先,要辨析进程与线程的概念:
进程是程序执行的过程,它持有资源和线程,相对于程序本身而言具有动态性。
线程是系统中最小的执行单元,同一个进程中可能有多个线程,它们共享该进程持有的资源。线程的通信也称为线程的交互,方式主要有互斥和同步。同步是指线程之间通过共同协作完成某项工作,线程间具有次序性;互斥是指线程间对某一资源的竞争,一次只能有一个线程访问该资源。
介绍完了这些基本概念,下面简单介绍一下Java对多线程的支持.
java中通过类Thread和接口Runnable来实现多线程的操作。它们都有一个run方法来指定线程工作时执行的代码。
Thread类的常用方法
类别 |
名称 |
简介 |
线程的创建 |
Thread() |
|
Thread(String name) |
||
Thread(Runnable target) |
||
Thread(Runnable target, String name) |
||
线程的方法 |
void start() |
启动线程 |
static void sleep(long millis) |
线程休眠 |
|
static void sleep(long millis, int nanos) |
||
void join() |
某线程调用join方法后,使其他线程等待该线程终止 |
|
void join(long millis) |
||
void join(long millis, int nanos) |
||
static void yield() |
当前正在运行的线程立刻释放处理器,重新加入竞争处理器的队列 |
|
获取线程引用 |
static Thread currentThread() |
返回当前正在运行的线程的引用 |
使用多线程有两种方式,一种是直接继承Thread类,一种是实现Runnable接口
- 继承Thread类,在run方法中指定需要线程执行的代码,通过getName()和setName()方法去访问线程名称。通过该类对象的start方法来开启线程。
- 实现Runnable接口,重写run方法来指定需要线程执行的代码,然后把该类的对象作为参数传递给Thread对象来执行。
另外,通过volatile关键字声明的成员变量可以保证当其他线程修改该成员变量后,本线程可以正确读取到此成员变量的值。
线程的状态
请结合OS中进程的5态来思考。以下线程状态和方法的介绍来自DreamSea(张小哲)
- 新生状态(New): 当一个线程的实例被创建即使用new关键字和Thread类或其子类创建一个线程对象后,此时该线程处于新生(new)状态,处于新生状态的线程有自己的内存空间,但该线程并没有运行,此时线程还不是活着的(not alive);
- 就绪状态(Runnable): 通过调用线程实例的start()方法来启动线程使线程进入就绪状态(runnable);处于就绪状态的线程已经具备了运行条件,但还没有被分配到CPU即不一定会被立即执行,此时处于线程就绪队列,等待系统为其分配CPCU,等待状态并不是执行状态; 此时线程是活着的(alive);
- 运行状态(Running): 一旦获取CPU(被JVM选中),线程就进入运行(running)状态,线程的run()方法才开始被执行;在运行状态的线程执行自己的run()方法中的操作,直到调用其他的方法而终止、或者等待某种资源而阻塞、或者完成任务而死亡;如果在给定的时间片内没有执行结束,就会被系统给换下来回到线程的等待状态;此时线程是活着的(alive);
- 阻塞状态(Blocked):通过调用join()、sleep()、wait()或者资源被暂用使线程处于阻塞(blocked)状态;处于Blocking状态的线程仍然是活着的(alive)
- 死亡状态(Dead):当一个线程的run()方法运行完毕或被中断或被异常退出,该线程到达死亡(dead)状态。此时可能仍然存在一个该Thread的实例对象,当该Thread已经不可能在被作为一个可被独立执行的线程对待了,线程的独立的call stack已经被dissolved。一旦某一线程进入Dead状态,他就再也不能进入一个独立线程的生命周期了。对于一个处于Dead状态的线程调用start()方法,会出现一个运行期(runtime exception)的异常;处于Dead状态的线程不是活着的(not alive)。
Ø线程的方法(Method)、属性(Property)
每个类都有自己的优先级,一般property用1-10的整数表示,默认优先级是5,优先级最高是10;优先级高的线程并不一定比优先级低的线程执行的机会高,只是执行的机率高;默认一个线程的优先级和创建他的线程优先级相同;
2)Thread.sleep()/sleep(long millis)
当前线程睡眠/millis的时间(millis指定睡眠时间是其最小的不执行时间,因为sleep(millis)休眠到达后,无法保证会被JVM立即调度);sleep()是一个静态方法(static method) ,所以他不会停止其他的线程也处于休眠状态;线程sleep()时不会失去拥有的对象锁。 作用:保持对象锁,让出CPU,调用目的是不让当前线程独自霸占该进程所获取的CPU资源,以留一定的时间给其他线程执行的机会;
让出CPU的使用权,给其他线程执行机会、让同等优先权的线程运行(但并不保证当前线程会被JVM再次调度、使该线程重新进入Running状态),如果没有同等优先权的线程,那么yield()方法将不会起作用。
5)object.wait() 当一个线程执行到wait()方法时,他就进入到一个和该对象相关的等待池(Waiting Pool)中,同时失去了对象的机锁—暂时的,wait后还要返还对象锁。当前线程必须拥有当前对象的锁,如果当前线程不是此锁的拥有者,会抛出IllegalMonitorStateException异常,所以wait()必须在synchronized block中调用。sleep()和wait()方法的最大区别是:sleep()睡眠时,保持对象锁,仍然占有该锁;而wait()睡眠时,释放对象锁。但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。
唤醒在当前对象等待池中等待的第一个线程/所有线程。notify()/notifyAll()也必须拥有相同对象锁,否则也会抛出IllegalMonitorStateException异常。
正确停止Java线程
非正确停止的方法:stop()
要正确停止线程,应该使用正确的退出标记。正常情况下run方法执行完毕线程就停止了,但是有些情况下run方法中需要while循环保持轮询,所以应该为while循环设置合适的退出条件,即退出标记,保证线程正确停止。
或者使用interrupt()方法的初衷并不是要停止线程,正常情况下,调用该方法可以将中断标记设置为TRUE。但是,当某个线程因为调用了wait()或者sleep()等方法而被阻塞时,此时调用interrupt()方法并不能正确的将interrupted标记设置为TRUE,并且同时会抛出中断异常。
争用条件:当多个线程同时共享访问同一个内存数据时,每个线程都尝试操作该数据,从而导致数据被破坏。
线程的互斥与同步
互斥是指在同一时刻只有一个线程可以对临界区进行操作。在JAVA中,可以通过synchronized块或者方法来实现。
在synchronized块中,需要对某个对象进行加锁,并将需要互斥的代码放入synchronized块中,从而来实现互斥行为。获得该锁的进程可以进入该synchronized块中.
而同步则是线程之间的一种通信机制,通过同步可以规定线程执行的顺序。例如,当线程不满足访问某资源时,可以通过调用锁对象的lock.wait()方法,使该线程让出CPU,让出锁,并在该锁对象的waitset中等待。满足条件时,可以通过lock.notify()方法随机唤醒一条线程,lock.notifyAll()则唤醒该waitset中所有的线程,使它们重新竞争该锁。被唤醒的线程从wait()方法处继续执行。