要搞懂多线程,就如下几个方面就必须要先去理解,才能更加深层次的去理解和运用多线程,毋庸置疑,多线程是java开发中的一个重点。
一. 进程
要了解线程,首先肯定要了解进程,那么什么叫做进程?
进程: 进程就是系统平台上正在运行的程序,每个程序或者进程都有一个执行顺序,该顺序是个执行路径,也可以说是程序的控制单元,占有着内存中中某一个存储空间,程序的执行需要依靠这空间上的程序内部的控制线程单元。
举例:我们常常使用的QQ叫做一个进程,还有迅雷、暴风影音等都是一个进程。
二. 线程
先举例: 那么什么又叫做线程呢?借用毕老师上课时候讲的一个例子,说迅雷下载的时候,他要下载一个资源,大家都觉得它下载很快,是个下载利器,可是它为什么能下载这么快呢?那是因为,迅雷在下载的时候,它不是单纯的向服务器发出一条资源下载请求去下载整个资源,而是将资源分为多个部分级资源,然后程序中设计出多个资源下载请求向服务器发出下载请求,这样才达到了高速下载,那么这里的同时发出多个下载请求,这些个请求就相当于多个线程。
线程:线程是进程中的一个独立的控制单元,或一个独立的执行路径,线程控制着程序的执行,一个进程至少有一个或者多个线程。
大家都知道CPU是个中央处理器,我们的进程就是CPU在读取执行的,而真正读取执行的是进程中的那么线程。
三. 多线程存在的意义
A. 还是说上面这个迅雷下载的例子,很显然他的一个作用是效率所在。
B. 再拿我们用java写的一些小程序,里面定义了一些对象的引用,这些对象都存在了堆内存中了,当程序顺序执行的时候,当程序执行到某个地方的时候,另外一个线程就会得到运行,比如说JVM的垃圾回收机制这个执行路径就将那些不在被使用的对象当作垃圾回收,由此可见多线程的存在的意义不仅提升了程序的性能,而且还提升了程序的效能。
C. 还有比如说一个售票厅,有多个售票窗口,售出的都是一个系统的票,那么肯定要实现多个窗口同时销售,那么这就要用多线程来实现了,这是一个最实际不过的意义了。
四. 多线程的创建方式
大家知道我们平时学习的时候,都知道说main方法是程序的入口点,其实它是一个默认的主线程,是由JVM自动去启动和运行的,那出了main方法和垃圾回收机制之外,难道我们就不能创建其他的线程了吗?我们通过查看API,可以找到java已经给我们提供好了创建线程的类和对象,那就是Thread类,这个类是位于java.lang.Thread包中的,所以第一种方法如下:
1. 让一个类去继承Thread这个类,并且去覆盖这个类中的run()方法:
package com.thread;
public class ThreadDemo1
{
public static void main(String[] args)
{
}
}
/*
* 定义一个类Thread1并且继承Thread类,且覆盖父类的run这个方法。
*/
class Thread1 extends Thread
{
@Override
public void run()
{
System.out.println("thisis my first thread!");
}
}
如上代码就很简单的创建了一个线程,用来输出"thisis my first thread!"
可是线程创建之后,如何去运行这个线程呢?不多说,老规矩,我们先在main方法中创建一个Thread1对象,然后去调用run()方法就可以了。
Thread1 t1 = new Thread1();
t1.run();
运行结果: thisis my first thread!
可是这样就是一个线程被运行了吗?是不是感觉跟平常在main中调用普通方法差不多?对,这就只是在main方法中很普通的调用一个普通方法而已。那到底怎么样此能算是一个运行了一个线程呢?
我们查看API,发现Thread中有一个start()方法,它使该线程开始执行;Java虚拟机调用该线程的run
方法。
所以把上面的代码改一下:
Thread1t1 = newThread1();
t1.start();
运行结果: thisis my first thread!
由此可见两次运行的结果是一样的,可是使用start()方法去启动并运行run()方法才是正确的启动一个线程。好了,我们在main方法中和run方法中多添加一下代码进一步演示一下看看吧。
package com.thread;
public class ThreadDemo1
{
public static voidmain(String[] args)
{
//创建一个Thread1对象,然后去调用start方法启动线程
Thread1t1 = newThread1();
t1.start();
for (int i = 0 ;i < 15 ; i++)
{
System.out.println("mainmethod -->"+i);
}
}
}
/*
* 定义一个类Thread1并且继承Thread类,且覆盖父类的run这个方法。
*/
class Thread1 extends Thread
{
@Override
public void run()
{
for(int i = 0 ; i< 15 ; i++)
{
System.out.println("thisis my first thread! -->"+i);
}
}
}
执行结果:
main method -->0
main method-->1
main method -->2
main method-->3
this ismy first thread!-->0
this is my firstthread! -->1
this is my firstthread! -->2
this is my firstthread! -->3
this is my firstthread! -->4
this is my firstthread! -->5
this is my firstthread! -->6
this is my firstthread! -->7
this is my firstthread! -->8
this is my firstthread! -->9
this is my firstthread! -->10
this is my firstthread! -->11
this is my firstthread! -->12
this is my firstthread! -->13
this is my firstthread! -->14
main method -->4
main method-->5
main method-->6
main method-->7
main method-->8
main method-->9
main method-->10
main method-->11
main method-->12
main method-->13
main method -->14
大家感觉上面的结果是不是很奇怪,为什么会出现打印错乱的现象呢?其实不难理解,main是住线程,程序一运行,JVM就会启动main这个主线程,当进入主线程之后,创建一个Thread1对象并调用它的start()方法之后,这时这个程序中就存在了两个线程,即main主线程和
Thread1 t1 = new Thread1();
t1.start();
t1 这个线程,所以这两个线程都要运行,可是CPU去处理程序的规则是,CPU在某一个时刻,只能去执行某一个线程,而其他的线程都会暂停下来,当执行都某一部分的时候,CPU又将执行运交给里给你一个线程,然后又恢复到第一个线程….所这样反复的切换,才导致了这样的一个结果,最后毕老师总结了一句:
我们可以形象的把多线程的运行方式实在互相抢夺CPU的执行权。
2. 接下来我们看一下创建线程的第二种方式:
创建线程的另一种方法是声明实现 Runnable
接口的类。该类然后实现run
方法。然后可以分配该类的实例,在创建Thread
时作为一个参数来传递并启动。采用这种风格的同一个例子如下所示:
class PrimeRunimplements Runnable
{
longminPrime;
PrimeRun(long minPrime)
{
this.minPrime = minPrime;
}
publicvoid run()
{
//compute primes larger than minPrime
. . .
}
}
然后,下列代码会创建并启动一个线程:
PrimeRunp = new PrimeRun(143);
new Thread(p).start();
五. 多线程的生命周期
通过以上的执行过程和运行结果,我们可以引诱出线程的生命周期了,接下来我们来看看多线程的生命周期。
下面我们来看一个图,并根据下图来作一些解释
线程创建:当我们使用new关键字创建一个Thread或Runnable线程实例的时候线程就表示被创建了。
運行:当我们调用线程的start()方法的時候,线程就处于开启状态,并准备开始运行。
阻塞狀態:当多个线程开始启动并开始运行的时候,因为在某一时刻,CPU只能让一个线程执行,或者说处理一个线程,就好比一个尖嘴漏斗,一大把的沙子装在里面,可是每次只能有一颗沙子流出来,所以这种状态就只能有一个线程在执行,执行多长时间决定权在CPU,而此时其他的线程都处于临时状态,要等待CPU的处理,接下来要执行哪一个,就要看CPU会将执行权交给谁(这样说比较形象点)。
冻结状态:
A. 当一个线程在运行的过程中,执行到sleep()语句之后,程序就会暂停执行,等于放弃了执行权,这个时候执行权可能由CPU调度给其他线程了,其他线程便进入的运行状态,sleep()必须要设置一个sleep时间参数,所以当sleep时间已过预设时间,那么这个线程有可能再次被运行,或者再次进入了阻塞状态。
B. 另一种情况是遇到wait()语句,程序就会暂停执行,进入等待状态,放弃执行权,CPU将执行权调度给其他线程了。如果要得到执行权,就要获得唤醒指令,即使获得唤醒指令,该线程也只是有可能被执行,否者就进入了阻塞状态。
消亡状态(被消亡的线程是无法再次start()或进入其他任何状态):
A. 当一个程序在运行过程中发生某种运行时异常就会导致线程终止,这个时候这个线程就已经结束或者说消亡了。
B. 当一个程序在完整的将run()方法中的代码执行完毕后,也就进入的消亡状态。
C. 当一个程序遇到stop()方法后,也会进入到消亡状态,不过stop()这个方法目前不建议被使用了。
接下来写一个售票的例子:
//售票大厅肯定是多个窗口同时售票以满足实际需求,所以要创建多个线程。
package com.thread;
public classSaleTicketDemo
{
public static voidmain(String[] args)
{
//创建了4个线程实例
SaleTicketstk1 = newSaleTicket();
SaleTicketstk2 = newSaleTicket();
SaleTicketstk3= newSaleTicket();
SaleTicketstk4 = newSaleTicket();
//4个线程都调用start()方法
stk1.start();
stk2.start();
stk3.start();
stk4.start();
}
}
class SaleTicket extends Thread
{
//假设售票大厅每天预售100张火车票
private int tickets =100;
public void run()
{
while(true)
{
if(tickets > 0 )
{
//每售出一张系统中就少一张
System.out.println(Thread.currentThread().getName()+ "-->"+tickets--);
}
}
}
}
执行结果为每个线程交替的都打印了一百次,根本就不符合我们的实际售票要求。最后导致的结果是售票局大亏,医院大赚。
不过有一种方式何以勉强实现,那就是将tickets这个变量修饰为static,这样每一个线程对象或者线程都共有一份tickets,但是我们一半不这样做。
PS: 非成员成员变量和局部变量是每一个对象都有自己的一份,二静态的成员变量是每一个对象公有一个。
那么我们换一个方式,使用是第二种创建线程的方式试试看吧。。。。
package com.thread;
public classSaleTicketsDemo3
{
public static voidmain(String[] args)
{
SaleTitkets st = new SaleTitkets3();
Threadtd1 = newThread(st);
Threadtd2 = newThread(st);
Threadtd3 = newThread(st);
Threadtd4 = newThread(st);
td1.start();
td2.start();
td3.start();
td4.start();
}
}
classSaleTitkets implements Runnable
{
private int tikets = 100;
@Override
public void run()
{
while(true)
{
if(tikets >0)
{
System.out.println(Thread.currentThread().getName()+"====="+tikets--);
}
else
{
break;
}
}
}
}
执行结果是是个线程是交替执行,而且是总共打印了100次。因为我们是之创建了一个Runnable实例对象,创建了四个Thread线程对象,将唯一个哪个Runnable对象作为实际参数传给了这个四个Thread线程,所以总体来说,这个四个线程共享了一个tikets变量。