【J2SE快速进阶】——Java多线程机制

简介: 学习Java中的线程时,自然而然地联想到之前学过的操作系统中处理器那一块的知识。

      定义

      文章开头,先大概说一下程序、进程和线程的概念及其之间的关系。


      程序:程序就是一段静态的代码,或者一个可执行程序。


      进程:进程是程序的一次动态执行的过程,它对应着从代码加载、运行到结束的一次动态的执行过程。


      线程:比进程更小的执行单位,一个进程在执行过程中,可以产生多个线程,也就是多个分支。它是程序执行流的最小单位。


      来看一个小程序:


public class Test {
  public static void main(String[] args) {
    fun1();
  }
  public static void fun1(){
    System.out.println(fun2()+fun3());    
  }
  public static String fun2(){
    return "Hello ";
  }
  public static String fun3(){
    return "World!";
  }
}


   它的执行顺序如下,在main方法中,从①执行到⑥是一条线,并没有分支,这就是一个线程。

78.png


       线程的实现


       当JVM执行到main方法时,就会启动一个线程,叫做主线程。如果main方法中还创建了其他线程,那么JVM就会在主线程和其他线程之间轮流切换,保证每个线程都有机会使用CPU资源。


      Java中的线程是通过java.lang.Thread类来实现的,每一个Thread对象都代表一个新的线程。


      Java中实现线程有两种方法:

      1、继承Thread类,并且重写其run方法(用来封装整个线程要执行的命令),调用start方法启动线程。


      比如下面这个类T要实现线程,则代码如下:


public class CreateThreadTest{
  public static void main(String[] args) {
    T r=new T();
    r.start();   //T类的线程开始执行
    for(int i=0;i<100;i++)
    {
      System.out.println("主线程正在执行~~~~"+i);
    }
  }
}
class T extends Thread
{
  public void run(){//重写父类中的run方法
    for(int i=0;i<100;i++)
    {
      System.out.println("我创建的线程正在执行~~~~"+i);
    }
  }
}


      现在,这一个小程序一共有两个线程正在执行,一个是主线程,还有一个是T类创建的线程。


     

       2、实现Runnable接口


       还有一种方法就是让实现线程的类实现Runnable接口,实现Runnable接口中唯一的方法run(),然后把此类的实例当做Thread类的构造函数的参数,创建线程对象。


       例如上面的例子,还可以这样写:

public class CreateThreadTest {
  public static void main(String[] args) {
    T r=new T();
    Thread t=new Thread(r);    //创建线程对象
    t.start();    //T类的线程开始
    for(int i=0;i<100;i++)
    {
      System.out.println("主线程正在执行~~~~"+i);
    }
  }
}
class T implements Runnable 
{
  public void run(){
    for(int i=0;i<100;i++)
    {
      System.out.println("我创建的线程正在执行~~~~"+i);
    }
  }
}


      两种方法的实质就是,都需要重写run方法,最终都是由Thread类的start方法启动线程。


      温馨提示:因为Java中不支持多继承,所以实现线程时,一旦继承了Thread类,就无法再继承其他类了。但Java支持实现多个接口,所以推荐采用第二中方法,比较灵活。


      多线程

      多线程主要是为了同步完成多项任务,即同时执行多个线程。多线程把一个进程划分为多个任务,它们彼此独立地工作


      我们都知道,现在大部分操作系统比如Windows、Mac OS X、Unix、Linux等,都是支持多线程的,但我们平时所说的多线程,并不意味着CPU在同时会处理多个线程,每个CPU在同一个时间点只会处理一个线程,只不过速度太快了,处理的时间极短,以至于我们可以认为它是在同一个时间段可以处理多个线程。所以只有当你的机器是“双核”甚至“多核”时,才能实现真正意义上的多线程。                                              


      下面看一个多线程的例子:


       两个线程t1和t2的执行        


public class ThreadTest {
  public static void main(String[] args) {
    Thread t1=new Thread(new T1());
    Thread t2=new Thread(new T2());
    t1.start();t2.start();
  } 
}
class T1 implements Runnable{
  public void run(){
    for(int i=0;i<10;i++){
      System.out.println("线程1正在执行中------"+i);
      if(i==9){
        System.out.println("线程1执行已结束------"+i);
        break;
      }
    }
  }
}
class T2 implements Runnable{
  public void run(){
    for(int i=0;i<10;i++){
      System.out.println("线程2正在执行中------"+i);
      if(i==9){
        System.out.println("线程2执行已结束------"+i);
        break;
      }
    }
  }
}


        看下面的结果之前,先充分发挥你的大脑想一想到底结果应该是什么样子的~~

        执行结果:

80.gif


      是不是跟您预测的不一样呢?如果不加线程的话,本来的结果应该前十行都是线程1在执行,线程1执行完后线程2才开始执行,线程调度算法使得每个线程执行一会进入等待状态,再去执行另一个线程。


      线程中常用的方法

      如果亲手尝试过上面这个例子中,会发现这两个线程t1和t2谁先执行,谁后执行,谁执行多长时间等等这些都是不确定、不可控的。下面就说一下线程中常用到的几个方法。

      ★ void yield()方法


      在一个线程中,如果执行到yield()方法,那么这个线程就会让出CPU资源,暂停当前正在执行的线程对象,转而让CPU去执行其他具有相同优先级的线程。充分体现了线程乐于谦让的精神!


      例:i的值从0到99,每次输出线程实例的名字,当i是10的倍数时,执行yield方法。


 public class TestYield {
  public static void main(String[] args) {
    MyThread thread1=new MyThread("thread1");
    MyThread thread2=new MyThread("thread2");
    thread1.start();thread2.start();
  }
}
class MyThread extends Thread{
  MyThread(String name){
    super(name);
  }
  public void run(){
    for(int i=0;i<100;i++){
      System.out.println(getName()+":"+i);
      if(i%10==0){
        yield();
      }
    }
  }
}


      从结果中的前几条输出可以发现,对thread1来说,每当i是10的倍数时,进程就会让出,thread2进程执行;thread2也是如此:


81.png82.png


     注意: yield()方法会将当前运行的线程切换到可运行状态,但可能没有效果,因为实际中执行yield方法的线程还有可能被调度程序再次选中。


     


     ★ void sleep(long millis)方法和void sleep(long millis,int nanos)


      让线程休眠(暂停执行)指定的时间长度,参数millis的单位是毫秒,参数nanos的单位是纳秒。线程执行了sleep方法后就会进入阻塞状态,指定的时间段过后,线程进入可执行状态,等待被操作系统调度。


      例:      


import java.util.*;
public class TestSleep {
  public static void main(String[] args)
  {
    MyThread thread=new MyThread();
    thread.start();
  }
}
class MyThread extends Thread
{
  public void run()
  {
    while(true)
    {
      System.out.println("==="+new Date()+"===");
      try{
        sleep(1000);
      }catch(InterruptedException e){
        return;
      }
    }
  }
}


       看完代码您应该猜到结果了,每隔一秒都会输出当前时间,相当于一个定时器。这个程序一共有两个线程,主线程(即main方法)中出了执行thread线程就没有其他任务需要执行,所以这里可以看做只执行thread这一个线程,如果把例子中的sleep方法去掉,那么就会“唰唰唰”地不断输出当前时间(你的CPU风扇也会“唰唰唰”~~~~)。


83.png


        ★void join()方法


        上面说线程执行了yield方法时,会自动让出CPU资源,使状态由运行状态转换为可运行状态。join()方法可以说是恰恰相仿,当一个线程执行了join方法时,那么它就会一直执行下去直到这个线程结束。


       还是举例来说明:


public class TestJoin {  
    public static void main(String[] args) throws InterruptedException {  
        Thread t1 = new Thread(new ThreadA());  
        Thread t2 = new Thread(new ThreadB());  
        t1.start();  
        t1.join(); // t1线程开始执行后,它将继续执行下去,直到t1线程结束,否则绝不让出CPU  
        t2.start();  
        t2.join(); // t2线程开始执行后,它将继续执行下去,直到t1线程结束,否则绝不让出CPU  
    }  
}
class ThreadA implements Runnable {   
    public void run() {  
        for (int i=0;i<10;i++) {  
            System.out.println("A线程正在运行~~~~" + i);  
        }   
    }  
}   
class ThreadB implements Runnable {   
  public void run() {  
        for (int i=0;i<10;i++) {  
            System.out.println("B线程正在运行~~~~" + i);  
        }   
    }   
} 


      先来设想一下,加入t1和t2两个线程启动后不执行join方法,那么CPU给它们分配的执行时间和顺序就不一定相同,如左下图;如果t1和t2执行了join方法,那么它们一旦开始执行,就将执行到底,如右下图。


84.gif85.gif


       为什么要用多线程

       最后一个问题,为什么要用多线程?


       在论坛里看到一个大牛的比喻:单线程就是牛逼老板从头到尾一个人做完,另开一个线程就是老板掰出一件事情叫一个小弟去做,这个小弟的进入会加快整个事情的进展,但有时可能会做起事来碍手碍脚。


      当然,想要更深地理解多线程的精髓所在,光靠学这些理论还是不够的,更重要的还是在实践和项目中去挖掘和思考。


相关文章
|
11天前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
7天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
7天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
24 3
|
8天前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
8天前
|
Java 程序员
深入理解Java异常处理机制
Java的异常处理是编程中的一块基石,它不仅保障了代码的健壮性,还提升了程序的可读性和可维护性。本文将深入浅出地探讨Java异常处理的核心概念、分类、处理策略以及最佳实践,旨在帮助读者建立正确的异常处理观念,提升编程效率和质量。
|
9天前
|
Java 开发者 UED
深入探索Java中的异常处理机制##
本文将带你深入了解Java语言中的异常处理机制,包括异常的分类、异常的捕获与处理、自定义异常的创建以及最佳实践。通过具体实例和代码演示,帮助你更好地理解和运用Java中的异常处理,提高程序的健壮性和可维护性。 ##
27 2
|
9天前
|
Java 开发者
Java中的异常处理机制深度剖析####
本文深入探讨了Java语言中异常处理的重要性、核心机制及其在实际编程中的应用策略,旨在帮助开发者更有效地编写健壮的代码。通过实例分析,揭示了try-catch-finally结构的最佳实践,以及如何利用自定义异常提升程序的可读性和维护性。此外,还简要介绍了Java 7引入的多异常捕获特性,为读者提供了一个全面而实用的异常处理指南。 ####
26 2
|
11天前
|
监控 Java 数据库连接
Java线程管理:守护线程与用户线程的区分与应用
在Java多线程编程中,线程可以分为守护线程(Daemon Thread)和用户线程(User Thread)。这两种线程在行为和用途上有着明显的区别,了解它们的差异对于编写高效、稳定的并发程序至关重要。
21 2
|
12天前
|
Java 程序员 UED
深入理解Java中的异常处理机制
本文旨在揭示Java异常处理的奥秘,从基础概念到高级应用,逐步引导读者掌握如何优雅地管理程序中的错误。我们将探讨异常类型、捕获流程,以及如何在代码中有效利用try-catch语句。通过实例分析,我们将展示异常处理在提升代码质量方面的关键作用。
25 3
|
11天前
|
监控 Java 开发者
Java线程管理:守护线程与本地线程的深入剖析
在Java编程语言中,线程是程序执行的最小单元,它们可以并行执行以提高程序的效率和响应性。Java提供了两种特殊的线程类型:守护线程和本地线程。本文将深入探讨这两种线程的区别,并探讨它们在实际开发中的应用。
17 1