【Java多线程-1】线程概述与线程创建和使用
线程概述与线程创建和使用
说到程序,离不开进程和线程这两个概念。那么这两者分别有什么作用和区别呢?
1 线程概述
进程是表示资源分配的基本单位,又是调度运行的基本单位。例如,用户运行自己的程序,系统就创建一个进程,并为它分配资源,包括各种表格、内存空间、磁盘空间、I/O设备等。然后,把该进程放人进程的就绪队列。进程调度程序选中它,为它分配CPU以及其它有关资源,该进程才真正运行。所以,进程是系统中的并发执行的单位。如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe文件的运行)。
线程是进程中执行运算的最小单位,亦即执行处理机调度的基本单位。如果把进程理解为在逻辑上操作系统所完成的任务,那么线程表示完成该任务的许多可能的子任务之一。例如,假设用户启动了一个窗口中的数据库应用程序,操作系统就将对数据库的调用表示为一个进程。假设用户要从数据库中产生一份工资单报表,并传到一个文件中,这是一个子任务;在产生工资单报表的过程中,用户又可以输人数据库查询请求,这又是一个子任务。这样,操作系统则把每一个请求――工资单报表和新输人的数据查询表示为数据库进程中的独立的线程。线程可以在处理器上独立调度执行,这样,在多处理器环境下就允许几个线程各自在单独处理器上进行。操作系统提供线程就是为了方便而有效地实现这种并发性。
一个cpu在任意时刻,只能执行一个线程任务,我们平时一边浏览网页,一边听音乐的场景其实是依赖cpu高速地线程间切换实现地,这也引出了一组容易混淆地概念:并发(Concurrency)和并行(Parallelism)。 它们都可以表示两个或者多个任务一起执行,但是偏重点有些不同。并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的,而并行是真正意义上的“同时执行”,它依赖于多核cpu而实现。
引入线程的好处:
- 易于调度。
- 提高并发性。通过线程可方便有效地实现并发性。进程可创建多个线程来执行同一程序的不同部分。
- 开销少。创建线程比创建进程要快,所需开销很少。
- 利于充分发挥多处理器的功能。通过创建多线程进程(即一个进程可具有两个或更多个线程),每个线程在一个处理器上运行,从而实现应用程序的并发性,使每个处理器都得到充分运行。
进程和线程的关系:
- 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。
- 资源分配给进程,同一进程的所有线程共享该进程的所有资源。 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。
- 处理机分给线程,即真正在处理机上运行的是线程。
- 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
1.1 线程生命周期
线程是一个动态执行的过程,它也有一个从产生到死亡的过程。 下图显示了一个线程完整的生命周期:
- 新建状态:New,使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
- 就绪状态:Runnable,当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
- 运行状态:Running,如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
-
阻塞状态:Blocked,如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
- 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
- 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
- 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
- 死亡状态:Dead,一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
2 多线程实现方式
Java 提供了三种创建线程的方法:
- 通过继承 Thread 类本身。
- 通过实现 Runnable 接口。
- 通过 Callable 和 Future 创建线程。
2.1 继承Thread
创建一个线程的第一种方法是创建一个类并继承 Thread 类,然后创建一个该类的实例。继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。 请看下面实例:
/**
* @author guozhengMu
* @version 1.0
* @date 2019/11/2 14:31
* @description
* @modify
*/
public class ThreadTest {
public static void main(String[] args) {
MyThread thread1 = new MyThread("Thread-A");
MyThread thread2 = new MyThread("Thread-B");
System.out.println();
thread1.start();
thread2.start();
}
}
class MyThread extends Thread {
private String name;
public MyThread(String name) {
this.name = name;
System.out.println("创建线程 " + name);
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("运行线程 " + name + " " + i);
// 线程休眠,增强线程交互执行的效果
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("rn退出线程 " + name);
}
}
运行结果如下:
创建线程 Thread-A
创建线程 Thread-B
运行线程 Thread-B 0
运行线程 Thread-A 0
运行线程 Thread-A 1
运行线程 Thread-B 1
运行线程 Thread-A 2
运行线程 Thread-B 2
退出线程 Thread-B
退出线程 Thread-A
多运行几次,会发现运行线程部分的结果是随机的,这也印证了多线程执行顺序的不确定性。
2.2 实现Runnable
创建一个线程第二种方法是创建一个实现 Runnable 接口的类。
/**
* @author guozhengMu
* @version 1.0
* @date 2019/11/2 14:31
* @description
* @modify
*/
public class ThreadTest {
public static void main(String[] args) {
MyRunnable runnable1 = new MyRunnable("Thread-A");
MyRunnable runnable2 = new MyRunnable("Thread-B");
System.out.println();
runnable1.start();
runnable2.start();
}
}
class MyRunnable implements Runnable {
private Thread thread;
private String name;
public MyRunnable(String name) {
this.name = name;
System.out.println("创建线程 " + name);
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("运行线程 " + name + " " + i);
// 线程休眠,增强线程交互执行的效果
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("rn退出线程 " + name);
}
public void start() {
System.out.println("启动线程 " + name);
if (thread == null) {
thread = new Thread(this, name);
thread.start();
}
}
}
运行结果:
创建线程 Thread-A
创建线程 Thread-B
启动线程 Thread-A
启动线程 Thread-B
运行线程 Thread-B 0
运行线程 Thread-A 0
运行线程 Thread-A 1
运行线程 Thread-B 1
运行线程 Thread-B 2
运行线程 Thread-A 2
退出线程 Thread-A
退出线程 Thread-B
2.3 通过 Callable 和 Future 创建线程
第三种方法是通过 Callable 和 Future 创建线程,前面两种都是无返回值,而这种方法适合获取线程执行结果。基本步骤如下:
- 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
- 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
- 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
- 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
/**
* @author guozhengMu
* @version 1.0
* @date 2019/11/2 18:36
* @description
* @modify
*/
public class CallableTest implements Callable<Integer> {
public static void main(String[] args) {
CallableTest callableTest = new CallableTest();
FutureTask<Integer> ft = new FutureTask<>(callableTest);
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " 的变量i的值" + i);
if (i == 5) {
new Thread(ft, "有返回值的线程").start();
}
}
try {
System.out.println("子线程的返回值:" + ft.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
@Override
public Integer call() {
int i = 0;
for (; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "-" + i);
}
return i;
}
}
输出结果:
main 的变量i的值0
main 的变量i的值1
main 的变量i的值2
main 的变量i的值3
main 的变量i的值4
main 的变量i的值5
main 的变量i的值6
main 的变量i的值7
main 的变量i的值8
main 的变量i的值9
有返回值的线程-0
有返回值的线程-1
有返回值的线程-2
有返回值的线程-3
有返回值的线程-4
有返回值的线程-5
有返回值的线程-6
有返回值的线程-7
有返回值的线程-8
有返回值的线程-9
子线程的返回值:10
3 线程的控制(常见方法)
下表列出了Thread类的一些重要方法:
方法 |
描述 |
---|---|
public void run() |
使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 |
public void start() |
使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 |
public final void setName(String name) |
改变线程名称,使之与参数 name 相同。 |
public final void setPriority(int priority) |
更改线程的优先级。 |
public final void setDaemon(boolean on) |
将该线程标记为守护线程或用户线程。 |
public final void join(long millisec) |
等待该线程终止的时间最长为 millis 毫秒。 |
public void interrupt() |
中断线程。 |
public final boolean isAlive() |
测试线程是否处于活动状态。 |
public static void yield() |
让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。 |
public static void sleep(long millisec) |
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
public static Thread currentThread() |
返回对当前正在执行的线程对象的引用。 |
public static boolean holdsLock(Object x) |
当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。 |
public static void dumpStack() |
将当前线程的堆栈跟踪打印至标准错误流。 |
一些重要方法的使用实例待完善…
- Android6.0锁屏源码分析之界面布局分析
- Android6.0源码分析之menu键弹出popupwindow菜单流程分析
- Android中初步自定义view
- Android中View研究自学之路 Android6.0源码分析之View(一)Android6.0源码分析之View(二)
- Android蓝牙配对弹出框过程分析 Android蓝牙配对弹出框过程分析
- Android6.0之修改或者查看系统属性值
- linux下Android7.0多用户编译问题
- 带你解锁蓝牙skill(0)
- WiFiAp探究实录--功能实现与源码分析
- Android7.1.1系统设置默认值大全
- Androidstudio编译c/c++jni方法
- 带你解锁蓝牙skill(三)
- 带你解锁蓝牙skill(二)
- 带你解锁蓝牙skill(一)
- java教程
- Java快速入门
- Java 开发环境配置
- Java基本语法
- Java 对象和类
- Java 基本数据类型
- Java 变量类型
- Java 修饰符
- Java 运算符
- Java 循环结构
- Java 分支结构
- Java Number类
- Java Character类
- Java String类
- Java StringBuffer和StringBuilder类
- Java 数组
- Java 日期时间
- Java 正则表达式
- Java 方法
- Java 流(Stream)、文件(File)和IO
- Java 异常处理
- Java 继承
- Java 重写(Override)与重载(Overload)
- Java 多态
- Java 抽象类
- Java 封装
- Java 接口
- Java 包(package)
- Java 数据结构
- Java 集合框架
- Java 泛型
- Java 序列化
- Java 网络编程
- Java 发送邮件
- Java 多线程编程
- Java Applet基础
- Java 文档注释
- IntelliJ IDEA 调试 Java 8 Stream,实在太香了!
- 仅需四步,写一个 Spring Boot Starter
- 一个简单案例,带你看懂GC日志!
- C#中ref和out的区别使用
- 图解 MySQL 索引,写得实在太好了!
- 10、图像的几何变换——平移、镜像、缩放、旋转、仿射变换 OpenCV2:图像的几何变换,平移、镜像、缩放、旋转(1)OpenCV2:图像的几何变换,平移、镜像、缩放、旋转(2)数字图像
- String、StringBuiler、StringBuffer,谁性能最高?
- 图像的几何变换——平移、镜像、缩放、旋转、仿射变换 OpenCV2:图像的几何变换,平移、镜像、缩放、旋转(1)OpenCV2:图像的几何变换,平移、镜像、缩放、旋转(2)数字图像处理笔
- 详说C#中的结构struct
- c#结构体总结
- 跨域问题(CORS / Access-Control-Allow-Origin)
- C#中的结构体与类的区别
- c#结构体与类的区别,及使用技巧 C#中的结构体与类的区别
- hikvision SDK使用(转)
- 《Scikit-Learn与TensorFlow机器学习实用指南》 第11章 训练深度神经网络(上)