Java基础(29)——多线程相关知识详解及示例分析一(线程的基本概念与示例分析)
版权声明
- 本文原创作者:清风不渡
- 博客地址:https://blog.csdn.net/WXKKang
一、进程和线程
1、基本概念
(1)进程
每个应用程序的执行单位就是一个进程,它包含了本应用程序运行时所需要的一切资源,例如:内存、CPU时间、IO等
在操作系统中,我们允许多个程序同时被加载到内存中,在操作系统的调度下,可以实现并发执行,每个用户感觉到自己独享CPU,这样的设计大大提高了CPU的利用率
通过任务管理器可看到许多不同的进程,如下图所示,我们发现只有运行的程序才会出现进程,所以说:进程就是正在运行的程序。进程是系统进行资源分配和调用的独立单位,每一个进程都有它自己的内存空间和系统资源
多进程的意义:单进程的计算机只能做一件事情,而我们现在的计算机都可以做多件事情。比如:一边玩游戏(游戏进程),一边听音乐(音乐进程)。也就是说计算机都是支持多进程的,可以在一个时间段内执行多个任务从而提高CPU的使用率
但是,虽然我们可以在电脑上一边听音乐,一边玩游戏,但是两者真的是同时进行的吗?当然不是的,因为单CPU在某一个时间点上只能处理一见事情,而一边听音乐,一边玩游戏则是CPU在做着程序间的高效切换(也就是在很短的时间内重复的来回切换程序)而让使用者觉得不同的程序是同时进行的
(2)线程
一个线程和顺序结构代码的执行逻辑非常相似,它只有一个入口、一个出口以及一个顺序执行的语句序列。从概念上说,一个线程的内部就是一个顺序控制流
在现代操作系统中,每个进程中都可以提供多个线程并发运行。线程才是程序中真正的执行体,进程只是一个应用程序中所有资源的分配单位
例如:我们使用微软的Word编写文档,实际上就是同时运行了多个线程。这些线程中有的负责显示,有的负责接受键盘输入,有的进行自动存盘等等。当这些线程一起运行时,我们感觉到键盘输入和屏幕显示是同时运行的,而不是输入一些字符之后过一段时间才能看到显示,同时在后台还自动执行保存文档,这就是线程给我们带来的方便之处
简单来说,在同一个进程内又可以执行多个任务,而这每一个任务就可看作是一个线程
线程:程序的执行单元,又叫做执行路径;它是程序使用CPU的最基本单位
单线程:程序只有一条执行路径
多线程:程序有多条执行路径
多线程存在的意义:
多线程的存在,不是提高程序的执行速度;而是为了提高应用程序的使用率。程序的执行其实都是在抢CPU的资源,CPU 的执行权。多个进程是在抢CUP资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权。但是,在某个时刻我们不敢保证哪一个线程能够抢到,所以线程的执行有随机性!!!
假如计算机只有一个CPU,那么CPU在某一个时刻只能执行一-条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。那么Java是如何对线程进行调用的呢?
线程有两种调度模型:
分时调度模型所有线程轮流使用 CPU的使用权,平均分配每个线程占用CPU的时间片
抢占式调度模型优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些
Java使用的是抢占式调度模型
(3)进程与线程的关系
1、这是两个不同层次上的概念,两者的粒度不同。进程是操作系统来管理的,线程则是由进程来管理
2、不同进程的内存都是完全独立的;一个进程内的不同线程则是共享本进程的内存和系统资源,不同线程之间有可能发生并生冲突
3、线程本身的数据通常只有寄存器数据,以及一个程序执行时使用的堆栈,所以线程的切换比进程切换的负担要小
4、一个进程中的所有非守护线程都退出后,进程也会终止
5、不同线程之间是并发运行的,它们之间的运行顺序是由操作系统调度的,是随机的。但是,一个线程内部还是按照程序本身的流程顺序执行,和线程调度是没有关系的
2、JVM进程和线程
(1)Java程序和进程、线程
每个应用程序执行时都会创建一个对应的进程,在这个进程中拥有一个主线程(main),它是整个应用程序的执行入口
java.exe实现了Java 虚拟机,因此运行java.exe就会启动一个新的虚拟机,实际上就是在本地操作系统中创建了一个JVM进程。如果同时运行多个java.exe,那么操作系统将会为每个java.exe分配一个独立的JVM进程,这样就会有多个JVM进程,它们互相独立,互不干扰。
每个Java虚拟机运行后,都会自动启动一个主线程( main thread)和垃圾回收线程,主线程首先加载有main方法的类,并自动调用main方法开始执行
(2)简单的多线程实现
既然main函数是一个程序的入口,那么一个Java程序在运行的时候就会在主线程中调用main方法,如果我们在main方法中创建新的线程,那么这样就可以实现一个多线程程序,示例如下:
package qfbd.com;
/*
原创作者:清风不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class Demo {
public static void main(String[] args) throws Exception {
System.out.println("主线程[main]正在运行");
Thread thread = new TestThread();
thread.start();
//TestThread启动后,主线程[main]将和其并行运行
}
}
class TestThread extends Thread{
@Override
public void run() {
// 线程执行体
System.out.println("TestThread正在运行");
}
}
这样,就实现了一个简单的多线程
二、线程编程
1、线程API
(1)Thread和Runnable
Runnable接口用于定义线程的执行体,其中仅仅声明了一个run方法。Thread 实现了Runnable接口,但是它的run方法中没有实现任何东西,需要在Thead子类中实现线程执行体
(2)创建线程的两种方式
1、实现一个继承Thread的子类,在子类中重写run()方法
2、定义一个Runnable的实现类,通过Thread启动新线程和执行这个实现类
2、使用Thread创建线程
Java本身没有实现线程机制,而是通过Thread类封装了底层操作系统的线程,为Java提供了线程的支持
(1)API
Thread类中的主要方法如下:
(2)示例
现在我们来通过一个小的示例学习一下Thread类中的主要方法:
首先,我们创建一个Thread的子类,输出当前线程的名字,代码如下:
package qfbd.com;
/*
原创作者:清风不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class UserThread extends Thread{
public UserThread() {
}
//带参构造函数
public UserThread(String threadName) {
super(threadName);
}
//重写run方法
@Override
public void run() {
//调用printThreadName();方法
printThreadName();
}
public void printThreadName(){
for(int i=0;i<10;i++){
System.out.println(this.getName()+":"+i);
}
}
}
然后我们在Demo类中创建其对象并开启线程,代码如下:
package qfbd.com;
/*
原创作者:清风不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class Demo {
public static void main(String[] args) throws Exception {
//创建对象
UserThread thread1 = new UserThread("张三");
UserThread thread2 = new UserThread("李四");
//普通调用run方法
thread1.run();
thread2.run();
//开启线程
thread1.start();
thread2.start();
}
}
执行结果如下:
在本示例中就可以更好的看到多线程的抢占式执行,开启线程后两个线程将开始竞争CPU时间,没有固定的执行顺序,这就证明多线程起到了效果
(3)线程的使用细节
1、Thread类的run()方法是一个空方法体,需要在子类中重写run()方法实现处理逻辑
2、如果直接调用线程对象的run()方法,那么JVM不会作为一个新线程来运行,这只是一个普通的方法调用
3、使用Thread的start()方法启动一个新线程
3、使用Runnable创建线程
Runnable接口用于定义线程的执行体,其中仅仅声明了一个run方法。该方式可以避免由于Java单继承带来的局限性。该方式适合多个相同程序的代码去处理同一个资源的情况(例如卖票),把线程同程序的代码,数据有效分离,较好的体现了面向对象的设计思想,其使用方法步骤如下:
(1)定义Runable的实现类
定义一个类MyRunnable实现Runnable接口,并且在run();方法中定义线程的执行体,代码如下:
package qfbd.com;
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("MyRunnable:"+i);
}
}
}
(2)启动线程
启动线程时,首先需要在Thread构造方法中使用Runnable实现类的对象作为参数创建对象,调用Thread的start方法启动一个新线程,这样将会自动调用Runnable实现类的run方法
那么,我们为什么需要将Runnable的实现类对象传递给Thread的构造函数呢?因为Runnable接口的实现类对象不是线程类,它无法启动一个新线程,只有Thread才能启动一个新线程,代码如下:
package qfbd.com;
/*
原创作者:清风不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class Demo {
public static void main(String[] args) throws Exception {
//创建MyRunnable对象
MyRunnable myRunnable = new MyRunnable();
//使用Thread类构造函数传入MyRunnable对象并创建线程对象
Thread thread = new Thread(myRunnable);
//开启线程
thread.start();
//下面的for循环属于main线程执行体
for (int i = 0; i < 100; i++) {
System.out.println("main:"+i);
}
}
}
执行结果如下:
4、线程常用方法
(1)常用API
注意:
1、针对不是Thread类的子类中如何获取线程对象名称呢?
public static Thread currentThread():返回当前正在执行的线程对象
Thread.currentThread().getName():返回当前正在执行的线程对象的名字
2、public static void sleep(long millis)方法中输入参数的单位是毫秒
3、public final void join():等待该线程终止——该方法必须在其对应的start方法之后调用
4、public static void yield():暂停当前正在执行的线程对象,并执行其他线程。即调用该方法的线程会暂停,让别的线程先执行。但是,该方法只能在一定程度上礼让;即在一定程度上让多个线程的执行更和谐,但是不能靠它保证每个线程轮次执行
5、public final void stop():让线程停止;该方法已经过时,推荐不再使用
public void interrupt():中断线程。中断线程的方式:把线程的状态终止,并抛出一个InterruptedException
(2)线程优先级
A、线程调度规则
Java提供一个线程调度器来监控程序中启动后进入可运行状态的所有线程。多个线程运行时,若线程的优先级相同,由操作系统按时间片轮转方式和独占方式来分配线程的执行时间
线程调度器按照线程的优先级决定调度哪些线程来执行,具有高优先级的线程会在较低优先级的线程之前得到执行。同时线程的调度是抢先式的,即如果当前线程在执行过程中,一个具有更高优先级的线程进入可执行状态,则该高优先级的线程会被立即调度执行
B、线程优先级
在Java中线程的优先级是用数字来表示的,分为三个级别:
线程被创建后,其缺省的优先级是缺省优先级Thread.NORM_ PRIORITY,具有相同优先级的多个线程,若它们都为高优先级Thread.MAX_ PRIORITY, 则每个线程都是独占式的,也就是说这些线程将被顺序执行;若该优先级不为高优先级,则这些线程将同时执行,也就是说这些线程的执行是无序的
垃圾回收线程的优先级是4
如何获取线程对象的优先级呢?
public final int getPriority():返回线程对象的优先级
如何设置线程对象的优先级呢?
public final void setPriority(int newPriority):更改线程的优先级
注意:
线程默认优先级是5
线程优先级的范围是: 1-10
线程优先级高仅仅表示线程获取的CPU时间片的几率高,但是要在次数比较多,或者多次运行的时候才能看到比较好的效果。也就是说:不能保证优先级高的线程一直完全占有执行权
(3)示例
线程实现类UserThread代码如下:
package qfbd.com;
/*
原创作者:清风不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class UserThread extends Thread{
public UserThread() {
}
//带参构造函数
public UserThread(String threadName) {
super(threadName);
}
//重写run方法
@Override
public void run() {
int i = 0;
while(i<30){
i++;
try {
//线程睡眠
Thread.sleep((int)(Math.random()*1000));
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//得到当前执行的线程对象
System.out.println("获取的线程对象是否为当前执行的:"+(Thread.currentThread() == this));
System.out.println("this.getName-->"+this.getName()+" "+",i="+i);
System.out.println("Thread.currentThread().getName();-->"+Thread.currentThread().getName()+" "+",i="+i);
}
}
}
测试类代码如下:
package qfbd.com;
/*
原创作者:清风不渡
博客地址:https://blog.csdn.net/WXKKang
*/
public class Demo {
public static void main(String[] args) throws Exception {
UserThread thread1 = new UserThread("线程1");
UserThread thread2 = new UserThread("线程2");
//查询thread2线程名
System.out.println("当前线程名:"+thread2.getName());
//设置thread1线程名
thread1.setName("重新设置名称的线程1");
//获得优先级
System.out.println("thread1当前线程优先级为:"+thread1.getPriority());
//设置线程优先级
thread1.setPriority(10);
System.out.println("thread1线程优先级设置后为:"+thread1.getPriority());
//启动线程
thread1.start();
thread2.start();
System.out.println("thread1线程是否活动:"+thread1.isAlive());
System.out.println("Hello World!");
}
}
执行此代码可发现run();方法中每次循环都会使线程睡眠一段时间,执行的一部分结果如下:
好了就到这里吧,下一篇我们将会到一个非常有意思的线程——守护线程,码字不易,希望自己的文章能起到抛砖引玉的效果,大家多多点赞评论呦