4.6 多线程高级&网络编程

1、线程状态

当线程被创建并启动以后,它并不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态。那么Java中的线程存在哪几种状态呢?

image

Java中的线程状态被定义在了java.lang.Thread.State枚举类中,State枚举类的源码如下:

public class Thread {

    public enum State {

        /* 新建 */
        NEW , 

        /* 可运行状态 */
        RUNNABLE , 

        /* 阻塞状态 */
        BLOCKED , 

        /* 无限等待状态 */
        WAITING , 

        /* 计时等待 */
        TIMED_WAITING , 

        /* 终止 */
        TERMINATED;

    }

    // 获取当前线程的状态
    public State getState() {
        return jdk.internal.misc.VM.toThreadState(threadStatus);
    }

}

通过源码我们可以看到Java中的线程存在6种状态,每种线程状态的含义如下

线程状态

具体含义

NEW(新建)

创建线程对象

RUNNABLE(就绪)

start 方法被调用,但是还没有抢到CPU执行权

BLOCKED(阻塞)

线程开始运行,但是没有获取到锁对象

WAITING(等待)

wait 方法

TIMED_WAITING(计时等待)

sleep 方法

TERMINATED(结束状态)

代码全部运行完毕

2、线程池

介绍

提到池,大家应该能想到的就是水池,池子中可以养东西

而线程池,指的也是一种池子,只不过里面养的都是线程。

问题:

为什么我们需要将线程,交给线程池进行管理呢?

线程池存在的意义:

系统创建一个线程的成本是比较高的

因为它涉及到与操作系统交互,当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程

对系统的资源消耗有可能大于业务处理是对系统资源的消耗,这样就有点”舍本逐末”了。

针对这一种情况,为了提高性能,我们就可以采用线程池。

线程池在启动的时,会创建大量空闲线程,当我们向线程池提交任务的时,线程池就会启动一个线程来执行该任务。

等待任务执行完毕以后,线程并不会死亡,而是再次返回到线程池中变成为空闲状态。

等待下一次任务的执行。

总结:

将线程对象交给线程池维护, 可以降低系统成本, 从而提升程序的性能

线程池使用

JDK 对线程池也进行了相关的实现,我们可以使用Executors中所提供的静态方法来创建线程池

方法

介绍

static ExecutorService newCachedThreadPool ( )

创建一个默认的线程池

static newFixedThreadPool ( int nThreads )

创建一个指定最多线程数量的线程池

  • 代码实现(默认线程池)
package com.itheima.mythreadpool;

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

public class MyThreadPoolDemo {
    public static void main(String[] args) throws InterruptedException {

        // 1,创建一个默认的线程池对象.池子中默认是空的.默认最多可以容纳int类型的最大值.
        ExecutorService executorService = Executors.newCachedThreadPool();
        // Executors --- 可以帮助我们创建线程池对象
        // ExecutorService --- 可以帮助我们控制线程池

        executorService.submit(()->{
            System.out.println(Thread.currentThread().getName() + "在执行了");
        });

        // Thread.sleep(2000);

        executorService.submit(()->{
            System.out.println(Thread.currentThread().getName() + "在执行了");
        });

        executorService.shutdown();
    }
}
  • 代码实现(指定最多线程数量)
package com.itheima.mythreadpool;

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

public class MyThreadPoolDemo2 {
    public static void main(String[] args) {
        // 参数不是初始值而是最大值
        ExecutorService executorService = Executors.newFixedThreadPool(10);


        executorService.submit(()->{
            System.out.println(Thread.currentThread().getName() + "在执行了");
        });

        executorService.submit(()->{
            System.out.println(Thread.currentThread().getName() + "在执行了");
        });

    }
}

注意:

在《阿里巴巴java开发手册》中指出了线程资源必须通过线程池提供,不允许在应用中自行显示的创建线程

这样一方面是线程的创建更加规范,可以合理控制开辟线程的数量;

另一方面线程的细节管理交给线程池处理,优化了资源的开销。

而线程池不允许使用Executors去创建,而要通过 ThreadPoolExecutor 方式


ThreadPoolExecutor

构造方法

构造方法

描述

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

用给定的初始参数创建一个新的 ThreadPoolExecutor

参数详解:

  • 参数一:核心线程数量
  • 参数二:最大线程数
  • 参数三:空闲线程最大存活时间
  • 参数四:时间单位
  • 参数五:任务队列
  • 参数六:创建线程工厂
  • 参数七:任务的拒绝策略

任务队列

  1. 有界队列:new ArrayBlockingQueue<>(10)
  2. 无界队列:new LinkedBlockingQueve<>() tips:并不是真正的无界,最大支特Integer.MAX_VALUE;

拒绝策略:

new ThreadPoolExecutor.AbortPolicy():拒绝新提交的任务,抛出异常对象

代码实现:

package com.itheima.mythreadpool;

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

public class MyThreadPoolDemo3 {
    public static void main(String[] args) {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
            2,
            5,
            2,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(10),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy());

        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());

        pool.shutdown();
    }
}

参数详解:

image

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

corePoolSize:   核心线程的最大值,不能小于0
maximumPoolSize:最大线程数,不能小于等于0,maximumPoolSize >= corePoolSize
keepAliveTime:  空闲线程最大存活时间,不能小于0
unit:           时间单位
workQueue:      任务队列,不能为null
threadFactory:  创建线程工厂,不能为null      
handler:        任务的拒绝策略,不能为null

线程池-非默认任务拒绝策略

RejectedExecutionHandler 是 jdk 提供的一个任务拒绝策略接口,它下面存在4个子类。

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

注:明确线程池最多可执行的任务数 = 队列容量 + 最大线程数

案例演示1:演示ThreadPoolExecutor.AbortPolicy任务处理策略

public class ThreadPoolExecutorDemo01 {

    public static void main(String[] args) {

        /**
         * 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            1 ,		// 核心线程数:平均值
            3 ,		// 最大线程数:平均值 * 1.5
            20 ,
            TimeUnit.SECONDS ,
            new ArrayBlockingQueue<>(1) ,	// 最大线程数 * 2
            Executors.defaultThreadFactory() , 
            new ThreadPoolExecutor.AbortPolicy()) ;

        // 提交5个任务,而该线程池最多可以处理4个任务,当我们使用AbortPolicy这个任务处理策略的时候,就会抛出异常
        for(int x = 0 ; x < 5 ; x++) {
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "---->> 执行了任务");
            });
        }
    }
}

控制台输出结果

pool-1-thread-1---->> 执行了任务
pool-1-thread-3---->> 执行了任务
pool-1-thread-2---->> 执行了任务
pool-1-thread-3---->> 执行了任务

Exception in thread "main" java.util.concurrent.RejectedExecutionException

控制台报错,仅仅执行了4个任务,有一个任务被丢弃了

案例演示2:演示ThreadPoolExecutor.DiscardPolicy任务处理策略

public class ThreadPoolExecutorDemo02 {
    public static void main(String[] args) {
        /**
         * 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
                                                                       new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.DiscardPolicy()) ;

        // 提交5个任务,而该线程池最多可以处理4个任务,当我们使用DiscardPolicy这个任务处理策略的时候,控制台不会报错
        for(int x = 0 ; x < 5 ; x++) {
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "---->> 执行了任务");
            });
        }
    }
}

控制台输出结果

pool-1-thread-1---->> 执行了任务
pool-1-thread-1---->> 执行了任务
pool-1-thread-3---->> 执行了任务
pool-1-thread-2---->> 执行了任务

控制台没有报错,仅仅执行了4个任务,有一个任务被丢弃了

案例演示3:演示ThreadPoolExecutor.DiscardOldestPolicy任务处理策略

public class ThreadPoolExecutorDemo02 {
    public static void main(String[] args) {
        /**
         * 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
         */
        ThreadPoolExecutor threadPoolExecutor;
        threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
                                                    new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.DiscardOldestPolicy());
        // 提交5个任务
        for(int x = 0 ; x < 100 ; x++) {
            // 定义一个变量,来指定指定当前执行的任务;这个变量需要被final修饰
            final int y = x ;
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "---->> 执行了任务" + y);
            });     
        }
    }
}

控制台输出结果

pool-1-thread-2---->> 执行了任务3
pool-1-thread-1---->> 执行了任务1
pool-1-thread-3---->> 执行了任务4
pool-1-thread-1---->> 执行了任务100

案例演示4:演示ThreadPoolExecutor.CallerRunsPolicy任务处理策略

public class ThreadPoolExecutorDemo04 {
    public static void main(String[] args) {

        /**
         * 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
         */
        ThreadPoolExecutor threadPoolExecutor;
        threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
                                                    new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.CallerRunsPolicy());

        // 提交5个任务
        for(int x = 0 ; x < 5 ; x++) {
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "---->> 执行了任务");
            });
        }
    }
}

控制台输出结果

pool-1-thread-1---->> 执行了任务
pool-1-thread-3---->> 执行了任务
pool-1-thread-2---->> 执行了任务
pool-1-thread-1---->> 执行了任务
main---->> 执行了任务

通过控制台的输出,我们可以看到次策略没有通过线程池中的线程执行任务,而是直接调用任务的run()方法绕过线程池直接执行。

面试题 :

1. 临时线程什么时候创建
新任务提交时发现核心线程都在忙 ( 任务队列也满了 )
此时创建新的线程
public class Demo {
    public static void main(String[] args) {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
            2,      
            3,  
            60,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(10), 
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.CallerRunsPolicy()
        );


        // 只提交4个线程任务, 队列没装满
        for(int i = 1; i <= 5; i++){
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "线程任务");
                }
            });
        }
    }
}
pool-1-thread-1线程任务
pool-1-thread-2线程任务
pool-1-thread-1线程任务
pool-1-thread-2线程任务
2. 什么时候会开启拒绝策略

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

细节补充 :

提交线程任务的方式 :

方法名

void execute(Runnable command)

Future<T> submit(Callable<T> task)

推荐使用 submit , 因为能够同时接收 Runnable 和 Callable

3、单例设计模式

  • 设计模式介绍:
    • 在软件开发中经常会遇到一些问题,这些问题如何解决,前人总结了一些好的方法,这些方法就称之为设计模式。
    • 用设计模式去解决这些问题,可以使代码的安全性、重用性、扩展性更强。
    • 简单来说:就是一套好的编码风格,前人总结出的一种套路
  • 单例设计模式介绍:
    • 保证类的对象在内存中,只有一个对象
    • 举例:
      • Windows任务管理器
      • 回收站
      • 网站的计数器对象

饿汉式 :

  1. 私有化构造方法
  2. 定义一个 public static final 修饰的常量,常量值就是对象
  3. 在外界直接调用这个常量
写法一:
  1. 私有化构造方法
  2. 定义一个 public static final 修饰的常量,常量值就是对象
  3. 在外界直接调用这个常量即可
public class Single {
    //1.私有化构造方法 --- 不让别人创建对象
    private Single(){}

    // 非常饿, 上来就要吃, 上来不看任何条件, 直接创建对象

    //2.在本类中,自己创建对象
    public static final Single s = new Single();
}
写法二:
  1. 私有化构造方法
  2. 定义一个 private static final 修饰的常量,常量值就是对象
  3. 再定义一个getInstance方法
  4. 在外界直接调用方法即可

代码示例:

public class Single {
    //1.私有化构造方法 --- 不让别人创建对象
    private Single(){}

    //把这个变量定义为私有
    private static final Single s = new Single();

    public static Single getInstance() {
        return s;
    }
}

懒汉式 :

写法一:
  • 弊端 : 多线程操作的时候, 很有可能会出创建多个对象
public class Single {

    private Single(){}

    // 懒汉式
    // 核心:延迟加载
    private static Single s;

    public static Single getInstance() {  
        if(s == null){
            s = new Single();
        }
        return s ;
    }
}
写法二:
  • 弊端 : 效率低
public class Single {

    private Single(){}

    private static Single s ;

    public static Single getInstance() {   

        synchronized (Single.class) {

            if(s == null){

                s = new Single();
            }
        }
    }
    return s ;
    }
}
写法三 :
public class Single {

    private Single(){}

    private static Single s ;

    public static Single getInstance() {   
        // 如果现在对象已经有了,而且,锁关闭了
        // 为了提高代码的效率,所以,我们要在外面,再加一个非空判断
        if(s == null){ // 作用:提高效率
            synchronized (Single.class) {
                if(s == null){ // 作用:保证唯一
                    s = new Single();
                }
            }
        }
        return s ;
    }
}

1、服务器介绍

  • 服务器就是一台配置很高的电脑
  • 服务器常见的场景
    • 通过网址访问网站
    • 登陆
    • 注册
    • 外卖软件
    • 打车软件
    • 只要涉及到网络的操作, 都需要连接服务器

2、互联网架构分类

BS架构 :

  • Browser / Server

优点:不需要下载客户端,使用起来非常方便。

缺点:用户体验比较差

CS架构 :

  • Client / Server

缺点:需要下载客户端,而且每一次要更新内容的时候,都要更新客户端,甚至要重新下载,非常麻烦。

优点:画面非常精美,用户体验比较好。

3、网络编程三要素

1)IP地址

设备在网络中的唯一标识

2)端口

程序在设备中的唯一标识

3)协议

通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样。在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换。常见的协议有 UDP协议 和 TCP协议

image

4、IP地址

IP(Internet Protocol):全称 【互联网协议地址】,是分配给上网设备的唯一标志。

  • 常见的IP分类为:IPv4 和 IPv6

IPV4 :

image

IPV6 :

image

IP的分类

image

ip和域名

image

5、InetAddress

常用方法:

getByName :确定主机名称的IP地址。主机名称可以是机器名称,也可以是IP地址

getHostName:获取主机名

getHostAddress :获取IP

//1.获取一个IP地址(在网络中计算机的对象)
InetAddress address = InetAddress.getByName("127.0.0.1");

//2.获取主机名获取ip地址
//细节:如果能获取到主机名返回的就是主机名
//但是如果有的情况下,获取不到,返回的就是ip地址
String hostName = address.getHostName();
System.out.println(hostName);

//3.获取IP
String ip = address.getHostAddress();
System.out.println(ip);

6、端口号

  • 端口:应用程序在设备中唯一的标识。
  • 端口号:用两个字节表示的整数,它的取值范围是0~65535。其中0~1023之间的端口号用于一些知名的网络服务或者应用。 我们自己使用1024以上的端口号就可以了。
    • 常见的端口号
      • 8080 :tomcat 服务器
      • 3306 :MySQL 数据库
  • 注意:写代码的时候, 注意端口绑定, 不要出现端口冲突

7、UDP协议和TCP协议介绍

UDP :
  • 用户数据报协议(User Datagram Protocol)
  • UDP是面向无连接通信协议。

速度快,有大小限制一次最多发送64K,数据不安全,易丢失数据

TCP :
  • 传输控制协议TCP(Transmission Control Protocol)
  • TCP协议是面向连接的通信协议。速度慢,没有大小限制,数据安全。

8、UDP协议收发数据

发送数据

1 创建发送端对象 : DatagramSocket ds = new DatagramSocket();

2 创建数据包 : DatagramPacket dp = new DatagramPacket(字节数组, 数据个数, 接收端IP, 接收端端口);

3 发送数据 : ds.send(dp);

4 释放资源 : ds.close();

package com.itheima.udp;

import java.net.*;

public class Client {
    /*
        DatagramPacket(byte[] buf, int length, InetAddress address, int port)

                    参数1: 将传输的数据, 转换为字节, 并存入一个数组
                    参数2: 数组的长度
                    参数3: InetAddress(IP对象)
                    参数4: 端口号
     */
    public static void main(String[] args) throws Exception {
        // 1. 创建DatagramSocket对象 (码头)
        DatagramSocket socket = new DatagramSocket();

        // 2. 创建DatagramPacket对象 (包裹对象)
        String content = "你可能说不出哪里好";
        byte[] bytes = content.getBytes();

        DatagramPacket packet =
        new DatagramPacket
        (bytes, bytes.length,
         InetAddress.getByName("127.0.0.1"),
         8888);

        // 3. 调用码头对象的发送方法, 将包裹对象发送出去
        socket.send(packet);

        // 4. 关闭流释放资源
        socket.close();
    }
}

接收数据

1 创建接收端对象 : DatagramSocket ds = new DatagramSocket(端口);

2 创建数据包 : DatagramPacket dp = new DatagramPacket(字节数组, 数组的长度);

3 接收数据 : ds.receive(dp);

4 使用接收的数据 : System.out.println(new String(bys, 0, dp.getLength()));

5 释放资源 : ds.close();

package com.itheima.udp;

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

public class Server {
    public static void main(String[] args) throws IOException {
        // 1. 创建DatagramSocket对象(码头), 绑定端口为8888
        DatagramSocket socket = new DatagramSocket(8888);
        // 2. 创建DatagramPacket对象(包裹对象)
        byte[] bys = new byte[1024];
        DatagramPacket packet = new DatagramPacket(bys, bys.length);
        // 3. 调用码头对象的接受方法, 将数据, 接收到自己的包裹当中
        socket.receive(packet);

        System.out.println(packet.getAddress());

        String s = new String(bys, 0, packet.getLength());
        System.out.println(s);

        // 4. 关闭流释放资源
        socket.close();
    }
}
练习

UDP发送数据:数据来自于键盘录入,直到输入的数据是886,发送数据结束

UDP接收数据:因为接收端不知道发送端什么时候停止发送,故采用死循环接收

操作细节:

需要把发送端运行多次。

IDEA 默认只能运行一次,所以需要进行配置。

需要保证第一个红色箭头是当前要运行多次的类,再点击下面的Edit Configurations,再按照下面的图解进行设置即可

image

image

© 版权声明
THE END
喜欢就支持一下吧
点赞792赞赏 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容