JavaSE进阶(16~17)
16-多线程
一、概述&Thread
1、多线程概述
是指从软件或者硬件上实现多个线程并发执行的技术。
具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。
并发和并行
并行
:在同一时刻,有多个指令在多个CPU上同时
执行。
并发
:在同一时刻,有多个指令在单个CPU上交替
执行。
2、多线程概述——进程和线程
进程
:就是操作系统中正在运行的一个应用程序。
独立性
:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位。动态性
:进程的实质是程序(代码)的一次执行过程,进程是动态产生,动态消亡的。并发性
:任何进程都可以同其他进程一起并发执行
线程
:是进程中的单个顺序控制流,是一条执行路径。(就是应用程序中做的事情,比如:360软件中的杀毒,扫描木马,清理垃圾)
单线程
:一个进程如果只有一条执行路径,则称为单线程程序(之前的程序都是)多线程
:一个进程如果有多条执行路径,则称为多线程程序
多线程有三种实现方案:
继承Thread类的方式进行实现
实现Runnable接口的方式进行实现
利用Callable和Future接口方式实现
3、实现多线程方式一:继承Thread类【应用】
-
方法介绍
方法名 说明 void run() 在线程开启后,此方法将被调用执行 void start() 使此线程开始执行,Java虚拟机会调用run方法() -
实现步骤
- 定义一个类MyThread继承Thread类
- 在MyThread类中重写run()方法
- 创建MyThread类的对象
- 启动线程
代码实现:
package com.itheima.threaddemo1;
public class MyThread extends Thread { // 定义一个类MyThread继承Thread类
@Override
public void run() { // 在MyThread类中重写run()方法,run()是用来封装被线程执行的代码
// 代码就是线程在开启之后执行的代码
for (int i = 0; i < 100; i++) {
System.out.println("线程开启了" + i);
}
}
}
package com.itheima.threaddemo1;
public class Demo {
public static void main(String[] args) {
// 创建一个线程对象
MyThread t1 = new MyThread();
// 创建一个线程对象
MyThread t2 = new MyThread();
// t1.run();//表示的仅仅是创建对象,用对象去调用方法,并没有开启线程.
// 开启一条线程
t1.start();
// 开启第二条线程
t2.start();
}
}
为什么要重写run()方法?
因为run()
是用来封装被线程执行的代码
run()方法和start()方法的区别?
run()
:封装线程执行的代码,直接调用,相当于普通方法的调用,并没有开启线程。
start()
:启动线程;然后由JVM(虚拟机)调用此线程的run()方法
4、实现多线程方式二:实现Runnable接口【应用】
-
Thread构造方法
方法名 说明 Thread(Runnable target) 分配一个新的Thread对象 Thread(Runnable target, String name) 分配一个新的Thread对象 -
实现步骤
- 定义一个类MyRunnable实现Runnable接口
- 在MyRunnable类中重写run()方法
- 创建MyRunnable类的对象
- 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
- 启动线程
-
代码演示
代码实现:
package com.itheima.threaddemo2;
public class MyRunnable implements Runnable { // 定义一个类MyRunnable实现Runnable接口
@Override
public void run() {// 在MyRunnable类中重写run()方法
// 线程启动后执行的代码
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "第二种方式实现多线程" + i); //Thread.currentThread()获取当前线程的对象,再调用getName()方法就可以得到线程的名字了;
}
}
}
package com.itheima.threaddemo2;
public class Demo {
public static void main(String[] args) {
// 创建了一个参数的对象
MyRunnable mr = new MyRunnable(); // 创建MyRunnable类的对象
// 创建了一个线程对象,并把参数传递给这个线程.
// 在线程启动之后,执行的就是参数里面的run方法
Thread t1 = new Thread(mr); // 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
// 开启线程
t1.start();
// 创建第二个线程
MyRunnable mr2 = new MyRunnable();
Thread t2 = new Thread(mr2);
t2.start();
}
}
5、多线程的实现方式三——实现Callable接口【应用】
-
方法介绍
方法名 说明 V call() 计算结果,如果无法计算结果,则抛出一个异常 FutureTask(Callable<V> callable)
创建一个 FutureTask,一旦运行就执行给定的 Callable V get() 如有必要,等待计算完成,然后获取其结果 -
实现步骤
- 定义一个类MyCallable实现Callable接口
- 在MyCallable类中重写call()方法
- 创建MyCallable类的对象
- 创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数
- 创建Thread类的对象,把FutureTask对象作为构造方法的参数
- 启动线程
- 再调用get方法,就可以获取线程结束之后的结果。
代码实现:
package com.itheima.threaddemo3;
public class MyCallable implements Callable<String> { // String表示返回一个这个类型的数据;定义一个类MyCallable实现Callable接口
@Override
public String call() throws Exception { // 在MyCallable类中重写call()方法(抽象方法)
for (int i = 0; i < 100; i++) {
System.out.println("跟女孩表白" + i);
}
// 返回值就表示线程运行完毕之后的结果
return "答应";
}
}
package com.itheima.threaddemo3;
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 线程开启之后需要执行里面的call方法
MyCallable mc = new MyCallable(); // 创建MyCallable类的对象
// 可以获取线程执行完毕之后的结果.也可以作为参数传递给Thread对象
FutureTask<String> ft = new FutureTask<>(mc); // 创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数
// 创建线程对象
Thread t1 = new Thread(ft); // 创建Thread类的对象,把FutureTask对象作为构造方法的参数
String s = ft.get(); // 调用get方法,就可以获取线程结束之后的结果。
// 开启线程
t1.start();
// String s = ft.get();
System.out.println(s);
}
}
- 三种实现方式的对比
- 实现Runnable、Callable接口
- 好处: 扩展性强,实现该接口的同时还可以继承其他的类
- 缺点: 编程相对复杂,不能直接使用Thread类中的方法
- 继承Thread类
- 好处: 编程比较简单,可以直接使用Thread类中的方法
- 缺点: 可以扩展性较差,不能再继承其他的类
- 实现Runnable、Callable接口
6、设置和获取线程名称【应用】
-
方法介绍
方法名 说明 void setName(String name)
将此线程的名称更改为等于参数name String getName()
返回此线程的名称 Thread currentThread()
返回对当前正在执行的线程对象的引用
通过构造方法也可以设置线程名称
代码实现:
package com.itheima.threaddemo4;
public class MyThread extends Thread {
public MyThread() {// 空参构造
}
public MyThread(String name) { // 带参构造
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "@@@" + i); // getName()获得现成的名字
}
}
}
package com.itheima.threaddemo4;
public class MyThreadDemo {
public static void main(String[] args) {
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
//void setName(String name):将此线程的名称更改为等于参数 name
my1.setName("高铁");
my2.setName("飞机");
//Thread(String name)
MyThread my1 = new MyThread("高铁");
MyThread my2 = new MyThread("飞机");
my1.start();
my2.start();
//static Thread currentThread() 返回对当前正在执行的线程对象的引用
System.out.println(Thread.currentThread().getName());
}
}
7、Thread方法——线程休眠(应用)
- 相关方法
方法名 说明 static void sleep
(long millis)使当前正在执行的线程停留(暂停执行)指定的毫秒数
代码实现:
package com.itheima.threaddemo6;
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(100); // 睡眠100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}
package com.itheima.threaddemo6;
public class Demo {
public static void main(String[] args) throws InterruptedException {
/*System.out.println("睡觉前");
Thread.sleep(3000);//主线程睡眠3秒
System.out.println("睡醒了");*/
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
t1.start();
t2.start();
}
}
8、Thread方法——线程的优先级【应用】
线程有两种调度模型
分时调度模型
:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
抢占式调度模型
:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些(只是一个几率问题)
Java使用的是抢占式调度模型
- 优先级相关方法
方法名 说明 final int getPriority()
返回此线程的优先级 final void setPriority
(int newPriority)更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10
代码实现:
package com.itheima.threaddemo7;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
}
return "线程执行完毕了";
}
}
package com.itheima.threaddemo7;
public class Demo {
public static void main(String[] args) {
// 优先级: 1 - 10 默认值:5
MyCallable mc = new MyCallable();
FutureTask<String> ft = new FutureTask<>(mc);
Thread t1 = new Thread(ft);
t1.setName("飞机");
t1.setPriority(10); // 设置优先级
System.out.println(t1.getPriority());//5 打印优先级
t1.start();
MyCallable mc2 = new MyCallable();
FutureTask<String> ft2 = new FutureTask<>(mc2);
Thread t2 = new Thread(ft2);
t2.setName("坦克");
t2.setPriority(1); // 设置优先级
System.out.println(t2.getPriority());//5 打印优先级
t2.start();
}
}
9、Thread方法——后台线程/守护线程【应用】
-
相关方法
方法名 说明 void setDaemon
(boolean on)将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出 -
代码实现:
设置两个线程类
package com.itheima.threaddemo8;
public class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + "---" + i);
}
}
}
package com.itheima.threaddemo8;
public class MyThread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "---" + i);
}
}
}
测试类:
package com.itheima.threaddemo8;
public class Demo {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.setName("女神");
t2.setName("备胎");
// 把第二个线程设置为守护线程
// 当普通线程执行完之后,那么守护线程也没有继续运行下去的必要了,但不会立即停止执行.
t2.setDaemon(true);
t1.start();
t2.start();
}
}
二、线程安全问题——线程同步
1、线程的安全问题——买票案例实现【应用】
需求: 某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
思路:
- 定义一个类Ticket实现Runnable接口,里面定义一个成员变量:private int ticketCount = 100;
- 在Ticket类中重写run()方法实现卖票,代码步骤如下
A:判断票数大于0,就卖票,并告知是哪个窗口卖的
B:票数要减1
C:卖光之后,线程停止 - 定义一个测试类TicketDemo,里面有main方法,代码步骤如下
A:创建Ticket类的对象
B:创建三个Thread类的对象,把Ticket对象作为构造方法的参数,并给出对应的窗口名称
C:启动线程
代码实现:
package com.itheima.threaddemo9;
public class Ticket implements Runnable {
// 票的数量
private int ticket = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {// 多个线程必须使用同一把锁,解决重复票和负数票的问题;
if (ticket <= 0) {
// 卖完了
break;
} else {
try {
Thread.sleep(100); // 如果不休息一下,会导致打印时剩余票数的顺序问题
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
}
}
}
}
}
package com.itheima.threaddemo9;
public class Demo {
public static void main(String[] args) {
/*Ticket ticket1 = new Ticket();
Ticket ticket2 = new Ticket();
Ticket ticket3 = new Ticket();
Thread t1 = new Thread(ticket1);
Thread t2 = new Thread(ticket2);
Thread t3 = new Thread(ticket3);*/ //这样写就成了,每个窗口都有100张票了
//创建Ticket类的对象
Ticket ticket = new Ticket();
//创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
Thread t1 = new Thread(ticket);
Thread t2 = new Thread(ticket);
Thread t3 = new Thread(ticket);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
思考
刚才讲解了电影院卖票程序,好像没有什么问题。但是在实际生活中,售票时出票也是需要时间的,所以,在出售一张票的时候,需要一点时间的延迟,接下来我们去修改卖票程序中卖票的动作:
每次出票时间100毫秒,用sleep()方法实现
-
卖票出现了问题
- 相同的票出现了多次
- 出现了负数的票
-
问题产生原因
线程执行的随机性导致的,可能在卖票过程中丢失cpu的执行权,导致出现问题
2、线程的安全问题【解决】——同步代码块【应用】
-
安全问题出现的条件
- 是多线程环境
- 有共享数据
- 有多条语句操作共享数据
-
如何解决多线程安全问题呢?
- 基本思想:让程序没有安全问题的环境
-
怎么实现呢?
- 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
- Java提供了同步代码块的方式来解决
-
同步代码块
锁多条语句操作共享数据,可以使用同步代码块实现
格式:synchronized(任意对象) { 多条语句操作共享数据的代码 }
- 默认情况是打开的,只要有一个线程进去执行代码了,锁就会关闭
- 当线程执行完出来了,锁才会自动打开
同步的好处和弊端
- 好处:解决了多线程的数据安全问题
- 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
代码实现:
package com.itheima.threaddemo9;
public class Ticket implements Runnable {
// 票的数量
private int ticket = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {// 多个线程必须使用同一把锁,解决重复票和负数票的问题;
if (ticket <= 0) {
// 卖完了
break;
} else {
try {
Thread.sleep(100); // 如果不休息一下,会导致打印时剩余票数的顺序问题
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
}
}
}
}
}
package com.itheima.threaddemo9;
public class Demo {
public static void main(String[] args) {
/*Ticket ticket1 = new Ticket();
Ticket ticket2 = new Ticket();
Ticket ticket3 = new Ticket();
Thread t1 = new Thread(ticket1);
Thread t2 = new Thread(ticket2);
Thread t3 = new Thread(ticket3);*/ //这样写就成了,每个窗口都有100张票了
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket);
Thread t2 = new Thread(ticket);
Thread t3 = new Thread(ticket);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
锁对象唯一
package com.itheima.threaddemo010;
public class MyThread extends Thread {
private static int ticketCount = 100; // 静态修饰
private static Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) { // 就是当前的线程对象,obj写成this的话,在测试类中创建了两个线程对象,那么就会出现两个锁,还是会出现重复票和符号票;
if (ticketCount <= 0) {
// 卖完了
break;
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticketCount--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticketCount + "张票");
}
}
}
}
}
package com.itheima.threaddemo010;
public class Demo {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("窗口一");
t2.setName("窗口二");
t1.start();
t2.start();
}
}
3、线程安全问题——同步方法【应用】
-
同步方法的格式
同步方法:就是把synchronized关键字加到方法上修饰符 synchronized 返回值类型 方法名(方法参数) { 方法体; }
同步方法的锁对象是什么呢?
this -
静态同步方法
同步静态方法:就是把synchronized关键字加到静态方法上修饰符 static synchronized 返回值类型 方法名(方法参数) { 方法体; }
同步静态方法的锁对象是什么呢?
类名.class
同步代码块和同步方法的区别:
- 同步代码块可以锁住指定代码,同步方法是锁住方法中所有代码
- 同步代码块可以指定锁对象,同步方法不能指定锁对象
同步方法的锁对象是什么呢?
this
同步静态方法: 就是把synchronized
关键字加到静态方法上
- 格式:
修饰符 static synchronized 返回值类型 方法名(方法参数) { }
同步静态方法的锁对象是什么呢?
-
类名.class
-
代码实现:
定义类
package com.itheima.threaddemo011;
public class MyRunnable implements Runnable {
private static int ticketCount = 100;
@Override
public void run() {
while (true) {
if ("窗口一".equals(Thread.currentThread().getName())) {
// 同步方法
boolean result = synchronizedMthod();
if (result) {
break;
}
}
if ("窗口二".equals(Thread.currentThread().getName())) {
// 同步代码块
synchronized (MyRunnable.class) {// 同步代码块,括号中是当前类的字节码对象;
if (ticketCount == 0) {
break;
} else {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticketCount--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticketCount + "张票");
}
}
}
}
}
private static synchronized boolean synchronizedMthod() { // 同步方法
if (ticketCount == 0) { // 是最后一张
return true;
} else { // 不是最后一张
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticketCount--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticketCount + "张票");
return false;
}
}
}
测试类,创建锁对象
package com.itheima.threaddemo011;
public class Demo {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
t1.setName("窗口一");
t2.setName("窗口二");
t1.start();
t2.start();
}
}
4、线程安全问题——Lock锁【应用】
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
-
ReentrantLock构造方法
方法名 说明 ReentrantLock()
创建一个ReentrantLock的实例 -
加锁解锁方法
方法名 说明 void lock()
获得锁 void unlock()
释放锁
代码实现:
package com.itheima.threaddemo012;
public class Ticket implements Runnable {
// 票的数量
private int ticket = 100;
private Object obj = new Object();
private ReentrantLock lock = new ReentrantLock(); // 创建一个ReentrantLock的实例,来实例化lock
@Override
public void run() {
while (true) {
// synchronized (obj){//多个线程必须使用同一把锁.
try {
lock.lock();// 上锁
if (ticket <= 0) {
// 卖完了
break;
} else {
Thread.sleep(100);
ticket--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();// 开锁
}
// }
}
}
}
测试类,创建线程对象
package com.itheima.threaddemo012;
public class Demo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket);
Thread t2 = new Thread(ticket);
Thread t3 = new Thread(ticket);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
5、死锁【应用】
-
概述
线程死锁
是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行 -
什么情况下会产生死锁
- 资源有限
- 同步嵌套
- 代码实现:
package com.itheima.threaddemo013;
public class Demo {
public static void main(String[] args) {
Object objA = new Object();// 定义两把锁对象
Object objB = new Object();
new Thread(() -> {
while (true) {
synchronized (objA) {
// 线程一
synchronized (objB) {
System.out.println("小康同学正在走路");
}
}
}
}).start(); // 拉姆达表达式方式实现
new Thread(() -> {
while (true) {
synchronized (objB) {
// 线程二
synchronized (objA) {
System.out.println("小薇同学正在走路");
}
}
}
}).start();
}
}
三、生产者消费者
1、生产者消费者——模式概述【应用】
-
概述
生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻。所谓生产者消费者问题,实际上主要是包含了两类线程:
一类是生产者线程用于生产数据
一类是消费者线程用于消费数据为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库
生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为
消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为
消费者步骤:
1,判断桌子上是否有汉堡包。
2,如果没有就等待。
3,如果有就开吃
4,吃完之后,桌子上的汉堡包就没有了
叫醒等待的生产者继续生产
汉堡包的总数量减一
生产者步骤:
1,判断桌子上是否有汉堡包
如果有就等待,如果没有才生产。
2,把汉堡包放在桌子上。
3,叫醒等待的消费者开吃。
- Object类的等待和唤醒方法
方法名 说明 void wait()
导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法 void notify()
唤醒正在等待对象监视器的单个线程 void notifyAll()
唤醒正在等待对象监视器的所有线程
2、生产者消费者——【案例】代码实现【应用】
- 案例需求
-
桌子类(Desk):定义表示包子数量的变量,定义锁对象变量,定义标记桌子上有无包子的变量
-
生产者类(Cooker):实现Runnable接口,重写run()方法,设置线程任务
1.判断是否有包子,决定当前线程是否执行
2.如果有包子,就进入等待状态,如果没有包子,继续执行,生产包子
3.生产包子之后,更新桌子上包子状态,唤醒消费者消费包子 -
消费者类(Foodie):实现Runnable接口,重写run()方法,设置线程任务
1.判断是否有包子,决定当前线程是否执行
2.如果没有包子,就进入等待状态,如果有包子,就消费包子
3.消费包子后,更新桌子上包子状态,唤醒生产者生产包子 -
测试类(Demo):里面有main方法,main方法中的代码步骤如下
创建生产者线程和消费者线程对象
分别开启两个线程
-
代码实现:
消费者:
package com.itheima.threaddemo014;
// 消费者
public class Foodie extends Thread {
@Override
public void run() {
// 1,判断桌子上是否有汉堡包。
// 2,如果没有就等待。
// 3,如果有就开吃
// 4,吃完之后,桌子上的汉堡包就没有了
// 叫醒等待的生产者继续生产
// 汉堡包的总数量减一
// 套路:
// 1. while(true)死循环
// 2. synchronized 锁,锁对象要唯一
// 3. 判断,共享数据是否结束. 结束
// 4. 判断,共享数据是否结束. 没有结束
while (true) {
synchronized (Desk.lock) {
if (Desk.count == 0) {
break;
} else {
if (Desk.flag) {// 桌子上是否有汉堡包,有
System.out.println("吃货在吃汉堡包");
Desk.flag = false;
Desk.lock.notifyAll();// 唤醒,智能唤醒单个线程
Desk.count--;
} else {
// 没有就等待
// 使用什么对象当做锁,那么就必须用这个对象去调用等待和唤醒的方法.
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
生产者:
package com.itheima.threaddemo014;
public class Cooker extends Thread {
// 生产者步骤:
// 1,判断桌子上是否有汉堡包
// 如果有就等待,如果没有才生产。
// 2,把汉堡包放在桌子上。
// 3,叫醒等待的消费者开吃。
@Override
public void run() {
while (true) {
synchronized (Desk.lock) {
if (Desk.count == 0) {
break;
} else {
if (!Desk.flag) {
// 生产
System.out.println("厨师正在生产汉堡包");
Desk.flag = true;
Desk.lock.notifyAll();// 唤醒单个线程
} else {
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
桌子:
package com.itheima.threaddemo014;
public class Desk {
// 定义一个标记
// true 就表示桌子上有汉堡包的,此时允许吃货执行
// false 就表示桌子上没有汉堡包的,此时允许厨师执行
public static boolean flag = false;
// 汉堡包的总数量
public static int count = 10;
// 锁对象
public static final Object lock = new Object();// 加一个final是不想锁的地址值被更改
}
测试类:
package com.itheima.threaddemo014;
public class Demo {
public static void main(String[] args) {
/*消费者步骤:
1,判断桌子上是否有汉堡包。
2,如果没有就等待。
3,如果有就开吃
4,吃完之后,桌子上的汉堡包就没有了
叫醒等待的生产者继续生产
汉堡包的总数量减一*/
/*生产者步骤:
1,判断桌子上是否有汉堡包
如果有就等待,如果没有才生产。
2,把汉堡包放在桌子上。
3,叫醒等待的消费者开吃。*/
Foodie f = new Foodie();//创建吃货对象
Cooker c = new Cooker();//创建初始对象
f.start();
c.start();
}
}
17-网络编程&基础加强
一、网络编程入门
1、网络编程概述【理解】
网络编程:在网络通信协议
下,不同计算机
上运行的程序
,可以进行数据传输
通讯过程:
1,确定接收端在网络中的位置
2,确定接收端中飞秋接收输数据的入口
3,确定网络中传输数据的规则
2、网络编程三要素【理解】
网络编程三要素:
IP地址:设备
在网络中的地址
,是唯一的标识。
端口:应用程序
在设备中唯一的标识
。
协议:数据在网络中传输的规则
,常见的协议有UDP协议和TCP协议。
3、IP地址【理解】
IP:(设备在网络中的唯一标识)全称”互联网协议地址”,也称IP地址。是分配给上网设备的数字标签。常见的IP分类为:ipv4和ipv6
通讯过程: (计算机传递域名给dns服务器,dns服务器将域名解析成IP地址返回给计算机,计算机通过IP地址访问黑马服务器,访问成功后黑马服务器返回数据给电脑)
IPv4:
IPv6: 由于互联网的蓬勃发展,IP地址的需求量愈来愈大,而IPv4的模式下IP的总数是有限的。采用128位地址长度,分成8组。
-
DOS常用命令:
ipconfig
:查看本机IP地址
ping IP地址
:检查网络是否连通 -
特殊IP地址:
127.0.0.1
:是回送地址也称本地回环地址,可以代表本机的IP地址,一般用来测试使用
4、InetAddress【应用】
为了方便我们对IP地址的获取和操作,Java提供了一个类InetAddress
供我们使用
InetAddress
:此类表示Internet协议(IP)地址
-
相关方法
方法名 说明 static InetAddress getByName(String host)
确定主机名称的IP地址。主机名称可以是机器名称,也可以是IP地址(返回InetAddress对象,参数可以是主机名或IP) String getHostName()
获取此IP地址的主机名 String getHostAddress()
返回文本显示中的IP地址字符串 -
代码示例:
package com.itheima.socketdemo1;
//static InetAddress getByName(String host) 确定主机名称的IP地址。主机名称可以是机器名称,也可以是IP地址
//String getHostName() 获取此IP地址的主机名
//String getHostAddress() 返回文本显示中的IP地址字符串
public class InetadressDemo1 {
public static void main(String[] args) throws UnknownHostException {
InetAddress address = InetAddress.getByName("沉迷代码");// 参数可以使主机名或IP,返回InetAddress对象
String hostName = address.getHostName();// 返回主机名
System.out.println("主机名为" + hostName);
String ip = address.getHostAddress(); // 返回IP地址字符串
System.out.println("IP为" + ip);
}
}
5、端口和协议【理解】
-
端口:应用程序在设备中唯一的标识。
-
端口号:用两个字节表示的整数,它的取值范围是0~65535。
其中0~1023之间的端口号用于一些知名的网络服务或者应用。
我们自己使用1024以上的端口号就可以了。
如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败注意:一个端口号只能被一个应用程序使用。
-
协议:计算机网络中,连接和通信的规则被称为网络通信协议
-
UDP协议
- 用户数据报协议(User Datagram Protocol)
- UDP是
面向无连接
通信协议。(发送前不检查是否连接,如果没有连接也会发送,只是会通讯失败)
速度快
,有大小限制
一次最多发送64K,数据不安全
,易丢失数据。
-
TCP协议
- 传输控制协议 (Transmission Control Protocol)
- TCP协议是
面向连接
的通信协议。
速度慢
,没有大小限制,数据安全
。
-
小结
- 网络编程:就是可以让两台计算机进行数据交互。
- 网络编程三要素:
IP:设备在网络中唯一的标识。
端口号:应用程序在设备中唯一的标识。
协议:数据在传输过程中遵守的规则。
二、UDP通信程序
1、UDP发送数据【应用】
-
Java中的UDP通信
- UDP协议是一种不可靠的网络协议,它在通信的两端各建立一个Socket对象,但是这两个Socket只是发送,接收数据的对象,因此对于基于UDP协议的通信双方而言,没有所谓的客户端和服务器的概念
- Java提供了
DatagramSocket
类作为基于UDP协议的Socket
-
构造方法
方法名 说明 DatagramSocket()
创建数据报套接字并将其绑定到本机地址上的任何可用端口 DatagramPacket
(byte[] buf,int len,InetAddress add,int port)创建数据包,发送长度为len的数据包到指定主机的指定端口 -
相关方法
方法名 说明 void send
(DatagramPacket p)发送数据报包 void close()
关闭数据报套接字 void receive
(DatagramPacket p)从此套接字接受数据报包 -
发送数据的步骤
- 创建发送端的Socket对象(DatagramSocket)
- 创建数据,并把数据打包
- 调用DatagramSocket对象的方法发送数据
- 关闭发送端
代码实现:
package com.itheima.socketdemo2;
public class ClientDemo {
public static void main(String[] args) throws IOException {
// 1.找码头
DatagramSocket ds = new DatagramSocket();// 空参,随机找一个端口
// 2.打包礼物
// DatagramPacket(byte[] buf, int length, InetAddress address, int port)
String s = "送给村长老丈人的礼物";
byte[] bytes = s.getBytes();
InetAddress address = InetAddress.getByName("127.0.0.1");//告诉把数据发送到那台电脑上
int port = 10000;
//把数据打包;
DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);// 第一个参数是要传递的内容(字节数组),第二个参数是传几个,第三个参数是要传的IP地址,第四个参数是要传的端口号
// 3.由码头发送包裹
ds.send(dp);
// 4.付钱走羊,释放资源
ds.close();
}
}
2、UDP接收数据【应用】
-
接收数据的步骤
- 创建接收端的Socket对象(DatagramSocket)
- 创建一个数据包,用于接收数据
- 调用DatagramSocket对象的方法接收数据
- 解析数据包,并把数据在控制台显示
- 关闭接收端
-
构造方法
方法名 说明 DatagramPacket
(byte[] buf, int len)创建一个DatagramPacket用于接收长度为len的数据包 -
相关方法
方法名 说明 byte[] getData()
返回数据缓冲区 int getLength()
返回要发送的数据的长度或接收的数据的长度
- 代码示例:
public class ReceiveDemo { public static void main(String[] args) throws IOException { //创建接收端的Socket对象(DatagramSocket) DatagramSocket ds = new DatagramSocket(12345); //创建一个数据包,用于接收数据 byte[] bys = new byte[1024]; DatagramPacket dp = new DatagramPacket(bys, bys.length); //调用DatagramSocket对象的方法接收数据 ds.receive(dp); //解析数据包,并把数据在控制台显示 System.out.println("数据是:" + new String(dp.getData(), 0, dp.getLength())); } } }
3、UDP通信程序练习【应用】
- 案例需求
UDP发送数据:数据来自于键盘录入,直到输入的数据是886,发送数据结束
UDP接收数据:因为接收端不知道发送端什么时候停止发送,故采用死循环接收
改写该代码,实现一个简易的聊天室
- 代码实现:
发送端:
package com.itheima.socketdemo3;
public class ClientDemo {
public static void main(String[] args) throws IOException {
Scanner sc = new Scanner(System.in);// 创建键盘录入对象
DatagramSocket ds = new DatagramSocket();// 码头
while (true) {
String s = sc.nextLine();
if ("886".equals(s)) {// 判断是否退出;
break;
}
byte[] bytes = s.getBytes(); // 把要传递的字符串转成字节数组
InetAddress address = InetAddress.getByName("127.0.0.1");// 把数据发送到那台设备上;
int port = 10000;// 发送给设备的端口号;
DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port); // 打包数据
ds.send(dp);// 发送
}
ds.close();// 释放资源;
}
}
接收端:
package com.itheima.socketdemo3;
public class ServerDemo {
public static void main(String[] args) throws IOException {
DatagramSocket ds = new DatagramSocket(10000);// 码头
while (true) {
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);// 箱子打包数据
ds.receive(dp); // 开始接收数据,并把数据放到箱子当中
//解析数据包,并把数据在控制台显示
System.out.println("数据是:" + new String(dp.getData(), 0, dp.getLength()));
}
// ds.close(); //接收端不停地接收
}
}
4、UDP的三种通信方式【理解】
单播:一对一
组播:一对多
广播:一对所有
5、UDP通信组播代码实现【理解】
- 实现步骤
- 发送端
- 创建发送端的Socket对象(DatagramSocket)
- 创建数据,并把数据打包(DatagramPacket)
- 调用DatagramSocket对象的方法发送数据(在单播中,这里是发给指定IP的电脑但是在组播当中,这里是发给组播地址)
- 释放资源
- 接收端
- 创建接收端Socket对象(MulticastSocket)
- 创建一个箱子,用于接收数据
- 把当前计算机绑定一个组播地址
- 将数据接收到箱子中
- 解析数据包,并打印数据
- 释放资源
- 发送端
-
组播的发送端跟单播是类似的
组播地址:224.0.0.0 ~ 239.255.255.255
其中224.0.0.0 ~ 224.0.0.255 为预留的组播地址 -
组播的接收端跟单播是类似的
-
代码实现:发送端
package com.itheima.socketdemo4;
import java.io.IOException;
import java.net.*;
public class ClinetDemo {
public static void main(String[] args) throws IOException {
DatagramSocket ds = new DatagramSocket();
String s = "hello 组播";
byte[] bytes = s.getBytes();
InetAddress address = InetAddress.getByName("224.0.1.0");
int port = 10000;
DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
ds.send(dp);
ds.close();
}
}
接收端:
package com.itheima.socketdemo4;
public class ServerDemo {
public static void main(String[] args) throws IOException {
// 1. 创建接收端Socket对象(MulticastSocket)
MulticastSocket ms = new MulticastSocket(10000);
// 2. 创建一个箱子,用于接收数据
DatagramPacket dp = new DatagramPacket(new byte[1024], 1024);
// 3. 把当前计算机绑定一个组播地址,表示添加到这一组中.
ms.joinGroup(InetAddress.getByName("224.0.1.0"));
// 4. 将数据接收到箱子中
ms.receive(dp);
// 5. 解析数据包,并打印数据
byte[] data = dp.getData();
int length = dp.getLength();
System.out.println(new String(data, 0, length));
// 6. 释放资源
ms.close();
}
}
5、UDP通信广播代码实现【理解】
- 实现步骤
- 发送端
- 创建发送端Socket对象(DatagramSocket)
- 创建存储数据的箱子,将广播地址封装进去
- 发送数据
- 释放资源
- 接收端
- 创建接收端的Socket对象(DatagramSocket)
- 创建一个数据包,用于接收数据
- 调用DatagramSocket对象的方法接收数据
- 解析数据包,并把数据在控制台显示
- 关闭接收端
- 发送端
- 代码实现-发送端:
package com.itheima.socketdemo5;
public class ClientDemo {
public static void main(String[] args) throws IOException {
DatagramSocket ds = new DatagramSocket(); // 码头
String s = "广播 hello";
byte[] bytes = s.getBytes();
InetAddress address = InetAddress.getByName("255.255.255.255"); // 发送的地址
int port = 10000;
DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port); // 发送的包
ds.send(dp); // 发送
ds.close();// 释放资源
}
}
- 代码实现-发送端:
package com.itheima.socketdemo5;
public class ServerDemo {
public static void main(String[] args) throws IOException {
DatagramSocket ds = new DatagramSocket(10000); // 码头
DatagramPacket dp = new DatagramPacket(new byte[1024], 1024);// 包,参数1:使用该数组接收,参数二:长度,使用数组的多长去接收;
ds.receive(dp);
byte[] data = dp.getData();
int length = dp.getLength();
System.out.println(new String(data, 0, length));
ds.close();
}
}
三、TCP通信程序
- Java中的TCP通信
- Java对基于TCP协议的的网络提供了良好的封装,使用Socket对象来代表两端的通信端口,通信之前要保证连接已经建立,并通过Socket产生IO流来进行网络通信。
- Java为客户端提供了
Socket
类,为服务器端提供了ServerSocket
类
1、TCP发送数据【应用】
-
构造方法
方法名 说明 Socket(InetAddress address,int port)
创建流套接字
并将其连接到指定IP指定端口号Socket(String host, int port)
创建流套接字
并将其连接到指定主机上的指定端口号 -
相关方法
方法名 说明 InputStream getInputStream()
返回此套接字的输入流 OutputStream getOutputStream()
获取输出流,写数据,返回此套接字的输出流 -
释放资源
void close()
代码示例:
package com.itheima.socketdemo6;
public class ClientDemo {
public static void main(String[] args) throws IOException {
//创建客户端的Socket对象(Socket)
//Socket(String host, int port) 创建流套接字并将其连接到指定主机上的指定端口号
Socket socket = new Socket("127.0.0.1", 10000);// 参数一:服务器IP,参数二:服务器端口
// 2.获取一个IO输出流,写数据
//OutputStream getOutputStream() 返回此套接字的输出流
OutputStream os = socket.getOutputStream(); // OutputStream是字节流
os.write("hello".getBytes());// 不能直接写字符串,要写字节数据
// 3.释放资源
os.close();
socket.close();
}
}
2、TCP接收数据【应用】
-
构造方法
方法名 说明 ServletSocket
(int port)创建绑定到指定端口的服务器套接字 -
相关方法
方法名 说明 Socket accept()
监听要连接到此的套接字并接受它,返回一个Socket对象 -
释放资源
void close()
-
注意事项
- accept方法是阻塞的,作用就是等待客户端连接
- 客户端创建对象并连接服务器,此时是通过三次握手协议,保证跟服务器之间的连接
- 针对客户端来讲,是往外写的,所以是输出流
针对服务器来讲,是往里读的,所以是输入流 - read方法也是阻塞的
- 客户端在关流的时候,还多了一个往服务器写结束标记的动作
- 最后一步断开连接,通过四次挥手协议保证连接终止
-
三次握手和四次挥手
-
三次握手
-
四次挥手
-
代码示例:
package com.itheima.socketdemo6;
public class ServerDemo {
public static void main(String[] args) throws IOException {
//创建服务器端的Socket对象(ServerSocket)
//ServerSocket(int port) 创建绑定到指定端口的服务器套接字
ServerSocket ss = new ServerSocket(10000);
// 2. 等待客户端连接
//Socket accept() 侦听要连接到此套接字并接受它
Socket accept = ss.accept();
// 3.获取输入流,读数据,并把数据显示在控制台
InputStream is = accept.getInputStream();
byte[] bys = new byte[1024];
int len = is.read(bys);
String data = new String(bys,0,len);
System.out.println("数据是:" + data);
//释放资源
accept.close();
ss.close();
}
3、TCP程序练习【应用】
-
案例需求
客户端:发送数据,接受服务器反馈
服务器:收到消息后给出反馈 -
案例分析
- 客户端创建对象,使用输出流输出数据
- 服务端创建对象,使用输入流接受数据
- 服务端使用输出流给出反馈数据
- 客户端使用输入流接受反馈数据
代码示例:客户端
package com.itheima.socketdemo7;
public class ClientDemo {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1",10000);
OutputStream os = socket.getOutputStream(); //输出流
os.write("hello".getBytes());//写到服务器
// os.close();如果在这里关流,会导致整个socket都无法使用
socket.shutdownOutput(); //仅仅关闭输出流.并写一个结束标记,对socket没有任何影响
//把字节流转换成字符流,客户端使用输入流接受反馈数据
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line;
while((line = br.readLine())!=null){
System.out.println(line);
}
br.close();
os.close();
socket.close();
}
}
服务器:
package com.itheima.socketdemo7;
public class ServerDemo {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10000); //服务端创建对象
Socket accept = ss.accept();
InputStream is = accept.getInputStream();//输入流接受数据
int b;
while((b = is.read())!=-1){
System.out.println((char) b);
}
System.out.println("看看我执行了吗?");
//输出流给出反馈数据
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream()));
bw.write("你谁啊?");
bw.newLine();
bw.flush();
bw.close();
is.close();
accept.close();
ss.close();
}
}
4、TCP程序文件上传练习【应用】
-
案例需求
客户端:将本地文件上传到服务器,接收服务器反馈
服务器:接收到的数据写入本地文件,给出反馈 -
案例分析
- 创建客户端对象,创建输入流对象指向文件,每读一次数据就给服务器输出一次数据,输出结束后使用shutdownOutput()方法告知服务端传输结束
- 创建服务器对象,创建输出流对象指向文件,每接受一次数据就使用输出流输出到文件中,传输结束后。使用输出流给客户端反馈信息
- 客户端接受服务端的回馈信息
-
相关方法
方法名 说明 void shutdownInput()
将此套接字的输入流放置在“流的末尾” void shutdownOutput()
禁止用此套接字的输出流
客户端代码:
package com.itheima.socketdemo8;
public class ClientDemo {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 10000);
// 是本地的流,用来读取本地文件的.
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("socketmodule\\ClientDir\\1.jpg"));
// 写到服务器 --- 网络中的流
OutputStream os = socket.getOutputStream();// 字节流
BufferedOutputStream bos = new BufferedOutputStream(os); // 包装一下,提高效率
int b;
while ((b = bis.read()) != -1) {
bos.write(b);// 通过网络写到服务器中
}
bos.flush();//刷新流;
// 给服务器一个结束标记,告诉服务器文件已经传输完毕
socket.shutdownOutput();
// 接收数据从输入流
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));// 接收数据从,
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
bis.close();
socket.close();
}
}
服务器代码:
package com.itheima.socketdemo8;
// 服务器
public class ServerDemo {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10000);
Socket accept = ss.accept();
//网络中的流,从客户端读取数据的
BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
//本地的IO流,把数据写到本地中,实现永久化存储
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("socketmodule\\ServerDir\\copy.jpg"));
int b;
while((b = bis.read()) !=-1){
bos.write(b);
}
// 通过输出流发送数据
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream()));
bw.write("上传成功");
bw.newLine();
bw.flush();//刷新流
bos.close();
accept.close();
ss.close();
}
}
5、TCP程序服务器优化【应用】
-
优化方案一
-
需求:服务器只能处理一个客户端请求,接收完一个图片之后,服务器就关闭了。
-
解决方案:使用循环
-
代码实现
// 服务器代码如下,客户端代码同上个案例,此处不再给出 public class ServerDemo { public static void main(String[] args) throws IOException { ServerSocket ss = new ServerSocket(10000); while (true) { Socket accept = ss.accept(); //网络中的流,从客户端读取数据的 BufferedInputStream bis = new BufferedInputStream(accept.getInputStream()); //本地的IO流,把数据写到本地中,实现永久化存储 BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("optimizeserver\\ServerDir\\copy.jpg")); int b; while((b = bis.read()) !=-1){ bos.write(b); } BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream())); bw.write("上传成功"); bw.newLine(); bw.flush(); bos.close(); accept.close(); } //ss.close(); } }
-
-
优化方案二
- 需求:第二次上传文件的时候,会把第一次的文件给覆盖。
- 解决方案:
UUID
类中的UUID. randomUUID()
方法生成唯一且随机
的文件名 - 代码实现
// 服务器代码如下,客户端代码同上个案例,此处不再给出 public class ServerDemo { public static void main(String[] args) throws IOException { ServerSocket ss = new ServerSocket(10000); while (true) { Socket accept = ss.accept(); //网络中的流,从客户端读取数据的 BufferedInputStream bis = new BufferedInputStream(accept.getInputStream()); //本地的IO流,把数据写到本地中,实现永久化存储 BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("optimizeserver\\ServerDir\\" + UUID.randomUUID().toString() + ".jpg")); int b; while((b = bis.read()) !=-1){ bos.write(b); } BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream())); bw.write("上传成功"); bw.newLine(); bw.flush(); bos.close(); accept.close(); } //ss.close(); } }
-
优化方案三
-
需求: 使用循环虽然可以让服务器处理多个客户端请求。但是还是无法同时跟多个客户端进行通信。
-
解决方案:开启多线程处理
-
代码实现
// 线程任务类 public class ThreadSocket implements Runnable { //多线程类,实现一个接口 private Socket acceptSocket; public ThreadSocket(Socket accept) { this.acceptSocket = accept; } @Override public void run() { BufferedOutputStream bos = null; try { //网络中的流,从客户端读取数据的 BufferedInputStream bis = new BufferedInputStream(acceptSocket.getInputStream()); //本地的IO流,把数据写到本地中,实现永久化存储 bos = new BufferedOutputStream(new FileOutputStream("optimizeserver\\ServerDir\\" + UUID.randomUUID().toString() + ".jpg")); int b; while((b = bis.read()) !=-1){ bos.write(b); } BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(acceptSocket.getOutputStream())); bw.write("上传成功"); bw.newLine(); bw.flush(); } catch (IOException e) { e.printStackTrace(); } finally { if(bos != null){ try { bos.close(); } catch (IOException e) { e.printStackTrace(); } } if (acceptSocket != null){ try { acceptSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } } } // 服务器代码 public class ServerDemo { public static void main(String[] args) throws IOException { ServerSocket ss = new ServerSocket(10000); while (true) { Socket accept = ss.accept(); ThreadSocket ts = new ThreadSocket(accept); new Thread(ts).start(); } //ss.close(); } }
-
-
优化方案四
- 需求:使用多线程虽然可以让服务器同时处理多个客户端请求。但是资源消耗太大。
- 解决方案:加入线程池
- 代码实现
// 服务器代码如下,线程任务类代码同上,此处不再给出
public class ServerDemo {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10000);
ThreadPoolExecutor pool = new ThreadPoolExecutor( //创建线程池
3,//核心线程数量
10, //线程池的总数量
60, //临时线程空闲时间(s)
TimeUnit.SECONDS, //临时线程空闲时间的单位
new ArrayBlockingQueue<>(5),//阻塞队列(允许有多少个等待)
Executors.defaultThreadFactory(),//创建线程的方式
new ThreadPoolExecutor.AbortPolicy()//任务拒绝策略
);
while (true) {
Socket accept = ss.accept();
ThreadSocket ts = new ThreadSocket(accept);
//new Thread(ts).start();
pool.submit(ts);//有任务,把ts传入
}
//ss.close();
}
}
四、日志
1、日志概述【理解】
-
输出语句的弊端
- 想要取消打印语句,需要修改代码才可以完成;
- 打印的内容只能打印在控制台,不能将其记录到其他的位置(文件、数据库)
-
日志概述
程序中的日志可以用来记录程序在运行的时候点点滴滴。并可以进行永久存储。 -
日志与输出语句的区别
输出语句 日志技术 取消日志 需要修改代码,灵活性比较差 不需要修改代码,灵活性比较好 输出位置 只能是控制台 可以将日志信息写入到文件或者数据库中 多线程 和业务代码处于一个线程中 多线程方式记录日志,不影响业务代码的性能
2、日志体系结构和logback【理解】
-
体系结构
-
logback
- 通过使用日志技术,我们可以控制日志信息输送的
目的地是控制台、文件
等位置。 - 我们也可以
控制
每一条日志
的输出格式
。 - 通过
定义
每一条日志
信息的级别
,我们能够更加细致地控制日志的生成过程。 - 最令人感兴趣的就是,这些可以通过一个
配置文件
来灵活地进行配置,而不需要修改应用的代码。
- 通过使用日志技术,我们可以控制日志信息输送的
3、入门案例【应用】
-
使用步骤
- 导入logback的相关jar包
- 编写logback配置文件
- 在代码中获取日志的对象
- 按照级别设置记录日志信息
-
代码示例
package com.itheima.logdemo;
public class LogDemo {
// 获取日志对象
private static final Logger LOGGER = LoggerFactory.getLogger(LogDemo.class);
public static void main(String[] args) {
// 打日志 --- 类似于写输出语句
//1.导入jar包
//2.编写配置文件
//3.在代码中获取日志的对象
//4.按照日志级别设置日志信息
LOGGER.debug("debug级别的日志");
LOGGER.info("info级别的日志");
LOGGER.warn("warn级别的日志");
LOGGER.error("error级别的日志");
Scanner sc = new Scanner(System.in);
System.out.println("请输入您的姓名");
LOGGER.debug("用户开始输入信息了"); //日志
String name = sc.nextLine();
// System.out.println(name);
LOGGER.info("用户输出录入姓名为:" + name);//日志
System.out.println("请输入您的年龄");
String age = sc.nextLine();
try {
int ageInt = Integer.parseInt(age);
LOGGER.info("用户输入的年龄格式正确" + age);//日志
} catch (NumberFormatException e) {
LOGGER.info("用户输入的年龄格式错误" + age);//日志
}
}
}
// 测试类
public class Test01 {
//获取日志的对象
private static final Logger LOGGER = LoggerFactory.getLogger(Test01.class);
public static void main(String[] args) {
//1.导入jar包
//2.编写配置文件
//3.在代码中获取日志的对象
//4.按照日志级别设置日志信息
LOGGER.debug("debug级别的日志"); //如果日志级别不高,就不会被打印
LOGGER.info("info级别的日志");
LOGGER.warn("warn级别的日志");
LOGGER.error("error级别的日志");
}
}
五、枚举
1、概述【理解】
为了间接的表示一些固定的值,Java就给我们提供了枚举
是指将变量的值一一列出来,变量的值只限于列举出来的值的范围内
2、定义格式【应用】
-
格式
public enum s { 枚举项1,枚举项2,枚举项3; } 注意: 定义枚举类要用关键字enum
-
示例代码
// 定义一个枚举类,用来表示春,夏,秋,冬这四个固定值 public enum Season { //枚举项后面什么都不写,默认使用空参构造方法;下面一定不能只写有参构造; SPRING,SUMMER,AUTUMN,WINTER; }
3、枚举的特点【理解】
-
特点
-
所有枚举类都是Enum的子类
-
我们可以通过"枚举类名.枚举项名称"去访问指定的枚举项
-
每一个枚举项其实就是该枚举的一个对象
-
枚举也是一个类,也可以去定义成员变量
-
枚举类的第一行上必须是枚举项,最后一个枚举项后的分号是可以省略的,但是如果枚举类有其他的东西,这个分号就不能省略。建议不要省略
-
枚举类可以有构造器,但必须是private的,它默认的也是private的。
枚举项的用法比较特殊:枚举(“”);
-
枚举类也可以有抽象方法,但是枚举项必须重写该方法
-
-
代码示例
定义枚举类
package com.itheima.demo2;
public enum Season {
SPRING("春") {
// 如果枚举类中有抽象方法
// 那么在枚举项中必须要全部重写
@Override
public void show() { // 重写show方法;
System.out.println(this.name);
}
},
SUMMER("夏") {
@Override
public void show() {
System.out.println(this.name);
}
},
AUTUMN("秋") {
@Override
public void show() {
System.out.println(this.name);
}
},
WINTER("冬") {
@Override
public void show() {
System.out.println(this.name);
}
};
public String name;
// 空参构造
// private Season(){}
// 有参构造
private Season(String name) {
this.name = name;
}
// 抽象方法
public abstract void show();
}
测试代码
package com.itheima.demo2;
public class EnumDemo {
public static void main(String[] args) {
/*
* 1.所有枚举类都是Enum的子类
* 2.我们可以通过"枚举类名.枚举项名称"去访问指定的枚举项
* 3.每一个枚举项其实就是该枚举的一个对象
* 4.枚举也是一个类,也可以去定义成员变量
* 5.枚举类的第一行上必须是枚举项,最后一个枚举项后的分号是可以省略的,
* 但是如果枚举类有其他的东西,这个分号就不能省略。建议不要省略
* 6.枚举类可以有构造器,但必须是private的,它默认的也是private的。
* 枚举项的用法比较特殊:枚举("");
* 7.枚举类也可以有抽象方法,但是枚举项必须重写该方法
*/
// 第二个特点的演示
// 我们可以通过"枚举类名.枚举项名称"去访问指定的枚举项
System.out.println(Season.SPRING);
System.out.println(Season.SUMMER);
System.out.println(Season.AUTUMN);
System.out.println(Season.WINTER);
// 第三个特点的演示
// 每一个枚举项其实就是该枚举的一个对象
Season spring = Season.SPRING; // 创建对象
}
}
4、枚举的方法【应用】- 方法介绍
方法名 | 说明 |
---|---|
String name() | 获取枚举项的名称 |
int ordinal() | 返回枚举项在枚举类中的索引值 |
int compareTo(E o) | 比较两个枚举项,返回的是索引值的差值 |
String toString() | 返回枚举常量的名称 |
static <T> T valueOf(Class<T> type,String name) | 获取指定枚举类中的指定名称的枚举值 |
values() | 获得所有的枚举项 |
- 示例代码
定义类
package com.itheima.demo3;
public enum Season {
SPRING, SUMMER, AUTUMN, WINTER;
}
测试代码
package com.itheima.demo3;
public class EnumDemo {
public static void main(String[] args) {
// String name() 获取枚举项的名称
String name = Season.SPRING.name();
System.out.println(name); // 返回枚举项的名字;
System.out.println("-----------------------------");
// int ordinal() 返回枚举项在枚举类中的索引值
int index1 = Season.SPRING.ordinal();
int index2 = Season.SUMMER.ordinal();
int index3 = Season.AUTUMN.ordinal();
int index4 = Season.WINTER.ordinal();
System.out.println(index1); // 0
System.out.println(index2); // 1
System.out.println(index3); // 2
System.out.println(index4); // 3
System.out.println("-----------------------------");
// int compareTo(E o) 比较两个枚举项,返回的是索引值的差值
int result = Season.SPRING.compareTo(Season.WINTER);
System.out.println(result);// -3
System.out.println("-----------------------------");
// String toString() 返回枚举常量的名称
String s = Season.SPRING.toString();
System.out.println(s);
System.out.println("-----------------------------");
// static <T> T valueOf(Class<T> type,String name)//静态方法
// 获取指定枚举类中的指定名称的枚举值
Season spring = Enum.valueOf(Season.class, "SPRING");
System.out.println(spring);
System.out.println(Season.SPRING == spring); // ture
System.out.println("-----------------------------");
// values() 获得所有的枚举项
Season[] values = Season.values();
for (Season value : values) { // 增强for
System.out.println(value);
}
}
}