Java多线程总结之---概念、创建/启动、状态变换

来源:互联网 发布:小语网络加速器注册 编辑:程序博客网 时间:2024/06/05 05:09

一、进程和线程的概念

现在的操作系统是多任务操作系统。多线程是实现多任务的一种方式。

进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。比如在Windows系统中,一个运行的exe就是一个进程。

线程是指进程中的一个执行流程,一个进程中可以运行多个线程。比如Java.exe进程中可以运行很多线程。线程总是属于某个进程,线程没有自己的虚拟地址空间,与进程内的其他线程一起共享分配给该进程的所有资源。

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

多个线程“同时”执行是人的感觉,在Java的线程之间实际上是抢占调度的策略(JVM线程调度程序是基于优先级的抢先调度机制),即CPU的时间片轮换执行

在Java中,每次程序运行至少启动2个线程:一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM实际上就是在操作系统中启动了一个进程。

二、Java中创建线程的几种方式

(1)、继承Thread类创建线程类

通过继承Thread类创建线程类的具体步骤和具体代码如下:
• 定义一个继承Thread类的子类,并重写该类的run()方法;
• 创建Thread子类的实例,即创建了线程对象;
• 调用该线程对象的start()方法启动线程。

 public class MyThread extends Thread {    @Override    public void run() {        for (int i = 0; i < 30; i ++) {            System.out.println(Thread.currentThread().getName() + '-' + i);        }    }    public static void main(String []args) {        new MyThread().start();        new MyThread().start();    }}

(2)、实现Runnable接口创建
• 定义Runnable接口的实现类,并重写该接口的run()方法;
• 创建Runnable实现类的实例,并以此实例作为Thread的target对象,即该Thread对象才是真正的线程对象。

   public class MyRunnable implements Runnable {    public int sum = 15;    @Override    public void run() {        for (int i = 0; i < 10; i++) {            --sum;            System.out.println(Thread.currentThread() + " sum:"+sum);        }    }    public static void main(String[] args) {        Runnable runnable = new MyRunnable();        Thread thread1 = new Thread(runnable,"thread1");        Thread thread3 = new Thread(runnable,"thread3");        thread1.start();        thread3.start();    }}

(3)、通过Callable和Future创建线程
• 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
• 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
• 使用FutureTask对象作为Thread对象的target创建并启动新线程。
• 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值其中,Callable接口(也只有一个方法)定义如下:

   public class CallableThread{    public static void main(String[] args) throws Exception{       Callable<Integer> callable = new Callable<Integer>() {           @Override           public Integer call() throws Exception {               return 3;           }       };       FutureTask<Integer> futureTask = new FutureTask<Integer>(callable);       Thread thread = new Thread(futureTask);       thread.start();       System.out.println(futureTask.get());    }}

(4)、三种方法的优缺点分析
1、采用实现Runnable、Callable接口方式创建多线程
优点
A.线程只是实现了Runable接口或者Callable接口,还可以继承其它类。
B.在这种方式下,多个线程可以共享同一个target对象,比较适合多个相同线程来处理同一份资源的情况。
缺点:编程略复杂,如果需要访问当前线程,则必须使用Thread currentThread()方法。

2、采用继承Thread类来创建多线程
优点:编写简单,如果需要访问当前线程,只需使用this即可获得当前线程。
缺点:因为继承了Thread类,所以不能再继承其他父类。

分析:一般推荐采用实现Runnable、Callable接口的方式来创建多线程。

三、状态变换

这里写图片描述

1. 新建(new):新创建了一个线程对象。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态。
注意:不能对已经启动的线程再次调用start()方法,否则会出现Java.lang.IllegalThreadStateException异常。

2. 可运行(runnable):其他线程(比如main线程)调用了该对象的start()方法。处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的)。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。

3. 运行(running):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。

4. 阻塞(block):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
A. 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。进入这个状态会释放所占有的全部资源,与堵塞状态不同。进入这个状态后。是不能自己主动唤醒的,必须依靠其它线程调用notify()notifyAll()方法才干被唤醒(因为notify()仅仅是唤醒一个线程,但我们由不能确定详细唤醒的是哪一个线程。或许我们须要唤醒的线程不可以被唤醒,因此在实际使用时,一般都用notifyAll()方法,唤醒有所线程),线程被唤醒后会进入锁池。等待获取锁标记
B. 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
C. 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了阻塞式IO请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

5. 死亡(dead):当线程的run()方法执行完,或者被强制性地终止,就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

四、改变线程状态的常见方法

1、线程睡眠——sleep
如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread的sleep方法。
注:
(1)、sleep是静态方法,它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。

   public class Test {      public static void main(String[] args) throws InterruptedException {          System.out.println(Thread.currentThread().getName());          MyThread myThread=new MyThread();        myThread.start();         myThread.sleep(1000);//这里sleep的就是main线程,而非myThread线程          Thread.sleep(10);  //这里sleep的也是main线程       }  } 

(2)、Java中不管程序员怎么编写调度,只能最大限度的影响线程执行的次序,而不能做到精准控制。因为使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。

2、线程让步——yield
yield()方法和sleep()方法有点相似,也是Thread类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出cpu资源给其他的线程。但是它不会进入到阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。

实际上,当某个线程调用了yield()方法暂停之后,优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程更有可能获得执行的机会,当然,只是有可能,因为我们不可能精确的干涉cpu调度线程。

注:关于sleep()方法和yield()方的区别如下:
、sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。
、sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。
、sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行。

3、线程合并——join
线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时,Thread类提供了join方法来完成这个功能,
注:
、JOIN不是静态方法
、如果在主线程中调用t.join(),实际上的意思是阻塞主线程,知道t线程执行完,这里要注意理解,join()阻塞的是当前所在的线程,而不是join方法的调用线程。

join() method suspends the execution of the calling thread until the object called finishes its execution.

4、设置线程的优先级
每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。

每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main线程具有普通优先级。

setPriority(int newPriority)getPriority()方法来设置和返回一个指定线程的优先级,其中setPriority方法的参数是一个整数,范围是1~·0之间,也可以使用Thread类提供的三个静态常量:

MAX_PRIORITY   =10MIN_PRIORITY   =1NORM_PRIORITY   =5

注:虽然Java提供了10个优先级别,但这些优先级别需要操作系统的支持。不同的操作系统的优先级并不相同,而且也不能很好的和Java的10个优先级别对应。所以我们应该使用MAX_PRIORITYMIN_PRIORITYNORM_PRIORITY三个静态常量来设定优先级,这样才能保证程序最好的可移植性。

5、后台(守护)线程

守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。守护线程的用途为:
• 守护线程通常用于执行一些后台作业,例如在你的应用程序运行时播放背景音乐,在文字编辑器里做自动语法检查、自动保存等功能。
• Java的垃圾回收也是一个守护线程。守护进程的好处就是你不需要关心它的结束问题。例如你在你的应用程序运行的时候希望播放背景音乐,如果将这个播放背景音乐的线程设定为非守护线程,那么在用户请求退出的时候,不仅要退出主线程,还要通知播放背景音乐的线程退出;如果设定为守护线程则不需要了。

:JRE判断程序是否执行结束的标准是所有的前台执线程行完毕了,而不管后台线程的状态,因此,在使用后台线程时候一定要注意这个问题。

6、正确结束线程
Thread.stop()Thread.suspendThread.resumeRuntime.runFinalizersOnExit这些终止线程运行的方法已经被废弃了,使用它们是极端不安全的!想要安全有效的结束一个线程,可以使用下面的方法:
• 正常执行完run方法,然后结束掉;
• 控制循环条件和判断条件的标识符来结束掉线程。

class MyThread extends Thread {      int i=0;      boolean next=true;      @Override      public void run() {          while (next) {              if(i==10)                  next=false;              i++;              System.out.println(i);          }      }  }

7、wait, notify 和 notifyAll

wait(),notify()和notifyAll()都是java.lang.Object的方法:wait(): Causes the current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object.notify(): Wakes up a single thread that is waiting on this object's monitor.notifyAll(): Wakes up all threads that are waiting on this object's monitor.

在调用wait(), notify()或notifyAll()的时候,必须先获得锁(如果没有锁,wait和notify有可能会产生竞态条件(Race Condition),比如在wait方法之前执行了notify方法,从而导致某线程无法被唤醒),且状态变量须由该锁保护,而固有锁对象与固有条件队列对象又是同一个对象。也就是说,要在某个对象上执行wait,notify,先必须锁定该对象,而对应的状态变量也是由该对象锁保护的。还有一点就是永远在while循环而不是if语句中使用wait!

考虑以下生产者和消费者的情景:
1.1生产者检查条件(如缓存满了)-> 1.2生产者必须等待
2.1消费者消费了一个单位的缓存 -> 2.2重新设置了条件(如缓存没满) -> 2.3调用notifyAll()唤醒生产者

我们希望的顺序是: 1.1->1.2->2.1->2.2->2.3
但在多线程情况下,顺序有可能是 1.1->2.1->2.2->2.3->1.2。也就是说,在生产者还没wait之前,消费者就已经notifyAll了,这样的话,生产者会一直等下去。
所以,要解决这个问题,必须在wait和notifyAll的时候,获得该对象的锁,以保证同步。
关于这三个方法的介绍,可以参考如下博文:http://www.cnblogs.com/techyc/p/3272321.html

这一篇文章就介绍到这里,下一部分就总结一下java线程同步的知识点

阅读全文
0 0
原创粉丝点击