java 线程详解

简介: 一、概念1.1 基本概念进程是程序执行的一个实例,比如说,10个用户同时执行IE,那么就有10个独立的进程(尽管他们共享同一个可执行代码)。进程的特点,每一个进程都有自己的独立的一块内存空间、一组资源系统。其内部数据和状态都是完全独立的。怎么看待多进程?进程的优点是提高CPU运行效率,在同一时间内执行多个程序,即并发执行。但是从严格上讲,也不是绝对的同一时刻执

一、概念

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

目录
相关文章
|
9天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
19天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
6天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
25 9
|
9天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
6天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
9天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
23 3
|
7天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
8天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
20 1
|
9天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
9天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
37 1