多线程Thread
多任务同时进行,宏观上说是多任务同时进行,微观上在同一时刻还是只进行一个任务的片段。
CPU 的每个时钟时间内只能做一样事,但是CPU的运算速度很快,所以在人的感知范围内感受到计算机在同一时刻做多件事。
多线程与方法的调用的区别
- 方法的调用是:一个方法调用另一个方法的时候必须等待被调用的方法执行完毕之后才回到自己的方法中;
- 多线程是两个线程同时进行,并不是严格的同时,表现在人的感受范围内是同时进行。
程序、进程与线程
程序是静态的概念,进程是动态的概念,一个进程匹配一个程序,线程指的是一个进程中开辟多条路径,充分利用进程中的计算资源。
例如一个程序:
现在有一个问题:如果多线程同时运行,那么如果需要访问修改数据,不同的线程对同一个数据的操作不同且冲突,比如一个需要删除另一个却要访问,如何解决?
令我意外的是,同一个进程(程序),如果开辟了多个线程,这些线程是同时运行,但是上面说线程的运行是由调度器安排调度,先后顺序是不能人为干预的。我理解的是,这里说的 “线程的运行是由调度器安排调度,先后顺序是不能认为干预的” 指的是具体到每个CPU时钟内,CPU分配几个时钟给一个线程这个是不能人为干预的。
需要提醒自己的一点是,虽然严格来说并不是绝对的多线程,但是在以后的编程实现过程中思想上不能总是越不过这个坎,否则会影响理解和编程思想的培养,就认为程序是多线程的,不同线程就是同时运行的。
创建多线程的方法
根据面向对象的思想:少用继承(extends),多用实现(implements)。因为java只能单继承,如果一个线程继承了一个类,后来又有需求需要继承其他类,但是由于java的单继承,所以不能继承其他类,这是只能大量重构代码,所以多使用Runnable接口进行实现。
下面看Thread类的API:
每个线程都具有优先级,但是优先级高的并不代表CPU一定会优先执行它,只是CPU分配给它更多计算资源的概率大一些。
线程分两类:
用户线程
守护线程
用户线程是自定义的线程,必须执行的线程,守护线程是保护用户线程。默认情况下,每个线程都是用户线程,但是用户线程可以被标记为守护线程。
如果程序需要停止,Java虚拟机会等待用户线程运行完才会停止,但是不会等待守护线程。
Thread类implements实现了Runnable接口:
实现Runnable接口就必须重写它的run方法
run方法就是一个线程执行的内容。Nice!
创建线程的两种方法:
下面是使用继承Thread类实现线程的程序
package ThreadStudy;
import sun.security.mscapi.CPublicKey;
/**
* 测试使用线程
* 方法一:
* 1. 创建:继承Thread类 + 实现run方法
* 2. 启动:创建子类对象 + start()
*
* @author 发达的范
* @version 1.0
* @date 2021/04/16 20:13
*/
public class Thread01 extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("计数器1:"+i);
}
}
public static void main(String[] args) {
Thread01 thread01 = new Thread01();
thread01.start();
for (int i = 0; i < 20; i++) {
System.out.println("计数器2:"+i);
}
}
}
这个程序的执行顺序应该是:从main方法进入程序,首先创建一个线程,并开启这个线程,线程开启之后就交给CPU决定什么时候开始执行,然后开始运行main方法里面的for循环,此时这个程序有两个线程,一个是主线程,一个是开启的线程,两个线程同时运行。
但是程序的运行结果显示,每次都是主线程结束后才运行开启的线程,循环次数20-500次均是如此,不知为何。
系统未分配资源,这跟很多因素有关系,但是与多线程的同时运行不冲突。
下面是使implements Runnable接口实现多线程的程序:
package ThreadStudy;
/** 使用线程 实现implements Runnable接口
* 1. 创建:实现Runnable接口,重写run方法
* 2. 启动:创建实现类对象,创建Thread类的对象,start
*
* 推荐优先使用接口,避免单继承的局限性,方便共享资源
* @author 发达的范
* @version 1.0
* @date 2021/04/16 20:34
*/
public class Thread02 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("计数器1:"+i);
}
}
public static void main(String[] args) {
//Thread02 thread02 = new Thread02();
//new Thread(thread02).start();
new Thread(new Thread02()).start();//在开发的过程中,如果一个类的对象只使用一次,就是用匿名类
for (int i = 0; i < 20; i++) {
System.out.println("计数器2:"+i);
}
}
}
令人奇怪的是这个程序的执行结果和上一个是一样的。
这种现象的出现只有一种解释:启动线程后,不保证立即运行,由CPU调度,只是CPU并没有让两个线程同时运行而已。 start方法的作用就是开启线程,告诉CPU你可以调用我了,但是CPU什么时候调用由CPU决定。
另外需要注意的是:开启线程的时机很重要,比如在计数器2的for循环之后开启线程,那运行结果必定是先计数器2,再计数器1。
2021/05/11 11:32补充:
使用两种不同的方法创建线程(一种是extends Thread,另一种是implements Runnable)均可实现多线程,由于java的单继承特性,所以推荐使用implements Runnable实现多线程,这里想提醒的是两种创建线程后开启线程的方式有所不同,如下:
- extends Thread:直接实例化一个线程类的对象,使用这个对象调用start方法;
- implements Runnable:先创建一个线程类的对象,然后再把这个对象作为参数传递给实例化thread的操作中,然后再调用start方法,具体如下:
//Thread02 thread02 = new Thread02(); //new Thread(thread02).start(); //或者 new Thread(new Thread02()).start();//在开发的过程中,如果一个类的对象只使用一次,就是用匿名类
下面使用多线程下载三张图片的程序
package ThreadStudy;
import com.sun.xml.internal.ws.policy.privateutil.PolicyUtils;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
/**专门下载的工具类
* 要有面向对象的思想,专门的操作封装成一个类,且需要用的参数是传递进来的,所以才能体现多态的思想
*
* @author 发达的范
* @version 1.0
* @date 2021/04/16 20:50
*/
public class WebDownload {
//下载方法,专门封装了一个工具方法(下载),还可以放其他方法做成一个Utils类
public void download(String url, String path) {
try {
FileUtils.copyURLToFile(new URL(url), new File(path));
} catch (IOException e) {
e.printStackTrace();
System.out.println("Illegal URL & failed download!");
}
}
}
package ThreadStudy;
/**多线程的实现类,这里需要添加属性,站在面向对象的角度想想:
* 既然面向对象,意思是面向很多不同的对象,那么不同的对象的属性很定会有不同,所以这个线程类需要属性来传递不同对象的
* 区别,既然有了属性,那肯定要有构造器,那么这一切就通了。
* @author 发达的范
* @version 1.0
* @date 2021/04/16 21:12
*/
public class ThreadDownload extends Thread {
private String url;
private String path;
public ThreadDownload(String url, String name) {
this.url = url;
this.path = name;
}
@Override
public void run() {
WebDownload wd = new WebDownload();
wd.download(url,path);
System.out.println(path);
}
public static void main(String[] args) {
ThreadDownload td1 = new ThreadDownload("https://tse1-mm.cn.bing.net/th/id/OIP.mi-tcBzKoPlKj6iIKUTtJAAAAA?pid=ImgDet&rs=1","src/ThreadStudy/0qiushuzhen.jpg");
ThreadDownload td2 = new ThreadDownload("https://tse4-mm.cn.bing.net/th/id/OIP.Yo6Q1dTHcTbKl90M8HZ3pQHaHa?pid=ImgDet&rs=1","src/ThreadStudy/0wangzuxian.jpg");
ThreadDownload td3 = new ThreadDownload("https://th.bing.com/th/id/R5bd6bfdb067037f64cb352f68a13cc75?rik=HSa%2fMn3vIsdv%2bw&riu=http%3a%2f%2fwww.wenyu77.com%2fuploads%2fimg1%2f20200709%2fb6a632487dc2d14d1ef8c2990b8bdb00.jpg&ehk=wQH6Nu%2bhQXOCZsSjxVocot3M%2buKdrjF4zLldpd0CwOA%3d&risl=&pid=ImgRaw","src/ThreadStudy/0zhouhuiming.jpg");
td1.start();
td2.start();
td3.start();
}
}
运行结果:
注意:多次的运行结果的下载顺序是不一定相同的,因为是使用三个线程同时下载三张图片,但是三个线程的执行顺序由CPU决定。
一个类必须要有属性和构造器,就这样记忆,必须要有构造器,因为面向对象的编程中,每个类都能够实例化成若干个对象,而这若干个对象都具有这个类的属性和方法,每人都有一套,而且每个对象都是一个单个的个体,对象之间互不干扰,这个时候就必须要有有参构造器,因为有参构造器是实例化对象时,赋予每个对象区别于其他对象的工具。
现在就上面这个程序,ThreadDownload类继承了( extends )Thread类,所以他是一个线程类,必须实现run方法,run方法里面是下载文件的操作,我想有多个线程同时下载多个不同的文件,所以肯定有多个ThreadDownload类的对象,每个对象都具有ThreadDownload类的属性和run方法,每个对象都能够开启一个线程(姑且认为每个对象都是一个线程),区分不同线程的关键之处在于每个ThreadDownload类的对象都具有不同的属性,这些不同的属性时由有参构造器赋予不同的对象的。
这个线程类有属性和构造器,因为需要使用多线程同时下载不同的资源,需要传递这些资源的网址和保存路径所以就需要这两个属性和构造方法。