Java基础进阶篇-第四天

58 阅读9分钟

多线程

创建线程的三种方式

  • 继承Thread类:线程无法继承其他类,不利于功能的扩展
  • 实现Runnable接口:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强
  • 实现Callable接口:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果
// 方式一
class MyThread extends Thread{
    // 重写run方法,设置线程任务
    @Override
    public void run() {
        XXX
    }
}
MyThread t = new MyThread();
t.start();

// 方法二
class MyRun implements Runnable {
    @Override
    public void run() {
        XXX
    }
}
Runnable t = new MyRun();
Thread ts = new Thread(t);
ts.start();

// 方法三
class MyCall implements Callable<Integer> {
    private Integer n;
    public Thread2(Integer n){
        this.n = n;
    }
    @Override
    public Integer call() throws Exception {
        int num = 0;
        XXX
        return num;
    }
}
Callable<Integer> call = new MyCall(10);
FutureTask<Integer> ft = new FutureTask<>(call); // 将Callable包装成FutureTask类型
Thread ts = new Thread(ft);
ts.start();

int result = ft.get(); // 获取任务类返回的结果
System.out.println(result);

**new FutureTask<>(call).get():**如果任务尚未完成ft.get()方法会阻塞当前线程,直到任务执行完成。如果任务尚未完成,ft.get()方法会阻塞当前线程,直到任务执行完成。

Callable/Runnable/Thread关系图

Thread线程.png

Thread常用方法

Thread提供的常见构造器说明
public Thread(String name)可以为当前线程指定名称
public Thread(Runnable target)封装Runnable对象成为线程对象
public Thread(Runnable target, String name)封装Runnable对象成为线程对象,并指定线程名称
Thread提供的常用方法说明
public void run()线程的任务方法
public void start()启动线程
public String getName()获取当前线程的名称,线程名称默认是Thread-索引
public void setName(String name)为线程设置名称
public static Thread currentThread()获取当前执行的线程对象
public static void sleep(long time)让当前执行的线程休眠多少毫秒后,再继续执行
public final void join()...让调用当前这个方法的线程先执行完!
public class Test1 {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        Thread s = new Thread(t,"飞机");
        s.run(); // 执行线程任务
        s.setName("线程1"); // 设置线程名称 
        s.start(); // 启动线程
    }
}
class MyThread implements Runnable {
    @Override
    public void run() {
        Thread t1 = Thread.currentThread(); // currentThread静态方法,返回当前执行的线程对象
        for(int i=0;i<5;i++){
            System.out.println(t1.getName()+"线程-->"+i);
            try {
                t1.sleep(1000); // 线程休眠方法
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

线程同步三种方法

线程安全:多线程、共享资源、修改资源

同步代码块

  • 对于实例方法建议使用this作为锁对象
  • 对于静态方法建议使用字节码(类名.class)对象作为锁对象
synchronized (this) { 异常核心代码块 }
synchronized (xx.class) { 异常核心代码块 }

同步方法

public synchronized void funcation() {
	XXX
}
public synchronized static void funcation() {
    XXX
}

lock锁

public class Account {
    private final Lock lk = new ReentrantLock(); // ReentrantLock为Lock的实现类
    public void funcation() {
        lk.lock(); // 获得锁
        try {
            XXX
        } finally {
            // 保证 lk 锁一定会得到释放
            lk.unlock(); // 释放锁
        }
    }
}

线程池

线程池就是一个可以复用线程的技术

线程池.png

线程池创建的两种方式
  • 使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。(推荐不使用)

**注意 :**这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象。

方法名称说明
public static ExecutorService newFixedThreadPool(int nThreads)创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。
public static ExecutorService newSingleThreadExecutor()创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。
public static ExecutorService newCachedThreadPool()线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了60s则会被回收掉。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。
public class ExecutorTest {
    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(4); // 最大线程数量等于核心线程数量
        ExecutorService poo12 = Executors.newSingleThreadExecutor(); // 最大线程数量和核心线程数量为1
        ExecutorService pool3 = Executors.newCachedThreadPool(); 
        // 最大线程数量为Integer.MAX_VALUE,核心线程数量为0
        ScheduledExecutorService pool4 = Executors.newScheduledThreadPool(4); // 可以延迟执行任务
        Future<String> f1 = pool.submit(new MyCallable2());
        Future<String> f2 = pool.submit(new MyCallable2());
        Future<String> f3 = pool.submit(new MyCallable2());
        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
    }
}
class MyCallable2 implements Callable<String> {
    @Override
    public String call() throws Exception {
        return Thread.currentThread().getName();
    }
}
  • 使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。
参数1:指定线程池的核心线程的数量;
参数2:指定线程池的最大线程数量;
参数3:指定临时线程的存活时间;(空闲线程的最大存活时间)
参数4:指定临时线程存活的时间单位(秒、分、时、天);
参数5:指定线程池的任务队列;
参数6:指定线程池的线程工厂;
参数7:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理);
ExecutorService executor =  new ThreadPoolExecutor(3,5,10,TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());

// 三个核心线程
executor.execute(new MyRunnable());
executor.execute(new MyRunnable());
executor.execute(new MyRunnable());

// 阻塞队列长度为3
executor.execute(new MyRunnable());
executor.execute(new MyRunnable());
executor.execute(new MyRunnable());

// 两个临时线程:当核心线程忙且阻塞队列满,则会启用临时线程
// 临时线程存在期间不断尝试获取队列中的任务,当临时线程空闲时间超过存活时间则会销毁
executor.execute(new MyRunnable());
executor.execute(new MyRunnable());

// 触发任务拒绝策略,当核心和临时线程忙且阻塞队列满
executor.execute(new MyRunnable());

executor.shutdown(); // 等任务都执行完,再关闭线程池
executor.shutdownNow(); // 立刻关闭线程池,不再接受新任务,打断正在进行的任务
                  
// 处理callable任务        
Future<String> f1 = executor.submit(new MyCallable());
try {
	System.out.println(f1.get());
} catch (Exception e) {
	throw new Exception(e);
}
线程池常用方法
方法名称说明
void execute(Runnable command)执行 Runnable 任务
Future submit(Callable task)执行 Callable 任务,返回未来任务对象,用于获取线程返回的结果
void shutdown()等全部任务执行完毕后,再关闭线程池!
List shutdownNow()立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务
线程池注意事项

**新建临时线程的时机:**新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。

**拒绝新任务:**核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。

任务拒绝策略
策略说明
ThreadPoolExecutor.AbortPolicy()丢弃任务并抛出RejectedExecutionException异常。是默认的策略
ThreadPoolExecutor. DiscardPolicy()丢弃任务,但是不抛出异常,这是不推荐的做法
ThreadPoolExecutor. DiscardOldestPolicy()抛弃队列中等待最久的任务 然后把当前任务加入队列中
ThreadPoolExecutor. CallerRunsPolicy()由主线程负责调用任务的run()方法从而绕过线程池直接执行

并发/并行

进程

  • 运行的程序就是一个进程
  • 线程属于进程,一个进程可以同时运行多个线程
  • 进程中的多个线程其实是并行和并发同时执行的

**并发:**CPU分时轮询的执行线程

**并行:**同一个时刻同时在执行

Java网络编程

基本的通信架构:CS架构( Client客户端/Server服务端 ) 、 BS架构(Browser浏览器/Server服务端)

网络通信三要素:IP地址;端口;协议

端口用来标记标记正在计算机设备上运行的应用程序,被规定为一个16位的二进制,范围是0~65535

端口分类

  • 周知端口:0~1023,被预先定义的知名应用占用(如:HTTP占用80,FTP占用21)
  • 注册端口:1024~49151,分配给用户进程或某些应用程序。
  • 动态端口:49152~65535,之所以称为动态端口,是因为它一般不固定分配某种进程,而是动态分配。
  • 注意:我们自己开发的程序一般选择使用注册端口,且一个设备中不能出现两个程序的端口号一样,否则报错。

IP地址分为IPv4和IPv6

IPv4使用32位地址,通常以点分十进制表示:地址分四段,每段用一个十进制数表示,每段用点分开

IPv6使用128位地址,使用冒分十六进制表示:地址分为八段,每段每四位编码成一个十六进制位表示, 每段之间用冒号(:)分开

IP域名:互联网中识别和定位网站的人类可读的名称

DNS域名解析:互联网中用于将域名转换为对应IP地址的分布式命名系统

公网IP是可以连接网络的IP地址;内网IP也称局域网,是只能组织结构内部使用的的IP地址

InetAddress常用方法

InetAddress id = InetAddress.getLocalHost(); // 获取本机IP,返回一个InetAddress对象
System.out.println(id.getHostName()); // 获取IP对象对应的主机名
System.out.println(id.getHostAddress()); // 获取IP对象对应的ip地址信息

InetAddress id1 = InetAddress.getByName("www.baidu.com"); // 根据ip或域名获取IP对象

System.out.println(id1.isReachable(5000));// 判断本机与id1是否可以通信

网络模型

网络模型.png

通信协议

计算机中,连接和通信数据规则被成为网络通信协议

UDP协议

  • 用户数据报协议
  • UDP面向无连接,不可靠传输的通信协议
  • 速度快,有大小限制一次最多发送64K,数据不安全,易丢失数据

TCP协议(全双工)

  • TCP是一种面向连接的可靠通信协议
  • 传输前,采用三次握手方式建立连接,点对点的通信
  • 在连接中可进行大数据量的传输
  • 传输后,采用四次挥手方式断开连接,确保消息全部收发完毕
  • 通信效率相对较低,可靠性相对较高

UDP通信的实现

UDP通信实现.png

// 多收多发:只需对主要收发动作进行while死循环
// Client
DatagramSocket socket = new DatagramSocket();
InetAddress id = InetAddress.getLocalHost();

byte[] bytes = "我爱中国".getBytes();
DatagramPacket packet = new DatagramPacket(bytes,bytes.length,id,9999);

socket.send(packet);
socket.close();

// Server
DatagramSocket socket = new DatagramSocket(9999);

byte[] bytes = new byte[1024*64];
DatagramPacket packet = new DatagramPacket(bytes,bytes.length);

socket.receive(packet);

int len = packet.getLength();
String str = new String(packet.getData(),0,len);
System.out.println(str);

System.out.print(packet.getAddress().getHostAddress()+":");
System.out.println(packet.getPort());

socket.close();

TCP通信的实现

TCP通信实现.png

// 多收多发:将收发的动作使用while死循环
// Server接收多个客户端:while死循环负责接收客户端的Socket连接,每接收到一个Socket通信管道后分配给一个独立的线程进行处理
// 高并发时,每次创建新线程容器容易宕机,所以可以使用线程池进行优化

// Client
InetAddress inet = InetAddress.getLocalHost();
Socket socket = new Socket(inet,8787);

OutputStream os = socket.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);

dos.writeUTF("黑马程序员");

dos.close();
socket.close();

// Server
ServerSocket serverSocket = new ServerSocket(8787);
Socket socket = serverSocket.accept();

InputStream is = socket.getInputStream();
DataInputStream in = new DataInputStream(is);

System.out.println(in.readUTF());

in.close();
socket.close();