多线程基础
1、多线程
多线程对应的一个概念叫多任务,就是在同时的时候执行了多个任务。现实中很多这样的例子,看起来是多个任务在同时都在做,但是其实本质上我们的大脑在同一时间依旧是只做了一件事。
多条执行路径,主线程和主线程同时进行。
程序:程序是指令和数据的有序集合,其实就是我们编写的源代码,其本身没有任何运行的含义,是一个静态的概念。
进程:进程是执行程序的一次执行过程,它是一个动态的概念,是系统资源分配的单位。
线程:一个进程中通常可以包含若干个线程,哪怕你啥也不干,进程里也有一个主线程在。当然一个进程中至少有一个线程,不然没有存在的意义。线程是CPU调度和执行的单位。
注意,很多的多线程其实是模拟出来的,只是一个逻辑上的多线程,真正的多线程所指的是有多个cpu,即多核,比如服务器。如果是模拟出来的多线程,即在一个CPU的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同事执行的错觉,这就是并发和并行的概念所在。
总结:
- 线程就是独立的执行路径;
- 在程序运行时,即便没有自己创建线程,后台也会有多个线程,如主线程,gc线程;
- main()称之为主线程,为系统的入口,用于执行整个程序;
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是和操作系统紧密相关的,先后顺序不是人为可以干预的;
- 对同一份资源操作时,会存在资源抢夺的问题,需要假如并发控制;
- 线程会带来额外的开销,比如cpu调度时间,并发控制开销;
- 每个线程在自己的工作内存交互,内存控制不当会造成线程之间互相影响,造成数据不一致。
2、多线程实现方式
2.1、继承Thread类实现(不推荐使用)
package com.lwq.demo01;
/**
* 创建线程方式1:继承Thread类实现线程
* 1、继承Thread类
* 2、重写run方法,run方法中执行的就是线程的执行体
* 3、调用start方法开启线程
* 注意线程开启之后不一定立即执行,因为线程的启动在不同的系统中有不同得时间
* 但总归是要时间的,所以不会立即执行,是由CPU调度执行的,所以可能每次打印输出的
* 执行结果是不一样的。不受人为控制。
*/
public class TestThread01 extends Thread{
//重写run方法体
@Override
public void run() {
for(int i = 0;i<300;i++){
System.out.println("子线程在执行:"+i);
}
}
//主线程。和子线程交替执行,是并发执行的
public static void main(String[] args) {
TestThread01 th1 = new TestThread01();
th1.start();
for (int i = 0;i<300;i++){
System.out.println("主线程在执行:"+i);
}
}
}
最终的打印输出的结果就是交替的,主线程和子线程就是交替执行的。这就是多线程的体现,不行就多循环几个,可能你电脑快,还没启动子线程呢,主线程就执行完了。
2.1.1、多线程实战下载网络文件
多线程下载远程网络文件:
package com.lwq.demo01;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
/**
* 实战多线程下载网络文件
*/
public class TestDownFile extends Thread{
private String url;
private String fileName;
public TestDownFile(String url,String fileName){
this.url = url;
this.fileName = fileName;
}
@Override
public void run() {
WebDownLoadFile webDownLoadFile = new WebDownLoadFile();
webDownLoadFile.downFile(url,fileName);
System.out.println(fileName + "下载完成");
}
public static void main(String[] args) {
TestDownFile th1 = new TestDownFile("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1603099364613&di=8d0eb530879636ad03082d68f4355695&imgtype=0&" +
"src=http%3A%2F%2Fphotocdn.sohu.com%2F20090309%2FImg262681568.jpg","1.jpg");
TestDownFile th2 = new TestDownFile("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1603099491319&di=e5a4d59a9f1b4ee76d3f13c6471eb619&imgtype=0&" +
"src=http%3A%2F%2Fimg.mp.itc.cn%2Fupload%2F20160711%2F4ff5aecbde4e4f9995887852fbd1a4a5_th.jpg","2.jpg");
TestDownFile th3 = new TestDownFile("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1603099529442&di=b6c7ab7ec465624c3709c0379b2685fb&imgtype=0&" +
"src=http%3A%2F%2Fdingyue.ws.126.net%2F2020%2F0501%2Fcd303a0cj00q9mwr4004ic000xc00p9c.jpg","3.jpg");
th1.start();
th2.start();
th3.start();
}
}
class WebDownLoadFile{
public void downFile(String url,String fileName){
try {
FileUtils.copyURLToFile(new URL(url),new File(fileName));
} catch (IOException e) {
e.printStackTrace();
System.out.println("downFile方法的文件下载出错");
}
}
}
最后的结果执行的就不是1,2,3的顺序,这就是多线程,由cpu执行调度的,所以具有随机性。
2.2、实现Runnable接口(推荐使用)
package com.lwq.demo01;
/**
* 多线程实现方式2:实现Runnable接口方式
* 1、实现Runnable接口
* 2、重写run方法,run实现的就是这个子线程的实现内容
* 3、创建Runnable实现类
* 4、创建一个Thread对象,Runnable传进Thread1对象中。用Thread对象启动,静态代理模式(设计模式)
*/
public class TestRunnable02 implements Runnable{
//重写run方法,是子线程的实现内容就在这里
public void run() {
for(int i = 0;i<500;i++){
System.out.println("子线程运行:" + i);
}
}
public static void main(String[] args) {
//创建Runnable接口的实现类
TestRunnable02 tr = new TestRunnable02();
//创建线程对象,通过线程对象启动子线程,这里涉及一个代理模式
Thread th = new Thread(tr);
//启动线程
th.start();
//new Thread(tr).start();
for(int i = 0;i<500;i++){
System.out.println("主线程运行:" + i);
}
}
}
对比
实现方式就不说了,主要说下优缺点,继承Thread方式是java的继承方式,但是java的面向对象特性决定了是单继承,单继承是具有局限性的。
实现Runnable接口方式就不会被单继承局限,灵活方便,而且只需要你有一个Runnable的实现类,就能到处使用这个实现类对象然后去创建代理启动线程。
//创建一份资源
TestRunnable02 tr = new TestRunnable02();
//可以在三个代理甚至多个地方使用
new Thread(tr,a).start();
new Thread(tr,b).start();
new Thread(tr,c).start();
2.2.1、多线程实战模拟抢票
package com.lwq.demo01;
public class TestBuyTickets implements Runnable {
//火车站的总票数
private int ticketNums = 10;
public void run() {
while(true){
if(ticketNums <= 0){
break;
}
try {
//模拟抢票时候的出票延迟,不可能是一下就出来了,延迟200毫秒
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
//--模拟抢票,抢完一张之后票数减一
System.out.println("线程" + Thread.currentThread().getName() + "--》抢到了第" + ticketNums -- + "张票");
}
}
public static void main(String[] args) {
//runnnable实现类
TestBuyTickets testBuyTickets = new TestBuyTickets();
//开三个线程,模拟三人抢票
new Thread(testBuyTickets,"刘文强").start();
new Thread(testBuyTickets,"顾海滨").start();
new Thread(testBuyTickets,"王月").start();
}
}
运行结果如下:
线程刘文强--》抢到了第10张票
线程王月--》抢到了第9张票
线程顾海滨--》抢到了第10张票
线程顾海滨--》抢到了第8张票
线程王月--》抢到了第7张票
线程刘文强--》抢到了第8张票
线程顾海滨--》抢到了第6张票
线程王月--》抢到了第6张票
线程刘文强--》抢到了第6张票
线程顾海滨--》抢到了第5张票
线程刘文强--》抢到了第4张票
线程王月--》抢到了第5张票
线程王月--》抢到了第3张票
线程刘文强--》抢到了第3张票
线程顾海滨--》抢到了第2张票
线程王月--》抢到了第1张票
线程刘文强--》抢到了第0张票
线程顾海滨--》抢到了第0张票
我们看到线程之间是交错执行的,但是出现另一个问题就是在多个线程之间操作一份数据的时候,数据会紊乱,这就是线程的不安全问题。这块我们初识线程不安全,之后在学习到同步的时候会具体处理这个问题。
2.2.2、多线程实战模拟龟兔赛跑
package com.lwq.demo01;
/**多线程模拟龟兔赛跑问题
* 需求:
* 1、百米赛跑,所以设置执行范围是一百次,执行的结果就是距离越来越近,最终到达为0
* 2、龟兔一起跑,需要判断比赛是否结束,一旦有人跑到终点比赛结束
* 3、输出谁是胜利者
* 4、故事中我们都知道乌龟赢了,兔子因为睡觉输了,所以我们模拟可以让兔子睡觉,用sleep
* 5、最终的胜者是乌龟
*/
public class TestRace implements Runnable{
//赛道长度不变,所以直接弄成全局变量,100米
private int distance = 100;
//定义一个胜利者
private String winner;
//线程内容实现,这里就是赛跑的实现
public void run() {
for(int i = 0;i<=distance;i++){
//如果哪个进程达到100米,就设置出胜利者,并且输出胜利者
if(i == 100){
winner = Thread.currentThread().getName();
System.out.println("本次的胜利者是:" + Thread.currentThread().getName());
}else{
System.out.println(Thread.currentThread().getName() + "--》跑了" + i + "米");
}
//兔子模拟每十米就睡10毫秒
if("兔子".equals(Thread.currentThread().getName()) && (i%10 == 0)){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果一旦发现有了胜利者,不管是谁的都停止,这就是我们设置一个胜利者为全局变量的原因,就是让多个进程选手都能识别到
if(winner != null && !"".equals(winner)){
break;
}
}
}
public static void main(String[] args) {
TestRace testRace = new TestRace();
//两个线程模拟两个选手
new Thread(testRace,"兔子").start();
new Thread(testRace,"乌龟").start();
}
}
这个其实没什么难的,就是一个简单的实现,但是其中只有一个学习的点在我看来就是把实际的模型映射到代码逻辑中的一个能力。这个其实很重要其实也很难。
2.3、实现Callable接口(不重要,用得少)
package com.lwq.demo02;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;
/**
* 多线程第三种方式:实现Callable接口
* 1、重写call方法,这里相当于之前的run方法,是线程的实现体
* 2、创建线程池,ExecutorService ex = Executors.newFixedThreadPool(1);
* 3、实现接口实现类
* 4、线程池对象启动实现类
* 5、关闭线程池
* 这个不同的地方是实现的时候有一个返回值,可以自己指定,在call实现的时候要有对应的返回
*/
public class TestCallable03 implements Callable<Integer> {
private String url;
private String fileName;
public Integer call() throws Exception {
WebDownLoadFile webDownLoadFile = new WebDownLoadFile();
webDownLoadFile.downFile(url,fileName);
System.out.println("下载了文件:" + fileName);
return 0;
}
public TestCallable03(String url,String fileName){
this.url = url;
this.fileName = fileName;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建执行服务
ExecutorService ex = Executors.newFixedThreadPool(1);
TestCallable03 th1 = new TestCallable03("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1603099364613&di=8d0eb530879636ad03082d68f4355695&imgtype=0&" +
"src=http%3A%2F%2Fphotocdn.sohu.com%2F20090309%2FImg262681568.jpg","1.jpg");
TestCallable03 th2 = new TestCallable03("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1603099491319&di=e5a4d59a9f1b4ee76d3f13c6471eb619&imgtype=0&" +
"src=http%3A%2F%2Fimg.mp.itc.cn%2Fupload%2F20160711%2F4ff5aecbde4e4f9995887852fbd1a4a5_th.jpg","2.jpg");
TestCallable03 th3 = new TestCallable03("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1603099529442&di=b6c7ab7ec465624c3709c0379b2685fb&imgtype=0&" +
"src=http%3A%2F%2Fdingyue.ws.126.net%2F2020%2F0501%2Fcd303a0cj00q9mwr4004ic000xc00p9c.jpg","3.jpg");
//提交执行
Future<Integer> future1 = ex.submit(th1);
Future<Integer> future2 = ex.submit(th2);
Future<Integer> future3 = ex.submit(th3);
//获取结果
Integer result1 = future1.get();
Integer result2 = future2.get();
Integer result3 = future3.get();
System.out.println(result1);
System.out.println(result2);
System.out.println(result3);
//关闭服务
ex.shutdown();
}
}
class WebDownLoadFile{
public void downFile(String url,String fileName){
try {
FileUtils.copyURLToFile(new URL(url),new File(fileName));
} catch (IOException e) {
e.printStackTrace();
System.out.println("downFile方法的文件下载出错");
}
}
}
下载结果和之前方式下载一样。
2.4、Lamda表达式(看起来帅,不好debug)
小知识,没啥必须的,先不研究。