在讲解线程之前,本人要先来讲解下进程
因为线程是依赖于进程存在的。
那么,什么是进程呢?
进程就是正在运行的程序,
是系统进行资源分配和调用 的 独立单位。
每一个进程都有它自己的内存空间和系统资源。
可能通过上述的讲解,同学们有这种疑惑:
线程是依赖进程存在的,那么,进程完全可以处理线程能做到的事,为什么还要存在线程呢?
答曰:
对于单核计算机来讲,同一时刻,CPU只能运行一个线程/进程。 但是 进程是只能一个进程运行完才运行下一个进程,
而多个线程可以同时竞争CPU,也就是我们常说的“多线程” 那么,就拿游戏来举个例:游戏进程 和 音乐进程 就会冲突。
这时,多线程就体现出了它的作用: 多线程使得 游戏进程和音乐进程 间做着频繁切换,且切换速度很快。
所以,我们感觉游戏和音乐在同时进行,其实并不是同时执行的。
线程
====
基本概念:
首先,本人来介绍下什么是线程:
概述:
在一个进程内部又可以执行多个任务,而这每一个任务我们就可以看成是一个线程。
线程 是 程序使用CPU的基本单位。
所以,进程是拥有资源的基本单位, 线程是CPU调度的基本单位。
在上文中,本人提到了多线程这个名词。
现在,本人来总结下多线程的意义:
多线程的意义:
多线程的作用不是提高执行速度,而是为了提高应用程序的使用率。
那么我们怎么来理解这个问题呢?
解释:
我们程序在运行的使用,都是在抢CPU的时间片(执行权)。
如果是多线程的程序,那么在抢到CPU的执行权的概率应该比较单线程程序抢到的概率要大。
那么也就是说:
CPU在 多线程程序 中 执行的时间 要比 单线程 多。
所以就提高了程序的使用率。
但是即使是多线程程序,那么他们中的哪个线程能抢占到CPU的资源呢?
这个是不确定的,所以多线程具有随机性
现在,本人来介绍下 线程的五种状态:
线程的五种状态:
-
创建态:
用new运算符和Thread类或其子类建立一个线程对象后,该线程对象就处于创建态。处于创建态的线程有自己的内存空间,通过调用start()方法进入就绪态。 -
就绪态:
处于就绪态的线程已经具备了运行条件,但是还没有分配到CPU,因而将进入线程就绪队列,等待系统为其分配CPU。一旦获得CPU,线程就进入运行状态并自动调用自己的run()方法。 -
运行态:
在运行态的线程执行自己的run()方法中的代码,直到调用其它方法而终止,或等待某资源而阻塞,或完成任务而销毁。 -
阻塞态:
处于运行状态的线程在某些情况下,如执行了sleep()方法,或等待I/O设备等资源,让出CPU并暂时终止自己的运行,进入阻塞态。
在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪态,重新到就绪队列中排队等待CPU。当再次获得CPU时,便从原来中止位置开始继续运行。 -
销毁态:
销毁态是线程生命周期中的最后一个阶段。线程销毁的原因有两个:- 是正常运行的线程完成了它的全部工作;
- 是线程被强行终止运行,如通过stop()或destroy()方法来终止一个线程。
下图是这五种状态的转换关系:
那么,对于上图,本人来做以下几点说明:
- 进程创建后并不是立刻进入运行态,而实现进入就绪态,在就绪态的进程队列中,与其他进程一起竞争CPU
- 只有处于就绪态的进程才有资格竞争CPU
- 就绪态的进程,除了CPU以外,不需要等待其他计算机资源
- 处于阻塞态的进程,只能由处于运行态的进程唤醒
- 阻塞态进程被唤醒后进入就绪态,与就绪态队列内的进程一起竞争CPU
- 一般地,有几个CPU,就能同时运行几个进程
- 进程是在运行态时,将自己阻塞起来,使自己进入阻塞态的
那么,现在,本人再来介绍两个名词 —— 并行 、并发:
并行 与 并发:
并行:
指应用能够 同时执行不同的任务。
例:吃饭的时候,边吃饭边打电话
并发:
指应用能够交替执行不同的任务。
其实并发有点类似于多线程的原理(多线程并非是如果你开两个线程同时执行多个任务)
例:吃饭的时候,吃一口饭喝一口水
那么,现在,本人再来扩展一个知识点:
Java程序运行原理:
Java命令会启动java虚拟机,
启动JVM,等于启动了一个应用程序,也就是启动了一个进程。
该进程会自动启动一个 “主线程” ,然后主线程去调用该类的 main 方法。
所以:main方法运行在主线程中。
并且,JVM启动至少启动了垃圾回收线程和主线程,所以是多线程的。
在本人上面的讲解中,本人已经提到了:
线程只有得到 CPU时间片,也就是使用权,才可以执行指令。
那么Java是如何对线程进行调用的呢?
线程有两种调度模型:
- 分时调度模型:
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片- 抢占式调度模型:
优先让优先级高的线程使用 CPU,如果线程的优先级相同,
那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。
而 Java使用的是抢占式调度模型。
现在,本人来介绍下 多线程程序实现的方式:
多线程程序实现的方式:
方式1 —— 继承Thread类:
本人先来展示下这种实现方式:
首先本人给出一个 继承了Thread类 的自定义线程类:
package edu.youzg.about_thread.core;
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 4; i++) {
//获取线程的名称
Thread thread = Thread.currentThread();
String name = thread.getName();
System.out.println(name + ":" + i);
}
}
}
现在,本人来给出一个测试类:
package edu.youzg.about_thread.core;
public class Test {
public static void main(String[] args) {
new MyThread().start();
}
}
现在,本人来展示下运行结果:
那么,可能同学们对上面的展示有如下疑问:
- 启动线程使用的是那个方法?
答曰:start()方法
- run()和start()方法的区别 是什么?
答曰:
start()方法,使该线程开始执行;
然后Java 虚拟机运行run()方法 里的代码。、
- 为什么要重写run方法?
答曰:
run()方法中封装应该是必须被线程执行的代码。
- 同一个线程能不能多次启动?
答曰:
不能。
会报 IllegalThreadStateException。
那么,现在,本人来介绍下第二种实现方式:
方式2 —— 实现Runnable接口:
本人先来展示下这种实现方式:
首先,本人来给出一个 实现了 Runnable 接口的类:
package edu.youzg.about_thread.core;
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 4; i++) {
//获取线程的名称
Thread thread = Thread.currentThread();
String name = thread.getName();
System.out.println(name + "线程的第" + i + "次输出");
}
}
}
现在,本人来给出一个测试类:
package edu.youzg.about_thread.core;
public class Test {
public static void main(String[] args) {
new MyRunnable().run();
}
}
现在,本人来展示下运行结果:
优势:
这种方式扩展性强 实现一个接口 还可以再去继承其他类,
可以避免由于Java单继承带来的局限性
那么,现在,本人来介绍下第三种实现方式:
方式3 —— 实现 Callable 接口:
首先,本人来给出一个 实现了 Callable 接口的类:
package edu.youzg.about_thread.core;
import java.util.concurrent.Callable;
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 666;
}
}
现在,本人来给出一个测试类:
package edu.youzg.about_thread.core;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable myCallable = new MyCallable();
FutureTask<Integer> task = new FutureTask<>(myCallable);
Thread thread = new Thread(task);
thread.start();
//线程执行完之后,可以获取结果
Integer integer = task.get();
System.out.println(integer);
}
}
现在,本人来展示下运行结果:
本人现在来讲解下 第三种实现方式的条件:
条件:
- 调用get()方法,需要抛出 ExecutionException, InterruptedException 异常;
- 执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果。
其实第三种实现方式,是第二种实现方式的变形,具体理由如下:
Callable接口 是 Runnable接口 的 子接口,
实现了该接口,相当于变相地实现了 Runnable接口
现在本人来讲解下这种实现方式的优势:
优势:
- 这种方式扩展性强 实现一个接口 还可以再去继承其他类,可以避免由于Java单继承带来的局限性
- 实现线程的同时,可以有返回值
现在,本人来介绍下 我们处理多线程问题 时所要调用的方法:
常用方法:
Thread类的基本获取和设置方法:
- public final String getName():
获取线程名称- public final void setName(String name):
设置线程名称
展示:
package edu.youzg.about_thread.core;
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();
System.out.println(myThread.getName());
myThread.setName("右转哥的展示线程");
System.out.println(myThread.getName());
}
}
运行结果:
设置和获取线程优先级:
- public final int getPriority():
获取线程的优先级- public final void setPriority(int newPriority):
设置线程的优先级
展示:
package edu.youzg.about_thread.core;
public class Test {
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.setName("1号嘉宾");
myThread2.setName("2号嘉宾");
Thread.currentThread().setName("男主角");
myThread1.setPriority(Thread.MAX_PRIORITY);
myThread2.setPriority(Thread.MIN_PRIORITY);
Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
System.out.println(myThread1.getName()+ "优先级为:" + myThread1.getPriority());
System.out.println(myThread2.getName() + "优先级为:" + myThread2.getPriority());
System.out.println(Thread.currentThread().getName() + "优先级为:" + Thread.currentThread().getPriority());
myThread1.start();
myThread2.start();
System.out.println("男主角的代码1");
System.out.println("男主角的代码2");
System.out.println("男主角的代码3");
}
}
运行结果:
现在,本人来对这两个方法可能会出现的问题做下讲解:
- 有的时候我们给线程设置了指定的优先级,
但是该线程并不是按照优先级高的线程执行,那是为什么呢?
因为线程的优先级的大小仅仅表示这个线程被CPU执行的概率增大了.
但是我们都知道多线程具有随机性,
所以有的时候一两次的运行说明不了问题
- 为什么主线程能够比所有线程都先运行?
主线程就在CPU,
所以,若是我们不对其他线程调用join()方法,
即使主线程优先级最低,也会 大几率 优先执行完主线程
线程休眠:
- public static void sleep(long millis) :
线程休眠
展示:
package edu.youzg.about_thread.core;
import java.util.concurrent.ExecutionException;
public class Test {
public static void main(String[] args) throws InterruptedException, ExecutionException {
long startTime = System.currentTimeMillis();
Thread.sleep(1000);
long endTime = System.currentTimeMillis();
System.out.println("休眠了" + ((endTime - startTime) / 1000) + "秒");
System.out.println("之后的代码");
}
}
运行结果:
加入线程:
- public final void join():
等待该线程执行完毕了以后,其他线程才能再次执行
(注:该方法需 待线程启动之后,才能调用方法)
展示:
package edu.youzg.about_thread.core;
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThread th1 = new MyThread();
MyThread th2 = new MyThread();
MyThread th3 = new MyThread();
//也是说现在,三个线程是并发执行
//join()在线程开启之后,去调用
th1.setName("大傻");
th1.start();
th1.join();
th2.setName("二傻");
th2.start();
th2.join();
th3.setName("三傻");
th3.start();
th3.join();
//串行:多个线程按顺序执行
}
}
运行结果:
礼让线程:
- public static void yield():
暂停当前正在执行的线程对象,并执行其他线程
展示:
package edu.youzg.about_thread.core;
public class Test extends Thread {
public Test() {
super();
}
@Override
public void run() {
for (int i = 0; i <= 4; i++) {
System.out.println("" + this.getName() + "-----" + i);
// 当i为2时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)
if (i == 2) {
this.yield();
}
}
}
public static void main(String[] args) throws InterruptedException {
Test th1 = new Test();
Test th2 = new Test();
th1.setName("孔融");
th2.setName("孔融他弟");
th1.start();
th2.start();
}
}
运行结果:
上面的例子看起来貌似并没有礼让,反而有种更加争取CPU的样子了,这是为什么呢?
答曰:
这个礼让是要暂停当前正在执行的线程,并不会释放锁
这个暂停的时间是相当短的,
如果在这个线程暂停完毕以后,其他的线程还没有抢占到CPU的执行权,
那么这个时候这个线程应该再次和其他线程抢占CPU的执行权
中断线程:
- public final void stop():
停止线程的运行- public void interrupt():
当线程调用wait(),sleep(long time)方法的时候处于阻塞状态,
可以通过这个方法清除阻塞(只能是wait()、sleep()造成的阻塞)
展示:
package edu.youzg.about_thread.core;
public class Test2 extends Thread {
public static void main(String[] args) throws InterruptedException {
MyThread th1 = new MyThread();
th1.setName("测试线程");
th1.start();
Thread.sleep(1000);
//th1.stop(); //强行停止线程
th1.interrupt();//打断线程的一个阻塞状态
}
}
运行结果:
守护线程:
- public final void setDaemon(boolean on):
将该线程标记为 守护线程 或 用户线程 (被守护的线程)
当正在运行的线程都是守护线程时,Java 虚拟机退出
展示:
package edu.youzg.about_thread.core;
public class Test{
public static void main(String[] args) {
//主线程也称之为用户线程
Thread.currentThread().setName("刘备");
MyThread th1 = new MyThread();
th1.setName("张飞");
MyThread th2 = new MyThread();
th2.setName("关羽");
//在线程开启之前,可以设为守护线程
th1.setDaemon(true);
th2.setDaemon(true);
th1.start();
th2.start();
System.out.println("刘备亡了");
}
}
运行结果:
那么,现在,本人就来讲解下 有关这个 用户线程 和 守护线程 的知识点吧:
用户线程 和 守护线程:
首先,本人来讲解下这两个知识点的异同吧:
相同点:
用户线程和守护线程都是线程。
不同点:
Java虚拟机在所有用户线程dead后,程序就会结束。
而不管是否还有守护线程还在运行。
若守护线程还在运行,则会马上结束。
很好理解,守护线程是用来辅助用户线程的,
如公司的保安和员工,各司其职,当员工都离开后,保安自然下班了。
现在,本人来讲解下这两种线程的适用场景:
适用场景:
由两者的区别及dead时间点可知:
守护线程不适合用于输入输出或计算等操作:
因为用户线程执行完毕,程序就dead了。
守护线程适用于辅助用户线程的场景:
如JVM的垃圾回收,内存管理都是守护线程。
还有就是在做数据库应用的时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监听连接个数、超时时间、状态等。
那么,本人再来讲解下 如何创建守护线程:
调用线程对象的方法setDaemon(true),设置线程为守护线程。
条件:
- thread.setDaemon(true)必须在thread.start()之前设置。
- 在Daemon线程中产生的新线程也是Daemon的。
- 不是所有的应用都可以分配给Daemon线程来进行服务
(比如读写操作或者计算逻辑)
因为Daemon Thread还没来得及进行操作,虚拟机可能已经退出了。
还有最后一点本人要强调一下:
可能会有同学学习过Linux,本人在这里要说明一点:
Java中的守护线程 与 Linux中的守护线程不是一个概念。
Linux守护进程是后台服务进程,没有控制台。
在Windows中,你可以运行javaw来达到释放控制台的目的,
在Unix下你加&在命令的最后就行了。
所以守护进程并非一定需要的。
那么,在最后,本人来通过一个例子来总结下我们这篇博文所学的内容:
题:
一部由大导演右转哥拍的电影《右转哥大战Bug星人》正在热映
但是,只有一家影院被授权可以播放这部电影。
于是,这家影院为了防止观众太多,导致卖票环节太过于耗时,就开放了三个售票窗口
请用代码实现下卖票过程。
要求:
创建3个线程,分别处理一个静态资源的分配。
那么,本人现在来展示下代码:
首先,本人来创建个售票线程:
package edu.youzg.about_thread.core;
public class SellTicketsThread extends Thread {
static int piao = 100;
@Override
public void run() {
while (piao != 0){
if(piao > 0){
String name = this.getName();
System.out.println(name+"正在出售"+(piao--)+"张票");
}
}
}
}
现在,本人来给出一个测试类:
SellTicketsThread th1 = new SellTicketsThread();
SellTicketsThread th2 = new SellTicketsThread();
SellTicketsThread th3 = new SellTicketsThread();
th1.setName("窗口1");
th2.setName("窗口2");
th3.setName("窗口3");
th1.start();
th2.start();
th3.start();
现在,本人来展示下运行结果:
可以看到,出现了出售同一张票的错误。
那么,若是我们多次运行,还会出现这样的错误:
这是为什么呢?
本人现在来通过几张图来解释下:
起初,当售票线程都还没有跑起来的时候:
然后,假设th1争取到了CPU:
这时,假设th2争取到了CPU:
由于num--并不是原子操作,而是由对num的读、改、写 三步 组成的,
这时,假设th1完成了num--的操作,而th2还没完成:
这时,假设th3获得了CPU,读取到的num是99,然后将num--的操作也完成了:
这时,th2又争取到了CPU,然后,就会将num=99传回主存:
这就是上述两个问题的出现的原因。
那么,对于上述问题,我们有一个专门 的名称来形容 —— 线程安全问题:
线程安全问题:
首先,本人来讲解下线程安全问题的出现原因:
出现原因:
- 多线程环境
- 有共享数据
- 有多条语句操作共享数据
那么,我们该如何处理这种问题呢?
把多个语句操作共享数据的代码给锁起来,
让任意时刻只能有一个线程执行即可
那么,线程安全问题该如何解决呢?
请观看本人博文 —— 《详解 锁》
(本人 《详解 多线程》 博文 链接:https:////www.cnblogs.com/codderYouzg/p/12418935.html)