Java 多线程创建和线程状态
一、进程和线程
多任务操作系统中,每个运行的任务是操作系统运行的独立程序。
为什么引进进程的概念?
为了使得程序能并发执行,并对并发执行的程序加以描述和控制。
因为通常的程序不能并发执行,为使程序(含数据)能独立运行,为它配置PCB——描述和和控制进程的的运行。
PCB记录了了操作系统所需的、用于描述进程的当前情况以及控制进程运行的全部信息。
PCB是使一个在多道程序环境下不能独立运行的程序,成为一个能独立运行的基本单位、一个能与其它进程并发执行的基本单位。
OS是根据PCB来对并发执行的进程进行控制和管理。
所谓创建进程,实际上是创建进程实体中的PCB;撤销进程,实质上是撤销进程的PCB。
程序:只是一组有序指令的集合,存放于某种介质上,本身是静态的。
进程实体:由程序段、相关数据段和PCB组成。是动态的,有生命周期。
进程的实质是进程实体的一次执行过程。
进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。
1、进程
进程是操作系统任务调度的单位,每个任务对应一个进程。
进程是操作系统的资源管理单位,进程包含代码和数据的地址空间以及其他的资源,比如打开的文件和信号量。不同的进程的地址空间是相互隔离的。
应用程序在启动后会创建一个进程,系统需要为进程分配ID号和内存。
2、线程介绍
线程是比进程更小的调度单位,它依附于进程存在,多线程是在一个进程内执行多段代码序列。
线程有自己的程序计数器、寄存器、栈等。
引入线程的动机在于操作系统中阻塞式I/O的存在,当一个线程所执行的I/O被阻塞的时候,同一进程中的其他线程可以使用处理器执行计算,从而提高程序的执行效率。
3、进程和线程比较
为什么不使用多进程?
进程的调度代价高,而多线程的调度代价小,更高效。
为什么线程间通信效率高?
进程中的多个线程共享进程的内存空间,当有新的线程产生时,操作系统不分配新的内存,而是让新线程共享原有进程块的内存。因此,线程间通信效率高。
4、JVM中的进程和线程
Java程序都运行在JVM中,每启动一个应用程序,就会启动一个JVM进程。在JVM环境中,所有的程序代码的执行都以线程实现。
Thread类提供多线程支持,应用可以创建多个并发执行的线程。
应用总是从main()方法开始运行,main()方法运行在一个线程内,称为主线程。
每个线程都有一个调用栈,一旦创建一个新的线程,就产生一个新的调用栈
5、应用中可以创建两类线程:用户线程和守护线程。
用户线程执行完毕时,JVM自动关闭当前程序。
守护线程独立于JVM,守护线程一般是由操作系统或者用户自己创建的,
二、创建线程
继承Thread类,实现Runnable接口和使用Timer类。
1、继承Thread类
Thread类实例只是一个对象,有变量和方法,创建于堆内存上,具有生命周期,可以与其他线程对象通信并协作完成特定任务。
线程动作放在Thread中的run()方法,它代表了线程需要完成的具体任务,因此,run()被称为线程体~
run()不是自动执行的,为了让run()执行必须调用Thread类的start()方法,调用start()方法的目的是创建新线程并执行该线程对象的run()方法,新线程与启动它的线程将并发执行。start()方法启动后立即返回,并不等待新建线程的执行,这意味着新建线程的run()方法在调用start()方法后不一定立即执行,需要等待JVM的调度。这种多线程之间的不确定性,是并发编程的难点所在。
public class InheritThread extends Thread { private String name; public InheritThread(String name){ this.name=name; } public void run(){ int c=0; while(c<5){ System.out.println("Greetings from thread '"+name+"'!"); c++; } } } class Main { public static void main(String args[]) { InheritThread greetingsA = new InheritThread("Inherited"); greetingsA.start();//创建新的线程 System.out.println("Thread has been started!"); } }
2、实现Runnable接口
定义实现java.lang.Runnable接口的类,并实现该接口的run()方法,在run()中编写线程执行代码。
public class RunnableThread implements Runnable{ private String name; public RunnableThread(String name){ this.name=name; } public void run(){ System.out.println("Greetings from runnable'"+name+";!"); } } class Main { public static void main(String args[]) { /*创建线程,需要利用RunnableThread对象生成一个Tread类的对象,然后调用start()*/ RunnableThread greetingsB=new RunnableThread("runnable"); Thread greetingsThread=new Thread(greetingsB); greetingsThread.start(); } }
这种方式的好处是,任何对象都可以实现Runnable接口,从而不受Java单继承泛型的限制。run()方法能访问类中所有的变量和方法,包括私有变量和方法。这种方式的缺点是,违反了每个对象应该有一个单一的、明确的职责的原则
3、使用Timer类和TimerTask类
Timer是一种线程设施,用于安排以后在后台线程中执行的任务。可安排任务执行一次,或者定期重复执行,可以看成一个定时器,可以调度TimerTask。
TimerTask是一个抽象类,实现了Runnable接口,所以具备了多线程的能力。
一个Timer可以调度任意多个TimerTask,它会将TimerTask存储在一个队列中,顺序调度,如果想两个TimerTask并发执行,则需要创建两个Timer。看一个简单的例子:
import java.util.Timer; import java.util.TimerTask; class PaintTask extends TimerTask{ public void run(){ System.out.println("update"); System.out.println("repaint"); } } public class TimeDemo { public static void main(String args[]){ Timer timer=new Timer(); timer.schedule(new PaintTask(),1000,1*1000); /*1秒后执行,周期为1秒*/ } }
我们用其它方法实现同样的功能:
/*在线程的run()中执行循环,并休眠30ms,唤醒后调用更新和重绘*/ public class Animator extends Thread{ public void run(){ while(true){ try{ Thread.sleep(2000); } catch (InterruptedException e){ System.out.println("Thread is interrupted"+e.getMessage()); } updateForNextFrame(); repaint(); } } private void repaint(){ System.out.println("repaint"); } private void updateForNextFrame(){ System.out.println("update"); } public static void main(String args[]){ Animator animator=new Animator(); animator.start();//会调用run方法 System.out.println("Thread has been started!"); } }
三、线程状态的转换
1、JVM的线程调度
线程调度是指按照一定的策略为多个线程分配CPU的使用权。
分时调度:所有线程轮流获得CPU的使用权,并平均分配占用时间。
抢占式调度:根据线程的优先级别来获取CPU使用权,这也是JVM采用的策略。
2、线程的5种状态
新建(new)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。
阻塞意味着等待,阻塞的线程不参与线程调度,自然不占用CPU。多线程环境下,非阻塞的线程才能被调度运行。
新线程在start()方法被调用后就进入就绪状态,随着JVM调度的程序状态的改变在运行和就绪之间切换。遇到阻塞进入阻塞状态,当run()方法结束或发生异常线程终止执行,进入死亡状态。
3、使线程离开当前运行状态的情况有三种:
(1) 线程的run()方法执行完毕;
(2) 在对象上调用wait()方法(不是在线程上调用);
(3) 线程试图调用对象的方法时不能在对象上获得锁
4、线程状态的转换
线程的调度程序是JVM的一部分,JVM可以决定将当前运行状态线程切换到就绪状态,以便让另一个线程获得运行机会,而不需要任何理由。对于处于就绪状态的线程,他们被选择执行的顺序是没有保障的。对于任何一组启动的线程来说,JVM不能保证其执行次序,持续时间也不能保证。
线程状态及转换:
5、关于线程的阻塞状态
阻塞状态是线程因为某种原因放弃了CPU使用权,暂时停止运行,直到线程进入就绪状态。
阻塞情况分为3种:
(1)等待阻塞:运行的线程执行了wait()方法,JVM把该线程放入等待池中。
(2)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池。
(3)其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()方法状态超时、join()方法等待线程终止或超时、或者IO处理完毕时,线程重新转入就绪状态。