Java 里的thread (线程)简介

简介:
Java里 thread 就是线程的意思.
  说到线程的概念, 自然离不开另外两个词: 程序和进程.
  从最基本的程序讲起:
   一. 什么是程序(Program)
  所谓程序, 就是1个严格有序的指令集合. 程序规定了完成某一任务时,计算机所需要做的各种操作, 以及操作的顺序.
  1.1 单道程序运行环境
  所谓单道程序环境就是指, 计算机除了 操作系统之外, 只允许运行1个用户程序.
  以前的DOS系统就是1个典型的单道程序运行环境.
  单道程序有如下特点:
  1. 资源的独占性: 任何时候, 内存内只有1个用户程序, 这个程序独享系统的所有资源.
  2. 执行顺序性: 内存中只执行1个程序, 各程序是按次序执行的, 即是做完1件事后, 才做另一件事.
  绝不可能执行1个程序的中途暂停, 然后去执行另1个程序.
  3.结果的再现性: 只要执行环境和初始条件相同, 重复执行1个程序, 获得的结果总是一样的.
  1.2 多道程序运行环境
  所谓多道程序运行环境是指, 计算机除了操作系统之外, 允许同时运行多个用户程序.
  当今的 unix, linux都是典型的多道程序运行环境.
  多道程序有如下特点:
  1. 间断性: 也就是cpu会在多个程序之间切换执行, 例如cpu执行程序A一段时间后, 会切换去执行程序B一段时间.
  由于这个时间段很短, 所以对用户造成1个程序A和程序B同时执行的假象.
  2. 失去封闭性:  程序执行受外界影响, 也就是多个程序之间共享资源, 互相制约.
  3. 不可再现性:  重复执行时, 可能得到不同的结果. 原因就是上面的点, 执行时可能会受到内存中其他程序的影响.
   二. 什么是进程(process)
  进程这个概念是基于多道程序运行环境来讲的.
  1个进程就是程序在内存中运行的1个实例
  由于在多道程序运行环境中, 同1个程序可以同时产生两个实例在内存里运行.
  举个简单例子:  例如你可以在操作系统打开两个gvim 文本编辑器, 同时编辑两个不同的文件.
  这时执行ps -ef | grep gvim 就会见到系统中有两个名字是gvim的进程, 但是它们的进程id是不同的.
  也就是将,  gvim这个程序在内存中生成两个进程同时运行.
  在多道程序运行环境中.  引用进程这个概念来描叙程序在内存里事例, 用来区别于程序本身.
  真正深入了解进程并不简单, 进程其实算是属于操作系统这门课的范畴. 1个进程包括程序段, 数据段, 程序控制块等,  但是作为一个普通的程序猿, 没必要理解得这么深入.
   三. 什么是线程(thread).
  相对地, 我认为作为1个普通的程序猿, 必须深入了解线程这个概念.
  其实, 线程就是1个进程的执行路径.
  一般的java程序都是从启动类的main函数入口开始执行, main函数的结束而停止.   这条执行路径就是java程序的主线程.
  也就是讲:
  线程是相对于进程来讲的, 1个进程至少存在1条线程.
  而java允许多线程编程, 也就是1个进程除主线程外,  还允许存在1个或若干个其他线程, cpu会在这些线程当中间断切换执行, 给用户造成同时执行的假象.
   四. 1个单线程java程序的简单例子.
  例子如下:
package Thread_kng;
class S_thrd_1{
public void f(){
while (true){
System.out.printf("Thread main is runing!\n");
}
//System.out.printf("f() is done!\n");  //compilation fail
}
}
public class S_thread_expl{
public static void g(){
S_thrd_1 s = new S_thrd_1();
s.f();
System.out.printf("g() is done!\n");
}
}


 上面的例子中,  g() 函数调用了f() 函数,
  g()函数在最后尝试输出"g() is done!" , 但是因为f() 是一个无限循环.  所以g() 调用f()后, 根本没机会执行后面的语句!
  也就是说, 虽然g() 跟 f() 是两个不同类的函数, 但是它们是在程序中的同一个线程(主线程)内执行.
  在同一个线程内, 语句总是按程序猿编写的顺序依次执行, 一条语句未执行完时, 不会跳到后面执行其他的语句.
   五. 1个多线程java程序的简单例子.
  例子如下:
package Thread_kng;
class M_thrd_1 extends Thread{  //extends
public M_thrd_1(String name){
super(name);
}
public void run(){ //overwrite the method run() of superclass
while (true){
System.out.printf("Thread " + this.getName()+ " is runing!\n");
}
//System.out.printf("f() is done!\n");  //compilation fail
}
}
public class M_thread_expl{
public static void g(){
M_thrd_1 s = new M_thrd_1("T1");
s.start(); //start()method is extended from superclass, it will call the method run()
M_thrd_1 s2 = new M_thrd_1("T2");
s2.start();
System.out.printf("g() is done!\n");
}
  上面例子中, 类 M_thrd_1继承了线程类Thread.  并重写了run方法.
  在下面的g()函数中.
  实例化了两个类M_thrd_1的对象s, s1 的对象.
  执行了start()方法.
  start()方法继承字Thread, 它会启动1新线程, 并调用run()函数.
  而g()后面本身也在不断循环输出"THread Main is running"
  所以输出如下:
  可以 见到, 输出结果是T1,T2 和Main 交替输出在屏幕上.
  实际上, 这个程序有3个线程.
  其中1个就是主线程.
  主线程main通过实例化两个M_thrd_1的对象, 调用其start()函数开启了两个子线程.
  其中1个子线程不断输出T1
  另1个子线程不断输出T2
  但是主线程并不会等待其子线程执行完成, 会继续执行下面的代码,所以不断循环输出Main!
  所以这个例子实际上要3个线程在输出信息到屏幕了!
   六. Java 开启一条新进程的两种方式.
  java开启一条新线程, 有两种方式. 但是都要利用java的线程基类Thread.
  6.1 方式1. 创建Thread的派生类
  这个就类似上面的例子:
  具体步骤如下:
  6.1.1 线程准备: 新建1个类, 继承线程类Thread.  将业务代码写入重写后的run() 方法中.
  例如上面的例子中的M_thrd_1类
class M_thrd_1 extends Thread{  //extends
public M_thrd_1(String name){
super(name);
}
public void run(){ //overwrite the method run() of superclass
while (true){
System.out.printf("Thread " + this.getName()+ " is runing!\n");
}
//System.out.printf("f() is done!\n");  //compilation fail
}
}
  可以见到, 我们将线程要执行的业务代码写入了run() 方法.
  6.1.2 启动线程: 新建1个上述类的对象, 运行start()方法
  例如
  M_thrd_1 s = new M_thrd();
  s. start();
  其中start() 是类M_thrd_1 继承自超类Thread的.
  我们看看JDK API 对start() 方法的定义:
  void  start()
  使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
  可以见到, 调用了start() 方法后, 会开始执行改线程, 也就是在当前线程下开启一条新线程.
  而这条新线程做什么呢, 就是执行run()里面的代码.
  所以我们一般只需要把也业务的代码写入run()里面就ok了. 不要重写start()方法.
  注意, 如果直接执行s.run(); 则会在当前线程下执行run里面的代码, 并没有开启一条新线程. 区别很大的.
  6.2 方式2. 创建1个类, 实现Runnable 接口
  步骤跟上面的方式差别不大
  6.2.1 线程准备: 新建1个类, 实现接口Runnable. 将业务代码写入重写后的run() 方法中.
  例子:
class M_thrd_2 implements Runnable{  //extends
public int id = 0;
public void run(){ //overwrite the method run() of superclass
while (true){
System.out.printf("%s: Thread " + this.id+ " is runing!\n", Thread.currentThread().getName());
}
//System.out.printf("f() is done!\n");  //compilation fail
}
}
  上面的例子 M_thrd_2 就实现了接口Runnable
  并重写了接口Runnable 的抽象方法run();
  注意.  currentThread() 是类Thread的一个静态方法, 作用是获取当前语句所在的线程.

  6.2.2 启动线程: 新建1个上述类的对象s, 在新建1个类Thread的对象t, 把s作为一个参数用于t的构造方法.  并执行t.start()
  貌似比方式一复杂.
  其实非如下:
  new Thread(new M_thrd_2()).start()
  例子:
public class M_thread_expl2{
public static void g(){
M_thrd_2 s = new M_thrd_2();
s.id = 1;
Thread t1 = new Thread(s);
t1.start();
Thread t2 = new Thread(s);
s.id = 2;
t2.start();
}
}
  在g() 函数中,
  首先新建1个类M_thrd_2的 对象s.
  并利用s作为参数 建立了两个线程类Thread的对象t1 和 t2. 并启动这两个线程.
  注:
  1. 这个例子中有3个线程, 其中1个主线程(也就是g() 函数所在的线程). 开启了两个子线程, 该两个子线程都在不断循环输出信息到屏幕上.
  2. Thread(object ob) 是1个属于类Thread的构造方法,  其中的对象ob 必须实现Runnable 接口.
  3. 这个例子执行时 , 第一个t1首先会输出id = 1,    但是当第二个线程t2开始执行后, t1会输出id=2,  因为t1, 和t2都是利用同1个对象建立的.
  也就是说, 这个对象的变动会同时影响两个线程.
  输出如下:
  6.3 两种方式的区别
  1.  方式1是创建继承类Thread的派生类,  方式2是创建实现Runnable接口的类.
  2.  启动方式:  方式1是直接调用业务类的对象的start()方法, 方式2是利用业务类类的对象新建1个类Thread的对象, 并调用该对象的start()方法.
  3.  如果要启动多个线程, 通过方式1需要新建多个不同业务类的对象, 方式2 则可以通过业务1个对象 构建多个Thread类对象, 但是业务对象的变动会同时影响对应的多个线程.
   七. 关于线程的一些常用方法介绍.
  7.1 run()
  我们要吧线把要执行的业务代码写入这个方法. 上面提过了.  注意, 如果我们直接执行1个线程对象的run()方法可以合法的, 但是仍然是在当前线程内执行, 并没有开始一条新线程.
  7.2 Start()
  当1个线程or其派生类执行1个start()方法. 就开启1个新线程, 并调用该类的run()方法.
  注意: 1个线程如果已经调用过一次start(), 再调用start()则或抛异常.
  7.3 stop()
  停止1个1个线程.
  7.4 setName() 和 getName()
  设置和获得对应线程的名字.
  7.5 Thread.currentThread()
  注意这个方法是一个static方法,  而上面的都不是静态方法.
  这个方法返回当前执行语句所属的线程.
  例如1个线程对象A.    它的run() 方法里调用了 Thread.currentThread().getName() 函数.
  假如直接执行A.run()  则返回的是当前线程(而不是A线程) 的名字,  因为对象A并没有启动自己的线程.
  假如执行难过A.start(), 那么该函数就返回A启动的线程的名字了.
   八, 小结
  这篇文章只是简介, 很多重要的方法例如 sleep() , wait() 等都没有介绍, 这些是线程控制的内容. 本吊打算在另一篇博文里介绍.


最新内容请见作者的GitHub页:http://qaseven.github.io/

相关文章
|
11天前
|
存储 JavaScript Java
Java 中的 String Pool 简介
本文介绍了 Java 中 String 对象及其存储机制 String Pool 的基本概念,包括字符串引用、构造方法中的内存分配、字符串文字与对象的区别、手工引用、垃圾清理、性能优化,以及 Java 9 中的压缩字符串特性。文章详细解析了 String 对象的初始化、内存使用及优化方法,帮助开发者更好地理解和使用 Java 中的字符串。
Java 中的 String Pool 简介
|
6天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
36 6
|
19天前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
21天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
15天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
15天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
40 3
|
16天前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
21天前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
74 6
|
19天前
|
监控 Java 数据库连接
Java线程管理:守护线程与用户线程的区分与应用
在Java多线程编程中,线程可以分为守护线程(Daemon Thread)和用户线程(User Thread)。这两种线程在行为和用途上有着明显的区别,了解它们的差异对于编写高效、稳定的并发程序至关重要。
27 2
|
19天前
|
监控 Java 开发者
Java线程管理:守护线程与本地线程的深入剖析
在Java编程语言中,线程是程序执行的最小单元,它们可以并行执行以提高程序的效率和响应性。Java提供了两种特殊的线程类型:守护线程和本地线程。本文将深入探讨这两种线程的区别,并探讨它们在实际开发中的应用。
27 1
下一篇
DataWorks