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 进程线程的区别

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

注: 关于并发与并行

 并发:多个事件在同一时间段内一起执行
 并行:多个事件在同一时刻同时执行
AI 代码解读

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;
}
AI 代码解读

注意:在实际启动进程的时候,我们直接调用的并不是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();       
  }
}
AI 代码解读

2.3 两者区别

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

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

三、线程的状态

3.1 线程的五种状态类型

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

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

  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() 方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。
AI 代码解读

五、线程的其它问题

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++;  
    }  
    }  
}  
AI 代码解读

执行上面程序从运行结果可以看出两个线程是顺序执行的。其实是当主线程调用子线程的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;
        }
    }
}
AI 代码解读

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(); //中断线程运行

  }
}
AI 代码解读

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();
  }
}
AI 代码解读

注意:不要误以为优先级越高就先执行,谁先执行还是取决于谁先取得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();
      }
}
AI 代码解读

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

目录
相关文章
|
3月前
|
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
214 60
【Java并发】【线程池】带你从0-1入门线程池
|
22天前
|
【源码】【Java并发】从InheritableThreadLocal和TTL源码的角度来看父子线程传递
本文涉及InheritableThreadLocal和TTL,从源码的角度,分别分析它们是怎么实现父子线程传递的。建议先了解ThreadLocal。
57 4
【源码】【Java并发】从InheritableThreadLocal和TTL源码的角度来看父子线程传递
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
100 23
|
2月前
|
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
当我们创建一个`ThreadPoolExecutor`的时候,你是否会好奇🤔,它到底发生了什么?比如:我传的拒绝策略、线程工厂是啥时候被使用的? 核心线程数是个啥?最大线程数和它又有什么关系?线程池,它是怎么调度,我们传入的线程?...不要着急,小手手点上关注、点赞、收藏。主播马上从源码的角度带你们探索神秘线程池的世界...
162 0
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
190 14
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
80 13
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。
|
4月前
|
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
165 17
|
5月前
|
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
118 1
Java—多线程实现生产消费者
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等