概述
首先要介绍进程与线程的概念。
所谓进程,即在一个系统中,每一个程序就是一个进程。而这个程序又可以拥有 >1 的线程。比如一个浏览器,你可以打开一个网页进行播放音乐,也可以打开一个网页播放视频。那么简单的来说,每一个网页都是单独的一个线程。他们可以分开进行工作。
所谓左手画方,右手画圆,两能不成,如果我们同时需要进行两个任务,那么就需要掌握线程方面的知识了。
不过值得说明的是,一般情况下线程并不是意味着同时执行,而是CPU运算速度很快,给你同步的错觉~
常见线程调用方式
继承Thread
继承Thread后可以重写它下面的run()方法,然后获得ThreadDemo这个对象类,就可以使用.start()方法启动了。
public class ThreadDemo extends Thread{
@Override
public void run() {
System.out.println(1);
}
public static void main(String[] args) {
ThreadDemo t1 = new ThreadDemo();
t1.start();
}
}
不过这个只是一个简单的demo,并不能体现出线程的用处。接下来我们思考一下,如果我们在线程中循环500次print,而在主线程(main)中也循环500次print,那么结果如何呢?
很显然,因为我们创建的线程独立与我们的主线程,所以他们会出现”同时“
执行命令的状况,而不是先执行谁/后执行谁。
多线程下载器
既然我们学习了前面的知识那么就着手写一个小Demo吧!
我们的目的是: 在一个时间段内同时下载三张图片。
这里就不阐述如何进行下载了,我们调用一个叫做commons-io的工具类,它的下面拥有一个copyURLToFile()方法,它可以下载对应url中的内容,并且保存到本地。
static class Download {
public void download(String url, String name) {
try {
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
}
}
}
我们在线程的run()方法中这样写:
@Override
public void run() {
Download download = new Download();
download.download(url, name);
System.out.println(name + " -> 下载完毕!");
}
在主线程中创建三个对象,即三个线程:
public static void main(String[] args) {
String url = "https://ku-m.cn/logo.jpg";
webDownloadTest w1 = new webDownloadTest(url, "yy1.jpg");
webDownloadTest w2 = new webDownloadTest(url, "yy2.jpg");
webDownloadTest w3 = new webDownloadTest(url, "yy3.jpg");
w1.start();
w2.start();
w3.start();
}
可以看到,它的顺序并不是按照我们初定的:1、2、3进行执行,而是由那个线程先创建速度以及下载速度而决定的。
Runnable接口
前面介绍了继承Thread的方式,可能你就会发现,如果那样编写岂不是一个对象只能生成一个线程?如果我想要下载一个文件三次该怎么办(假设有这个需求)。
所以我们一般并不是使用继承Thread的方式进行开启线程,而是调用runnable接口~
那么我们更改一下下载器吧。
我们只需要这样更改:
String url = "https://ku-m.cn/logo.jpg";
webDownloadTest w1 = new webDownloadTest(url, "yy1.jpg");
new Thread(w1).start();
new Thread(w1).start();
new Thread(w1).start();
因为new Thread()中传输过去的参数就是一个runnable,所以我们可以直接这样调用。
Callable方式
与以上方式的区别是,当使用了callable<>接口之后,run()方法被替换为call()方法,不过好处是,可以拥有一个返回值。
@Override
public Boolean call() throws Exception {
webDownload wd = new webDownload();
wd.download(fileUrl, fileName);
System.out.println(fileName + "下载完毕。");
return true;
}
在执行这个线程的步骤也发生了改变,我们需要先创建一个线程池,然后把线程对象提交过去。
String url = "https://www.baidu.com/img/bd_logo1.png";
callable t1 = new callable(url, "1.jpg");
callable t2 = new callable(url, "2.jpg");
callable t3 = new callable(url, "3.jpg");
ExecutorService ser = Executors.newFixedThreadPool(3);
Future<Boolean> r1 = ser.submit(t1);
Future<Boolean> r2 = ser.submit(t2);
Future<Boolean> r3 = ser.submit(t3);
System.out.println(r1.get());
System.out.println(r2.get());
System.out.println(r3.get());
可以通过.get()方法获得一个返回值。
这种方法暂时没有深入研究,后面学到了会进行补充。
补充
经过后面学习,发现线程池是在一开始直接初始化那些数量的线程,所以后期无需等待就可以直接运行!并且经过测试,使用线程池后的线程速度更快。
long start = System.currentTimeMillis();
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
ExecutorService ex = Executors.newFixedThreadPool(1000);
for (int i = 0; i < 10000; i++) {
ex.execute(()->{
list.add(1);
});
}
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start) + "毫秒");
ex.shutdown();
以上代码结果为最大100毫秒,而正常的new Thread()则是600毫秒。
游戏:龟兔赛跑
下面我们使用多线程的思想来抽象化一个游戏龟兔赛跑,可能你直接就会想到,我们创建两个线程,其中一个为”兔子“,另外一个为”乌龟“。比那个for循环先增加到100,为成功。
按照原定故事,兔子每走10步,我们给它用Thread.sleep()函数将线程暂停10毫秒。
public class Races implements Runnable {
private static String winner;
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (Thread.currentThread().getName().equals("兔子") && i % 10 == 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
boolean flag = gameOver(i);
if (flag) {
break;
}
System.out.println(Thread.currentThread().getName() + "跑到了 => " + i);
}
}
public boolean gameOver(int steps) {
if (winner != null) {
return true;
} else {
if (steps >= 100) {
winner = Thread.currentThread().getName();
System.out.println("winner is " + winner);
return true;
}
}
return false;
}
public static void main(String[] args) {
Races races = new Races();
new Thread(races, "兔子").start();
new Thread(races, "乌龟").start();
}
}
值得说明的是,当我们new THread(reces,)后,是拥有第二个参数的,这个参数名是”name“,可以为这个线程进行命名。然后在线程中可以使用Thread.currentThread().getname()函数进行获得。
lambda表达式
这个表达式的意义是——当借口下只有一个方法的时候,我们可以大幅度的简写无意义的代码,而保留主要逻辑代码。
使用它可以去掉引入runnable接口以及重写run方法的流程。
可以直接通过类名 名字 = (参数)-> {}; 这样的方式进行编写。和JavaScript中的es6语法很是相似。
因此,我们的下载器代码就可以更改为:
String url = "https://www.baidu.com/img/bd_logo1.png";
String[] fileName = {"1.jpg","2.jpg","3.jpg"};
for (int i = 0; i < fileName.length; i++) {
final int temp = i;
Runnable r = ()->{
webDownload wd = new webDownload();
wd.download(url, fileName[temp]);
System.out.println(fileName[temp] + "下载完毕。");
};
new Thread(r).start();
}
线程停止
一般情况下并不建议使用jdk中自带的线程停止方法,像stop()、destroy()方法都是属于已经废弃的。一般使用一个标志位,当主线程更改标志位后,主动让线程停止。
我们在之前的”龟兔赛跑“游戏之中,其实就已经使用了这种思想。
boolean flag = gameOver(i);
if (flag) {
break;
}
线程礼让
含义是,一个线程创建完毕之后一直执行它的内容,通过Thread.yield()函数可以暂时停止这个线程,转而让后面那个线程优先执行。不过这一切都是由CPU进行自动调度,无法认为操作。就是说,有的时候会成功,有的时候不会。
听完课感觉这个有些鸡肋?暂时不清楚有什么意义。
Runnable r1 = () -> {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(1);
Thread.yield();
}
};
Runnable r2 = () -> {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(2);
Thread.yield();
}
};
new Thread(r1).start();
new Thread(r2).start();
优先执行
线程中内置了join()函数用来优先让某一个线程执行,我们可以理解为插队。当两个线程都建立完毕之后,使用join函数的线程将优先执行,而其他线程则需要它结束之后才可以,我们一般不会使用这个,因为容易造成阻塞。
Runnable r1 = () -> {
for (int i = 0; i < 20; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("平民用户");
Thread.yield();
}
};
Runnable r2 = () -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 20; i++) {
System.out.println("至尊vip");
}
};
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
t2.join();
线程优先级
在前面我们说了,线程启动顺序实际上都是按照CPU进行调度的,是不能手动进行设置的。但是Java在CPU调度之前增加了一个调度器,以此为设置线程启动优先级。不过这个嘛..要看脸... 引入CPU很可能会优先运行你设置优先级较低的线程。
使用setPriority()函数进行设置优先级(1-10),getPriority()查看优先级。
如果不设置,默认优先级为5.
priorityDemo p1 = new priorityDemo();
priorityDemo p2 = new priorityDemo();
priorityDemo p3 = new priorityDemo();
priorityDemo p4 = new priorityDemo();
Thread t1 = new Thread(p1, "p1");
Thread t2 = new Thread(p2, "p2");
Thread t3 = new Thread(p3, "p3");
Thread t4 = new Thread(p4, "p4");
t1.setPriority(Thread.MIN_PRIORITY);
t1.start();
t2.setPriority(6);
t2.start();
t3.setPriority(7);
t3.start();
t4.setPriority(Thread.MAX_PRIORITY); //内置的常量,很显然,MAX是10,MIN是1.
t4.start();