写在前面:我们知道程序在运行的时候,CPU在很多时候都是出于空闲状态,合理的利用空闲的CPU,这就是多线程编程。多线程能够提高程序的性能,因此多线程也是每个程序员必须掌握的技能。
1.线程和进程的区别
当前的操作系统是多任务操作系统,其中多线程就是实现多任务的一种方式。
进程:
进程是一个计算机程序的运行实例,包含了需要执行的指令,有自己的独立地址空间,包含程序内容和数据。不同进程的地址空间是互相隔离的,进程拥有各种资源和状态信息,一个进程中可以启动多个线程。(进程是资源分配的最小单位)
线程:
表示程序的执行流程,是CPU调度执行的基本单位。每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)
区别:
(1)可以把进程想象成是一个容器,线程是属于容器中的。可将进程看成是一个包工头,为线程提供必要的资源,然后监督他们干活。而线程才是真正占用CPU执行指令集的苦逼男。因此一般一个进程中至少包含一个线程。
(2)由于进程是容器,构建的时候比较费力,所以进程是重量级的。而线程作用就是执行指令,运行时只需要方法栈、寄存器和本地变量等少数资源,因此是轻量级的。
(3)构建工程时,必然涉及到物理内存的使用,而构建线程时,只是把操作系统已经分配给进程的内存再分配而已,因此线程构建和销毁的成本低,而进程高。
(4)另外,进程是独立的程序,多个进程间不共享数据,如果要共享,必须使用IPC技术。而线程是在同一个容器中,数据交互通信容易。
线程分类:
线程总体分两类:用户线程和守候线程。
当所有用户线程执行完毕的时候,JVM自动关闭。但是守候线程却不独立于JVM,守候线程一般是由操作系统或者用户自己创建的。守护线程在后台运行,提供程序运行时所需的服务。当虚拟机中运行的所有线程都是守护线程时,虚拟机终止运行。
2.线程执行过程
(1)在线程的Thread对象上调用start()方法,而不是run()或者别的方法。
(2)在调用start()方法之前:线程处于新状态中,新状态指有一个Thread对象,但还没有一个真正的线程。
(3)在调用start()方法之后:发生了一系列复杂的事情
(4)启动新的执行线程(具有新的调用栈);
(5)该线程从新状态转移到可运行状态;
(6)当该线程获得机会执行时,其目标run()方法将运行。
注意:对Java来说,run()方法没有任何特别之处。像main()方法一样,它只是新线程知道调用的方法名称(和签名)。因此,在Runnable上或者Thread上调用run方法是合法的。但并不启动新的线程。
3.线程的状态
如2中图所示,线程状态总的可分为五大状态:分别是生、死、可运行、运行、等待/阻塞。
(1)新状态:线程对象已经创建,还没有在其上调用start()方法。
(2)可运行状态:当线程有资格运行,但调度程序还没有把它选定为运行线程时线程所处的状态。当start()方法调用时,线程首先进入可运行状态。在线程运行之后或者从阻塞、等待或睡眠状态回来后,也返回到可运行状态。
(3)运行状态:线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。
(4)等待/阻塞/睡眠状态:这是线程有资格运行时所处的状态。实际上这个三状态组合为一种,其共同点是:线程仍旧是活的,但是当前没有条件运行。换句话说,它是可运行的,但是如果某件事件出现,他可能返回到可运行状态。三种状态分别为:
(一)等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
(5)死亡态:当线程的run()方法完成时就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
4.创建线程方式
(1)继承java.lang.Thread类。
此类中有个run()方法,应该注意其用法:public void run()
如果该线程是使用独立的Runnable运行对象构造的,则调用该Runnable对象的run方法;否则,该方法不执行任何操作并返回。
Thread的子类应该重写该方法。
示例:
public class Demo1 extends Thread {
@Override //把自定义线程的任务代码写在run方法中。
public void run() {
for(int i = 0 ; i < 100 ; i++){
System.out.println("自定义线程:"+i);
}
}
public static void main(String[] args) {
//创建了自定义的线程对象。
Demo1 d = new Demo1();
//调用start方法启动线程
d.start();
for(int i = 0 ; i < 100 ; i++){
System.out.println("main线程:"+i);
}
}
}
2)实现java.lang.Runnable接口
使用实现接口Runnable的对象创建一个线程时,启动该线程将导致在独立执行的线程中调用对象的run方法。
示例:
public class Demo1 {
public static void main(String[] args) {
MyRun my = new MyRun();
Thread t1 = new Thread(my);
t1.start();
for (int i = 0; i < 200; i++) {
System.out.println("main:" + i);
}
}
}
class MyRun implements Runnable {
public void run() {
for (int i = 0; i < 200; i++) {
System.err.println("MyRun:" + i);
}
}
}
区别:
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
推荐使用Runnable接口,因为实现Runnable接口比继承Thread类具有优势:
(1)适合多个相同的程序代码的线程去处理同一个资源
(2)可以避免java中的单继承的限制
(3)增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
(4)线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
5.线程调度
(1)调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会。
Java线程的优先级用整数表示,取值范围是1到10。线程默认的优先级是创建它的执行线程的优先级。可以通过setPriority(int newPriority)更改线程的优先级。例如:
Thread t = new MyThread();
t.setPriority(8);
t.start();
线程默认优先级是5,Thread类中有三个常量,定义线程优先级范围:
static int MAX_PRIORITY //线程可以具有的最高优先级
static int MIN_PRIORITY //线程可以具有的最低优先级
static int NORM_PRIORITY //分配给线程的默认优先级
注意:当设计多线程应用程序的时候,一定不要依赖于线程的优先级。因为线程调度优先级操作是没有保障的,只能把线程优先级作用作为一种提高程序效率的方法,但是要保证程序不依赖这种操作。当线程池中线程都具有相同的优先级,调度程序的JVM实现自由选择它喜欢的线程。这时候调度程序的操作有两种可能:一是选择一个线程运行,直到它阻塞或者运行完成为止。二是时间分片,为池内的每个线程提供均等的运行机会。
(2)线程睡眠:使线程转到阻塞状态。
采用Thread.sleep(long millis)和Thread.sleep(long millis, int nanos)静态方法强制将当前正在执行的线程休眠(暂停执行),以“减慢线程”。当线程睡眠时,它入睡在某个地方,在苏醒之前不会返回到可运行状态。当睡眠时间到期,则返回到可运行状态。
线程睡眠的原因:线程执行太快,或者需要强制进入下一轮,因为Java规范不保证合理的轮换。
睡眠的实现:调用静态方法。
try {
Thread.sleep(123);
} catch (InterruptedException e) {
e.printStackTrace();
}
睡眠的位置:为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会睡眠。
注意:
1.线程睡眠是帮助所有线程获得运行机会的最好方法。
2.线程睡眠到期自动苏醒,并返回到可运行状态,不是运行状态。sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就开始执行.
3.sleep()是静态方法,只能控制当前正在运行的线程。
(3)线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。
(4)线程让步:Thread.yield() 方法让当前运行线程回到可运行状态,以允许具有相同优先级或者更高优先级的其他线程获得运行机会。因此,使用yield()的目的是让线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
(5)线程加入:join()方法,等待其他线程终止,该线程是指的主线程等待子线程的终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
Thread t = new AThread();
t.start();
t.join();
为什么要用join()方法?
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。
(6)线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。
6.常见的线程方法
Thread(String name) //初始化线程的名字
getName() //返回线程的名字
setName(String name) //设置线程对象名
currentThread() //返回CPU正在执行的线程的对象
sleep(long millions) //线程睡眠指定的毫秒数。
join() //等待其它线程终止
yield() //暂停当前正在执行的线程对象,并执行其它线程
getPriority() //返回当前线程对象的优先级 默认线程的优先级是5
setPriority(int newPriority) //设置线程的优先级,虽然设置了线程的优先级,但是具体的实现取决于底层的操作系统的实现(最大的优先级是10,最小的1,默认是5)。
interrupt() //线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出抛出,从而结束线程
wait() //线程在获取对象锁后,主动释放对象锁,同时本线程休眠,直到被唤醒才能继续获取对象锁并执行
notify()/notifyAll() //对象锁的唤醒操作
7.常见问题:线程离开运行状态的方法?
(1)调用Thread.sleep():使当前线程睡眠至少多少毫秒(尽管它可能在指定的时间之前被中断)。
(2)调用Thread.yield():不能保障太多事情,尽管通常它会让当前运行线程回到可运行性状态,使得有相同优先级的线程有机会执行。
(3)调用join()方法:保证当前线程停止执行,直到该线程所加入的线程完成为止。然而,如果它加入的线程没有存活,则当前线程不需要停止。
(4)线程的run()方法完成。
(5)在对象上调用wait()方法(不是在线程上调用)。
8.常见问题:sleep()和wait()的区别?
sleep()方法:使当前线程进入停滞状态(阻塞当前线程),让出CPU的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,留一定时间给其他线程执行的机会。当在一个Synchronized块中调用Sleep()方法,线程虽然休眠了,但是对象的锁并木有释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。
wait()方法:是Object类里的方法。当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时释放了对象的锁,wait(long timeout)超时时间到后还需要返还对象锁,其他线程可以访问。wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。wiat()必须放在synchronized block中,否则会在program runtime时抛java.lang.IllegalMonitorStateException异常。
区别:
相同点:
(1)都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。
(2)都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException。
不同点:
(1)Thread类的方法:sleep(),yield()等 。Object的方法:wait()和notify()等
(2)sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
(3)wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
9.常见问题:建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。
public class MyThread implements Runnable {
private String name;
private Object prev;
private Object self;
private MyThread(String name, Object prev, Object self) {
this.name = name;
this.prev = prev;
this.self = self;
}
@Override
public void run() {
int count = 10;
while (count > 0) {
synchronized (prev) {
synchronized (self) {
System.out.print(name);
count--;
self.notify();
}
try {
prev.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws Exception {
Object a = new Object();
Object b = new Object();
Object c = new Object();
MyThread pa = new MyThread("A", c, a);
MyThread pb = new MyThread("B", a, b);
MyThread pc = new MyThread("C", b, c);
new Thread(pa).start();
Thread.sleep(100); //确保按顺序A、B、C执行
new Thread(pb).start();
Thread.sleep(100);
new Thread(pc).start();
Thread.sleep(100);
}
}