Java多线程(一)
多线程在面试中经常会被问到,所以也是非常重要的知识。
看到一篇写的很不错的博客:http://www.cnblogs.com/GarfieldEr007/p/5746362.html
一、进程与线程概述
1.1、进程和线程定义
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
1.2、进程和线程关系
一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行.
相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,并且线程拥有自己的栈空间.
一个程序至少有一个进程,一个进程至少有一个线程,同时线程不能脱离进程而单独存在。
1.3、进程和线程的区别
进程和线程的主要区别在于它们是操作系统不同的资源管理方式。进程有独立的地址空间,一个进程崩溃后,一般是不会对其它进程产生影响;
而线程只是一个进程中的不同执行路径,线程有自己的堆栈和局部变量,但线程没有单独的地址空间.
1.4、操作系统中的进程和线程
在操作系统中,以多进程形式,允许多个任务同时运行(其实是进程之间切换运行的);以多线程形式,允许单个任务分成不同的部分运行(每个部分的代码由一个线程来负责执行)。
注:可以看出来一个应用程序的代码,主要是由线程负责在内存中执行,同时这些代码可以分为不同的部分交给多个线程分别执行,在线程执行代码过程中,如果需要用到计算的机资源,那么就可以从线程所属的进程中获取,而进程则是操作系统进行资源分配和调度的独立单位。
思考:为什么运行我们运行的java程序的时候要先启动JVM虚拟机?
二、java中的线程
2.1、Thread类
java.lang.Thread类
public class Thread extends Object implements Runnable{..}
Thread是java中的线程类,是对java中线程的抽象,Thread类型的对象就可以表示java中的一个线程。
注:一个线程对象的作用就是可以单独运行我们所交给它的任务(代码)
注:Thread类及其子类的对象都可以表示一个线程对象
2.2、线程的分类
在Java中有两类线程:
用户线程 (User Thread)
也可以称为前台线程、执行线程
守护线程 (Daemon Thread)。
也可以称为后台线程、精灵线程(Daemon有精灵的意思)
守护线程:是指程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。
因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。
用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的退出: 如果用户线程已经全部处于死亡状态,虚拟机也就退出了,这是也不用管守护线程是否还存在了
举例:在战场上的将兵和后勤的人之间的关系。
注:java中创建出来的线程默认是用户线程,但是在启动线程前可以通过特定方法(setDaemon)把该线程转为守护线程
2.3、名字叫"main"的线程
当我们运行一个java程序的时候,其实就是让JVM创建一个名字叫"main"的线程,然后让这个线程去执行我们所编写的类中main方法的代码。
我们可以把这个线程称之为main线程或主线程,因为这个线程是第一个执行我们编写代码的线程,但是这时候并不是只有这一个线程在JVM中,可以通过jconsole观察到当前的所有线程。
注:jconsole是JDK自带的监测java程序运行的工具
2.4、多线程程序
我们之前所编写的代码绝大多数都是main线程执行的(单线程),但也有一些是多线程的程序,例如在GUI中的定时器Timer的使用,其实就是启动了一个新的线程,要不然怎么可能做到一边打地鼠加分,一边还可以进行倒计时显示.
由于java中允许在一个线程中创建并启动另一个线程,所以我们可以很容易的编程出一个多线程程序来。
思考:为什么要编写多线程程序,单线程程序不好么?
2.5、多线程程序的执行
为了提高程序执行效率,很多应用中都会采用多线程模式,这样可以将任务分解以及并行执行,从而提高程序的运行效率。但这都是代码级别的表现,而硬件上需要使用CPU的时间片模式来提供支持。
程序的任何指令的执行都要竞争CPU这个最宝贵的资源,不论程序分成了多少个线程去执行各自的任务,这些线程都必须通过一定的方式来获取时间片,从而得到CPU的使用权进行代码的执行。
注:时间片就是CPU分配程序的使用时间,每个线程获得一个时间片后,在此段时间内是可以使用CPU进行运算的,但时间用完后就要交出CPU的使用权.
注:不同操作系统中,或者同类操作系统的不同算法中,时间片的大小是不一样的,但是不论哪种情况,对象我们来讲,这个时间片都是一个极短的一段时间.
让线程获得时间片的算法有多种,但是现在一般都是"抢占式",就是默认情况下,多个线程具有同等几率抢占到CPU的下一个时间片,最终谁能抢到那么这个时间片就算是谁的,使用完之后再退出来重新再争夺一下CPU的时间片。
三、Thread类和Runnable接口
3.1、Thread类中的run方法
线程对象中的run方法,就是线程独立运行之后,必须要执行的方法,如果我们有什么代码要交给一个线程独立运行,那么就需要把这些代码放到run中.
3.2、Thread类中的start方法
在代码中,我们并不能直接调用一个线程对象的run方法,而且需要调用线程对象的start方法来启动这个线程,然后这个线程会自动的调用run方法的,如果直接调用了run方法,那就不是多线程代码了。
3.3、Thread类和Runnable接口的关系
Runnable接口中只有一个方法: public interface Runnable{ public void run(); } Thread类是Runnable接口的实现类,大致代码如下: public class Thread implements Runnable{ private Runnable target; public Thread(){} public Thread(Runnable target) { this.target = target; } public void run(){ if (target != null) { target.run(); } } }
3.4、创建和启动线程
第一种方式:创建Thread的子类对象,子类中重写run方法
//如果需要,可以考虑使用匿名内部类 Thread t = new Thread(){ public void run(){ //代码... } }; //启动线程 t.start();
第二种方式:创建Thread类对象,在构造器中传Runnable接口的实现类,实现类中重写run方法
//如果需要,可以考虑使用匿名内部类 Thread t = new Thread(new Runnable(){ public void run(){ //代码... } }); //启动线程 t.start();
注:观察直接调用线程对象的run方法和start方法后有什么不同?
四、线程对象的状态
在java中使用枚举类型Thread.State可以表示出一个线程对象当前的状态,调用线程对象的getState()方法可以获得线程的当前状态
java.lang.Thread.State枚举类型 public class Thread implements Runnable{ public enum State { NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED; } }
4.1、Thread.State枚举类型每个对象表示的状态含义
NEW(新建尚未运行/启动)
A thread that has not yet started is in this state.
一般是还没调用start方法,或者刚刚调用了start方法,start方法不一定"立即"改变线程状态,中间可能需要一些步骤才完成一个线程的启动。
RUNNABLE(可运行状态: 包括正在运行或准备运行)
A thread executing in the Java virtual machine is in this state.
start方法调用结束,线程由NEW变成RUNNABLE.
线程存活着,并尝试抢占CPU资源,或者已经抢占到CPU资源正在运行的状态都显示为RUNNABLE
BLOCKED(等待获取锁时进入的状态)
A thread that is blocked waiting for a monitor lock is in this state.
线程A和线程B都要执行方法test,而且方法test被加了锁,线程A先拿到了锁去执行test方法,线程B这时候需要等待线程A把锁释放。这时候线程B就是处理BLOCKED
WAITING(通过wait方法进入"无限期"的等待)
A thread that is waiting indefinitely for another thread to perform a particular action is in this state.
线程A和线程B都要执行方法test,而且方法test被加了锁,线程A先拿到了锁去执行test方法,线程B这时候需要等待线程A把锁释放(线程B处于BLOCKED状态),如果这时候线程A调用了wait方法,那么线程A就会
马上交出CPU的使用权以及刚才拿到的锁,从而进入到WAITING状态,而线程B发现锁已经被释放了,线程B就从BLOCKED状态进入到了RUNNABLE,如果线程B拿到了锁之后在运行期间,调用了notify或者notifyAll方法,这时候线程A就会从WAITING状态进入到BLOCKED状态,从而等待锁的是释放.
TIMED_WAITING(通过sleep或wait等方法进入的"有限期"等待的状态)
A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state.
线程对象的sleep或wait等方法都可以传一个时间参数,表示就算没有其他线程调用特定方法来改变自己状态的时候,也可以通过这个时间参数让自己自动改变状态(因为时间到了)。
TERMINATED(线程终止状态)
A thread that has exited is in this state.
线程结束了,就处于这种状态,也就是run方法运行结束了。
4.2、通常对线程对象状态的描述
为了便于理解和记忆,通过会对Thread.State中定义的状态进行整理归类,最终可得到书中所描述的线程状态图。
图中将线程状态分为:
初始化状态
就绪状态
运行状态
死亡状态
阻塞状态(阻塞在等待池、阻塞在锁池、其他情况阻塞)
五、ThreadGroup线程组
Java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,对线程组的控管理,即同时控制线程组里面的这一批线程
用户创建的所有线程都属于指定线程组,如果没有显示指定属于哪个线程组,那么该线程就属于默认线程组(即名字叫"main"的线程组)
默认情况下,子线程和父线程处于同一个线程组
只有在创建线程时才能指定其所在的线程组,线程运行中途不能改变它所属的线程组,也就是说线程一旦指定所在的线程组,就直到该线程结束
1)创建线程组
java.lang.ThreadGroup类
创建线程组的时候需要指定一个线程组的名字,或者创建线程组的时候指定名字和它的父线程组。
创建线程组的时候需要指定线程组名字和它的父线程组,如果不指定其父线程组,那么默认是父线程组是当前线程组。(类中提供俩种构造器)
public ThreadGroup(String name);
public ThreadGroup(ThreadGroup parent, String name);
//获得当前线程的所属的线程组 ThreadGroup currentGroup = Thread.currentThread().getThreadGroup(); //默认其父线程组是currentGroup ThreadGroup tg1 = new ThreadGroup("线程组1"); //指定其父线程组tg1 ThreadGroup tg2 = new ThreadGroup(tg1,"线程组2");
2)线程和线程组
//不指定则属于默认线程组 Thread t1 = new Thread("t1线程"); //也可以指定线程组 ThreadGroup tg = new ThreadGroup("我的线程组"); Thread t1 = new Thread(tg,"t1线程");
六、Thread类中常用方法
注:Thread类中有一些方法已经被标注为过时,不推荐使用.
官网中还给出了弃用哪些方法的原因:
http://docs.oracle.com/javase/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html
6.1、第一类:静态方法
public static int activeCount() 返回当前线程的线程组中活动线程的数目 public static Thread currentThread() 返回对当前正在执行的线程对象的引用 public static int enumerate(Thread[] tarray) 将当前线程的线程组及其子组中的每一个活动线程复制到指定的数组中 public static void sleep(long millis) 让当前线程在指定的毫秒数内休眠 public static void yield() 暂停当前运行的线程,让给其他线程使用CPU执行 public static boolean holdsLock(Object obj) 判断当前线程先是否拿着指定的锁
6.2、第二类:非静态方法
public void run() 线程要执行的代码在此方法中 注:我们并不能直接调用run方法,而且启动线程后让线程自动调用run方法 public void start() 线程启动时必须调用的方法 public long getId() 返回该线程的标识符,线程ID是一个正的long数,在创建该线程时生成。线程ID是唯一的,线程终止时,该线程ID可以被重新使用 public void setName(String name) 设置该线程的名称 public String getName() 返回该线程的名称 public int setPriority() 设置线程的优先级 public int getPriority() 返回线程的优先级 public Thread.State getState() 返回该线程的状态 public ThreadGroup getThreadGroup() 返回该线程所属的线程组 public boolean isAlive() 测试线程是否处于活动状态 public void setDaemon(boolean on) 将该线程标记为守护线程或用户线程,默认false表示用户线程 public boolean isDaemon() 测试该线程是否为守护线程 public void join() 当前线程等待某个线程执行结束 public void join(long millis) 给定一个等待的限定时间
6.3、容器混淆的方法
public void interrupt() 中断线程 public boolean isInterrupted() 测试线程是否已经中断 public static boolean interrupted() 测试当前线程是否已经中断
分析:
如果线程a对象是处于阻塞状态的话,在线程b中调用a.interrupt()是会打断线程a的阻塞状态的(后抛出打断异常)
但是如果线程a对象是处于就绪等状态,在线程b中调用a.interrupt()只是会改变对象a内部的一个boolean类型标识,用来表示线程b想打断线程a
isInterrupted和interrupted的返回值就是这个boolean类型的值
区别在于静态方法interrupted在返回boolean值后,会把这个打断的标示符给清理掉,而且非静态方法isInterrupted不会清理
public class Thread implements Runnable{ public void interrupt() { if (this != Thread.currentThread())checkAccess(); synchronized (blockerLock) { Interruptible b = blocker; //判断当前线程是否是阻塞状态 if (b != null) { // Just to set the interrupt flag interrupt0(); b.interrupt(this); return; } } //如果不是阻塞状态就只set一下打断的flag interrupt0(); } private native void interrupt0(); public boolean isInterrupted() { return this.isInterrupted(false); } public static boolean interrupted() { return currentThread().isInterrupted(true); } private native boolean isInterrupted(boolean ClearInterrupted); }