1,相关概念简介
(1)进程:是一个正在执行的程序。每一个进程执行都有一个执行的顺序,该顺序就是一个执行路径,或者叫一个控制单元。用于分配空间。
(2)线程:就是进程中一个独立的控制单元,线程在控制着进程的执行,一个进程中至少有一个线程。
java虚拟机启动的时候会有一个进程java.exe,该进程中至少有一个线程在负责java程序的执行,这个线程运行的代码在main方法中,因此main方法是主线程。在更细节一点,java虚拟机不止一个线程,在启动main方法这个主线程时还有垃圾回收机制,其实这也是多线程。因此java虚拟机也是多线程。
2,线程的创建和启动
(1)继承Thread类创建线程
步骤:
(a)定义Thread类的子类。
(b)重写该Thread 类的run()方法。
(c) 创建一个线程对象。
(d)调用对象的start()方法启动进程,调用run()方法。
class Demo extends Thread //(a)定义Thread类的子类。 { public void run(){ //(b)重写该Thread 类的run()方法。 for(int i = 0; i < 100; i++){ System.out.println(i + " demo run!"); } } public static void main(String[] args) { Demo d = new Demo(); //(c) 创建一个线程对象。 d.start(); //(d)调用对象的start()方法启动进程。 //一下代码进行测试 for(int i = 0; i < 100; i++){ System.out.println(i + " main run!"); } } }
测试结果:
可以看出主线程和我们自定义的线程交替执行。
注意:
(a)创建的线程对象不能调用run()方法。如果调用run()方法就和平常的函数调用一样,达不到多线程执行的效果。调用start()方法,不仅开启线程,而且还调用了run()方法。
(b) 该程序多次运行的结果每次都不同,这是因为多个线程都在争夺cpu的执行权,每一个时刻只有一个线程在运行,cpu在做着快速切换,以达到看上去是同时运行的结果。这就是多线程的一个特点:随机性。谁抢到谁执行,执行多长时间是有cpu决定的。
(c)为什么要覆盖run()方法?Thead类用于描述线程,该类中的run方法只是存放要运行的代码的位置。
(d)其实Thread d = new Thread()可以创建一个线程的实例,但是d.start()却是开启Thread类的run()方法,该方法没有做任何定义。
(2)实现Runnable接口创建线程类
先看一个问题
//火车站有16张票,需要从四个窗口卖出,如果按照上面的多线程实现,程序如下 class Ticket extends Thread { private int tick = 16;//票的张数---16 public void run(){ while(true){ if(tick>0){ //此处打印看是哪个窗口卖出的哪张票,这里简单的让票号从1 --- 16 System.out.println(Thread.currentThread().getName() + "...sale:" + tick--); } } } public static void main(String[] args) { Ticket t1 = new Ticket(); Ticket t2 = new Ticket(); Ticket t3 = new Ticket(); Ticket t4 = new Ticket(); t1.start(); t2.start(); t3.start(); t4.start(); } }
结果我们可以看出不止卖了16张,其实每个窗口或者说线程都卖出16张票,这是不能忍受的。
有个方法:private int tick = 16;这句变为:private static int tick = 16。但一般不定义静态,因为它的生命周期太长。
正式进入第二种创建线程的方法:
//火车站有16张票,需要从四个窗口卖出,如果按照上面的多线程实现,程序如下 class Ticket implements Runnable { private int tick = 16;//票的张数---16 public void run(){ while(true){ if(tick>0){ //此处打印看是哪个窗口卖出的哪张票,这里简单的让票号从1 --- 16 System.out.println(Thread.currentThread().getName() + "...sale:" + tick--); } } } public static void main(String[] args) { Ticket t = new Ticket(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); Thread t3 = new Thread(t); Thread t4 = new Thread(t); t1.start(); t2.start(); t3.start(); t4.start(); } }
从这个结果我们可以看出达到了我们的目标。总结步骤:
(a)定义Runnable接口的实现类,并且重写该接口的run()方法。
(b)创建Runnable实现类的实例对象,并以此对象来作为Thread类的Target来创建Thread类对象。
Ticket t = new Ticket(); 该对象t并非是线程对象
Thread t1 = new Thread(t); 以对象t作为参数而创建的t1才是真正的线程对象
(c)调用该线程对象t1的方法start()来启动多线程。
可以在创建Thread对象时为该对象指定一个名字。
(3)常用方法
(a)返回对当前正在执行的线程对象的引用。
public static Thread currentThread()
(b)返回该线程对象的的名称。
public final String getName()
(4)注意事项
(a)继承Thread类的对象可以使用currentThread()方法,也可以使用this关键字。但是实现Runnable的实现类只能通过currentThread()方法获得当前对象。
(b)Runnable接口方式创建的多个线程可以共享线程类的实例的属性。所以非常适合多个相同的线程来处理同一份资源。(也就是为什么引出这种线程方式的卖票的那个例子)
(c)Runnable接口实现的方式下,还可继承其他的类。
3,线程的生命周期
新建:当程序使用new关键字创建一个线程对象时,该线程就处于新建状态,此时这个对象和其他java对象一样,仅仅有虚拟机为其分配内存,并初始化其初始值,此时的对象没有表现出线程的特征。
就绪:当新建的线程对象调用start()方法时(图中的1),该线程就处于就绪状态,虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程没有开始执行,只是表示可以执行了,至于什么时候执行,关键看虚拟机里面的调度器的调度。
运行:当就绪状态的现场获取了cpu的执行权(图中的2)开始执行run方法,则表示该线程处于运行状态。
阻塞:当线程开始运行时不可能一直处于运行状态,除非他的运行时间非常短,cpu还没有切换就已经运行结束,但这样的情况并不是大多数。一般情况下,cpu所分的时间片很多线程在这个时间内是完成不了的,此时线程运行就要中断,让其他线程获取执行机会。因此当一个运行中的线程发生如下情况时会进入阻塞状态。其实阻塞状态是一种运行中的线程的一种中断状态,此时不能运行,但是线程不能运行还有其他状态,比如说又恢复到就绪状态,下面就来说明。
(a)运行中的线程进入阻塞状态
(1)线程调用sleep()方法主动放弃所占用的处理器资源。
(2)线程调用了一个阻塞式的IO方法,在方法在返回前线程被阻塞。
(3)线程在等待某个通知。
(4)线程试图获得一个同步监视器,但是该同步监视器被别的线程所持有。
(5)程序调用了线程的suspend方法将该线程挂起。这个方法容易导致死锁,已过时。
(b)运行中的线程进入就绪状态
(1)并非通过sleep()方法使得线程失去资源
(2)调用yield()方法就可以使得运行中的程序进入就绪状态。
(c)阻塞状态的线程进入就绪状态
(1)阻塞状态的线程只能进入就绪状态,不能直接进入运行状态
(2)调用sleep()方法经过了指定的时间
(3)线程调用的阻塞式IO方法已经返回
(4)线程成功的获得了某个等待的同步监视器
(5)线程在等待某个通知,而其他线程发出了一个通知
(6)处于被挂起的线程被调用了resume()方法。这个方法容易导致死锁,已过时。
死亡:线程结束就是死亡状态。
(1)正常结束线程
(2)线程抛出一个未捕获到的Exception或Error。
(3)使用该线程的stop()方法来终止线程。容易死锁,已过时。
注意:
(a)当新建一个线程对象时,线程处于新建状态,此时如果要是调用了该对象的run()方法,说明此时线程对象已经不是新建状态了,不能再来调用start()方法,只能对处于新建状态的线程对象调用start()方法。但是也不能对新建的线程对象调用两次start()方法。
(b)不要对一个已经死亡的线程调用start()方法。
(c)有一个方法可以检测该线程对象是否已经死亡。-----isAlive()方法,当线程处于新建和死亡状态,该方法返回false,反之!