1. 线程池
1.1 认识线程池
- 线程池: 是一个可以复用线程的技术
- 不使用线程池的问题
- 用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的,而创建新线程的销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能。
- 线程池: ExecutorService接口,ThreadPoolExecutor实现类
- 创建线程池的第一种方式: 创建ThreadPoolExecutor类的对象
- 创建线程池的第二种方式: Executors工具类
1.2 ThreadPoolExecutor
创建线程池
- 使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。
七个参数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
参数 说明 理解 参数一:corePoolSize 指定线程池的核心线程的数量 正式工 3 参数二:maximumPoolSize 指定线程池的最大线程数量 最大员工数:5 临时工:2 参数三:keepAliveTime 指定临时线程的存活时间。 临时工空闲多久被开除 参数四:unit 指定临时线程存活的时间单位(秒、分、时、天等) 参数五:workQueue 指定线程池的任务队列。 客人排队的地方 参数六:threadFactory 指定线程池的线程工厂。 负责招聘员工的(hr) 参数七:handler 指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理) 忙不过来咋办? public class Demo { public static void main(String[] args) { // JDK5 出现的接口 ExecutorService 表示线程池 // 实现类 ThreadPoolExecutor ExecutorService pool = new ThreadPoolExecutor( 3, // 核心线程数量 5,// 最大线程数量 (核心线程3 + 临时线程2) 10, // 临时线程空闲多长时间,销毁 TimeUnit.SECONDS, // 时间单位(纳秒,微秒,秒,分钟,小时,天) new ArrayBlockingQueue<>(5), // 任务队列,需要指定队列的大小, 最多可以有5个任务在这里排队 Executors.defaultThreadFactory(), // 使用JDK线程池中提供的默认的线程工厂 new ThreadPoolExecutor.AbortPolicy() // 所有的线程都在,队列也满了,该怎么处理 ); } }
TimeUnit.时间单位详解
1.3 ExecutorService中的常见方法
方法 说明 void execute(Runnable command) 执行 Runnable 任务 Future submit(Callable task) 执行 Callable 任务,返回未来任务对象,用于获取线程返回的结果 void shutdown() 等全部任务执行完毕后,再关闭线程池! ListshutdownNow() 立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务 线程池处理Runnable任务
public class MyRunnable implements Runnable { @Override public void run() { // 线程开启后执行的任务 System.out.println(Thread.currentThread().getName() + "执行了~"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } }
/* ExecutorService提供常见的方法 提交Runnable任务 演示复用线程特点 线程池使用的注意事项 1.问题: 核心线程都在忙,再有新的任务,直接创建临时线程,还是排队? 回答: 优先放到队列中排队,如果队列满了 再创建临时线程 2.问题: 什么时候触发任务拒绝策略 回答: 核心线程,临时线程都在忙,队列也满了,再提交任务 线程池提供了4种拒绝策略 */ public class Demo { public static void main(String[] args) { ExecutorService pool = new ThreadPoolExecutor( 3, // 核心线程数量 5,// 最大线程数量 (核心线程3 + 临时线程2) 10, // 临时线程空闲多长时间,销毁 TimeUnit.SECONDS, // 时间单位(纳秒,微秒,秒,分钟,小时,天) new ArrayBlockingQueue<>(5), // 任务队列,需要指定队列的大小, 最多可以有5个任务在这里排队 Executors.defaultThreadFactory(), // 使用JDK线程池中提供的默认的线程工厂 new ThreadPoolExecutor.AbortPolicy() // 所有的线程都在,队列也满了,该怎么处理 ); // Runnable实现类对象,封装着线程开启执行的任务 MyRunnable m = new MyRunnable(); pool.execute(m); // 创建了一个核心线程,处理任务 pool.execute(m); // 创建了一个核心线程,处理任务 pool.execute(m); // 创建了一个核心线程,处理任务 pool.execute(m); // 排队 pool.execute(m); // 排队 pool.execute(m); // 排队 pool.execute(m); // 排队 pool.execute(m); // 排队 pool.execute(m); pool.execute(m); pool.execute(m); // 拒绝 pool.execute(m); // 拒绝 // 线程池不会主动关闭,需要调用关闭线程池方法(线程池一般不需要关闭) // pool.shutdown(); // 等线程池中所有的任务都执行完,再关闭 // pool.shutdownNow(); // 立即关闭线程池 } }
###线程池注意事项
- 临时线程什么时候创建?
- 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
- 什么时候会开始拒绝新任务?
- 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。
任务拒绝策略
策略 详解 ThreadPoolExecutor.AbortPolicy 丢弃任务并抛出RejectedExecutionException异常。是默认的策略 ThreadPoolExecutor.DiscardPolicy: 丢弃任务,但是不抛出异常 这是不推荐的做法 ThreadPoolExecutor.DiscardOldestPolicy 抛弃队列中等待最久的任务 然后把当前任务加入队列中 ThreadPoolExecutor.CallerRunsPolicy 由主线程负责调用任务的run()方法从而绕过线程池直接执行 线程池处理Callable任务
// 求1~n的和 public class MyCallable implements Callable<Long> { private int n; public MyCallable(int n) { this.n = n; } @Override public Long call() throws Exception { long sum = 0; for (int i = 1; i <= n; i++) { sum+=i; } return sum; } }
/* 演示使用线程池提交Callable任务 Future<> submit(Callable) */ public class Demo { public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService pool = new ThreadPoolExecutor( 3, 5, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(4) // Executors.defaultThreadFactory(); // new ThreadPoolExecutor.AbortPolicy() ); // 创建MyCallable对象 MyCallable m1 = new MyCallable(10); MyCallable m2 = new MyCallable(20); MyCallable m3 = new MyCallable(30); MyCallable m4 = new MyCallable(40); // 提交4个Callable任务 Future<Long> f1 = pool.submit(m1); Future<Long> f2 = pool.submit(m2); Future<Long> f3 = pool.submit(m3); Future<Long> f4 = pool.submit(m4); System.out.println(f1.get()); // 通过get方法获取线程结束后的返回值 System.out.println(f2.get()); // 通过get方法获取线程结束后的返回值 System.out.println(f3.get()); // 通过get方法获取线程结束后的返回值 System.out.println(f4.get()); // 通过get方法获取线程结束后的返回值 } }
1.4 Executors工具类工具类创建线程池
- 这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象。
- 注意: 大型并发系统环境中使用Executors如果不注意可能会出现系统风险。
方法 说明 public static ExecutorService newFixedThreadPool(int nThreads) 创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。 public static ExecutorService newSingleThreadExecutor() 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。 public static ExecutorService newCachedThreadPool() 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了60s则会被回收掉。 public static ScheduledExecutorService newCachedThreadPool() 创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。
1.5 线程池小结
2. 网络编程
2.1 概述
- 什么是网络编程: 可以让设备中的程序与网络上其他设备中的程序进行数据交互(实现网络通信的)。
- 三要素
- IP: 是网络中设备的唯一标识 。
- 端口:应用程序在设备中的唯一标识。
- 计算机网络中,连接和通信的规则被称为网络通信协议
2.2 IP
- 互联网协议地址 ,也叫IP地址,是网络中设备的唯一标识 。
2.2.1 分类
分类: ipv4和ipv6
ipv4
- 32位(4字节一组) 2进制
- 点分十进制表示法 192.168.1.66
ipv6
- 128位(16字节一组) 2进制
- 冒分十六进制表示法 fe80:0000:0000:0000:589d:eadb:8890:a9bc
2.2.2 域名
- 公网IP,内网IP
- 公网IP:是可以连接互联网的IP地址;内网IP:也叫局域网IP,只能组织机构内部使用。
- 192.168. 开头的就是常见的局域网地址,范围即为192.168.0.0–192.168.255.255,专门为组织机构内部使用。
- 特殊IP
- 127.0.0.1、localhost:代表本机IP,只会寻找当前所在的主机。
- IP常用命令:
- ipconfig:查看本机IP地址。
- ping IP地址:检查网络是否连通。
2.3 InetAddress
- 代表IP
方法 说明 public static InetAddress getLocalHost() 获取本机IP,会以一个InetAddress的对象返回 public static InetAddress getByName(String host) 根据ip地址或者域名,返回一个InetAddress对象 public String getHostName() 获取此IP地址对应的主机名 public String getHostAddress() 获取此IP地址对应的ip地址信息 public boolean isReachable(int timeout) 在指定毫秒内,判断主机与该ip对应的主机是否能联通 public class Demo { public static void main(String[] args) throws IOException { // 1.获取本机的InetAddress对象 InetAddress ip1 = InetAddress.getLocalHost(); // 2.获取对象中的主机名 System.out.println(ip1.getHostName()); // 3.获取对象中的ip地址 System.out.println(ip1.getHostAddress()); // 4.通过ip地址或者域名获取一个InetAddress对象 InetAddress ip2 = InetAddress.getByName("www.baidu.com"); System.out.println(ip2.getHostAddress()); System.out.println(ip2.getHostName()); // 5.监测InetAddress封装的ip地址和本机在指定的毫秒内是否能够连通 System.out.println(ip2.isReachable(30)); } }
2.4 端口
- 端口号:应用程序在设备中的唯一标识。取值范围:0~65535
- 分类
- 周知端口:0~1023,被预先定义的知名应用占用。(如:HTTP占用80,FTP占用21)
- 注册端口:1024~49151,分配给用户进程或某些应用程序
- 动态端口:49152~65535,之所以称为动态端口,是因为它一般不固定分配某种进程,而是动态分配
- 注意:我们自己开发的程序一般选择使用注册端口,且一个设备中不能出现两个程序的端口号一样,否则会出错
2.5 协议
- 概念:计算机网络中,连接和通信的规则被称为网络通信协议
网络模型
UDP协议
- UDP(User Datagram Protocol):用户数据报协议
- 特点:无连接、不可靠通信。通讯效率高
- 不事先建立链接,数据按照数据包发,一包数据包含:
- 自己的IP、程序端口
- 目的地IP、程序端口
- 数据(限制在64KB内)等
- 发送方不管对放是否在线,数据中间丢失也不管,如果接收方收到数据也不返回确认,故是不可靠的
- 适用场景: 语音通话,视频直播…
TCP协议
TCP(Transmission Control Protocol):传输控制协议
特点:面向连接、可靠通信。通信效率相对不高
TCP的最终目的:要保证在不可靠的信道上实现可靠的传输
TCP主要有三个步骤实现可靠传输:三次握手建立连接,传输数据进行确认,四次挥手断开连接
适用场景:网页、文件下载、支付…
三次握手、传输数据确认
四次挥手
3. UDP编程
- DatagramSocket: 用于创建客户端、服务端
构造方法 说明 public DatagramSocket() 创建客户端的Socket对象,系统会随机分配一个端口号 public DatagramSocket(int port) 创建服务端的Socket对象,并指定端口号
方法 说明 public void send(DatagramPacket dp) 发送数据包 public void receive(DatagramPacket p) 使用数据包接收数据
- DatagramPacket:数据包对象
构造方法 说明 public DatagramPacket(byte[] buf, int length, InetAddress address, int port) 创建发出去的数据包对象 public DatagramSocket(byte[] buf, int length) 创建用来接收数据的数据包
方法 说明 public int getLength() 获取数据包,实际接收的字节个数
3.1 一发一收-客户端发消息
- 创建客户端DatagramSocket对象
- 创建数据包对象,封装要发出的数据
- 使用数据包发送
- 释放资源
/* 客户端程序 */ public class Client { public static void main(String[] args) throws IOException { // 1.创建DatagramSocket对象 DatagramSocket ds = new DatagramSocket(); // 客户端使用随机端口号 // 参数一 字节数组 数据 byte[] bytes = "你好吗".getBytes(); // 参数二 要发送数据字节个数 int length = bytes.length; // 参数三 服务端ip地址对象 InetAddress ip = InetAddress.getLocalHost(); // 由于服务端程序也是在本机 所以使用本机的ip // 参数四 服务端程序的端口号 int port = 10000; // 2.创建包裹对象用来装数据(DatagramPacket) DatagramPacket dp = new DatagramPacket(bytes,length,ip,port); // 3.send(包裹)方法发送 ds.send(dp); // 4.释放资源 ds.close(); } }
3.2 一发一收-服务端收消息
- 创建服务端DatagramSocket对象
- 创建数据包对象,封装接收到的数据
- 使用数据包接收
- 处理数据
- 释放资源
/* 服务端程序 先启动服务端程序,在启动客户端程序 */ public class Server { public static void main(String[] args) throws IOException { // 1.创建DatagramSocket对象 DatagramSocket ds = new DatagramSocket(10000); // 参数一: 接收数据的字节数组 byte[] bytes = new byte[1024 *64]; // 64kb // 参数二: 字节数组的大小 int length = bytes.length; // 2.创建数据包对象 准备接收数据 DatagramPacket dp = new DatagramPacket(bytes,length); // 3.接收服务端发送数据 ds.receive(dp); // 数据就进入到字节数组中了 等着接收 // 4.处理数据 // dp.getLength()这一次接收到的实际字节数 String s = new String(bytes,0,dp.getLength()); System.out.println(s); // 5.释放资源 ds.close(); } }
3.3 多发多收
客户端: 将用户键盘录入的数据发送到服务端,直到用户录入886
服务端: 一直接收客户端发送的数据
服务端: 将接收数据的代码放入循环包裹
/* 服务端程序 先启动服务端程序,在启动客户端程序 */ public class Server { public static void main(String[] args) throws IOException { // 1.创建DatagramSocket对象 DatagramSocket ds = new DatagramSocket(10000); // 参数一: 接收数据的字节数组 byte[] bytes = new byte[1024 *64]; // 64kb // 参数二: 字节数组的大小 int length = bytes.length; // 2.创建数据包对象 准备接收数据 DatagramPacket dp = new DatagramPacket(bytes,length); while (true) { // 3.接收服务端发送数据 ds.receive(dp); // 数据就进入到字节数组中了 等着接收 // 4.处理数据 // dp.getLength()这一次接收到的实际字节数 String s = new String(bytes,0,dp.getLength()); InetAddress ip = dp.getAddress();// 获取客户端IP地址 System.out.println(ip.getHostAddress() +": " +s); } // 5.释放资源 // ds.close(); } }
- 客户端
- 将用户键盘录入的数据发送到服务端
- 将录入数据和发送数据的代码放入循环中
- 当用户录入的数据为"886",break
/* 客户端程序 1.使用键盘录入,用户录入的数据,发送到服务端 2.如果用户录入的886 结束客户端的程序 3.使用while循环包裹录入数据,发送数据的代码 */ public class Client { public static void main(String[] args) throws IOException { Scanner sc = new Scanner(System.in); // 1.创建DatagramSocket对象 DatagramSocket ds = new DatagramSocket(); // 客户端使用随机端口号 while (true) { // 参数一 字节数组 数据 String s = sc.nextLine(); if ("886".equals(s)){ break; } byte[] bytes = s.getBytes(); // 参数二 要发送数据字节个数 int length = bytes.length; // 参数三 服务端ip地址对象 // InetAddress ip = InetAddress.getByName("其他人ip地址"); InetAddress ip = InetAddress.getLocalHost(); // 由于服务端程序也是在本机 所以使用本机的ip // 参数四 服务端程序的端口号 int port = 10000; // 2.创建包裹对象用来装数据(DatagramPacket) DatagramPacket dp = new DatagramPacket(bytes,length,ip,port); // 3.send(包裹)方法发送 ds.send(dp); } // 4.释放资源 ds.close(); } }