一、什么是线程
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速。
一组并发线程运行在一个进程的上下文中,每个线程都有它自己独立的线程上下文,例如:栈、程序计数器、线程ID、条件码、寄存器集合等,每个线程和其它的线程一起共享除此之外的进程上下文的剩余部分。这里有一点要特别注意,就是寄存器是从不共享的,而虚拟存储器总是共享的。
线程和进程的区别:
- 线程是进程的一部分,所以线程有的时候被称为是轻权进程或者轻量级进程。
- 一个没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个进程,进程的执行过程不是一条线(线程)的,而是多条线(线程)共同完成的。
- 系统在运行的时候会为每个进程分配不同的内存区域,但是不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程组只能共享资源。那就是说,除了CPU之外(线程在运行的时候要占用CPU资源),计算机内部的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源。
- 与进程的控制表PCB相似,线程也有自己的控制表TCB,但是TCB中所保存的线程状态比PCB表中少多了。
- 进程是系统所有资源分配时候的一个基本单位,拥有一个完整的虚拟空间地址,并不依赖线程而独立存在。
二、实现线程
JAVA语言对线程的支持主要体现在Thread类和Runnable接口上面,因此JAVA实现多线程有两种方法:①实现Runnable接口 ②继承Thread类。
2.1 线程创建及启动
①线程创建
Thread()
Thread(String name)
Thread(Runnable target)
Thread(Runnable target,String name)
②线程启动
void start()
注意别把start()方法和run()方法搞混了。start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。
1) start()
用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法run()称为线程体,它包含了要执行的这个线程的内容,run()方法运行结束,此线程随即终止。
2) run()
run()方法只是类的一个普通方法而已,如果直接调用run()方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。
总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。
2.2 获取线程的引用
static Thread currentThread():返回当前运行的线程引用
2.3 停止线程的方法
如果我们想停止线程,不应该用stop(),因为stop()使得线程戛然而止,完成了什么工作,哪些工作还没有做,都不知道,且清理工作也没有做,所以stop()不是正确的停止线程方法,所以也被弃用了。
之后Java API的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。当run()或者call()方法执行完的时候线程会自动结束,如果要手动结束一个线程,你可以用volatile布尔变量在线程执行中设置状态标识来退出run()方法的循环或者是取消任务来中断线程。
//volatile是可见性的关键,保证了线程正确读取变量的值
volatile boolean keepRunning = true;
public void run(){
while(keepRunning){
//让线程"发动5连击"
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName() +"正在运行");
//让出处理器时间,使得所有线程能同时再去竞争CPU资源从而获取运行的机会
Thread.yield();
}
}
//完成最后一次业务后跳出while循环后,之后进行一些清理工作
}
线程终止后,其生命周期结束了,即进入死亡态,终止后的线程不能再被调度执行。那么线程退出run()方法,是否代表该线程结束?还是释放完内存之后?答案就是:执行完run之后所有语句=该线程结束≠临时内存被回收。简单来说,就是线程结束就是run方法执行结束之后,线程进入死亡态,他占用的那部分就有可能被回收,但不一定是立即被回收。
2.4 测试线程状态的方法
可以通过Thread 中的isAlive() 方法来获取线程是否处于活动状态。线程由start() 方法启动后,直到其被终止之间的任何时刻,都处于'Alive'状态。
2.5 线程的暂停和恢复
有几种方法可以暂停一个线程的执行,在适当的时候再恢复其执行。
1.sleep() 方法
当前线程睡眠(停止执行)若干毫秒,线程由运行中状态进入不可运行状态,停止执行时间到后线程进入可运行状态。
static void sleep(long millis)
static void sleep(long millis,int nanos)
try{
Thread.sleep(500);
}catch(InterruptedException e){
e.printStackTrace();
}
线程休眠要点
①线程休眠总是暂停当前线程
②在被唤醒并开始执行前,线程休眠的实际时间取决于系统计时器和调度器。对比较清闲的系统来说,实际休眠的时间十分接近于指定的休眠时间,但对于繁忙的系统,两者之间的差距就较大。
③线程休眠并不会释放当前线程已经获取的任何锁
④线程休眠并不会丢失当前线程已经获取的任何监视器。
⑤其他线程可以中断当前进程的休眠,但会抛出InterruptedException异常。
2.join()
join()的作用是:“等待该线程终止”。这里需要理解的就是该线程是指的主线程等待子线程的终止,也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。
void join()
void join(long millis)
void join(long millis,int nanos)
//加入join是为了让Stage线程最后停止,如果不加有可能Stage线程结束,actor线程还未停止
//好比导演喊停,演员还在演
public class Stage extends Thread(){
public void run(){
ActorRunnable actorTask = new ActorRunnable();
Thread actor = new Thread(actorTask1,"演员1");
//当前是Stage和actor两个线程同时竞争CPU资源
actor.start();
try{
actor.join();
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
3)yield()
yield()使得当前运行线程释放处理器资源,但并不意味着退出和暂停,只是,告诉线程调度如果有人需要,可以先拿去,我过会再执行,没人需要,我继续执行。但是它的这种主动退出没有任何保障,就是在当前进入可运行状态时,还是有可能被JVM选中再回到运行状态的。
static void yield()
Thread.yield( ):使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。
2.6 使用线程代码示例
public class ActorRunnable implements Runnable{
volatile boolean keepRunning = true;
public void run(){
while(keepRunning){
//让线程"发动5连击"
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName() +"正在运行");
//让出处理器时间,使得所有线程能同时再去竞争CPU资源从而获取运行的机会
Thread.yield();
}
}
}
public class Stage extends Thread(){
public void run(){
//演员1的任务
ActorRunnable actorTask1 = new ActorRunnable();
//演员2的任务
ActorRunnable actorTask2 = new ActorRunnable();
//Thread中构造方法里面的runnable对象用于提供线程业务接口run方法
Thread actor1 = new Thread(actorTask1,"演员1");
Thread actor2 = new Thread(actorTask2,"演员2");
//actor1、actor2两个线程由本线程启动
actor1.start();
actor2.start();
//让舞台线程暂时休眠。因为在本实例中是actor1、actor2和stage三个线程共同竞争CPU资源的
//由于当stage竞争到了cpu时就会终止另外两个线程,所以在stage线程中调用了sleep方法来给actor1和actor2方法提供最少50ms的运行时间。
try{
Thread.sleep(500);
}catch(InterruptedException e){
e.printStackTrace();
}
//只有当Thread.yield()的时候,选中的是stage线程,才能执行到下面停止actor的方法。
//但是actor1停止了并不意味着actor2也会停止,因为执行完actor1.keepRunning = false后,可能stage线程的cpu资源被抢走了
actor1.keepRunning = false;
actor2.keepRunning = false;
}
}
public void main(String[] args){
new stage().start();
}
三、Thread VS Runnable
我们刚接触的时候可能会迷糊继承Thread类和实现Runnable接口实现多线程,其实在接触后我们会发现这完全是两个不同的实现多线程。
- Runnable方式可以避免Thread方式由于java单继承特性带来的缺陷
- Runnable的代码可以被多个线程(Thread实例)共享,适合于多个线程处理同一资源的情况;而Thread方式是多个线程分别完成自己的任务。
第二个是什么意思呢?我们用代码来说明。假设到年关了,现在还剩下五张火车票,那现在有三个窗口去卖这五张火车票,我们用三个线程去模拟这三个窗口去同时卖这五张火车票。
用Thread实现卖票系统
class MyThread extends Thread{
private int ticketsCont = 5; //5张火车票
private String name ; //窗口,即线程名字
public MyThread(String name){
this.name = name;
}
public void run(){
while(ticketCont>0){
ticketCont--;
System.out.println(name+"卖掉了一张票,剩余票数为:"+ticketCont);
}
}
}
public class TicketThread{
public void main(String[] args){
//创建三个线程,模拟三个窗口卖票
MyThread mt1 = new MyThread("窗口1");
MyThread mt2 = new MyThread("窗口2");
MyThread mt3 = new MyThread("窗口3");
//启动三个线程,也即是开始卖票
mt1.start();
mt2.start();
mt3.start();
}
}
运行结果:总共有5张票,但是却卖了15张票
用Runnable实现卖票系统
class MyThread implements Runnable{
private int ticketsCont = 5; //5张火车票
public void run(){
while(ticketCont>0){
ticketCont--;
System.out.println(Thread.currentThread().getName()+"卖掉了一张票,剩余票数为:"+ticketCont);
}
}
}
public class TicketThread{
public void main(String[] args){
MyThread mt = new MyThread();
//创建三个线程,模拟三个窗口卖票
Thread th1 = new Thread(mt,"窗口2");
Thread th2 = new Thread(mt,"窗口2");
Thread th3 = new Thread(mt,"窗口3");
//启动三个线程,也即是开始卖票
th1.start();
th2.start();
th3.start();
}
}
运行结果:正常卖出五张票
在Runnable模拟的方法中,因为MyThread实现Runnable接口的。MyThread mt = new MyThread();之后我们把mt传给三个线程对象,也就是说三个线程传递的对象是同一个Runnable对象,所以三个线程对象用的都是同一个Runnable对象里面的代码,自然而然资源也是共享的,所以它们三个加在一起总共卖了五张火车票。
但是为什么打印出来剩余票数是4、1、3、0、2呢?因为线程的执行时间是随机的。首先线程1启动,获取到CPU资源,然后转到run方法去执行。执行完之后又把CPU资源让出来,票数剩余4张。这个时候主线程又继续执行,然后线程1没有获取了CPU资源,就在等待,线程2获取到了CPU资源就去执行run方法,剩余票数变成3张......
四、守护线程
JAVA线程有两类:
①用户线程
- 运行在前台,执行具体的任务
- 程序的主线程、连接网络的子线程等都是用户线程
②守护线程
- 运行在后台,为其他前台线程服务
- 一旦所有用户线程都运行结束,守护线程会随JVM一起结束工作
- 最常见的守护线程就是垃圾回收线程、数据库连接池中的监测线程、JVM虚拟机启动后的监测线程。
如何设置守护线程?可以通过调用Thread类的setDaemon(true)方法来设置当前的线程为守护线程。要注意的是,setDaemon(true)必须在start()方法之前调用,否则会抛出IllegalThreadStateException异常
public class DeamonThredemo{
public static void main(String[] args){
//这里的主线程就是main
System.out.println("进入主线程"+Thread.currentThread().getName());
//deamonThread实现了Runnable接口
DeamonThread deamonThread = new DeamonThread();
Thread thread = new Thread(deamonThread);
//将thread设置为守护线程
thread.setDaemon(true)
//启动守护线程
thread.start();
//主线程工作。在主线程工作的同时守护线程会一直工作,直到主线程退出
System.out.println("程序退出了主线程"+Thread.currentThread().getName());
}
}
但不是所有的任务都可以分配给守护线程来运行,比如读写操作或者计算逻辑为什么呢?
一旦所有的用户线程都退出运行了,守护线程也觉得自己的存在没有必要了,因为它没有守护的对象了,这时守护线程也会结束掉工作。如果我在守护线程里面做了一些读写操作,而当这个读写操作做到一半的时候,所有的用户线程都退出来了,这个时候守护线程也没有存在的必要了,他也会结束掉自己。但是这个时候读写操作还没进行完,守护线程就退出来了,那程序就崩溃了。