Java 基础 - 进阶学习

Java 基础 - 进阶学习

Lambda 表达式

Arrays.sort(arr, (Integer o1, Integer o2) -> {
    return o1 - o2;
});

函数式编程

面向对象: 先找对象, 让对象做事情

函数式编程: 忽略面向对象的复杂语法, 强调做什么, 而不是谁去做

格式

() -> {}

  • 可以简化匿名内部类的书写

  • 只能简化函数式接口的匿名内部类的书写

    • 函数式接口: 有且仅有一个抽象方法的接口, 接口上方可以加 @FunctionalInterface
    • 若不止一个, 则不清楚是替代哪个方法, 接口上方也不能加 @FunctionalInterface
  • 省略格式
    可以(从对应的函数式接口)推导得到的东西可以省略不写

    • 参数类型可以省略
    Arrays.sort(arr, (o1, o2) -> {
        return o1 - o2;
    });
    
    • 如果只有一个参数, () 可以省略
    • 如果只有一行, 大括号, 分号, return 可以省略, 需要同时省略
    Arrays.sort(arr, (o1, o2) -> o1 - o2);
    

方法引用

把已有的方法拿过来用, 当作函数式接口中抽象方法的方法体

  • 引用处必须是函数式接口
  • 被引用方法已经存在
  • 被引用方法的形参与返回值要和抽象方法保持一致
  • 被引用方法的功能满足当前需求
// 自己写一个方法, 或者别人已经写好的方法
public static int substraction(int num1, int num2) {
    return num2 - num1;
}
Arrays.sort(arr, (o1, o2) -> o2 - o1); // Lambda 降序排序
Arrays.sort(arr, temp::substraction); // 方法引用 降序排序

引用静态方法

类名::静态方法

ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "1", "2", "3");

// 方法引用
list.stream().map(Integer::parseInt).forEach(s -> sout(s));

引用成员方法

对象::成员方法

  • 其他类 其他类对象::方法名
list.stream().filter(new StringJudge()::stringJugde).forEach(s -> sout(s));
  • 本类 this::方法名, 引用处不能是静态方法
  • 父类 super::方法名, 引用处不能是静态方法

引用构造方法

类名::new

public class Student {
    private String name;
    private int age;

    public Student() {

    }
    
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 一个特殊的构造方法, 参数只有一个 String s
    public Student(String s) {
        String[] arr = s.split(",");
        this.name = arr[0];
        this.age = Integer.parseInt(arr[1]);
    }
}
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "a,1", "b,2", "c,3");

// 方法引用
List<Student> newList = list.stream().map(Student::new).collect(Collectors.tolist());

其他引用方式

类名引用成员方法

类名::成员方法

list.stream().map(String::toUpperCase).forEach(s -> sout(s));
  • 理解
    • 抽象方法 apply
    list.stream().map(new Function<String, String>() {
        @Override
        public String apply(String s) {
            return s.toUpperCase();
        }
    }).forEach(s -> sout(s));
    
    • 第一个参数 String s, 表明引用方法的调用者, 决定了可以引用哪些类的方法
    • 第二个参数到最后一个参数, 需要和引用方法保持一致 (如果没有第二个参数, 则引用方法为无参的方法)
    • 引用方法返回值需要和抽象方法保持一致

引用数组的构造方法

数据类型[]::new

ArrayList<Integer> list = new ArrayList<>();
Integer[] arr = list.stream().toArray(Integer[]::new);

// 数组的类型, 需要和流中的数据的类型保持一致

异常

异常: 程序出现的问题

要学习如何处理异常
  • Error: 系统级别错误(属于严重问题)
  • Exception: 异常, 程序可能出现的问题
    • 编译时异常: 直接继承于 Exception, 编译阶段会出现异常提醒, 核心是提醒程序员检查本地信息
    • 运行时异常: RuntimeException 本身及其子类, 编译阶段不会出现异常提醒, 核心是参数错误导致的问题

异常的作用

  • 用来查 bug 的关键参考信息

    • 运行如下代码
    package exception;
    
    import student.Student;
    
    public class ExceptionDemo1 {
        public static void main(String[] args) {
            
            Student[] arr = new Student[3];
            String name = arr[0].getName();
            System.out.println(name);
        }
    }
    
    • 异常信息如下
    Exception in thread "main" java.lang.NullPointerException: Cannot invoke "student.Student.getName()" because "arr[0]" is null
            at exception.ExceptionDemo1.main(ExceptionDemo1.java:9)
    
    • 空指针异常: NullPointerException
    • 异常的信息: Cannot invoke "student.Student.getName()" because "arr[0]" is null
    • 异常的位置: at exception.ExceptionDemo1.main(ExceptionDemo1.java:9)
  • 可以作为方法内部的一种特殊返回值, 以便告知调用者底层的执行情况

    • 打印在控制台
    public void setAge(int age) {
        if (age < 18 || age > 40) {
            sout("年龄超出范围");
        } else {
            this.age = age;
        }
    }
    
    • 抛出异常
    public void setAge(int age) {
        if (age < 18 || age > 40) {
            throw new RuntimeException();
        } else {
            this.age = age;
        }
    }
    

异常的处理方式

JVM 默认的处理方式

  • 把异常的名称, 原因和出现的位置输出到控制台
  • 停止运行

自己处理 (捕获异常)

  • 格式
try {
    可能出现异常的代码;
} catch (异常类名 变量名) {
    异常的处理代码;
}
  • 目的: 当代码出现异常时, 让程序继续往下执行
int[] arr = {1, 2, 3, 4, 5};

try {
    System.out.println(arr[10]);

    // 出现异常, 程序会创建一个异常对象
    // 异常对象到 catch 小括号中对比, 看小括号中的变量能否接收这个对象
    // 如果能接收, 异常被捕获, 执行 catch 中代码
} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("索引越界");
}

System.out.println("这里执行了");
四个问题
  • 如果 try 中没有遇到问题, 怎么执行?
    • 执行 try
    • 不执行 catch
  • 如果 try 中可能遇到多个问题, 怎么执行?
    • 写多个 catch 与之对应
    try {
        System.out.println(arr[10]);
        System.out.println(2 / 0);
        String s = null;
        System.out.println(s.equals("abc"));
    } catch (ArrayIndexOutOfBoundsException e) {
        System.out.println("索引越界");
    } catch (ArithmeticException e) {
        System.out.println("除数不能为 0");
    } catch (NullPointerException e) {
        System.out.println("空指针异常");
    } catch (Exception e) {
        System.out.println("Exception");
    }
    
    • 遇到一个异常, 会对 catch 从上到下匹配
    • 所以如果这些异常之间有父子关系, 那么父类写在下面
    • JDK7 之后, 可以同时捕获多个异常, 用 | 隔开
    try {
    
    } catch (ArrayIndexOutOfBoundsException | ArithmeticException e) {
        
    }
    
  • 如果 try 中遇到的问题没有被捕获, 怎么执行?
    • 虚拟机默认的异常处理方式
  • 如果 try 中遇到了问题, 那么 try 下面的其他代码还会执行吗?
    • 不会执行, 会直接跳转到对应的 catch 里
    • 如果没有对应的, 则交给虚拟机处理
IO 流中捕获异常

需要执行 close, 但可能 close 前面的代码就有异常, 跳转到 catch 导致 close 没有执行

  • finally
try {
    FileOutputStream fos = new FileOutputStream("xxx");

} catch () {

} finally {
    // finally 中的代码一定被执行, 除非虚拟机停止

    fos.close();
}
  • 文件拷贝
FileInputStream fis = null;
FileOutputStream fos = null;

try {
    fis = new FileInputStream("D:\\temp\\movie.mp4");
    fos = new FileOutputStream("flow\\cpoy.mp4");

    int len;
    byte[] bytes = new byte[1024 * 1024 * 5];
    while ((len = fis.read(bytes)) != -1) {
        fos.write(bytes, 0, len);
    }
} catch (IOException e) {

} finally {
    if (fos != null) { // 否则会空指针异常
        try {
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • 简化方案
    • JDK7
    // 只有实现了 AutocCloseable 的类, 才可以在小括号中创建对象
    try (FileInputStream fis = new FileInputStream("D:\\temp\\movie.mp4");
         FileOutputStream fos = new FileOutputStream("flow\\copy.mp4")) {
        
        int len;
        byte[] bytes = new byte[1024 * 1024 * 5];
        while ((len = fis.read(bytes)) != -1) {
            fos.write(bytes, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    
    • JDK9
    FileInputStream fis = new FileInputStream("D:\\temp\\movie.mp4");
    FileOutputStream fos = new FileOutputStream("flow\\copy.mp4");
    
    try (fis; fos) {
        int len;
        byte[] bytes = new byte[1024 * 1024 * 5];
        while ((len = fis.read(bytes)) != -1) {
            fos.write(bytes, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    

抛出异常

见下

异常的方法

int[] arr = {1, 2, 3, 4, 5, 6};

try {
    sout(arr[10]);
} catch (ArrayIndexOutOfBoundsException e) {
    String message = e.getMessage();
    sout(message); // Index 10 out of bounds for length 6

    String s = e.toString();
    sout(s); // java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 6

    e.printStackTrace(); // 把异常的信息打印到控制台, 不中断程序
}

抛出异常

  • throws 写在方法定义处, 表示声明一个异常, 告诉调用者, 使用本方法可能会有哪些异常 (编译时异常: 必须写; 运行时异常: 可以不写)
public void 方法() throws 异常类名1, 异常类名2, ... {

}
  • throw 写在方法内, 结束方法, 手动抛出异常对象, 交给调用者, 方法中下面的代码不执行了
public void 方法() {
    throw new NullPointerException();
}
  • e.g.
public static void main(String[] args) {
    int[] arr = null;
    int max = 0;

    try {
        max = getMax(arr);
    } catch (NullPointerException e) {
        e.printStackTrace();
    } catch (ArrayIndexOutOfBoundsException) {
        e.printStackTrace();
    }

    sout(max);
}

public static int getMax(int[] arr) throws NullPointerException, ArrayIndexOutOfBoundsException {
    if (arr == null) {
        throw new NullPointerException();
    }

    if (arr.length == 0) {
        throw new ArrayIndexOutOfBoundsException();
    }
    
    int max = arr[0];
    for (int  i = 1; i < arr.length; i++) {
        if (arr[i] > max) {
            max = arr[i];
        }
    }
    return max;
}

自定义异常

  • 目的: 让报错信息更加的见名知意
  • 步骤:
    • 定义异常类
    • 写继承关系
    • 空参构造
    • 带参构造
public class NameFormatException extends RuntimeException {
    public NameFormatException() {

    }

    public NameFormatException(String message) {
        super(message);
    }
}
public class AgeOutOfBoundsException extends RuntimeException {
    public AgeOutOfBoundsException() {

    }

    public AgeOutOfBoundsException(String message) {
        super(message);
    }
}
throw new NameFormatException("名字格式有误");
throw new AgeOutOfBoundsException(age + "超出了年龄范围");

多线程

进程: 进程是程序的基本执行实体 (一个软件运行就是一个进程)
线程: 线程是操作系统能够进行运算调度的最小单位, 它被包含在进程之中, 是进程的实际运作单位 (应用软件中互相独立, 可以同时进行的功能)

单线程程序: 从上往下依次运行
多线程程序: 同时做多件事情 -> CPU 在多个程序间切换, 把等待的空闲时间利用起来, 提高效率

并发: 同一时刻, 有多个指令在单个 CPU 上交替执行
并行: 同一时刻, 有多个指令在单个 CPU 上同时执行

实现方式

继承 Thread 类

  • 自己定义一个类继承 Thread
  • 重写 run 方法
  • 创建子类的对象, 并启动线程
public class MyThread extends Thread {
    @Override
    public void run() {
        // 书写线程要执行代码

        for (int i = 0; i < 100; i++) {
            System.out.println(getName());
        }
    }
}
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();

t1.setName("线程 1");
t2.setName("线程 2");

// 开启线程
t1.start();
t2.start();

// 发现一会执行线程 1, 一会执行线程 2

实现 Runnable 接口

  • 自己定义一个类实现 Runnable 接口
  • 重写 run 方法
  • 创建类的对象
  • 创建一个 Thread 类的对象, 并开启线程
public class MyRun implements Runnable {
    @Override
    public void run() {
        // 书写线程要执行的代码
        for (int i = 0; i < 100; i++) {
            Thread t = Thread.currentThread(); // 获取当前线程的对象
            System.out.println(t.getName());
        }
    }   
}
MyRun mr = new MyRun();

Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);

t1.setName("线程 1");
t2.setName("线程 2");

t1.start();
t2.start();

利用 Callable 接口和 Future 接口

特点: 可以获取到多线程运行的结果

  • 创建一个类实现 Callable 接口
  • 重写 call (有返回值, 表示多线程运行的结果)
  • 创建对象 (表示多线程要执行的任务)
  • 创建 FutureTask 对象 (作用: 管理多线程运行的结果)
  • 创建 Thread 类对象, 并启动 (表示线程)
public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= 100; i++) {
            sum = sum + i;
        }
        return sum;
    }
}
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);

Thread t1 = new Thread(ft);
Thread t2 = new Thread(ft);

t1.start();
t2.start();

// 获取多线程运行的结果
Integer result = ft.get();
System.out.println(result); // 5050

三种方式比较

  • 继承 Thread 类

    • 优点: 编程较简单, 可以直接使用 Thread 类的方法
    • 缺点: 可以扩展性较差, 不能再继承其他的类
  • 实现 Runnable 接口/实现 Callable 接口

    • 优点: 扩展性强, 实现该接口的同时还可以继承其他类
    • 缺点: 编程相对复杂, 不能直接使用 T和read 类的方法

成员方法

/*
 * String getName()
 * void setName(String name)
 * 
 * 如果我们没有给线程设置名字, 线程有默认名字 Thread-X (X 序号, 从 0 开始)
 * MyThread 中用 super 调用父类 Thread 构造后, 可以在构造时命名
 */

MyThread t1 = new MyThread("线程 1");
MyThread t2 = new MyThread("线程 2");

t1.start();
t2.start();
// Thread.currentThread() 获取当前线程的对象

Thread t = Thread.currentThread();
String name = t.getName();
System.out.println(name); // main

// 当 JVM 虚拟机启动之后, 会自动启动多条线程
// 其中有一条为 main 线程, 作用时调用 main 方法, 执行里面的代码
// Thread.sleep(时间(毫秒));

System.out.println("111");
Thread.sleep(5000);
System.out.println("222");

// 哪条线程执行到这个方法, 哪条线程就在这停留多久
// 时间到了自动醒来, 继续执行下面的代码
  • 线程的调度
    • 抢占式调度 - 随机性
      • 线程的优先级越高, 抢到 CPU 的概率越大
      • 1-10, 默认 5
    • 非抢占式调度 - 轮流
MyRunnable mr = new MyRunnable();

Thread t1 = new Thread(mr, "线程 1");
Thread t2 = new Thread(mr, "线程 2");

// 获取线程的优先级
System.out.println(t1.getPriority());
System.out.println(t2.getPriority());

// 设置线程的优先级
t1.setPriority(1);
t2.setPriority(10);

t1.start();
t2.start();
// final void setDaemon(boolean on) 设置为守护线程

MyThread t1 = new MyThread();
MyThread t2 = new MyThread();

// 设置为守护线程 
t2.setDaemon(true);

t1.start();
t2.start();
// 当其他的非守护线程执行完毕后, 守护线程会陆续结束 (不一定执行完)
  • 应用场景: 聊天为线程 1, 传输文件为线程 2, 想要聊天结束时传输文件也结束, 可以把线程 2 设置为守护线程
// 出让线程
Thread.yield();

// 线程执行到这个方法时, 表示出让当前 CPU 的执行权
// 但还可以又抢回来
// 会使多线程显得更均匀
// 插入线程

MyThread t = new MyThread();
t.start();

// 表示把 t 这个线程, 插入到当前线程 (main) 之前
t.join();

// 执行在 main 线程当中的
for (int i = 0; i < 10; i++) {
    sout("main 线程" + i);
}

线程的生命周期

  • 创建线程对象 新建 状态
  • start() 后, 为 就绪 状态, 此时有执行资格, 没有执行权, 即在不停的抢 CPU
  • 抢到 CPU 的执行权, 变为 运行 状态
    • run() 执行完毕, 线程变为 死亡 状态, 变成垃圾
    • run() 执行完毕前
      • 其他线程抢走 CPU 的执行权, 回到 就绪 状态
      • sleep() 或其他阻塞式方法, 变为 阻塞 状态, 此时没有执行资格, 没有执行权; sleep() 时间到或其他阻塞式方法结束, 到 就绪 状态

线程的安全问题

// 多线程卖出了相同的票, 超出限制的票
public class MyThread extends Thread {
    static int ticket = 0;

    @Override
    public void run() {
        while (true) {
            if (ticket < 100) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                // 错误 e.g.
                // 线程 1 抢到 CPU
                // 线程 1 睡了 10 ms
                // 线程 2 抢到 CPU
                // 线程 2 睡了 10 ms
                // 线程 1 醒过来执行了 ticket++
                // 线程 1 还没执行下一句前, 线程 2 醒过来执行了 ticket++, 出现错误

                ticket++;
                sout("在卖第" + ticket + "张票");
            } else {
                break;
            }
        }
    }
}

线程执行时, 具有随机性, CPU 的执行权随时有可能被其他线程抢走

同步代码块

如果能把操作共享数据的这部分代码锁起来, 使一个线程执行完了, 下一个线程才能进来 -> 同步代码块

synchronized (锁) {
    操作共享数据的代码
}

// 锁默认打开, 有一个线程进去了, 锁自动关闭
// 里面的代码全部执行完毕, 线程出来, 锁自动打开
public class MyThread extends Thread {
    static int ticket = 0;

    // 锁对象
    static Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            // 同步代码块
            synchronized (obj) {
                if (ticket < 100) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    ticket++;
                    sout("在卖第" + ticket + "张票");
                } else {
                    break;
                }
            }
        }
    }
}

一般写为

public class MyThread extends Thread {
    static int ticket = 0;

    @Override
    public void run() {
        while (true) {
            // 同步代码块
            synchronized (MyThread.class) { // 唯一, 可以作为锁对象
                if (ticket < 100) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    ticket++;
                    sout("在卖第" + ticket + "张票");
                } else {
                    break;
                }
            }
        }
    }
}

同步方法

修饰符 synchronized 返回值类型 方法名(方法参数) {}
  • 同步方法是锁住方法里面所有的代码
  • 锁对象不能自己指定
    • 非静态方法: this, 表示当前方法的调用者
    • 静态: 当前类的字节码文件对象 xxx.class
MyRunnable mr = new MyRunnable();

Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
Thread t3 = new Thread(mr);

t1.start();
t2.start();
t3.start();
public class MyRunnable implements Runnable {
    // 这种实现不用 static
    // 因为三个线程创建时都传入一个 mr, 都指向内存中同一个 ticket
    int ticket = 0;

    @Override
    public void run() {
        while (true) {
            if (method()) break;
        } 
    }

    // 非静态, 锁对象为 this
    private synchronized boolean method() {
        if (ticket == 100) {
            return true;
        } else {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            ticket++;
            sout(Thread.currentThread().getName() + "在卖第" + tichet + "张票");
        }
    }
}

e.g. StringBuffer 相较 StringBuilder 是线程安全的, 适用于多线程

Lock 锁

更清晰的表达如何加锁和释放锁

手动上锁, 手动释放锁:

  • void lock() 获取锁
  • void unlock() 释放锁

Lock 是接口, 不能直接实例化, 采用其实现类 ReentrantLock 来实例化

public class MyThread extends Thread {
    static int ticket = 0;

    static Lock lock = new ReentrantLock(); // static 所有对象共用一把锁

    @Override
    public void run() {
        while (true) {
            // 同步代码块
            lock.lock();
                if (ticket < 100) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    ticket++;
                    sout("在卖第" + ticket + "张票");
                } else {
                    break;
                }
            lock.unlock();
        }
    }
}
  • 上面的代码存在问题
  • 比如在卖出第 100 张票后, 线程 1 抢到了 CPU, 执行了 lock.lock(), 然后由判断 break
  • 但线程 1 没有执行 lock.unlock(), 导致其他线程一直被 lock.lock() 锁住, 程序无法停止
  • 采用 finally 关键字解决
public class MyThread extends Thread {
    static int ticket = 0;

    static Lock lock = new ReentrantLock(); // static 所有对象共用一把锁

    @Override
    public void run() {
        while (true) {
            // 同步代码块
            lock.lock();
            try {
                if (ticket < 100) {
                    Thread.sleep(10);
                    ticket++;
                    sout("在卖第" + ticket + "张票");
                } else {
                    break;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

死锁

一个错误

public class MyThread extends Thread {

    static Object objA = new Object();
    static Object objB = new Object();

    @Override
    public void run() {
        while (true) {
            if ("线程A".equals(getName())) {
                synchronized (objA) {
                    System.out.println("线程 A 拿到了 A 锁, 准备拿 B 锁");
                    synchronized (objB) {
                        System.out.println("线程 A 拿到了 B 锁, 顺利执行完一轮");
                    }
                }
            } else if ("线程B".equals(getName())) {
                synchronized (objB) {
                    System.out.println("线程 B 拿到了 B 锁, 准备拿 A 锁");
                    synchronized (objA) {
                        System.out.println("线程 B 拿到了 A 锁, 顺利执行完一轮");
                    }
                }
            }
        }
    }
}

两个锁嵌套了, 可能出现:

  • A 拿了 A 锁, 需要 B 锁出去
  • 但 A 还没拿 B 锁时, B 抢到了 CPU, 从锁外的循环来到 B 锁的外面, 并拿了 B 锁, 而 B 又需要 A 锁才能出去
  • 线程 A 和线程 B 都在等待对方释放锁, 从而陷入死锁状态

生产者和消费者/等待唤醒机制

多线程协作模式

流程

  • 生产者: 生成数据
  • 消费者: 消费数据

理想情况: 生产者 - 消费者 - 生产者 - 消费者 ...

  • 消费者等待

    • 如果消费者先抢到执行权, 此时"桌子"上没有"吃的", 则消费者等待
    • 然后生产者拿到执行权, 生产吃的后"叫"消费者"去吃", 即唤醒
  • 生产者等待

    • 如果生产者抢到执行权, 但"桌子"上已经有"吃的", 生产者也要等待
  • 最终逻辑

    • 消费者
      • 判断桌子上是否有食物
      • 如果没有就等待
      • 如果有就开吃
      • 吃完后唤醒生产者继续做
    • 生产者
      • 判断桌子上是否有食物
      • 如果有就等待
      • 如果没有就制作食物
      • 把食物放桌子上
      • 叫醒等待的消费者开吃

方法

void wait() // 当前线程等待, 直到被其他线程唤醒
void notify() // 随机唤醒单个线程
void notifyAll() // 唤醒所有线程

实现

  • 桌子
public class Desk {
    // 控制生产者和消费者的执行

    // 是否有面条; 0 没有, 1 有
    public static int foodFlag = 0;

    // 总个数; 此处是用来限制这个机制的运行次数, 并非表示桌子上的食物数量
    public static int count = 10;

    // 锁对象
    public static Object lock = new Object();
}
  • 消费者
public class Foodie extends Thread {

    @Override
    public void run() {
        while (true) {
            synchronized (Desk.lock) {
                if (Desk.count == 0) {
                    break;
                } else {
                    if (Desk.foodFlag == 0) {
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    } else {
                        Desk.count--;
                        sout("有面条, 还能再吃" + Desk.count + "碗");
                        Desk.lock.notifyAll();
                        Desk.foodFlag = 0;
                    }
                }
            }
        }
    }
}
  • 生产者
public class Cook extends Thread {

    @Override
    public void run() {
        while (true) {
            synchronized (Desk.lock) {
                if (Desk.count == 0) {
                    break;
                } else {
                    if (Desk.foodFlag == 1) {
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    } else {
                        sout("厨师做了一碗");
                        Desk.foodFlag = 1;
                        Desk.lock.notifyAll();
                    }
                }
            }
        }
    }
}

阻塞队列实现

生产者 put 数据进队列, 放不进去会等着, 也叫阻塞
消费者从队列 take 数据, 会取出第一个数据, 取不到会等着, 也叫阻塞

  • 继承结构: 接口 Iterable -> 接口 Collection -> 接口 Queue -> 接口 BlockingQueue
  • 实现类
    • ArrayBlockingQueue 底层是数组, 有界
    • LinkedBlockingQueue 底层是链表, 无界(或最大为 MAX_INT)
public class Cook extends Thread {
    ArrayBlockingQueue<String> queue;

    public Cook(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                queue.put("面条"); // put 中有锁
                sout("厨师放了一碗面条"); // 输出语句在锁外面, 数据本身变化没有问题, 但显示会有问题
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Foodie extends Thread {
    ArrayBlockingQueue<String> queue;

    public Cook(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                String food = queue.take(); // take 中有锁
                sout(food); // 输出语句在锁外面, 数据本身变化没有问题, 但显示会有问题
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public static void main(String[] args) {
    
    ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);

    Cook c = new Cook(queue);
    Foodie f = new Foodie(queue);

    c.start();
    f.start();
}

线程的状态

  • 新建 - 创建线程对象
  • 就绪 - start
  • 运行 - 实际上, 虚拟机没有定义运行状态, 其实际是交给操作系统
  • 阻塞 - 无法获取锁对象
  • 等待 - wait
  • 计时等待 - sleep
  • 结束 - 全部代码运行完毕

练习

练习 1

两个窗口买票

package thread.Practice1;

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Lock;

public class SellTicket extends Thread {
    static int ticket = 1000;
    static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            lock.lock();
            try {
                if (ticket == 0) {
                    break;
                } else {
                    Thread.sleep(3000);
                    ticket--;
                    System.out.println(getName() + "卖出了一张票, 还剩" + ticket + "张票");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}
package thread.Practice1;

public class Test {
    public static void main(String[] args) {
        
        SellTicket st1 = new SellTicket();
        SellTicket st2 = new SellTicket();

        st1.setName("窗口 1");
        st2.setName("窗口 2");

        st1.start();
        st2.start();
    }
}

练习 2

两人发礼物, 剩下礼物小于 10 份时不发

package thread.Practice2;

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Lock;

public class GiveOutGift extends Thread {
    static int gift = 100;
    static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            lock.lock();

            try {
                if (gift < 10) {
                    break;
                } else {
                    Thread.sleep(10);
                    gift--;
                    System.out.println(getName() + "发出一份礼物, 还剩" + gift + "份礼物");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}
package thread.Practice2;

public class Test {
    public static void main(String[] args) {
        
        GiveOutGift g1 = new GiveOutGift();
        GiveOutGift g2 = new GiveOutGift();

        g1.setName("一号");
        g2.setName("二号");

        g1.start();
        g2.start();
    }
}

练习 3

两个线程共同获取 1-100 之间所有数字, 输出所有奇数

package thread.Practice3;

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Lock;

public class GetNumber extends Thread {
    static int number = 0;
    static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            lock.lock();

            try {
                if (number == 100) {
                    break;
                } else {
                    Thread.sleep(10);
                    number++;
                    if (number % 2 == 1) {
                        System.out.println(getName() + "获取了一个奇数, 为" + number);
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}
package thread.Practice3;

public class Test {
    public static void main(String[] args) {
        
        GetNumber g1 = new GetNumber();
        GetNumber g2 = new GetNumber();

        g1.setName("线程 1");
        g2.setName("线程 2");
        
        g1.start();
        g2.start();
    }
}

练习 4

抢红包: 100 块钱, 分成 3 个包, 五个人抢

package thread.Practice4;

import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Lock;

public class Get extends Thread {
    static int total = 100;
    static int[] each = new int[3];
    static int count = 0; // 已经被抢的红包数量
    static Lock lock = new ReentrantLock();

    static {
        Random rd = new Random();

        each[0] = rd.nextInt(total - 2) + 1;
        each[1] = rd.nextInt(total - 1 - each[0]) + 1;
        each[2] = total - each[0] - each[1];
    }

    @Override
    public void run() {
        while (true) {
            lock.lock();

            try {
                if (count == 3) {
                    System.out.println(getName() + "没抢到");
                } else {
                    Thread.sleep(10);
                    count++;
                    System.out.println(getName() + "抢到了" + each[count - 1] + "元");
                }

                break;
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}
package thread.Practice4;

public class Test {
    public static void main(String[] args) {
        
        Get g1 = new Get();
        Get g2 = new Get();
        Get g3 = new Get();
        Get g4 = new Get();
        Get g5 = new Get();
        
        g1.setName("一号");
        g2.setName("二号");
        g3.setName("三号");
        g4.setName("四号");
        g5.setName("五号");
        
        g1.start();
        g2.start();
        g3.start();
        g4.start();
        g5.start();
    }    
}

练习 5

两个抽奖箱从抽奖池获取奖项

package thread.Practice5;

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Lock;
import java.util.Random;

public class Box extends Thread {
    static int[] prizes = {10, 5, 20, 50, 100, 200, 500, 800, 2, 80, 300, 700};
    static int[] vis = new int[prizes.length];
    static int count = 0;
    static Lock lock = new ReentrantLock();
    
    @Override
    public void run() {
        while (true) {
            lock.lock();

            try {
                if (count == prizes.length) {
                    break;
                } else {
                    Random rd = new Random();
                    while (true) {
                        int choice = rd.nextInt(prizes.length);

                        if (vis[choice] == 0) {
                            Thread.sleep(100);
                            count++;
                            vis[choice] = 1;
                            System.out.println(getName() + "又产生了一个" + prizes[choice] + "元大奖");
                            break;
                        }
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}
package thread.Practice5;

public class Test {
    public static void main(String[] args) {
        
        Box box1 = new Box();
        Box box2 = new Box();

        box1.setName("抽奖箱 1");
        box2.setName("抽奖箱 2");

        box1.start();
        box2.start();
    }
}

练习 6

抽完打印最高奖项和总计额

package thread.Practice6;

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Lock;
import java.util.ArrayList;
import java.util.Random;
import java.util.StringJoiner;

public class Box extends Thread {
    static int[] prizes = {10, 5, 20, 50, 100, 200, 500, 800, 2, 80, 300, 700};
    static int[] vis = new int[prizes.length];
    static int count = 0;
    static Lock lock = new ReentrantLock();

    private ArrayList<Integer> list = new ArrayList<>();

    private String printList() {
        StringJoiner ans = new StringJoiner(", ");
        for (Integer i : list) {
            ans.add(i.toString());
        }
        return ans.toString();
    }

    @Override
    public void run() {
        while (true) {
            lock.lock();

            try {
                if (count == prizes.length) {
                    int sum = 0, max = 0;

                    for (Integer i : list) {
                        sum += i;
                        max = i > max ? i : max;
                    }
                    
                    System.out.println("在此次抽奖过程中, " + getName() + " 总共产生了 " + list.size() + " 个奖项");
                    System.out.println("分别为" + printList() + ", 最高奖项为 " + max + " 元, 总计额为 " + sum + " 元");
                    
                    break;
                } else {
                    Random rd = new Random();
                    while (true) {
                        int choice = rd.nextInt(prizes.length);

                        if (vis[choice] == 0) {
                            Thread.sleep(100);

                            count++;
                            vis[choice] = 1;
                            list.add(prizes[choice]);
                            
                            break;
                        }
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}
package thread.Practice6;

public class Test {
    public static void main(String[] args) {
        
        Box box1 = new Box();
        Box box2 = new Box();

        box1.setName("抽奖箱 1");
        box2.setName("抽奖箱 2");

        box1.start();
        box2.start();
    }
}

练习 7

两个抽奖箱比较

package thread.Practice7;

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Lock;
import java.util.ArrayList;
import java.util.Random;
import java.util.StringJoiner;

public class Box extends Thread {
    static int[] prizes = {10, 5, 20, 50, 100, 200, 500, 800, 2, 80, 300, 700};
    static int[] vis = new int[prizes.length];
    static Random rd = new Random();
    static int count = 0;

    static int countThread = 0;

    public Box() {
        countThread++;
    }

    public int getCountThread() {
        return countThread;
    }

    static int finish = 0;
    static int max = 0;
    static String maxBox = new String();

    static Lock lock = new ReentrantLock();

    private ArrayList<Integer> list = new ArrayList<>();

    private String printList() {
        StringJoiner ans = new StringJoiner(", ");
        for (Integer i : list) {
            ans.add(i.toString());
        }
        return ans.toString();
    }

    @Override
    public void run() {
        while (true) {
            lock.lock();

            try {
                if (count == prizes.length) {
                    int sum = 0, max = 0;

                    for (Integer i : list) {
                        sum += i;
                        max = i > max ? i : max;
                    }
                    
                    System.out.println("在此次抽奖过程中, " + getName() + " 总共产生了 " + list.size() + " 个奖项");
                    System.out.println("分别为" + printList() + ", 最高奖项为 " + max + " 元, 总计额为 " + sum + " 元");
                    
                    break;
                } else {
                    while (true) {
                        int choice = rd.nextInt(prizes.length);

                        if (vis[choice] == 0) {
                            Thread.sleep(100);

                            count++;
                            vis[choice] = 1;
                            list.add(prizes[choice]);

                            if (prizes[choice] > max) {
                                max = prizes[choice];
                                maxBox = Thread.currentThread().getName();
                            }
                            
                            break;
                        }
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }

        finish++;

        if (finish == getCountThread()) {
            System.out.println("在此次抽奖过程中, " + maxBox + " 中产生了最大奖项, 该奖项金额为 " + max + " 元");
        }
    }
}

package thread.Practice7;

public class Test {
    public static void main(String[] args) {
        
        Box box1 = new Box();
        Box box2 = new Box();

        box1.setName("抽奖箱 1");
        box2.setName("抽奖箱 2");

        box1.start();
        box2.start();
    }
}

线程池

  • 一个池子存放线程, 初始空
  • 此时有线程, 就拿去执行任务, 执行完放回线程池
  • 此时没有空闲线程, 就创建一个新线程
  • 此时没有空闲线程, 且线程数量达到上限, 任务等待

Executors

Executors: 线程池的工具类通过调用方法返回不同类型的线程池对象

  • public static ExecutorService newCachedThreadPool 创建没有上限的线程池
  • public static ExecutorService newFixedThreadPool(int nThreads) 创建有上限的线程池
package pool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class PoolDemo1 {
    public static void main(String[] args) throws InterruptedException {
        // 无上限
        ExecutorService pool1 = Executors.newCachedThreadPool();

        pool1.submit(new MyRunnable());

        Thread.sleep(100);
        
        pool1.submit(new MyRunnable());

        Thread.sleep(100);

        pool1.submit(new MyRunnable());

        pool1.shutdown();

        // 去掉 sleep 可观察线程池创建线程
        // 不去掉 sleep 可观察线程池复用线程
    }
}
package pool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class PoolDemo1 {
    public static void main(String[] args) throws InterruptedException {
        // 有上限
        ExecutorService pool1 = Executors.newFixedThreadPool(3);

        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());

        pool1.shutdown();
    }
}

自定义线程池

  • 核心元素

    • 正式员工数量 -> 核心线程数
    • 餐厅最大员工数 -> 线程池中最大线程数量
    • 临时员工空闲多长时间被辞退 (值) - 空闲时间 (值)
    • 临时员工空闲多长时间被辞退 (单位) - 空闲时间 (单位)
    • 排队的顾客 - 阻塞队列
    • 从哪里招人 - 创建线程的方式
    • 当排队人数过多, 超出顾客请下次再来 (拒绝服务) - 要执行任务过多时的解决方案
  • 细节

    • 核心线程都在工作, 且队伍已经排满时 -> 创建临时线程
    • 执行顺序不一定按提交顺序
    • 核心线程和临时线程都在工作, 且队伍已经排满时, 此时还有新的任务 -> 触发任务拒绝策略, 默认丢弃
      • AbortPolicy 丢弃任务并抛出 RejectedExecutionException 异常
      • DiscardPolicy 丢弃任务但不抛出异常 (不推荐)
      • DiscardOldestPolicy 抛弃队列中等待最久的任务, 然后把当前任务加入队列
      • CallerRunsPolicy 调用任务的 run() 方法绕过线程池直接执行
package pool;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class PoolDemo2 {
    public static void main(String[] args) {
        
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
            3, 
            6, 
            60, 
            TimeUnit.SECONDS, 
            new ArrayBlockingQueue<>(3), // 任务队列
            Executors.defaultThreadFactory(), // 创建线程工厂
            new ThreadPoolExecutor.AbortPolicy() // 任务的拒绝策略
        );
    }
}

最大并行数

package pool;

public class PoolDemo3 {
    public static void main(String[] args) {
        int cnt = Runtime.getRuntime().availableProcessors();
        System.out.println(cnt); // Java 可用的处理器数目为 20

        // 经查看, 电脑为 14 核 20 线程
    }
}

线程池的大小

  • 线程池多大合适?
    • CPU 密集型运算: 计算较多, 读取本地文件和读取数据库较少 -> 最大并行数 + 1
    • I/O 密集型运算: 最大并行数 * 期望 CPU 利用率 * (CPU 计算时间 + CPU 等待时间) / CPU 等待时间

扩展内容

暂略

网络编程

在网络通信协议下, 不同计算机上运行的程序, 进行的数据传输
(计算机和计算机之间通过网络进行数据传输)

Java 使用 java.net 包, 开发常见的网络应用程序

  • 常见的软件架构
    • C/S: Client/Server 客户端/服务器

      • 用户本地需要下载并安装客户端程序, 在远程有一个服务端程序
      • 优点
        • 画面可用做的精美, 用户体验好
      • 缺点
        • 需要开发客户端, 也要开发服务端
        • 用户需要下载和更新时麻烦
    • B/S: Browser/Server 浏览器/服务器

      • 只需要一个浏览器, 用户通过不同的网址, 客户访问不同的服务器
      • 优点
        • 不需要开发客户端, 只需要页面 + 服务端
        • 用户不需要下载, 打开浏览器就能使用
      • 缺点
        • 传输数据都需要网络, 如果应用过大, 用户体验受影响
    • 客户端, 浏览器都是展示数据; 核心业务逻辑在服务器

三要素

  1. 确定对方电脑在互联网上的地址 - IP
  2. 确定接收数据的软件 - 端口号
  3. 确定网络传输的规则 - 协议
  • IP
    • 设备在网络中的地址, 是唯一的标识
  • 端口号
    • 应用程序在设备中唯一的标识
  • 协议
    • 数据在网络中传输的规则, 常见的协议有 UCP, TCP, http, https, ftp 等

IP

Internet Protocol, 互联网协议地址, 是分配给上网设备的数字标签

常见 IP 分类: IPv4, IPv6

  • IPv4 互联网通信协议第四版

    • 采用 32 位地址长度, 分成 4 组
    32 bit (4 字节)
    11000000 10101000 00000001 01000010
    192.168.1.66 // 点分十进制表示法
    
    • 数量不够用
  • IPv6 互联网通信协议第六版

    • 采用 128 位地址长度, 分成 8 组
    2001:0D88:0000:0023:0008:0800:200C:417A // 冒分十六进制表示法
    2001:D88:0:23:8:800:200C:417A // 省略前面的 0
    FF01:0:0:0:0:0:0:1101 -> FF01::1101 // 0 位压缩表示法
    
  • IPv4 的地址分类形式

    • 公网地址 (万维网使用) 和私有地址 (局域网使用)
    • 192.168. 开头的为私有地址, 范围即为 192.168.0.0 -- 192.165.255.255, 专门为组织机构内部使用, 以此节省 IP
      • 比如网吧中电脑共用一个公网 IP, 再由路由器分配局域网 IP
    • 特殊 IP 地址 localhost: 127.0.0.1
      • (本地) 回送地址, 也称本地 IP, 永远只会寻找当前所在本机
      • 往 127.0.0.1 发送数据, 不用经过路由器
  • 常见 CMD

    • ipconfig - 查看本机 IP 地址
    • ping + xxx - 检查与 xxx 之间网络是否连通
  • InetAddress

    // 获取对象
    // IP 的对象 -> 电脑的对象
    InetAddress address = InetAddress.getByName("192.168.1.100");
    System.out.println(address);
    
    String name = address.getHostName();
    System.out.println(name);
    
    String ip = address.getHostAddress();
    System.out.println(ip);
    

端口号

应用程序在设备中唯一的标识

由两个字节表示的整数, 取值范围: 0 ~ 65535, 其中 0~1023 用于一些知名的网络服务或者应用, 我们自己使用 1024 以上的端口号就可以了

注意: 一个端口号只能被一个应用程序使用

协议

计算机网络中, 连接和通信的规则被称为网络通信协议

  • TCP/IP 参考模型

    • 应用层 - HTTP, FTP, DNS ...
    • 传输层 - TCP, UDP ...
    • 网络层 - IP, ICMP, ARP
    • 物理链路层 - 硬件设备 01010101
  • UDP 协议, 用户数据协议

    • 面向无连接通信协议 (不管是否已经连接成功)
    • 速度快, 有大小限制, 一次最多发送 64 K, 数据不安全, 易丢失数据
  • TCP 协议, 传输控制协议

    • 面向连接通信协议 (先确保连接成功)
    • 速度慢, 没有大小限制, 数据安全

UDP 通信程序

实现

  • 发送数据
    • 创建发送端 DatagramSocket 对象
    • 数据打包 DatagramPacket
    • 发送数据
    • 释放资源
package Inet;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class InetDemo2 {
    public static void main(String[] args) throws IOException {
        
        // 空参: 所有可用的端口中随机一个
        // 有参: 指定端口号绑定
        DatagramSocket ds = new DatagramSocket();

        String str = "Hello";
        byte[] bytes = str.getBytes();

        InetAddress address = InetAddress.getByName("127.0.0.1");

        int port = 10086;

        // 字节数组, 长度, (地址), (端口)
        DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
    
        ds.send(dp);

        ds.close();
    }
}
  • 接收数据
    • 创建接收端的 DatagramSocket 对象
    • 接收打包好的数据
    • 解析数据包
    • 释放资源
package Inet;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class InetDemo3 {
    public static void main(String[] args) throws IOException {
        
        // 接收时, 一定要绑定端口, 且与发送的端口保持一致
        DatagramSocket ds = new DatagramSocket(10086);
        
        byte[] bytes = new byte[1024];
        DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
        
        // 阻塞
        // 程序执行到这里, 会等待发送端发送消息
        ds.receive(dp);

        byte[] data = dp.getData();
        int len = dp.getLength();
        InetAddress address = dp.getAddress();
        int port = dp.getPort();

        System.out.println("接收到数据" + new String(data , 0, len));
        System.out.println("该数据是从" + address + "这台电脑中的" + port + "这个端口发出的");
        
        ds.close();
    }
}
  • 先运行接收端, 再运行发送端, 可接收到数据, 然后接收端程序运行结束

练习

发送数据来自键盘录入, 直到输入数据是 886, 发送数据结束, 同样接收数据结束

  • 发送
package Inet;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;

public class InetDemo4 {
    public static void main(String[] args) throws IOException {

        Scanner sc = new Scanner(System.in);
        
        DatagramSocket ds = new DatagramSocket();

        InetAddress address = InetAddress.getByName("127.0.0.1");
        int port = 10086;
        
        while (true) {
            String str = sc.nextLine();

            if ("886".equals(str)) {
                byte[] bytes = "STOP".getBytes();
                DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
                ds.send(dp);
                break;
            }

            byte[] bytes = str.getBytes();
            DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
            ds.send(dp);
        }

        ds.close();
        sc.close();
    }
}
  • 接收
package Inet;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class InetDemo5 {
    public static void main(String[] args) throws IOException {
        
        DatagramSocket ds = new DatagramSocket(10086);

        byte[] bytes = new byte[1024];
        DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
        
        while (true) {
            ds.receive(dp);

            byte[] data = dp.getData();
            int len = dp.getLength();
            String ip = dp.getAddress().getHostAddress();
            String name = dp.getAddress().getHostName();
            String str = new String(data, 0, len);

            if ("STOP".equals(str)) {
                break;
            }

            System.out.println("ip 为: " + ip + ", 主机名称为: " + name + " 的人, 发送了数据: " + str);
        }

        ds.close();
    }
}

三种通信方式

  • 单播

    • 一对一
  • 组播

    • 一对一组
    • 组播地址: 224.0.0.0 ~ 230.255.255.255, 其中 224.0.0.0 ~ 224.0.0.255 为预留的组播地址; 一个组播地址是多台电脑
    • 发送
    package Inet;
    
    import java.io.IOException;
    import java.net.DatagramPacket;
    import java.net.InetAddress;
    import java.net.MulticastSocket;
    
    public class InetDemo7 {
        public static void main(String[] args) throws IOException {
            
            MulticastSocket ms = new MulticastSocket();
    
            String s = "你好";
            byte[] bytes = s.getBytes();
            InetAddress address = InetAddress.getByName("224.0.0.1");
            int port = 10000;
    
            DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
    
            ms.send(dp);
    
            ms.close();
        }
    }
    
    • 接收 (打开多个接收端都可以接收到)
    package Inet;
    
    import java.io.IOException;
    import java.net.DatagramPacket;
    import java.net.InetAddress;
    import java.net.MulticastSocket;
    
    public class InetDemo6 {
        @SuppressWarnings("deprecation")
        public static void main(String[] args) throws IOException {
            
            MulticastSocket ms = new MulticastSocket(10000);
    
            // 将当前本机, 添加到 224.0.0.1 的这一组中
            InetAddress address = InetAddress.getByName("224.0.0.1");
            ms.joinGroup(address); // 已过时
    
            byte[] bytes = new byte[1024];
            DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
    
            ms.receive(dp);
    
            byte[] date = dp.getData();
            int len = dp.getLength();
            String ip = dp.getAddress().getHostAddress();
            String name = dp.getAddress().getHostName();
    
            System.out.println("ip 为: " + ip + ", 主机名称为: " + name + " 的人, 发送了数据: " + new String(date, 0, len));
    
            ms.close();
        }
    }
    
  • 广播

    • 一对所有
    • 广播地址: 255.255.255.255, 局域网内所有电脑
    • 实现
    // 将单播 "127.0.0.1" 改为 "255.255.255.255"  即可
    
    // 单播
    InetAddress address = InetAddress.getByName("127.0.0.1");
    
    // 广播
    InetAddress address = InetAddress.getByName("255.255.255.255");
    

TCP 通信程序

通信两端各创建一个 Socket 对象, 通信之前要保证连接已经建立, 通过 Socket 产生 IO 流来进行网络通信

  • 客户端

    • 创建 Socket 对象与指定服务端连接
    • 获取输出流, 写数据
    • 释放资源
    package Inet;
    
    import java.io.IOException;
    import java.io.OutputStream;
    import java.net.Socket;
    
    public class InetDemo8 {
        public static void main(String[] args) throws IOException {
            
            Socket socket = new Socket("127.0.0.1", 10000);
            
            OutputStream os = socket.getOutputStream();
    
            os.write("aaa".getBytes());
    
            os.close();
            socket.close();
        }
    }
    
  • 服务端

    • 创建 Socket 对象
    • 监听客服端连接, 返回一个 Socket 对象
    • 获取输入流, 读数据, 并把数据显示到控制台
    • 释放资源
    package Inet;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    public class InetDemo9 {
        public static void main(String[] args) throws IOException {
            
            ServerSocket ss = new ServerSocket(10000);
    
            Socket socket = ss.accept();
    
            // 这样不能接收中文
            {
                InputStream is = socket.getInputStream();
    
                int b;
                while ((b = is.read()) != -1) {
                    System.out.print((char)b);
                }
            }
    
            // 使用转换流才能接收中文
            {
                InputStream is = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(is);
    
                int b;
                while ((b = isr.read()) != -1) {
                    System.out.print((char)b);
                }
            }
    
            socket.close();
            ss.close();
        }
    }
    
  • 先运行服务端, 服务端会在 accept 方法等待, 直至客户端来连接

  • 客户端创建对象的同时会连接服务端

    • 三次握手协议保证连接建立
      • 客户端向服务端发出连接请求, 等待服务端确认
      • 服务端向客户端返回一个响应, 告诉客户端收到了请求
      • 客服端向服务端再次发出确认信息, 连接建立
    • 如果连接不上, 代码会报错
  • 四次挥手协议: 客户端释放资源时要断开连接并保证连接通道里的数据已经处理完毕了

    • 客服端向服务端发出取消连接请求
    • 服务端向客户端返回一个响应, 表示收到客户端取消请求
    • 服务器将数据处理完毕后, 服务端向客户端发出确认取消信息
    • 客户端再次发送确认消息, 连接取消

端口占用

原因尚不清楚, 解决方案如图

练习

练习 1

服务端回写数据给客户端

package Inet.Practice1;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws IOException {
        
        ServerSocket ss = new ServerSocket(10000);

        Socket socket = ss.accept();

        InputStreamReader isr = new InputStreamReader(socket.getInputStream());

        int b;
        // read 方法会从连接通道中读取数据
        // 但需要一个结束标记, 循环才会停止
        // 否则, 程序会一直停在这里
        while ((b = isr.read()) != -1) {
            System.out.print((char)b);
        }

        // 回写给客服端
        OutputStream os = socket.getOutputStream();
        os.write("received".getBytes());

        socket.close();
        ss.close();
    }
}
package Inet.Practice1;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

public class Client {
    public static void main(String[] args) throws IOException {
        
        Socket socket = new Socket("127.0.0.1", 10000);
        
        Scanner sc = new Scanner(System.in);

        OutputStream os = socket.getOutputStream();
        os.write("hello".getBytes());

        // 写出一个结束标记
        socket.shutdownOutput();

        // 接收服务端回写的数据
        InputStreamReader isr = new InputStreamReader(socket.getInputStream());

        int b;
        while ((b = isr.read()) != -1) {
            System.out.print((char)b);
        }        
        
        sc.close();
        socket.close();
    }
}

练习 2

客户端: 将本地文件上传到服务器, 接收服务器的反馈
服务器: 接收客户端上传的文件, 上传完毕之后给出反馈

package Inet.Practice2;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws IOException {
        
        ServerSocket ss = new ServerSocket(10000);

        Socket socket = ss.accept();

        BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());

        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\temp.jpg"));
        
        int len;
        byte[] bytes = new byte[1024];
        while ((len = bis.read(bytes)) != -1) {
            bos.write(bytes, 0, len);
        }
        bos.flush(); // 强制将这些残留的数据写入到目标文件中,确保文件内容的完整性

        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        bw.write("submit successfully");
        bw.newLine();
        bw.flush();

        bos.close();

        socket.close();
        ss.close();
    }
}
package Inet.Practice2;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;

public class Client {
    public static void main(String[] args) throws IOException {
        
        Socket socket = new Socket("127.0.0.1", 10000);
        
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\java_note\\images\\20250120181243.png"));

        BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());

        byte[] bytes = new byte[1024];
        int len;
        while ((len = bis.read(bytes)) != -1) {
            bos.write(bytes, 0, len);
        }
        bos.flush(); // 强制将这些残留的数据写入到目标文件中,确保文件内容的完整性

        socket.shutdownOutput();

        BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        System.out.println(br.readLine());

        bis.close();
        socket.close();
    }    
}
  • 如果想随机生成在服务端保存文件的文件名
    String name = UUID.randomUUID.toString().replace("-", "");
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\" + name + ".jpg"));
    

练习 3

服务器不停止, 能接收很多用户上传的图片

  • 问题: 如果直接在服务端死循环, 可能上一个客户上传的文件太大, 还没上传完第二个客户就在上传了, 但是无法建立连接

  • 解决方案: 采用多线程

package Inet.Practice3;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws IOException {

        @SuppressWarnings("resource")
        ServerSocket ss = new ServerSocket(10000);

        while (true) {
            Socket socket = ss.accept();

            new Thread(new Client(socket));
        }

        // 没有结束条件
        // 没写 ss.close(), 而且写在死循环外面会编译错误
        // 只能加了个 @SuppressWarnings("resource")
    }
}
package Inet.Practice3;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.Socket;

import cn.hutool.core.lang.UUID;

public class Client implements Runnable {

    Socket socket;

    public Client(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
            String name = UUID.randomUUID().toString().replace("-", "");
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\" + name + ".jpg"));
            int len;
            byte[] bytes = new byte[1024];
            while ((len = bis.read(bytes)) != -1) {
                bos.write(bytes,0, len);
            }
            bos.flush();

            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            bw.write("submit successfully");
            bw.newLine();
            bw.flush();

            bos.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }    
}
  • 如果使用线程池
package Inet.Practice3;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Server {
    public static void main(String[] args) throws IOException {

        @SuppressWarnings("resource")
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
            3, // 核心线程数量
            16, // 线程池总大小
            60, // 空闲时间 (值)
            TimeUnit.SECONDS, // 空闲时间 (单位)
            new ArrayBlockingQueue<>(2), // 阻塞队列
            Executors.defaultThreadFactory(), // 线程工厂
            new ThreadPoolExecutor.AbortPolicy() // 任务拒绝策略
        );

        @SuppressWarnings("resource")
        ServerSocket ss = new ServerSocket(10000);

        while (true) {
            Socket socket = ss.accept();

            pool.submit(new Client(socket));
        }
    }
}
posted @ 2025-01-28 01:55  wxgmjfhy  阅读(30)  评论(0)    收藏  举报