01Java入门
02数组、方法
03面向对象&Java语法
04-1Java高级(Stream流、异常处理、日志技术)
04-2Java高级(文件处理-IO流)
04-3Java高级(多线程、网络编程)
04-4Java高级(单元测试、反射、注解、动态代理、XML)
05-1常用API
05-2常用API(集合)
JavaSE(B站黑马)学习笔记 04-3Java高级(多线程、网络编程)
什么是线程?
线程(thread)是一个程序内部的一条执行路径。
我们之前启动程序执行后,main方法的执行其实就是一条单独的执行路径。
程序中如果只有一条执行路径,那么这个程序就是单线程的程序。
多线程是什么?
多线程用在哪里,有什么好处
常见的多线程例如12306购票,会使用多线程来处理多个用户的购票请求。包括百度网盘,上传文件的同时还可以下载文件,这时系统就会使用多线程来处理。
Thread类
步骤:
一般称main方法为主线程,其余创建的线程称为子线程。当执行时因为是多线程,所以两条线程是同时跑的,有可能子线程跑得快先执行完子线程,有可能主线程跑得快先执行完主线程,也有可能一下跑主线程一下跑子线程。在时间维度上它们是同时运行的,但在后台输出上肯定会有先输出后输出,所以会看到先打印谁也不一定
1、在案例中为什么不直接调用了run方法,而是调用start启动线程?
2、如果把主线程任务放在子线程之前会怎样。
方式一优缺点:
步骤:
方式二优缺点:
步骤:
1、前2种线程创建方式都存在一个问题:
2、怎么解决这个问题呢?
步骤:
得到任务对象
a. 定义类实现Callable接口,重写call方法,封装要做的事情。
b. 用FutureTask把Callable对象封装成线程任务对象。
把线程任务对象交给Thread处理。
调用Thread的start方法启动线程,执行任务
线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。
方式三的优缺点:
Thread常用API说明
1. 当有很多线程在执行的时候,我们怎么去区分这些线程呢?
注意:
线程安全问题
取钱模型演示
同一个账户对象交给两个线程,线程来取钱时调用账户对象的取钱方法,判断账户金额是否大于等于取钱金额,是则提示取钱成功并让账户金额减去,否则提示余额不足。当两个线程同时运行时,小明线程判断金额大于等于取钱金额会允许取钱,小明线程还没来得及减去金额小红线程就来判断金额也大于等于取钱金额也允许取钱,这时就造成了线程安全问题。
1、取钱案例出现问题的原因?
2、如何才能保证线程安全呢?
线程同步的核心思想
锁对象要求
对会发生线程安全问题的代码使用同步代码块进行加锁(选中后按快捷键ctrl+alt+T选择synchronized),在括号内给一个唯一的同步锁对象,例如甚至可以直接给一个字符串对象"gdit",它是一个常量,只有唯一的一个对象。不管时小明线程还是小红线程,进来后拿到的都是同一给对象,所以它满足锁的唯一性。它没有意义,只是一个代表,说白了就是一个flag标志。两个线程同时进来取钱,加锁后锁会随机让一个线程先进来取钱,例如当小明线程先进来取钱小红线程就会在后面排队,当小明线程取完钱金额减去后就自动解锁,小红线程进来后就会判断为余额不足。
锁对象用任意唯一的对象好不好呢?
例如有另一个账户对象ICBC-222,小黑对这个账户对象取钱,对于小明 小红 小黑来说这个锁都是唯一的(整个系统都是唯一的),这就造成了小明小红要排队,小黑也要参加排队,虽然它们是两个不同的账户对象,但当它们三个同时取钱时用的锁对象都是同一个,小明小红取钱就会干扰到小黑取钱。(相当于一把锁锁住了全部人),这时就有了锁对象的规范要求
锁对象的规范要求
同步方法底层原理
是同步代码块好还是同步方法好一点?
(形象点就是公共厕所,用同步代码块就像把坑锁起来了,让每个人在坑外面排队,同步方法是把整个厕所锁起来,让每个人在厕所外面排队)
Lock锁
什么是线程通信、如何实现?
线程通信常见形式
线程通信实际应用场景
线程通信案例模拟
注意:上述方法应该使用当前同步锁对象进行调用。(只有锁对象知道谁在用我,我要等待谁)
如果方法是实例方法:默认用this作为的锁对象。但是代码要高度面向对象!
如果方法是静态方法:默认用类名.class作为的锁对象。
运行程序后,有五条线程同时运行(小明、小红、亲爹、干爹、岳父)都操作的是同一个账户对象,所以锁对象都是同一个。任何一个线程都有抢到锁的可能性,有一个人抢到锁其它人就要排队,假设当亲爹抢到锁,进来判断发现没钱就进行存钱操作,然后把其他人唤醒,自己等待并解除锁的占用,让其他人来抢锁自己不参与。下一个人抢到锁的可以是除亲爹任何一个人,假设干爹抢到锁进来发现有钱就什么也不操作,任何唤醒别人,自己等待不参与下一次抢锁。假设这时小红抢到锁,发现里面有钱就取钱,然后唤醒其它人,自己等待不参与下一次抢锁。剩下四个人又抢锁,一直循环,三个爸爸进来判断没钱就存钱,存完唤醒其他人自己等待,有钱就什么也不操作,唤醒其他人自己等待。小明小红也是同一个逻辑,进来判断有钱就取,取完唤醒其他人自己等待,没钱就什么也不操作,唤醒其他人自己等待。因为有锁的存在,每次都会有人抢到锁然后其他人排队,根据遇到不同类型的线程执行什么样的操作。(这就是线程通信,生产消费生产消费生产消费,唤醒等待唤醒等待唤醒等待—PV操作)
弹幕解释:
1.这里五个线程任何一个都有可能被锁选中,选中之后就进入存钱或者取钱的流程——jump
2.假如是一个爸爸被选中,那就看有没有钱,没有就存钱,有就休眠,把其他四个唤醒——jump
3.爸爸休眠之后,如果来的又是爸爸,那就继续休眠,唤醒其他人——jump
4.直到锁选中小明或者小红,ta取了钱,然后休眠,唤醒另外四个——jump
5.接下来又继续选,直到选中一个爸爸存钱,然后休眠再选,直到选中一个人取钱——jump
6.如果在else里加一条输出语句,就会发现,输出了这条语句的线程比存取钱的线程要多,因为可能重复遇上爸爸或小红小明——jump
7.总之,它不是有序的一个存一个人取,而是需要看何时遇到不同类型的线程,才进入下一个环节——jump
8.那个唤醒实际上只对上一个休眠的线程有用,因为其他线程此时都在锁外等待,没有休眠——jump
什么是线程池?
不使用线程池的问题
线程池的工作原理
假设线程池里有三个固定线程,每来一个任务就分配一个线程去处理它,当第四个任务来了让线程先把前面分配的任务处理完再来处理第四个任务,这样就可以使线程复用。一般长久存活的线程称为工作线程或核心线程,需要处理人任务叫任务队列。
谁代表线程池?
如何得到线程池对象
举个例子就是,假设在KTV里,每来一个客人分配一个服务员,如果突然来500个客人难道招500个服务员吗,显然不现实。我们可以招3个正式员工,这3个正式员工可以一直招待客人,假如KTV最大支持10个员工工作,如果三个正式员工正在工作,门口等待的位置也坐满了,3个正式工忙不过来那么这时我们就可以再招7个临时工,忙的时候让它们过来工作,不忙直接开掉临时工让3个正式工继续工作,然后我们也可以规定临时工工作多少天后开掉。参数五任务队列就相当于门口的座位,等里面的顾客离开后再进去。线程工厂就相当于人力资源专门帮我们招人的(创建线程的)。最后一个参数是指当10个员工都在忙,门口的座位都坐满了又来了新客人该怎么办,要抛异常还是一脚把他踹出去,要告诉它应该怎么做的操作
线程池常见面试题
临时线程什么时候创建啊?
什么时候会开始拒绝任务?
Executors得到线程池对象的常用方法
注意:Executors的底层其实也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的。
Executors使用可能存在的陷阱
看newFixedThreadPool()和newSingleThreadExecutor()源码就知道它其实还是用ThreadPoolExecutor的方法创建线程池,只不过给了固定线程而已,它并没有设定任务队列数量,就是说可以无线加任务数量没有抛弃策略之类的。如果固定线程一直在忙任务又不断加就会出现内存溢出的现象。
所以还是自己用ThreadPoolExecutor的方法创建线程池吧。
定时器的实现方式
还有很多方法,工具需要去查文档,这里讲常用的。schedule(TimerTask task, long delay, long period),参数一:定时任务,其实现了Runnable接口、参数二:延时几秒执行、参数三:隔几秒循环执行
Timer定时器的特点和存在的问题
由于Timer是单线程,只有一个线程在执行任务,当处理多任务时,如果有某个任务执行时间过长就会影响其它任务设定的定时器时间有初入。如图,执行BBB本应该每2秒执行一次的,由于执行AAA耗时5秒,所以BBB就要等AAA执行完才能执行。
由于Timer是单线程,只有一个线程在执行任务,当处理多任务时,如果有某个任务执行出错,程序就挂掉了,其它定时任务也就不会执行了。
ScheduledExecutorService定时器
ScheduledExecutorService的优点
如图,执行AAA耗时5秒并不影响执行CCC的循环时间,每隔2秒CCC照样运行,BBB出错就它的线程挂掉,AAA和CCC还是可以运行。
并发与并行
补充:线程与进程的区别
并发的理解:
就像闪电侠发糖,100个人,闪电侠跑得特别快,1秒内就给100个人发完糖了,因为闪电侠的速度很快就像给每个人同时发糖一样(其实还是轮流发的),这就是并发。
并行的理解:
状态
线程的状态
Java线程的状态
线程的6种状态互相转换
(下面讲的东西涉及到计算机网络的知识,这里很多都是基础入门知识,深入了解要专门学计算机网络这门课)
什么是网络编程?
网络通信基本模式
实现网络编程关键的三要素
IP地址
IP地址形式:
IP常用命令(cmd命令):
特殊IP地址:
InetAddress 的使用
端口号
端口类型
注意:我们自己开发的程序选择注册端口,且一个设备中不能出现两个程序的端口号一样,否则出错。
通信协议
网络通信协议有两套参考模型
传输层的2个常见协议
TCP协议特点
TCP协议通信场景
TCP三次握手确立连接
UDP协议:
UDP协议通信场景
UDP协议的特点
测试运行先启动服务端(接收端)再启动客户端(发送端),不然客户端发完服务端还没启动就没收到了。服务端启动后会一直等待客户端发送数据。
可以启动多个客户端发送信息,服务端可以一直接收,但需要注意的是IDEA默认不允许程序多开运行,我可以修改配置让程序多开运行。
如图:再次运行客户端会提示先关闭再运行
但允许后运行发现报错(这是一个经典错误:端口已经被使用了,6666端口第一次运行以及使用了,再运行就要换端口了)
UDP的三种通信方式
UDP如何实现广播
使用广播地址:255.255.255.255
具体操作:
UDP如何实现组播
使用组播地址:224.0.0.0 ~ 239.255.255.255
具体操作:
public void joinGroup(InetAddress mcastaddr) 该方法在JDK14开始就被弃用了,企业还没用这么新的版本,所以还是可以用的。
public void joinGroup(SocketAddress mcastaddr, NetworkInterface netIf) 此方法为当前推荐用法,绑定组播IP和网段 参数一:绑定组播IP 参数二:绑定网段,这个网段一般用默认的(比如在局域网内就用局域网的网段)
TCP协议
TCP通信模式演示:
socket翻译过来就是端的意思,就是端对端通信(点对点通信)只要把管道打通,剩下的就是IO流传输数据
注意:在java中只要是使用java.net.Socket类实现通信,底层即是使用了TCP协议
发送完数据应该关闭管道资源,从资源的角度来说应该关闭,但不建议,因为Socket建立的是长连接(TCP连接),它不像UDP发完就不管了,UDP下次再发就重启就好。但Socket管道建立后是可以一直用的,如果发完就关闭就容易出现bug,它消息正在发突然就把管道关了,它关闭管道的消息是要告诉对方的,它是端对端的连接,一方断了另一方也立马断,关闭的信号是一个很小的数据,而传递的数据可能很大,数据传一半关闭的信号先来了就会导致对方收不到的数据极端情况出现。
调用ServerSocket对象的accept()方法等待客户端连接返回的是Socket管道对象,然后调用getInputStream()方法得到字节输入流,而字符缓冲输入流是高效的读取方式,但它的参数只接收字符输入流,我们可以使用字符输入转换流把字节流转换成字符流。字符输入转换流详看04-2Java高级(文件处理-IO流)—字符输入转换流
测试运行:
运行服务端等客户端发送消息,发现报错了,原因是我们在服务端是读取一行数据,而客户端发送数据没有用ps.println()或"\n"换行符,服务端会认为数据没发完一直等,而客户端发完数据后程序结束就断开了,因为是面向连接的端对端连接(TCP),服务端也断了,导致服务端还在循环读一行数据,就会报错。
客户端发送数据加上换行后,服务端会读取一行数据,但又报错了,原因也是一样的,客户端发完数据后程序结束就断开了,因为是面向连接的端对端连接(TCP),服务端也断了,而服务端还在循环读一行数据,所以就会报错
改进为if判断就行,一发一收,发完之后客户端程序结束断开,服务端也断开,但if就判断一次不会循环判断就不会报错了。所以只实现了一发一收,还只能收一行数据
总结:
TCP通信的基本原理?
本案例实现了多发多收,那么是否可以同时接收多个客户端的消息?
之前我们的通信是否可以同时与多个客户端通信,为什么?
如何才可以让服务端可以处理多个客户端的通信需求?
同时处理多个客户端消息
测试运行(将客户端程序设置为允许多开,模拟多个客户端发消息给一个服务端)
目前的通信架构模型
目前的通信架构存在什么问题?
每接收一个socket对象就封装成一个任务对象,然后把任务交给线程池分配线程去排队处理它,这样就不用担心线程过多的情况发生了。
测试运行(将客户端程序设置为允许多开)
前三个客户端会有三个核心线程(正式工)去处理,到第四 五个客户端就会进到任务队列里排队,到第六个客户端因为核心线程还在处理,任务队列也满了,就启动临时线程(临时工)处理,在四五六中随机挑一个进行处理,到八个客户端就会启用抛弃策略(三个核心线程再忙,两个临时线程也在忙,任务队列也满了),如果想要任务队列里的被处理,就要断开其中的核心线程或者临时线程。
使用线程池的优势在哪里?
即时通信是什么含义,要实现怎么样的设计?
例如微信发消息,不可能一个客户端直接发给另一个客户端,那相当于一个客户端就是一个服务器,没这么厉害,只能是客户端发消息给服务器,然后服务器把消息转发到另一个客户端来实现。
即时通信-端口转发
客户端:客户端需要两个线程,一个主线程负责发送消息,一个子线程负责接收服务端转发过来的消息,如果没消息过来子线程就会在那一直等。
服务端:需要一个静态集合来存储客户端socket管道,有管道来连接就加入到管道集合内,使用子线程读取消息,并将消息转发给给全部客户端
测试运行:(将客户端程序设置为允许多开)
客户端发送消息后会先到服务端,服务端将客户端socket端口加入到集合内,启用子线程将消息打印并将循环socket集合将消息转发到每一个客户端,由于客户端没有排除自己的端口,所以也会接收到自己发的消息。这实现的是相当于群发消息,如果要私法消息选择指定socket端口发送即可。
1、之前的客户端都是什么样的
2、BS结构是什么样的,需要开发客户端吗?
注意:服务器必须给浏览器响应HTTP协议格式的数据,否则浏览器不识别。
测试运行,运行服务端即可,B/S无需开发客户端,浏览器输入IP和端口连接到服务端
该内容是根据B站黑马程序员学习时所记,相关资料可在B站查询:Java入门基础视频教程,java零基础自学就选黑马程序员Java入门教程(含Java项目和Java真题)