多线程:
线程概述:一个任务通常是一个程序,每个运行中的程序就是一个进程。当程序运行时,内部可能包含了多个顺序执行流,每个执行流就是一个线程。
线程和进程:
当一个程序进入内存运行,即变成一个进程。进程是处于运行过程中的程序,并且具有一定独立功能,进程是系统进行资源分配和调度的一个独立单位。
一般而言,进程包含如下三个特征:
1. 独立性:进程是系统中独立存在的实体,他可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
2. 动态性:进程与程序的区别在于,程序知识一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念,进程具有自己的生命周期和各种不同的状态,这些概念在程序中都不具备的。
3. 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会相互影响。
并发性和并行性是两个概念,并行性指在同一时刻,有多条指令在多个处理器上同时执行,并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的结果。
线程也被称作轻量级进程,线程是进程的执行单元,线程在程序中是独立的,并发的执行流,当进程被初始化后,主线程就被创建了,对于绝大多数的应用程序来说,通常要求有一个主线程,但我们可以在该进程内创建多条顺序执行流,这些顺序执行流就是线程,每条线程就是独立的。
线程是进程的一个组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈,自己的程序设计计数器和自己的局部变量,但不再拥有系统资源,他与父进程和其他线程共享该进程所拥有的全部资源。因为多个线程共享父进程伦理的全部资源,因此编程更加方便,但必须确保线程不会妨碍同一进程里的其他线程。
线程可以完成一定的任务,可以与其他线程共享父进程中共享变量及部分环境,相互之间协同来完成进程所要完成的任务。线程是独立运行的,它并不知道进程中是否还有其他进程存在。线程的执行是抢占式的,也就是说,当前运行的线程子在任何时候都可能被挂起,以便另外一个线程可以运行。一个线程可以创建和撤销另一个进程,同一个进程中的多个线程之间可以并发执行。
线程的调度和管理由进程本身负责。
简而言之,一个程序运行后至少有一个进程,一个进程里可以包含多个线程,但至少要包含一个线程。
多线程的优势:
进程中的线程都具有一个共性:多个线程将共享同一个进程虚拟空间。线程共享的环境包括:进程代码段,进程公有数据等,
当操作系统创建一个进程时,必须为该进程分配独立的内存空间,并分配大量相关资源;使用多线程包含几个如下优点:
1. 进程间不能共享内存,但线程之间共享内存非常容易。
2. 系统创建进程需要为该进程重新分配系统资源,但创建线程则代价小的多,因此使用多线程来实现多任务并发比多进程的效率高。
3. Java语言内置多线程功能支持,而不是单纯的作为底层操作系统的调度方式,从而简化了Java多线程编程。
线程的创建和启动:
Java使用Thread类代表线程,所有线程对象都必须是Thread类或其子类的实例。Java使用run方法来封装一段程序流。
继承Thread类创建线程类。
通过继承Thread类来创建并启动多线程的步骤如下:
1. 定义Thread类的子类,并重写该类的run方法,该run方法,该run方法的方法体就是嗲表了线程需要完成的任务,因此我们通常把run方法称为线程的执行体。
2. 创建Thread子类实例,即创建了线程对象。
3. 用线程对象的start方法来启动该线程
如:
public class FirstThread extends Thread{
private int i;
//重写run方法,run方法的方法体就是线程的执行体
public void run() {
for (; i < 100; i++) {
//当线程类继承Thread类时,可以直接调用getName()方法来返回当前线程的名字
//如果想获取当前线程,直接使用this即可
//Thread对象的getName()返回当前线程的名字
System.out.println(getName()+" “+i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
//调用Thread的currentThread方法获取当前进程
System.out.println(Thread.currentThread().getName()+” "+i);
if(i==20)
{
//创建启动第一条进程
new FirstThread().start();
new FirstThread().start();
}}
}
}
}
}
进行多线程编程时不要忘记了Java程序运行时默认的主程序,main方法的方法体就是主线程的知执行体。
Thread.currentThread():currentThread是Thread类的静态方法,该方法总是返回当前正在执行的线程对象。
GetName():该方法是Thread的实例方法,该方法返回调用该方法的线程的名字。
程序可以通过setName(String name)方法设置名字,也可以通过getName()方法返回指定线程的名字,在默认情况下,主线程的名字为main,用户启动的多线程的名字依次为Thread-0,Thread-1,…
使用继承Thread类的方法创建线程类,多条线程之间无法共享线程类的实例变量
实现Runnable接口创建线程类
实现Runnable接口创建并启动多条线程的步骤如下:
1. 定义Runnable接口的实现类,并重写该接口的run方法,该方法的方法体同样是该线程的执行体。
2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
如:
//创建Runnable实现类的对象
SecondThread st=new SecondThread();
//以Runnable实现类的对象作为Thread的target来创建Thread对象,即线程对象
New Thread (st);
也可以在创建Thread对象时为该Thread对象指定一个名字,如:
//创建Thread对象时指定target和新线程的名字
New Thread(st,”新线程1”)
Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的Run方法仅作为线程的执行体。而实际线程对象依然是Thread实例,只是该Thread线程负责执行其target的run方法,Thread是不可以把任意对象的任意方法作为线程的执行体,Java语言的Thread必须使用Run able对象的Runnable对象的run方法作为线程的执行体,但C#可以把任何对象的任意方法来作为线程执行体。
3. 调用线程对象的Start方法来启动线程
如;
public class FirstThread implements Runnable{
private int i;
//重写run方法,run方法的方法体就是线程的执行体
public void run() {
for (; i < 100; i++) {
//当线程类继承Thread类时,可以直接调用getName()方法来返回当前线程的名字
//如果想获取当前线程,直接使用this即可
//Thread对象的getName()返回当前线程的名字
System.out.println(Thread.currentThread().getName()+" “+i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
//调用Thread的currentThread方法获取当前进程
System.out.println(Thread.currentThread().getName()+” "+i);
if(i==20)
{
FirstThread st=new FirstThread();
//创建启动第一条进程
new Thread(st,“新线程1”).start();
new Thread(st,“新线程2”).start();
}}
}
}
}
}
采用Runnable接口的方式来创建的多条线程可以共享线程类的实例属性,这是因为在这种情况下,程序所创建的Runnable对象只是线程的target,而多条线程共享一个target,所以多条线程可以共享同一个线程类的实例属性。
两种方式创建线程相比:
采用实现Runnable接口方法的多线程:
1. 线程类只实现Runnable接口,还可以继承其他类。
2. 在这种方式下,可以多个线程共享同一个target对象,所以非常适合多个相同的线程来处理同一份资源的情况,从而可以将CPU,代码和数据分开,形成清晰的模型,较好的体现面向对象的思想。
3. 劣势是:编程稍稍复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。采用继承Thread类方式的多线程。
采用Thread类方法的多线程:
4. 劣势是:因为线程类已经继承了Thread类,所以不能再继承其他类
5. 优势是:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
线程的生命周期:
在线程的生命周期中,他要经过新建(New),就绪(Runnable),运行(Running),阻塞(blocked)和死亡(Dead)五种状态。
新建和就绪状态:
当程序使用new关键字创建一个线程后,该线程处于新建状态,此时和其他Java对象一样,仅仅由虚拟机分配了内存,并初始化了其成员变量的值,此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。
当线程对象调用了start(),该线程处于新建状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,他只是表示该线程可以运行了
至于该线程何时开始运行,取决于JVM里线程调度器的调度。
启动线程使用Start方法方法,而不是run方法!永远不要调用线程对象的Run方法!调用start方法来启动线程,系统会把该run方法当成线程执行体来处理,但如果直接调用线程对象的Run方法,则Run方法立即就会被执行,而且Run方法返回之前其他线程无法并发执行,——也就是说系统把线程对象当成一个普通对象,而Run方法也是一个普通方法,而不是线程的执行体。如:
public class InvokeRun {
private int i;
//重写run方法,run方法的方法体就是线程的执行体
public void run() {
for (; i < 100; i++) {
//直接调用run方法时,Thread的this.getName返回的是该对象的名字
//而不是当前线程的名字
//使用Thread.currentThread().getName()总是获得当前线程名字
System.out.println(Thread.currentThread().getName()+" “+i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
//调用Thread的currentThread方法获取当前进程
System.out.println(Thread.currentThread().getName()+” "+i);
if(i==20)
{
//直接调用线程对象的Run方法
//系统会把线程当成一个普通对象,run方法当成普通方法
//所以下面不会启动两条线程
new InvokeRun().run();
new InvokeRun().run();
}}
}}
不要对已经处于启动状态的线程再次调用start方法,否则将引起IllegalThreadStateException异常。
如果程序希望调用子线程的Start方法后子线程立即执行,程序可以使用Thread.sleep()来让当前运行的线程(主线程)睡眠。
运行和阻塞状态:
如果处于就绪状态的线程获得CPU,开始执行Run方法的线程执行体,则该线程处于运行状态。
所有现代的桌面和服务器操作系统都采用抢占式调度策略
当地发生如下情况,线程将会进入阻塞状态:
1. 线程调用sleep方法主动放弃所占用的处理资源
2. 线程调用的阻塞式Io方法,在方法返回之前,该线程被阻塞。
3. 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
4. 线程在等待某个通知(notify)
5. 程序将调用了线程的suspend方法将该线程挂起。不过这个方法很容易导致死锁,所以程序应该尽量避免。
针对上面几种情况,当发生如下特定情况将可以解除上面的阻塞,让改线程重新进入就
绪状态:
1. 调用sleep方法的线程经过了指定时间
2. 线程调用的阻塞式IO方法已经返回。
3. 线程成功的获得了试图取得同步监视器。
4. 线程正在等待某个通知时,其他线程发出了一个通知。
5. 处于挂起状态的线程被调用了reSume恢复方法
线程从阻塞状态只能进入就绪状态,无法进行运行状态。调用yield()可以让当前处于运行状态的线程转入就绪状态。
线程死亡:
线程会以一下方式之一结束,结束后处于死亡状态:
1. run()方法执行完成,线程正常结束
2. 线程抛出一个未捕获的Exception或Error
3. 直接调用该线程的Stop()方法来结束该线程——该方法容易导致死锁,一般不用
当主线程结束的时候,其他线程不受任何影响,并不会随之结束,一旦子线程启动起来后,他就拥有和主线程相同地位,他不会受主线程的影响
为了测试某条线程是否已经死亡,可以调用线程对象的isAlive()方法,当线程处于就绪,运行,阻塞三种状态时,该方法返回true,当线程处于新建,死亡两种状态时,该方法返回false.
不要试图对一个已经死亡的线程调用start()方法使他重新启动,死亡就是死亡,该线程将不可再次作为线程执行。
如:
public class FirstThread extends Thread{
private int i;
//重写run方法,run方法的方法体就是线程的执行体
public void run() {
for (; i < 100; i++) {
//当线程类继承Thread类时,可以直接调用getName()方法来返回当前线程的名字
//如果想获取当前线程,直接使用this即可
//Thread对象的getName()返回当前线程的名字
System.out.println(Thread.currentThread().getName()+" “+i);
}
}
public static void main(String[] args) {
//创建线程对象
FirstThread sd=new FirstThread();
for (int i = 0; i < 300; i++) {
//调用Thread的currentThread方法获取当前进程
System.out.println(Thread.currentThread().getName()+” "+i);
if(i==20)
{
//启动线程
sd.start();
//判断启动后线程的isAlive()值,输出true
System.out.println(sd.isAlive());
}
if(i>20&&!sd.isAlive())
{
//试图再次启动线程
sd.start();
}
}
}}
程序只能对新建状态的线程调用start()方法,对新建状态的线程两次调用start()方法也是错误的。