java 线程详解

一、概念

1.1 基本概念

进程是程序执行的一个实例,比如说,10个用户同时执行IE,那么就有10个独立的进程(尽管他们共享同一个可执行代码)。

进程的特点,每一个进程都有自己的独立的一块内存空间、一组资源系统。其内部数据和状态都是完全独立的。怎么看待多进程?进程的优点是提高CPU运行效率,在同一时间内执行多个程序,即并发执行。但是从严格上讲,也不是绝对的同一时刻执行多个程序,只不过CPU在执行时通过时间片等调度算法不同进程高速切换。总结来说:

  • 进程由操作系统调度,简单而且稳定
  • 进程之间的隔离性好,一个进程崩溃不会影响其它进程
  • 单进程编程简单
  • 在多核情况下可以把进程和CPU进行绑定,充分利用CPU

当然,多进程也有一些缺点:

  • 一般来说进程消耗的内存比较大
  • 进程切换代价很高,进程切换也像线程一样需要保持上一个进程的上下文环境
  • 在web编程中,如果一个进程来处理一个请求的话,如果要提高并发量就要提高进程数,而进程数量受内存和切换代价限制

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

同类的多个线程共享一块内存空间和一组系统资源,线程本身的数据通常只有CPU的寄存器数据,以及一个供程序执行时的堆栈。线程在切换时负荷小,因此,线程也被称为轻负荷进程。一个进程中可以包含多个线程。

在JVM中,本地方法栈、虚拟机栈和程序计数器是线程隔离的,而堆区和方法区是线程共享的。关于JVM中的资源分配,可参考我的另一篇文章【JVM内存管理及GC】:http://blog.csdn.net/suifeng3051/article/details/48292193

1.2 进程线程的区别

  • 地址空间:进程内的一个执行单元;进程至少有一个线程;它们共享进程的地址空间;而进程有自己独立的地址空间
  • 资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源
  • 线程是处理器调度的基本单位,但进程不是
  • 二者均可并发执行

注: 关于并发与并行

 并发:多个事件在同一时间段内一起执行
 并行:多个事件在同一时刻同时执行

1.3 多任务

在一开始,一个计算机只有一个CPU,这个CPU一次也只能运行一个任务。然而随着计算机技术的发展,一个CPU也可以“同时”运行多个任务,这就诞生了多任务。但这里的同时并不是真正的同时,操作系统通过切换各个应用来实现CPU的共享,在CPU内部各个程序其实是交替执行的。

1.4 多线程

为了进一步提高CPU利用率,多线程便诞生了。一个程序中可以运行多个线程,多个线程可以同时执行,从整个应用角度上看,这个应用好像独自拥有多个CPU一样。虽然多线程进一步提高了应用的执行效率,但是由于线程之间会共享内存资源,这也会导致一些资源同步问题,另外,线程之间的切换也会对资源有所消耗(后面会讲到)。

这里需要注意的是,如果一台电脑只有一个CPU核心,那么多线程也并没有真正的“同时”运行,它们之间需要通过相互切换来共享CPU核心,所以,只有一个CPU核心的情况下,多线程不会提高应用效率。但是,现代计算机一般都会有多个CPU,并且每个CPU可能还会有多个核心,所以在现代硬件资源条件下,多线程编程可以极大的提高应用效率。

1.5 多线程的调度

在Java程序中,JVM负责线程的调度。线程调度是值按照特定的机制为多个线程分配CPU的使用权。

调度的模式有两种:分时调度和抢占式调度。分时调度是所有线程轮流获得CPU使用权,并平均分配每个线程占用CPU的时间;抢占式调度是根据线程的优先级别来获取CPU的使用权。JVM的线程调度模式采用了抢占式模式。

1.6 多线程编程面临的问题

  • 更复杂的设计 : 多线程在访问共享数据时需要进行同步(在java中需要使用synchronized关键字),某些情况下需要考虑线程的执行顺序和相互配合
  • 上下文切换: 上CPU需要从一个线程切换到另一个线程时,它需要先保存当前线程的本地数据和程序指针,然后再加载要切换线程的本地数据和程序指针
  • 更多的系统资源:处理需要CPU时间以外,每个线程还需要额外的内存空间来保存它的本地数据栈,更需要操作系统资源来管理多个线程,所以应用程序的线程数量一定要根据实际情况合理安排

关于多线程编程中的资源同步,请参考另一篇文章【 Java synchronized 介绍】:http://blog.csdn.net/suifeng3051/article/details/48711405

二、线程的实现

Java中实现多线程,一种是继承Thread类,一种是实现Runable接口。

2.1 继承Thread类

/**
 * 继承Thread类,直接调用run方法
 * */
class hello extends Thread {
public hello() {
}

public hello(String name) {
    this.name = name;
}
public void run() {
    for (int i = 0; i < 5; i++) {
        System.out.println(name + "运行     " + i);
    }
}
public static void main(String[] args) {
    hello h1=new hello("A");
    hello h2=new hello("B");
    h1.start();
    h2.start();
}
private String name;
}

注意:在实际启动进程的时候,我们直接调用的并不是Thread子类中run方法,而是调用的Thread线程的start方法,因为线程start运行需要本地操作系统支持,start启动线程会调用操作系统native函数来支持线程运行。

2.2 实现runnable接口

package com.heaven.xiancheng;
public class TestRunnable implements Runnable{
   private int count =100;
   public void run(){
         for(int i=0;i<200;i++){
               if(count >0){
                    System. out.println(Thread.currentThread().getName()+ " "+count --);
              }
        }
  }
   public static void main(String[] args) {
        TestRunnable r= new TestRunnable();
        Thread t1= new Thread(r,"A" );
        Thread t2= new Thread(r,"B" );
        t1.start();
        t2.start();
  }
}

2.3 两者区别

实现Runnable接口比继承Thread类有更多的优势,所以我推荐大家尽量使用实现runnable接口的形式,以下是其优点

- 适合多个相同的程序代码的线程去处理同一个资源
- 可以避免java中的单继承的限制
- 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。

三、线程的状态

3.1 线程的五种状态类型

1. 新建状态(New):新创建了一个线程对象。
2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4. 阻塞状态(Blocked):塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

其中阻塞又可能是由以下几种情况造成:

  1. 调用 sleep(毫秒数),使线程进入“睡眠”状态。在规定的时间内,这个线程是不会运行的。
  2. 用 suspend()暂停了线程的执行。除非线程收到 resume()消息,否则不会返回“可运行”状态。
  3. 用 wait()暂停了线程的执行。除非线程收到 nofify()或者 notifyAll()消息,否则不会变成“可运行“。
  4. 线程正在等候一些 IO(输入输出)操作完成。
  5. 线程试图调用另一个对象的“同步”方法,但那个对象处于锁定状态,暂时无法使用。

3.2 线程状态图

四、线程的阻塞

阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。Java 提供了大量方法来支持阻塞,下面让我们逐一分析。

4.1 sleep() 方法

sleep()允许指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU 时间,指定的时间一过,线程重新进入可执行状态。典型地,sleep() 被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段时间后重新测试,直到条件满足为止。

4.2 suspend() 和 resume() 方法

两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume() 被调用,才能使得线程重新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。

4.3 yield() 方法

yield() 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。

4.4 wait() 和 notify() 方法

两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种允许指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用。初看起来它们与 suspend() 和 resume() 方法对没有什么分别,但是事实上它们是截然不同的。区别的核心在于,前面叙述的所有方法,阻塞时都不会释放占用的锁(如果占用了的话),而这一对方法则相反。

在这里需要重点介绍下wait()和notify()

首先,前面叙述的所有方法都隶属于 Thread 类,但是这一对却直接隶属于Object 类,也就是说,所有对象都拥有这一对方法。初看起来这十分不可思议,但是实际上却是很自然的,因为这一对方法阻塞时要释放占用的锁,而锁是任何对象都具有的,调用对象的 wait() 方法导致线程阻塞,并且该对象上的锁被释放。而调用对象的notify()方法则导致因调用该对象的 wait() 方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。

其次,前面叙述的所有方法都可在任何位置调用,但是这一对方法却必须在 synchronized 方法或块中调用,理由也很简单,只有在synchronized 方法或块中当前线程才占有锁,才有锁可以释放。同样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有,这样才有锁可以释放。因此,这一对方法调用必须放置在这样的 synchronized 方法或块中,该方法或块的上锁对象就是调用这一对方法的对象。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现IllegalMonitorStateException 异常。

最后,关于 wait() 和 notify() 方法再说明两点:

1. 调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题
2. 除了 notify(),还有一个方法 notifyAll() 也可起到类似作用,唯一的区别在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。

五、线程的其它问题

5.1 Thread.Join

把指定的线程加入到当前线程,原本两个线程可以并发执行,join之后变成了两个线程顺序执行。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

public class TestJoin {
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(new JoinA(),"A");
    Thread t2 = new Thread(new JoinB(),"B");
    t1.start(); //main函数所在的主线程调用了实现了run()方法的JoinA子线程
    t1.join(); //主线程获得子线程的锁,阻塞直到子线程完成
    t2.start();
    }
}  

class JoinA implements Runnable {
private int i;
@Override
public void run() {
    while (i <= 10) {
        System.out.println(Thread.currentThread().getName() + i + " ");
        i++;
    }
    }
}  

class JoinB implements Runnable {
private int i;
@Override
public void run() {
    while (i <= 10) {
        System.out.println(Thread.currentThread().getName() + i + " ");
        i++;
    }
    }
}

执行上面程序从运行结果可以看出两个线程是顺序执行的。其实是当主线程调用子线程的join()方法时,主线程变获得了子线程对象的锁,因此被子线程阻塞直到子线程退出。

我们可以看一下join()的源码:

public final synchronized void join(long millis)
throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

join方法实现是通过wait。当main线程调用t.join()时候,main线程会获得线程对象t的锁,调用该对象的wait(),直到该对象唤醒main线程,比如退出后。

5.2 线程的休眠与中断

public class TestInterrupt implements Runnable{
   @Override
   public void run() {
        System. out.println("thread run..." );
         try {
              System. out.println("begin to sleep..." );
              Thread. sleep(10000);
        } catch (InterruptedException e) {
              System. out.println("sleep was interrupted" );
              e.printStackTrace();
        }
  }
   public static void main(String[] args) {
        TestInterrupt ti= new TestInterrupt();
        Thread t= new Thread(ti);
        t.start();
         try {
              Thread. sleep(1000);
        } catch (InterruptedException e) {
               // TODO Auto-generated catch block
              e.printStackTrace();
        }
        t.interrupt(); //中断线程运行

  }
}

5.3 线程的优先级

public class TestPriority implements Runnable {
   @Override
   public void run() {
         for (int i = 0; i < 5; ++i) {
              System. out.println(Thread.currentThread().getName() + "运行" + i);
        }
  }
   public static void main(String[] args) {
        TestPriority tp= new TestPriority();
        Thread t1= new Thread(tp,"A" );
        Thread t2= new Thread(tp,"B" );
        Thread t3= new Thread(tp,"C" );
        t1.setPriority(1);
        t2.setPriority(8);
        t3.setPriority(3);
        t1.start();
        t2.start();
        t3.start();
  }
}

注意:不要误以为优先级越高就先执行,谁先执行还是取决于谁先取得CPU资源。

5.4 线程的礼让

在线程操作中,也可以使用yield()方法,将一个线程的操作暂时交给其他线程执行。

public class TestYield implements Runnable{
   @Override
   public void run() {
         for(int i=0;i<10;++i){
        System. out.println(Thread.currentThread().getName()+ "运行"+i);
        if(i==3){
            System. out.println("线程的礼让" );
            Thread. yield();
        }
    }
  }
   public static void main(String[] args) {
          Thread h1= new Thread(new TestYield(),"A");
          Thread h2= new Thread(new TestYield(),"B");
          h1.start();
          h2.start();
      }
}

5.5 同步与死锁

线程同步问题,当各个线程共用一个资源时,有可能导致线程同步问题。在JAVA中,是没有类似于PV操作、进程互斥等相关的方法的。JAVA的进程同步是通过synchronized()来实现的,需要说明的是,JAVA的synchronized()方法类似于操作系统概念中的互斥内存块,在JAVA中的Object类型中,都是带有一个内存锁的,在有线程获取该内存锁后,其它线程无法访问该内存,从而实现JAVA中简单的同步、互斥操作。关于这部分内容,请参考我的另一篇文章:
Javasynchronized介绍】:http://blog.csdn.net/suifeng3051/article/details/48711405

参考文章:
http://blog.csdn.net/bzwm/article/details/3881392
http://www.cnblogs.com/techyc/p/3286678.html

时间: 2024-12-20 20:23:52

java 线程详解的相关文章

Python线程详解

  这篇文章主要介绍了Python线程详解,本文详细讲解了线程方方面面的知识,如线程基础知识线程状态.线程同步(锁).线程通信(条件变量)等内容,需要的朋友可以参考下 1. 线程基础 1.1. 线程状态 线程有5种状态,状态转换的过程如下图所示: 1.2. 线程同步(锁) 多线程的优势在于可以同时运行多个任务(至少感觉起来是这样).但是当线程需要共享数据时,可能存在数据不同步的问题.考虑这样一种情况:一个列表里所有元素都是0,线程"set"从后向前把所有元素改成1,而线程"p

Java虚拟机详解----JVM常见问题总结

[正文] 声明:本文只是做一个总结,有关jvm的详细知识可以参考本人之前的系列文章,尤其是那篇:Java虚拟机详解04----GC算法和种类.那篇文章和本文是面试时的重点. 面试必问关键词:JVM垃圾回收.类加载机制.   先把本文的目录画一个思维导图:(图的源文件在本文末尾)   一.Java引用的四种状态: 强引用: 用的最广.我们平时写代码时,new一个Object存放在堆内存,然后用一个引用指向它,这就是强引用. 如果一个对象具有强引用,那垃圾回收器绝不会回收它.当内存空间不足,Java

java关键字(详解)

基本类型 1 boolean 布尔型 2 byte 字节型 3 char 字符型 4 double 双精度 5 float 浮点 6 int 整型 7 long 长整型 8 short 短整型 9 null 空 10 true 真 11 false 假 程序控制语句 1 break 跳出中断 2 continue 继续 3 return 返回 4 do 运行 5 while 循环 6 if 如果 7 else 否则 8 for 循环 9 instanceof 实例 10 switch 观察 11

Java设计模式详解之门面模式(外观模式)_java

门面模式(Facade Pattern)也叫外观模式,它隐藏系统的复杂性,并向客户端提供一个可以访问系统的接口.这种类型的设计模式属于结构型模式,它向现有的系统添加一个接口,来隐藏系统的复杂性,为子系统中的一组接口提供了一个统一的高层访问接口,这个接口使得子系统更容易被访问或使用.这种模式涉及到一个单一的类,该类提供了客户端请求的简化方法和对现有系统类方法的委托调用. 简而言之,就是把一堆复杂的流程封装成一个接口供给用户更简单的使用,这个设计模式里有三个角色: 1)门面角色( facade ):

Java NIO 详解(一)

NIO即新的输入输出,这个库是在JDK1.4中才引入的.它在标准java代码中提供了高速的面向块的IO操作. 一.基本概念描述 1.1 I/O简介 I/O即输入输出,是计算机与外界世界的一个借口.IO操作的实际主题是操作系统.在java编程中,一般使用流的方式来处理IO,所有的IO都被视作是单个字节的移动,通过stream对象一次移动一个字节.流IO负责把对象转换为字节,然后再转换为对象. 关于Java IO相关知识请参考我的另一篇文章:Java IO 详解 1.2 什么是NIO NIO即New

HBase Java API详解

[本文转自HBase Java API详解] HBase是Hadoop的数据库,能够对大数据提供随机.实时读写访问.他是开源的,分布式的,多版本的,面向列的,存储模型. 在讲解的时候我首先给大家讲解一下HBase的整体结构,如下图: HBase Master是服务器负责管理所有的HRegion服务器,HBase Master并不存储HBase服务器的任何数据,HBase逻辑上的表可能会划分为多个HRegion,然后存储在HRegion Server群中,HBase Master Server中存

Java Annotation详解(二): 反射和Annotation

前面一篇文<Java Annotation详解(一): 理解和使用Annotation>中,我们或许会觉得,Annotation注释其实并没有多大的作用,除了几个内建的Annotation偶尔为了消除警告会使用下,自定义Annotation大家在实际的开发中应该都没有用过.其实呢,我在毕业后一年的工作里,也从未自定义使用过Annotation,只是在多处开发中使用过注释方便的内容,比如Servlet,Spring以及一些优秀的Android开源类库. 如果从简单的开发来讲,大家基本会使用一些开

请教:朋友跟我想写一本关于JAVA虚拟机详解方面的书。

问题描述 朋友跟我想写一本关于JAVA虚拟机详解方面的书.书的内容主要包括JVM的原理,JVM源码分析等方面的问题.书本身内容清晰,层次很分明,也很通俗易懂.目前书已经写了一半,大概6章的内容..不知道怎么联系出版社,如果出版以后销路会如何.也不知道有没有多少读者会关注JAVA虚拟机方面的知识..大家给点意见,或者渠道..谢谢. 解决方案 解决方案二:顶,一直有个小理想,自己写本jvm分析的书,不过未能实现.感觉这种书很小众.jvm原理的书还可以,但代码分析的未必对大部分java程序员有多大价值

如何正确使用Android线程详解_Android

前言 对于移动开发者来说,"将耗时的任务放到子线程去执行,以保证UI线程的流畅性"是线程编程的第一金科玉律,但这条铁则往往也是UI线程不怎么流畅的主因.我们在督促自己更多的使用线程的同时,还需要时刻提醒自己怎么避免线程失控. 多线程编程之所以复杂原因之一在于其并行的特性,人脑的工作方式更符合单线程串行的特点.一个接着一个的处理任务是大脑最舒服的状态,频繁的在任务之间切换会产生"头痛"这类系统异常.人脑的多任务和计算机的多任务性能差异太大导致我们在设计并行的业务逻辑之