多线程(一) 多线程基础
前言
我只是一个新上路的小菜鸟,通过分享自己的所学来巩固知识,文中如有错误希望能够指出改正。
一、线程的相关概念
程序
程序是为了完成特定任务,用某种语言编写的一组指令的集合。简单的说:就是我们写的代码。程序并不等于软件,软件是程序以及开发、使用和维护所需要的所有文档的总称,而程序是软件的一部分。 例如:下图就是一个获取计算机CPU个数的程序
进程
进程是指运行中的程序,比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存空间。当我们使用迅雷,又启动了一个进程,操作系统将为迅雷分配新的内存空间。进程是程序的一次执行过程,或是正在运行的一个程序。进程是一个动态过程:有它自身的产生、存在和消亡的过程。
比如这里打开了三个PotPlayer窗口,则有三个PotPlayer进程存在,它们每一个进程都会占用电脑资源,当关闭进程时会释放资源。
线程
a. 线程是由进程创建的,是进程的一个实体
b. 一个进程可以有多个线程,比如迅雷这个进程同时下载多个应用,那么每一个下载任务都是一个线程
或者在QQ进程中打开了多个聊天窗口,每一个窗口都是一个线程。
其他相关概念
● 单线程:同一个时刻只允许运行一个线程
● 多线程:同一个时刻可以允许运行多个线程,比如:一个qq可以同时打开多个聊天窗口
● 并发:同一个时刻,多个任务交替执行,造成一种"好像是同时运行"的错觉,简单的说单核cpu实现的多任务就是并发
并行:同一个时刻,多个任务同时执行。多核cpu可以实现并行。
二、线程的基本使用
创建线程的方式共有两种,分别是基础Thread类,重写run方法和实现Runnable接口,重写run方法。
Thread(线程)类的结构:
1.通过继承Thread类创建线程
应用案例:开启一个线程,该线程每隔1秒。在控制台输出"喵喵,我是小猫咪"
进行改进:当输出80次时,结束该进程
代码如下:
package com.maozaicc.threaduse;
/**
* 开启一个线程,该线程每隔1秒。在控制台输出"喵喵,我是小猫咪"
* 进行改进:当输出80次时,结束该进程
*/
public class Thread01 {
public static void main(String[] args) {
//创建 Cat 对象,可以当做线程使用
Cat cat = new Cat();
cat.start();
}
}
//当一个类继承了 Thread 类, 该类就可以当做线程使用
class Cat extends Thread {// 继承Thread类来创建线程
//这种写法只执行一次,原因是run并没有循环,执行一次就结束了
// @Override
// public void run() {// 在重写的run()方法中写代码实现自己的逻辑
// System.out.println("喵喵,我是小猫咪");
// }
//要输出80次可以设计一个计数器来控制
int nums = 0;
@Override
public void run() {
while (true) {
System.out.println("喵喵,我是小猫咪,我的编号是" + (++nums));
if (nums == 80) {
break;
}
try {
Thread.sleep(100);//程序运行太快加sleep函数可以更清楚看到输出过程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
以上程序设计到的几个问题:
- 为什么调用方法要写Cat.start()不直接写Cat.run()呢?
首先要搞清楚run()方法它只是一个普通的方法而已,如果直接写Cat.run(),则相当于cat对象去调用了以下run()方法,程序是串行化执行的,如果在cat.start();后面还有语句,则必须等待cat.start();执行完成才会执行,并没有创建线程。 - 源码分析
start()方法中最核心的一条语句是调用start0()方法:
public synchronized void start() { start0(); }//只写了一句最核心的start0,其实源码很长
关于start0()方法的说明:
private native void start0();
● start0() 是本地方法,由 JVM 调用, 底层是 c/c++实现
● 真正实现多线程的效果, 是 start0(), 而不是 run()方法
● start0()方法是Runnable接口中的方法
为了验证以上问题,可设计如下程序:
package com.maozaicc.threaduse;
/**
* 部分输出结果:
* 主线程继续运行...线程名:main
* 喵喵,我是小猫咪,我的编号是1 线程名=Thread-0
* main线程中的i = 0
* 喵喵,我是小猫咪,我的编号是2 线程名=Thread-0
* main线程中的i = 1
* main线程中的i = 2
* 喵喵,我是小猫咪,我的编号是3 线程名=Thread-0
* main线程中的i = 3
* 喵喵,我是小猫咪,我的编号是4 线程名=Thread-0
* 喵喵,我是小猫咪,我的编号是5 线程名=Thread-0
*/
public class Thread02 {
public static void main(String[] args) throws InterruptedException {
//创建 Cat 对象,可以当做线程使用
Cat1 cat = new Cat1();
cat.start();
System.out.println("主线程继续运行...线程名:" + Thread.currentThread().getName());//验证main线程和cat线程是否堵塞
for (int i = 0; i < 60; i++) {
System.out.println(Thread.currentThread().getName() + "线程中的i = " + i);
Thread.sleep(500);//让main线程睡眠
}
}
}
class Cat1 extends Thread {// 继承Thread类来创建线程
int nums = 0;
@Override
public void run() {
while (true) {
System.out.println("喵喵,我是小猫咪,我的编号是" + (++nums) + " 线程名=" + Thread.currentThread().getName());
if (nums == 80) {
break;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
由部分输出结果我们可以看出:
● 当程序开始执行时进程执行main()函数就会产生一个main线程
● 调用cat.start();会创建一个进程,名字是Thread-0,这个进程不会与main线程堵塞
● 根据输出结果的两种输出可以看出两个进程是交替执行的,并且在进程中main()线程结束并不是进程结束的判断依据,只有当进程中的所有线程都结束后进程才会结束
执行流程图:
其中main线程和Thread-0线程都可以产生多个线程。只有当所有线程都结束时进程才会结束。
start()方法调用了start0()方法后,线程并不一定会立马执行,只是将线程变为了可执行状态。具体什么时候执行由CPU统一调度。相当于玩游戏机,只有一个游戏机(cpu),可是有很多人要玩,于是,start是排队!等CPU选中你就是轮到你,你就run(),当CPU的运行的时间片执行完,这个线程就继续排队,等待下一次的run()。
2.通过实现Runnable 接口创建线程
什么时候采用这种方式呢?我们知道Java是单继承的,在某些情况下一个类可能已经继承了某个父类,这时再用继承Thread类方法来创建线程显然是不可能的,我们就要通过实现Runnable接口来创建线程。
应用案例:编写程序,该程序可以每隔1秒,在控制台输出"Hi!",当输出10次后,自动退出,使用实现Runnable接口的方式实现。这里底层使用了代理模式(软件设计模式)
代码如下:
package com.maozaicc.threaduse;
/**
* 使用实现Runnable接口的方式来创建线程
*/
public class Thread03 {
public static void main(String[] args) {
sayHi hi = new sayHi();
/**
* hi.start();这里调用start()方法会报错,因为start()方法是Runnable接口中的方法,这里并没有start()的定义
* Thread类中有这样一个构造器:
* public Thread(Runnable target) {
* init(null, target, "Thread-" + nextThreadNum(), 0);
* }
* 它可以接收一个Runnable接口类型对象,而这里sayHi类实现了Runnable接口,
* 根据接口的多态性:接口类型变量可以指向实现了接口的类的实例,则可以将sayHi类对象传递进去
* 所以此时target对象其实指向的是定义的hi对象。
* 因为thread对象的编译类型是Thread,所以可以调用Thread接口的start()方法,然后调用start0()方法
*/
Thread thread = new Thread(hi);
thread.start();
}
}
class sayHi implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("Hi,i = " + i);
}
}
}
3.多线程执行
应用案例:编写程序,创建两个线程,一个线程每隔1秒输出"Dog汪汪汪…“,输出10次退出,另一个线程输出"Cat喵喵喵…”,输出五次退出。
分析:该案例是要在main方法中创建两个线程,则一共有三个线程(还有main线程),main线程是最先退出的
package com.maozaicc.threaduse;
/**
* 创建两个线程,一个线程每隔1秒输出"Dog汪汪汪...",输出10次退出,另一个线程输出"Cat喵喵喵...",输出五次退出
*/
public class Thread04 {
public static void main(String[] args) {
DogT dogT = new DogT();
CatT catT = new CatT();
Thread thread = new Thread(dogT);
Thread thread1 = new Thread(catT);
thread.start();//启动第一个线程
thread1.start();//启动第二个线程
}
}
class DogT implements Runnable {//线程1
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("Dog汪汪汪..." + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class CatT implements Runnable {//线程2
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Cat喵喵喵..." + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4.线程的理解
猴子就相当于我们的一个进程,它一共创建了两个线程,分别是扫地和摘果,只有当这两个线程都结束时猴子这个进程才会结束。
4.创建线程的两种方式比较
- 从java的设计上来看,两种创建线程的方式本质上没有区别,本质都是调用Runnable接口的start()方法然后调用start0()方法来创建的;
- 实现Runnable接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制,推荐采用这种方式,分析如下代码:
// T3是一个实现Runnable接口的类
T3 t3 = new T3("Hello");
Thread thread01 = new Thread(t3);
Thread thread02 = new Thread(t3);
thread01.start();
thread02.start();
分析:在这个程序中,两个线程共用t3这一个资源,如果采用继承Thread类的方式,创建两个线程需要创建两个对象,而对象又是独立的个体存在,所以无法实现共用资源。
总结
以上就是线程继承的部分内容,本文仅仅简单介绍了线程的概念以及线程的创建方式,后续学习过程中还会继续分享。