多线程&网络编程

一、实现多线程

1.1 相关概念

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的一条执行路径、实际运作单位。简单理解:应用软件中互相独立,可以同时运行的功能。我们之前编写的代码属于单线程程序。

进程是程序的基本执行实体、正在运行的程序。进程是系统进行资源分配和调用的独立单位,每个进程都有它自己的内存空间和系统资源。进程具有以下特性

  • 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位。
  • 动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的。
  • 并发性:任何进程都可以同其他进程一起并发执行。

多线程就是有多个线程的程序,它可以同时执行多个线程以提高效率,可以在一些比较耗时的操作中应用该技术。如:拷贝大文件、复制传输大文件等。聊天软件、服务器、游戏中也有大量应用。

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

并行:在同一时刻,有多个指令在多个CPU上同时执行。

1.2 实现方式

1.2.1 Thread类

用来表示/操作线程。在Java中创建好的Thread实例,其实和操作系统中的线程是一一对应的关系,操作系统提供了一组关于线程的API(C语言),Thread类是Java对于这组API进一步封装。

  • 方法介绍

    方法名 说明
    启动方法
    void run() 在线程开启后,此方法将被调用执行
    void start() 使此线程开始执行,Java虚拟机会调用此线程的run方法()
    名称/对象相关方法 [点这里](#示例代码 1.1)
    void setName(String name) 将此线程的名称更改为等于参数name
    String getName() 返回此线程的名称
    static Thread currentThread() 返回对当前正在执行的线程对象的引用
    控制方法 [点这里](#示例代码 1.4)
    static void sleep(long milis) 使当前正在执行的线程停留(暂停执行)
    void join() 等待这个线程死亡
    [void setDaemon(boolean on)](#示例代码 1.6) 将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出
    优先级方法 [点这里](#示例代码 1.5)
    final int getPriority() 返回此线程的优先级
    final void setPriority(int newPriority) 更改此线程的优先级(线程默认优先级是5,线程优先级范围是:1-10)

    ​ 给线程设置名称还有带参的构造方法Thread(String name),使用带参构造方法需要先在继承子类中写入无参、String name参数构造方法。

    ​ public static Thread currentThread() 返回对当前正在执行的线程对象的引用

  • 实现步骤

    1. 定义一个类MyThread继承Thread类
    2. 在MyThread类中重写run()方法
    3. 创建MyThread类的对象
    4. 启动线程

示例代码 1.1

/*MyThread类*/
public class MyThread extends Thread{MyThread() {System.out.println("Thread.currentThread()---"+Thread.currentThread().getName());  //构造方法的执行线程为main}@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(this.getName()+"线程开始执行了"+i);}System.out.println("Thread.currentThread()---"+Thread.currentThread().getName());  //获取线程对象,返回当前线程对象的名字}
}
/*测试类*/
public class MyTreadDemo {public static void main(String[] args) {MyThread myThread1 = new MyThread();MyThread myThread2 = new MyThread();myThread1.setName("线程1"); //设置线程名字myThread2.setName("线程2");myThread1.start(); //currentThread.getName == 线程1myThread2.start(); //currentThread.getName 此时为“线程2”//myThread2.run();   //直接调用run时,执行线程为main}
}

线程执行有每次执行都可能不同的特点,因为start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统/CPU决定的。

重写run()方法是因为它是用来封装被线程执行的代码,调用start()方法时会自动执行run()方法。

start()方法和run()方法区别:

run()方法封装线程执行的代码。直接调用时,相当于普通方法,使用currentThread方法返回线程对象为main线程;

start()方法启动线程,然后由JVM调用此线程的run方法,使用currentThread方法返回的线程对象名为对应线程名。同一个线程不能多次执行start()方法

1.2.2 Runnable接口

由于Thread是线程类,需要将方法传入后才能执行。为了将线程和操作方法分离,Java提供了Runnable接口。Runnalbe 只是一个接口,提供了唯一一个方法 run(),可用于多线程中任务的运行定义。

  • Thread构造方法

    Thread(Runnable target) 分配一个新的Thread对象

  • 实现步骤

    1. 定义一个类MyRunnable实现Runnable接口
    2. 在MyRunnable类中重写run()方法
    3. 创建MyRunnable类对象
    4. 创建Thread类对象,把MyRunnable对象作为构造方法的参数
    5. 启动线程

示例代码 1.2

/*MyRunnable*/
public class MyRunnable implements Runnable {@Overridepublic void run() {for (int i = 0; i < 50; i++) {System.out.println(Thread.currentThread().getName() + "开动了" + i);}}
}
/*测试类*/
public class MyRunnableDemo {public static void main(String[] args) {MyRunnable mr = new MyRunnable();MyRunnable mr1 = new MyRunnable();Thread thread = new Thread(mr,"飞机");Thread t = new Thread(mr1,"导弹");thread.start();t.start();}
}

相比继承Thread类,实现Runnable接口避免了Java单继承的局限性,适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码、数据有效分离,较好的体现了面向对象的设计思想。

1.2.3 Callable和FutureTask

继承Thread和实现Runnable接口两种方式重写run()方法都不可以直接返回结果,不适合需要返回线程执行结果的业务场景。

Callable是一个函数式接口,Callable接口接受一个泛型作为接口中call方法的返回值类型。

FutureTask可以把Callable对象封装成线程任务对象以交给Thread处理,线程执行后可以通过FutureTask的get方法去获取任务执行的结果。

  • 方法介绍

    方法名 说明
    V call() 计算结果,如果无法计算结果,则抛出一个异常
    FutureTask(Callable callable) 创建一个FutureTask,一旦运行就执行给定的Callable
    V get() 如有必要,等待计算完成并获取结果
  • 实现步骤

    1. 定义一个MyCallable类实现Callable接口
    2. 在MyCallable类中重写call()方法
    3. 创建MyCallable类对象
    4. 创建FutureTask对象,把MyCallable对象作为构造方法的参数
    5. 创建Thread类的对象,把FutureTask对象作为构造方法的参数
    6. 启动线程
    7. 再调用get方法,就可以获取线程结束后的结果。
  • 注意事项

    get()方法的调用一定要在Thread类的对象调用start()方法之后

示例代码 1.3

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;/*** @author Lenovo*/
public class MyCallable implements Callable<String> {@Overridepublic String call() throws Exception {for (int i = 0; i < 10; i++) {System.out.println("跟妹子表白第" + i + "次");}//返回值就表示线程运行完毕之后的结果return "答应";}public static void main(String[] args) throws ExecutionException, InterruptedException {//线程开启之后需要执行里面的call方法MyCallable mc = new MyCallable();//Thread没有直接接收Callable接口实现类对象的构造方法//Thread t1 = new Thread(mc);//可以获取线程执行完毕之后的结果.也可以作为参数传递给Thread对象FutureTask<String> ft1 = new FutureTask<>(mc);FutureTask<String> ft2 = new FutureTask<>(mc);//创建线程对象Thread t1 = new Thread(ft1);Thread t2 = new Thread(ft2)//String s = ft.get();//开启线程t1.start();t2.start();System.out.println(ft1.get());System.out.println(ft2.get());}
}

1.2.4 Thread的[方法](#1.2.1 Thread类)

  • 设置和获取线程名称
  • 获取当前线程对象
  • 线程控制(睡眠和等待)

示例代码 1.4

public class ThreadDemo {public static void main(String[] args) {MYThread myThread1 = new MYThread();MYThread myThread2 = new MYThread();myThread1.setName("烧水");myThread2.setName("泡面");myThread1.start(); //currentThread.getName == 线程1try {myThread1.join();} catch (InterruptedException e) {e.printStackTrace();}myThread2.start(); //currentThread.getName 此时为“线程2”}
}
class MYThread extends Thread {MYThread() { }@Overridepublic void run() {for (int i = 0; i < 30; i++) {System.out.println(this.getName() + "开始执行了"+i);}
//        try {//            Thread.sleep(1000);
//        } catch (InterruptedException e) {//            e.printStackTrace();
//        }System.out.println("Thread.currentThread()---" + Thread.currentThread().getName());}
}
  • 线程优先级

    线程调度模型

    • 分时调度模型:

      ​ 所有线程轮流使用CPU的使用权,平均分配给每个线程占用CPU的时间片。

    • 抢占式调度模型:

      ​ 优先让优先极高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级搞得线程获取的CPU时间片相对多一些。

    ​ 线程默认优先级是5,范围为1~10的int类型整数,最高为10最低为1。线程优先级高仅仅代表线程获取CPU时间片的几率高,不代表他一定优先完成。

示例代码 1.5

public class ThreadDemo {public static void main(String[] args) {MYThread myThread1 = new MYThread();MYThread myThread2 = new MYThread();myThread1.setName("汽车");myThread1.setPriority(1);myThread2.setName("飞机");myThread2.setPriority(10);myThread1.start(); //currentThread.getName == 线程1myThread2.start(); //currentThread.getName 此时为“线程2”}
}class MYThread extends Thread {MYThread() { }@Overridepublic void run() {for (int i = 0; i < 30; i++) {System.out.println(this.getName() + "开始执行了" + i);}System.out.println("Thread.currentThread()---" + Thread.currentThread().getName());}
}
  • 守护线程

示例代码 1.6

public class ThreadDemo {public static void main(String[] args) {MYThread myThread1 = new MYThread();MYThread myThread2 = new MYThread();myThread1.setName("公主");myThread2.setName("骑士");//设置守护线程myThread2.setDaemon(true);myThread1.start(); myThread2.start(); }
}
class MYThread extends Thread {MYThread() { }@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(this.getName() + "开始执行了"+i);}System.out.println(Thread.currentThread().getName()+"死了");//标记终点}
}

运行结果

公主开始执行了98
公主开始执行了99
骑士开始执行了87
公主死了
骑士开始执行了88
骑士开始执行了89
骑士开始执行了90  //公主死了之后骑士并未执行到终点

1.2.5 线程生命周期

  1. 创建线程对象(new Thread)
  2. 执行(start),转为就绪状态(有执行资格但没执行权)
  3. 如果抢到执行权,开始运行
  4. 如果运行时执行权被抢走,返回就绪状态
  5. 如果运行时遇到阻塞方法,转为阻塞状态(没有执行资格和执行权)
  6. 阻塞状态结束(方法时间结束/阻塞方式结束),返回就绪状态
  7. 如果执行完成(run结束),线程死亡变成垃圾

小结

线程相关概念:线程是操作系统调度的最小单位,进程是程序运行的实体,多线程就是有多个线程的程序,并发就是同时有多个程序交替执行,并行就是有多个程序同时执行。

Java中有三种实现线程的方式:继承Thread类、实现Runnable接口、实现Callable接口,他们的最终实现类都是Thread。但Callable可以获取方法返回值,Thread和Runnable则不可以。Thread有设置/获取名字、返回线程对象、开始/等待/睡眠、设置/返回优先级、守护线程等方法。

线程自创建开始进入生命周期,线程对象调用start方法开启第二个周期,线程转为就绪状态,如果此时抢到执行权就开始运行,若没有或运行时执行权被抢走就转为就绪状态,如果运行时遇到阻塞方法则转为阻塞状态,阻塞状态结束返回就绪状态,执行完成run方法体内部代码后线程死亡。

二、 线程同步

2.1 线程的安全问题

2.1.1 卖票案例

需求:有100张票,有三个窗口卖票,请设计一个程序模拟卖票。

/*SellTicket*/
public class SellTicket implements Runnable {private int tickets = 100;@Overridepublic void run() {while (true) {if (tickets > 0) {try {Thread.sleep(100); //模拟出票动作} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "售出第" + tickets);tickets--;  //卖出后票数-1System.out.println("余票" + tickets + "张"); //显示余票} else {System.out.println("票没了");try {Thread.sleep(10000);  //模拟隔不久就有人来买票} catch (InterruptedException e) {e.printStackTrace();}}}}
}
/*测试类*/
public class SellTicketDemo {public static void main(String[] args) {SellTicket st = new SellTicket();Thread t1 = new Thread(st,"窗口1");Thread t2 = new Thread(st,"窗口2");Thread t3 = new Thread(st,"窗口3");t1.start();t2.start();t3.start();/*窗口2售出第97余票96张窗口2售出第96余票95张窗口1售出第96余票95张*///由于线程具有随机性,多个线程操作一个数据源时会造成线程安全问题}
}

2.1.2 出现线程安全问题原因

  • 是否是多线程环境
  • 是否有共享数据
  • 是否有多条语句操作共享数据

当以上三个条件同时出现,就会导致线程安全问题,解决方法为:把多条语句操作共享数据的代码起来,让任意时刻只能有一个线程执行即可。方式有:同步代码块、同步方法

一个线程只能有锁的时候才能对共享数据进项访问,结束访问后必须释放锁。

持有锁和释放锁之间所执行的代码叫做临界区(Critical Section)

锁具有排他性,即一个锁只能被一个线程持有,这种锁被称为互斥锁

2.1.3 同步代码块

  • 格式

    synchronized(任意对象){多条语句操作共享数据的代码
    }
    
  • 优势劣势

    • 优势:解决了多线程的数据安全问题
    • 劣势:当线程很多时,每个线程都会判断同步上的锁,会降低程序的运行效率。

锁对象需要唯一,如果不同线程锁的不是同一个对象,就解决不了线程安全的问题,所以synchronized后的对象不能为this。

示例代码 2.1

/*SellTicket类*/
public class SellTicket implements Runnable {private int tickets = 100;private Object obj = new Object();@Overridepublic void run() {while (true) {//假设t1先抢到执行权,t1进入run,遇到sychronized将自身锁入synchronized (obj){if (tickets > 0) {try {//t1 休眠,但仍占用锁Thread.sleep(100);//假设此时t2进入,但t1被锁在里面,t2无法执行锁内代码块} catch (InterruptedException e) {e.printStackTrace();}//t1休眠结束,继续执行System.out.println(Thread.currentThread().getName() + "售出第" + tickets);tickets--;} else {System.out.println("票没了");try {Thread.sleep(10000);} catch (InterruptedException e) {e.printStackTrace();}}}}}
}
/*测试类*/
public class SellTicketDemo {public static void main(String[] args) {SellTicket st = new SellTicket();Thread t1 = new Thread(st,"窗口1");Thread t2 = new Thread(st,"窗口2");Thread t3 = new Thread(st,"窗口3");t1.start();t2.start();t3.start();}
}
/*
窗口3售出第10
窗口3售出第9
窗口2售出第8
窗口2售出第7
窗口1售出第6
窗口1售出第5
窗口1售出第4
窗口1售出第3
窗口1售出第2
窗口1售出第1
票没了
*/

2.1.4 同步方法

同步方法就是将synchronized加到方法上。

  • 格式

    修饰符 synchronized 返回值类型 方法名(参数){方法代码块
    }
    

    同步方法的锁对象是this

  • 静态同步方法

    就是将synchronized加到静态方法上

    静态同步方法的锁对象是类名.class

示例代码 2.2

public class SellTicket implements Runnable {private static int tickets = 100;@Overridepublic void run() {while (true) {sellTicket();}}private synchronized static void sellTicket() {if (tickets > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "售出第" + tickets);tickets--;} else {System.out.println("票没了");try {Thread.sleep(10000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
/*测试类*/
public class SellTicketDemo {public static void main(String[] args) {SellTicket st = new SellTicket();Thread t1 = new Thread(st,"窗口1");Thread t2 = new Thread(st,"窗口2");Thread t3 = new Thread(st,"窗口3");t1.start();t2.start();t3.start();}
}

2.1.5 Lock锁

Lock是一个接口,控制多个线程对共享资源访问的工具,实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。

方法名 说明
void lock(); 获得锁
void unlock(); 释放锁
实现类构造方法 ReentrantLock
ReentrantLock(); 创建一个ReentrantLock实例

示例代码 2.3

public class LockDemo {public static void main(String[] args) {SellTicket sl = new SellTicket();Thread t1 = new Thread(sl, "窗口1");Thread t2 = new Thread(sl, "窗口2");Thread t3 = new Thread(sl, "窗口3");t1.start();t2.start();t3.start();}
}class SellTicket implements Runnable {private int tickets = 100;private Lock lock = new ReentrantLock();@Overridepublic void run() {while (true) {lock.lock();try {if (tickets > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "售出第" + tickets);tickets--;} else {System.out.println("票没了");break;}} finally {lock.unlock();}}}
}

2.2 死锁

多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。

java 死锁产生的四个必要条件:

  • 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
  • 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  • 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
  • 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

示例代码 2.4

public class DeadLock {public static void main(String[] args) {SubThread t1 =new SubThread();t1.setName("a");t1.start();SubThread t2 = new SubThread();t2.setName("b");t2.start();}static class SubThread extends Thread{private static final Object yitian = new Object();private static final Object tulong = new Object();@Overridepublic void run() {if ("a".equals(Thread.currentThread().getName())){synchronized (yitian){System.out.println("a线程获得了倚天剑,爽翻了,再来个屠龙刀就好了...");
//                    synchronized (tulong){   //造成死锁
//                        System.out.println("a线程获得了倚天剑和屠龙刀,直接称霸武林...");
//                    }}synchronized (tulong){System.out.println("a线程获得了倚天剑和屠龙刀,直接称霸武林...");}}if ("b".equals(Thread.currentThread().getName())){synchronized (tulong){System.out.println("b线程获得了屠龙宝刀,得劲,谁也不给....");
//                    synchronized (yitian){  //造成死锁
//                        System.out.println("b线程获得了屠龙后又来把倚天剑....b称霸武林");
//                    }}synchronized (yitian){System.out.println("b线程获得了屠龙后又来把倚天剑....b称霸武林");}}}}
}

小结

多个线程操作一个共享资源时可能造成数据不安全,解决方案为给操作数据的代码块上个“锁”,有同步代码块、同步方法、Lock锁三种锁解决线程的安全问题。

同步代码块有关键字synchronized,他会将第一个抢到该线程的任意对象锁住,只允许该对象执行锁中代码。执行完后会释放锁。劣势是当线程很多时,每个线程都需要判断一次锁会影响程序运行效率。

同步方法就是将该关键字放到方法声明上,修饰符 synchroized 方法返回值类型 方法名(方法形参){}。需要注意同步方法锁的对象是this,静态同步方法锁锁的是类名.class。

Lock锁是接口,使用前需要先获得Lock对象,有lock和unlock方法去锁代码块。

死锁出现原因是不同线程之间互相持有对方所需的资源,发生死锁将导致程序无法继续下去。

三、线程通信

3.1 等待/通知机制

在单线程编程中,要执行的操作需要满足一定的条件才能执行,可以把这个操作放在if语句快中。

在多线程编程中,可能A线程条件没有满足只是暂时的,稍后其他的线程B可能会更新条件使得A线程的条件得到满足,可以将A线程暂停,直到他的条件得到满足后再将A线程唤醒。

3.2 Object类的等待和唤醒方法

方法名 说明
void wait() 导致当前线程等待同时释放锁,直到另一个线程调用该对象的notify()方法
void wait(long) 如果在参数指定的时间内没有被唤醒,超时后会自动唤醒。
void notify() 唤醒正在等待单个线程,并不立即释放锁
void notifyAll() 唤醒正在等待的所有线程,并不立即释放锁

wait()方法只能在同步代码块中由锁对象调用,调用后当前线程会释放锁。

notify()方法也只能在同步代码块中由锁对象调用,没有使用锁对象调用wait()/notify()会抛出异常。如果有多个等待的线程,**notify()方法只能唤醒其中的一个,并不会立即释放锁对象。**一般将notify方法放在同步代码块的最后。

通知过早:线程wait()等待后,可以调用notify()唤醒线程,如果notify()唤醒过早,在等待之前就调用了notify()可能会打乱程序正常的执行逻辑。

3.3 生产者和消费者

生产者消费者问题是多线程同步问题的经典案例。生产者生成一定量的数据放到缓冲区中,然后重复此过程;与此同时,消费者也在缓冲区消耗这些数据。生产者和消费者之间必须保持同步,要保证生产者不会在缓冲区满时放入数据,消费者也不会在缓冲区空时消耗数据。不够完善的解决方法容易出现死锁的情况,此时进程都在等待唤醒。

3.3.1 案例分析

  • 缓冲区类(Box)定义表示奶箱牛奶数量的int变量,定义标记箱中是否有牛奶的boolean变量,定义存入方法put和取出方法get。
  • 生产者类(Productor)实现Runnable接口,重写run方法,设置线程任务
  • 消费者类(Consumer)实现Runnable接口,重写run方法,设置线程任务
  • 测试类(BoxDemo)main方法,创建生产者消费者对象,生产成对应线程对象启动线程。

3.3.2 代码实现

Box类

public class Box {/**定义一个成员变量表示第几瓶奶*/private int milk;/**定义一个成员变量表示奶箱状态,true表示有,false表示无*/private boolean state = false;/**提供存储牛奶的操作*/public void put(int milk) {//如果有就等待消费if (state){try {wait();} catch (InterruptedException e) {e.printStackTrace();}}//如果没有就生产this.milk = milk;System.out.println("生产出第" + this.milk + "瓶奶");//修改奶箱状态state = true;//唤醒其他线程notifyAll();}/**提供获取牛奶的操作*/public void get() {//如果没有牛奶就等待if (!state) {try {wait();} catch (InterruptedException e) {e.printStackTrace();}}//如果有就消费System.out.println("用户拿到第" + this.milk + "瓶奶");//消费完后修改奶箱状态state = false ;//唤醒其他线程notifyAll();}
}

Product类

public class Product implements Runnable {/**定义Box对象*/private Box box;/**将其他类中的Box对象传入这里*/Product(Box box) {this.box = box;}/**run方法内进行生产牛奶操作,将牛奶放入奶箱*/@Overridepublic void run() {//最多5瓶奶for (int i = 1; i <= 5; i++) {box.put(i);}}
}

Consumer类

public class Consumer implements Runnable{/**定义Box对象*/private Box box;/**将其他类中的Box对象传入这里*/public Consumer(Box box) {this.box = box;}/**run方法内进行消费牛奶操作,将牛奶从奶箱取出*/@Overridepublic void run() {while (true){box.get();}}
}

BoxDemo类

public class BoxDemo {public static void main(String[] args) {Box box = new Box();//创建生产者对象Product product = new Product(box);//创建消费者对象Consumer consumer = new Consumer(box);//生成对应线程Thread t1 = new Thread(product,"送奶工");Thread t2 = new Thread(consumer,"小明");//启动线程t1.start();t2.start();}
}

小结

线程通信主要有两类方法:wait和notify,wait用于让线程等待,notify用于唤醒线程。生产着消费者案例很好的解释了线程间通信问题。

四、 线程池

4.1 线程状态介绍

线程创建并启动后,不是立刻进入执行状态也不是一直处于执行状态。线程对象在不同时期有不同状态,状态被定义在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 一个尚未启动的线程的状态。也称之为初始状态、开始状态。线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程对象,没有线程特征。
RUNNABLE 当我们调用线程对象的start方法,那么此时线程对象进入了RUNNABLE状态。那么此时才是真正的在JVM进程中创建了一个线程,线程一经启动并不是立即得到执行,线程的运行与否要听令与CPU的调度,那么我们把这个中间状态称之为可执行状态(RUNNABLE)也就是说它具备执行的资格,但是并没有真正的执行起来而是在等待CPU的调度。
BLOCKED 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
WAITING 一个正在等待的线程的状态。也称之为等待状态。造成线程等待的原因有两种,分别是调用Object.wait()、join()方法。处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll();一个因为join()而等待的线程正在等待另一个线程结束。
TIMED_WAITING 一个在限定时间内等待的线程的状态。也称之为限时等待状态。造成线程限时等待状态的原因有三种,分别是:Thread.sleep(long),Object.wait(long)、join(long)。
TERMINATED 一个完全运行完成的线程的状态。也称之为终止状态、结束状态

各个状态的转换如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e2J1BmzA-1675157879226)(C:\Users\Lenovo\Desktop\我\实习培训\培训\SE进阶笔记\img\1591163781941.png)]

4.2 基本原理

线程池就是一个可以复用线程的技术。线程池内部可以预先创建一定数量的工作线程,客户端代码直接将任务作为一个对象提交给线程池, 线程池将这些任务缓存在工作队列中, 线程池中的工作线程不断地从队列中取出任务并执行。工作线程就是我们之前学的线程,任务就是实现了Runnable或Callable接口的实例对象。线程池应用场景有:网购商品秒杀、云盘文件上传下载、12306网上购票系统等等。

4.3 Executors

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

构造方法 说明
static ExecutorService newCachedThreadPool() 创建一个默认的线程池,线程数量会随着任务增加而增加,如果线程任务执行完毕且空闲一段时间则会被回收掉
static ExecutorService newFixedThreadPool(int nThreads) 创建一个指定最多线程数量的线程池,如果某个线程因为执行异常而结束,线程池会补充一个新线程替代它
static ExecutorService newSingleThreadExecutor() 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程
static ScheduleExecutorService newScheduledThreadPool(int corePoolSize) 创建一个线程池,可以实现在给定的延迟后运行任务或者定期执行任务

Executors的底层是基于ThreadPoolExecutor创建线程池对象的。大型并发系统环境中使用Executors如果不注意可能出现系统风险,所以建议使用ThreadPoolExecutor。

newFixedThreadPool(int nThreads)和newSingleThreadExecutor()允许请求的任务队列长度是Integer.MAX_VALUE,可能出现OOM错误。

newCachedThreadPool()和newScheduledThreadPool(int corePoolSize)创建的线程最大数量上限是Integer.MAX_VALUE,线程数可能会随着任务1:1增长,也可能出现OOM错误。

常用方法 说明
void execute(Runnable command) 执行任务/命令,没有返回值,一般用来执行Runnable任务
Future submit(Callable task) 执行任务,返回未来任务对象获取线程结果,一般拿来执行Callable任务
void shutdown() 等任务执行完毕后关闭线程池
LisshutdownNow() 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务

示例代码 4.1

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class ThreadPoolDemo {public static void main(String[] args) {//static ExecutorService newCachedThreadPool()   创建一个默认的线程池//static newFixedThreadPool(int nThreads)     创建一个指定最多线程数量的线程池//因为是static修饰的,所以用 类名. 形式即可,指定线程数量最多为10ExecutorService executorService = Executors.newFixedThreadPool(10);//Executors --- 可以帮助我们创建线程池对象//ExecutorService --- 可以帮助我们控制线程池//Lambda表达式创建Thread对象executorService.submit(() ->//获取线程对象名System.out.println(Thread.currentThread().getName() + "在执行了"));try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}executorService.submit(() ->System.out.println(Thread.currentThread().getName() + "在执行了"));executorService.shutdown();}
}

4.4 ThreadPoolExecutor

ThreadPoolExecutor是ExecutorService接口(线程池)的实现类,可以用来自创建一个线程池对象。

4.4.1 构造方法

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue) {this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(), defaultHandler);
}public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory) {this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,threadFactory, defaultHandler);
}public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler) {this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(), handler);
}public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) {if (corePoolSize < 0 ||maximumPoolSize <= 0 ||maximumPoolSize < corePoolSize ||keepAliveTime < 0)throw new IllegalArgumentException();if (workQueue == null || threadFactory == null || handler == null)throw new NullPointerException();this.corePoolSize = corePoolSize;this.maximumPoolSize = maximumPoolSize;this.workQueue = workQueue;this.keepAliveTime = unit.toNanos(keepAliveTime);this.threadFactory = threadFactory;this.handler = handler;
}

4.4.2 使用流程

  1. 利用Executors工厂类的静态方法,创建线程池对象;
  2. 编写Runnable或Callable实现类的实例对象;
  3. 利用ExecutorService的submit方法或ScheduledExecutorService的schedule方 法提交并执行线程任务
  4. 如果有执行结果,则处理异步执行结果(Future)
  5. 调用shutdown()方法,关闭线程池。

示例代码 4.2

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,   //参数123  核心线程数量为2,最大线程数为5,空闲线程最大存活时间2sTimeUnit.SECONDS,   //参数四指定时间单位为秒new ArrayBlockingQueue<>(10),   //创建任务队列Executors.defaultThreadFactory(),  //创建线程工厂new ThreadPoolExecutor.AbortPolicy());  //任务的拒绝策略pool.submit(new MyRunnable());pool.submit(new MyRunnable());pool.shutdown();}
}

4.5 线程池参数详解

根据前面的[构造方法](#4.4.1 构造方法)可以看到,其需要如下几个参数:

  • corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。

  • maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。

  • keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。

  • unit(必需):指定 keepAliveTime 参数的时间单位。常用的有TimeUnit.MILLISECONDS(毫 秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。

  • workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。常用的几个阻塞队列:

    1.LinkedBlockingQueue

    链式阻塞队列,底层数据结构是链表,默认大小是Integer.MAX_VALUE,也可以指定大小。

    2.ArrayBlockingQueue

    数组阻塞队列,底层数据结构是数组,需要指定队列的大小。

    3.SynchronousQueue

    同步队列,内部容量为0,每个put操作必须等待一个take操作,反之亦然。

    4.DelayQueue

    延迟队列,该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素 。

    注意有界队列和无界队列的区别:如果使用有界队列,当队列饱和时并超过最大线程数时就会执行拒绝策略;而如果使用无界队列,因为任务队列永远都可以添加任务,所以设置 maximumPoolSize 没有任何意义。

  • threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。

  • handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。四种拒绝处理的策略为 :

    1.ThreadPoolExecutor.AbortPolicy默认拒绝处理策略,丢弃任务并抛出RejectedExecutionException异常。
    2.ThreadPoolExecutor.DiscardPolicy:丢弃新来的任务,但是不抛出异常。
    3.ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列头部(最旧的)的任务,然后重新尝试执行程序(如果再次失败,重复此过程)。
    4.ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

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

4.6 定时器

4.6.1 Timer

Timer定时器主要做定时任务或者按照一定的时间间隔做任务,例如每天4点钟定时执行作业等

方法名 说明
schedule(TimerTask task,Date time) 在指定的时间执行指定的任务
schedule(TimerTask task,Date firstTime,long period) 从指定的时间开始,对指定的任务执行重复的固定的延迟执行
schedule(TimerTask task,long delay) 指定的延迟之后执行指定的任务
schedule(TimerTask task,long delay,long period) 指定的延迟之后重新执行任务,等待delay延迟后每period重复执行

Timer是单线程,处理多个任务按照顺序执行,存在延时与设置定时器的时间有出入。

可能因为其中的某个任务的异常使Timer线程死掉影响后续任务执行。

示例代码4.3

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;public class TimerDemo {public static void main(String[] args) {Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"AAA执行"+new Date());//睡眠线程使其乱轴
//                try {//                    Thread.sleep(5000);
//                } catch (InterruptedException e) {//                    e.printStackTrace();
//                }}},0,3000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"BBB执行"+new Date());
//                try {//                    Thread.sleep(5000);
//                } catch (InterruptedException e) {//                    e.printStackTrace();
//                }}},0,3000);//        timer.schedule(new TimerTask(){//            @Override
//            public void run() {//                System.out.println("定时执行"+new  Date());
//            }
//        },new DateTime());}
}

4.6.2 ScheduledExecutorService

ScheduledExecutorService是jdk1.5引入的并发包,目的是弥补Timer的缺陷,ScheduledExecutorService内部是线程池。

Executors的方法 说明
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 得到线程池对象
ScheduledExecutorService的方法 说明
ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit) 周期调度方法

基于线程池,某个任务的执行情况不会影响其他定时任务的执行。

示例代码4.4

import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;public class ScheduledExecutorServiceDemo {public static void main(String[] args) {ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10);scheduler.scheduleAtFixedRate(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "AAA开始执行了"+new Date());}},2,3, TimeUnit.SECONDS);}
}

小结

线程的六个状态:

线程池原理:

两个构造线程池的方法:

Executors:

ThreadPoolExecutor

ThreadPoolExecutor七个参数含义

两种定时器

五、 网络编程入门

5.1 网络编程三要素

5.1.1 IP地址

IP:全称互联网协议地址,是分配给上网设备的唯一标志。常见的IP分类为IPv4和IPv6。

IPv4:给每个链接在网络上的主机用二进制分配一个32字节的地址,常写成十进制形式,每个字节中间用“.”分隔(点分十进制法)。

IPv6:采用128bit地址长度,每16个字节一组,分成8组十六进制数,解决了网络地址资源数量不够的问题。

IP地址形式:分为公网地址和私有地址(局域网使用)

192.168.0.0–192.168.255.255就是常见的局域网地址,专门为组织机构内部使用。

特殊IP地址:

本机IP:127.0.0.1或者localhost:称为回送地址也可称本地回环地址,只会寻找当前所在本机。

相关DOS命令

  • ipconfig 查看本机IP地址

  • ping IP地址 检查网络是否连通

5.1.2 端口

端口是设备上应用程序的唯一标识,本质上是两个应用程序的通信。端口号是用两个字节表示的整数,它的取值范围是0 - 65535。

  • 公认端口:0 - 1023之间的端口号用于一些知名的网络服务和应用,比如80端口分配给www,21端口分配给FTP
  • 注册端口:1024 - 49151 分配给用户进程或应用程序
  • 动态/私有端口:49152 - 65535,因为它一般不固定分配某种进程,而是动态分配
  • 如果端口号被另外一个服务或应用程序占用,会导致当前程序启动失败。

一个设备中不可以出现两个或多个相同端口号的程序。

相关DOS命令

  • 查看所有端口:netstat -ano
  • 查看指定端口:netstat -aon|findstr "80"
  • 查看指定进程:tasklist|findstr "12476"
  • 查看具体程序:使用任务管理器查看PID

5.1.3 协议

计算机网络中,链接和通信的规则被称为网络通信协议。网络通信协议有两套参考模型:OSI参考模型和TCP/IP参考模型

OSI参考模型:世界互联协议标准,全球通信规范,由于此模型过于理想化,所以未能在因特网上进行广泛推广。

TCP/IP参考模型:事实上的国际标准。

传输层有两个常见协议:

  • UDP协议:用户数据报协议

    特点:

    • UDP是一种无连接不可靠传输的协议
    • 将数据源IP、目的地IP和端口封装成数据包,不需要建立连接
    • 每个数据包的大小限制在64KB内
    • 发送不管对方是否准备好,接收方收到也不确认,所以不可靠
    • 可以广播发送,发送数据结束时无需释放资源,开销小速度快。

    UDP协议通信场景:语音通话,视频会话等。

  • TCP协议:传输控制协议

    特点

    • 使用TCP协议,必须双方先建立链接,它是一种面向连接的可靠通信协议。
    • 传输前,采用“三次握手”方式建立连接,所以是可靠的。
    • 在连接中可进行大数据量的传输。
    • 链接、发送数据都需要确认,且传输完毕后,还需释放已建立的链接,通信效率低。

    TCP协议通信场景:对信息安全要求较高的场景,例如:文件下载,金融等数据通信。

    三次握手:

    1. 客户端向服务器发出连接请求等待服务期确认
    2. 服务器向客户端返回一个响应告诉客户端收到请求
    3. 客户端再次发出确认信息建立连接。

    四次挥手:

    1. 客户端向服务器发出取消链接请求
    2. 服务器向客户端返回一个响应表示收到客户端取消请求
    3. 服务器将最后的数据处理完毕向客户端发出确认取消信息
    4. 客户端再次发送确认信息链接取消

5.1.4 InetAddress的使用

InetAddress是IP地址操作类,表示协议(IP)地址。

方法名 说明
static InetAddress getLocalHost() 返回本主机的地址对象
static InetAddress getByName(String host) 得到指定主机的IP地址对象,参数是域名或者Ip地址
String getHostName() 获取此IP地址的主机名
String getHostAddress() 返回IP地址字符串
boolean isReachable(int timeout) 在指定毫秒内连通该IP地址对应的主机,连通返回true

示例代码

public class InetAdressDemo {public static void main(String[] args) throws IOException {//| static InetAddress getLocalHost() | 返回本主机的地址对象 |InetAddress address = InetAddress.getLocalHost();System.out.println(address);//static InetAddress getByName(String host)得到指定主机的IP地址对象,参数是域名或者Ip地址InetAddress address1 = InetAddress.getByName("baidu.com");
//        InetAddress address1 = InetAddress.getByName("39.156.66.10");System.out.println(address1);//String getHostName()获取此IP地址的主机名String name = address.getHostName();System.out.println(name);//String getHostAddress()返回IP地址字符串String hostAddress = address.getHostAddress();System.out.println(hostAddress);//boolean isReachable(int timeout)在指定毫秒内连通该IP地址对应的主机,连通返回trueboolean b = address.isReachable(5000);System.out.println(b);}
}

5.2 UDP通信

5.2.1 UDP发送数据

UDP协议是一种不可靠的网络协议,它在通信的两端各建一个Socket对象,但这两个Socket只是发送、接收数据的对象,因此对于基于UDP协议的通信双方而言,没有所谓的客户端和服务器的概念。Java提供了DatagramSocket类作为基于UDP协议的Socket。

构造方法

方法名 说明
DatagramSocket() 创建发送端的Socket对象,系统会随机分配端口号
DatagramPacket(byte[] buf,int len,InetAddress add,int port) 创建发送端数据包对象,发送长度为len的数据包到指定主机的指定端口

buf:要发送的内容,字节数组

len:发送的内容的字节长度

add:接收端的IP地址对象

port:接收端的端口号

相关方法

方法名 说明
void send(DatagramPacket p) 发送数据报包
void close() 关闭数据报套接字
void receive(DatagramPacket p) 从此套接字接受数据报包

发送数据的步骤

  • 创建发送端的Socket对象(DatagramSocket)
  • 创建数据,并把数据打包
  • 调用DatagramSocket对象的方法发送数据
  • 关闭发送端

示例代码5.1(仅作演示)

public class UDPDemo01 {public static void main(String[] args) throws IOException {//创建发送端的Socket对象(DatagramSocket)// DatagramSocket() 构造数据报套接字并将其绑定到本地主机上的任何可用端口DatagramSocket ds = new DatagramSocket();//创建数据,并把数据打包//DatagramPacket(byte[] buf, int length, InetAddress address, int port)//构造一个数据包,发送长度为 length的数据包到指定主机上的指定端口号。byte[] buf = "hello!".getBytes();DatagramPacket dp = new DatagramPacket(buf,buf.length, InetAddress.getByName("Devil"),10086);//调用DatagramSocket对象的方法发送数据//void send(DatagramPacket p) 从此套接字发送数据报包ds.send(dp);//关闭发送端//void close() 关闭此数据报套接字ds.close();}
}

5.2.2 UDP接收数据

接收数据步骤

  • 创建接收端的Socket对象(DatagramSocket)
  • 创建一个数据包,用于接收数据
  • 调用DatagramSocket对象的方法接收数据
  • 解析数据包,并把数据在控制台显示
  • 关闭接收端

构造方法

方法名 说明
DatagramSocket() 创建接收端的Socket对象并指定端口号
DatagramPacket(byte[] buf, int len) 创建一个DatagramPacket用于接收长度为len的数据包

相关方法

方法名 说明
byte[] getData() 返回数据缓冲区
int getLength() 返回要发送的数据的长度或接收的数据的长度

示例代码5.2(仅作演示)

public class UDPDemo02 {public static void main(String[] args) throws IOException {//创建接收端的Socket对象(DatagramSocket)DatagramSocket ds = new DatagramSocket(10086);//创建一个数据包,用于接收数据byte[] bys = new byte[1024];DatagramPacket packet = new DatagramPacket(bys, bys.length);//调用DatagramSocket对象的方法接收数据ds.receive(packet);System.out.println("数据是:" + new String(packet.getData(), 0, packet.getLength()));}
}

5.2.3 UDP通信程序练习

案例需求:

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

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

示例代码5.3

import java.io.IOException;
import java.net.*;/*** UDP接收数据:因为接收端不知道发送端什么时候停止发送,故采用死循环接收** @author Lenovo*/
public class ReceiveDemo {public static void main(String[] args) throws IOException {//创建接收对象,端口名19900DatagramSocket ds = new DatagramSocket(19900);while (true) {//创建一个数据包,用于接收数据byte[] bys = new byte[1024 * 64];DatagramPacket dp = new DatagramPacket(bys, bys.length);//调用DatagramSocket对象的方法接收数据ds.receive(dp);//解析数据包,并把数据在控制台显示       // dp.getData返回0~最后的数组元素由new String解析String s = new String(dp.getData(), 0, dp.getLength());System.out.println("数据是:" + s);if (s.equals("886")) {break;}}}
}import java.net.*;
import java.util.Scanner;/**UDP发送数据:数据来自于键盘录入,直到输入的数据是886,发送数据结束@author Lenovo*/
public class SendDemo {public static void main(String[] args) throws IOException {//创建发送对象和键盘录入对象DatagramSocket ds = new DatagramSocket();Scanner sc =new Scanner(System.in);while (true) {String s = sc.nextLine();//输入的数据是886,发送数据结束if ("886".equals(s)) {byte[] bys = s.getBytes();DatagramPacket dp = new DatagramPacket(bys, bys.length, InetAddress.getByName("127.0.0.1"), 19900);//调用DatagramSocket对象的方法发送数据ds.send(dp);break;}//创建数据,并把数据打包byte[] bys = s.getBytes();DatagramPacket dp = new DatagramPacket(bys, bys.length, InetAddress.getByName("127.0.0.1"), 19900);//调用DatagramSocket对象的方法发送数据ds.send(dp);}ds.close();}
}

5.2.4 UDP三种通讯方式

  • 单播

    单播用于两个主机之间的端对端通信

  • 组播

    组播用于对一组特定的主机进行通信

  • 广播

    广播用于一个主机对整个局域网上所有主机上的数据通信

5.2.5 UDP组播实现

UDP实现组播要用组播地址:224.0.0.0~239.255.255.255。发送端的数据包目的地是组播IP并指定端口,接收端必须绑定该组播地址,端口还要对应发送端的目的端口,这样即可接收该组播消息。DatagramSocket的子类MulticastSocket可以在接收端绑定组播IP。

示例代码5.4

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;/**UDP发送数据:数据来自于键盘录入,直到输入的数据是886,发送数据结束@author Lenovo*/
public class SendDemo {public static void main(String[] args) throws IOException {//创建发送对象和键盘录入对象DatagramSocket ds = new DatagramSocket();Scanner sc =new Scanner(System.in);while (true) {System.out.println("请讲");String s = sc.nextLine();//输入的数据是886,发送数据结束if ("886".equals(s)) {byte[] bys = s.getBytes();//将组播地址封装进去DatagramPacket dp = new DatagramPacket(bys, bys.length,InetAddress.getByName("255.255.255.255"), 19900);//调用DatagramSocket对象的方法发送数据ds.send(dp);ds.close();break;}//创建数据,并把数据打包byte[] bys = s.getBytes();DatagramPacket dp = new DatagramPacket(bys, bys.length, InetAddress.getByName("127.0.0.1"), 19900);//调用DatagramSocket对象的方法发送数据ds.send(dp);}ds.close();}
}import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;/*** UDP接收数据:因为接收端不知道发送端什么时候停止发送,故采用死循环接收** @author Lenovo*/
public class ReceiveDemo {public static void main(String[] args) throws IOException {//创建接收对象,端口指定SendDemo的Packet端口MulticastSocket ds = new MulticastSocket(19900);//把当前计算机绑定一个组播地址,表示添加到这一组中ds.joinGroup(InetAddress.getByName("225.25.25.25"));while (true) {//创建一个数据包,用于接收数据byte[] bys = new byte[1024 * 64];DatagramPacket dp = new DatagramPacket(bys, bys.length);//调用DatagramSocket对象的方法接收数据ds.receive(dp);//解析数据包,并把数据在控制台显示// dp.getData返回0~最后的数组元素由new String解析String s = new String(dp.getData(), 0, dp.getLength());System.out.print(dp.getSocketAddress().toString());System.out.println(dp.getPort());System.out.println("数据是:" + s);if (s.equals("886")) {break;}}}
}

5.2.6 UDP广播实现

UDP实现广播需要使用广播地址:255.255.255.255,发送端发送数据包的目的地要指定地址为广播地址,指定一个端口。接收端匹配该端口即可实现收到消息。

示例代码5.5

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;/**UDP发送数据:数据来自于键盘录入,直到输入的数据是886,发送数据结束@author Lenovo*/
public class SendDemo {public static void main(String[] args) throws IOException {//创建发送对象和键盘录入对象DatagramSocket ds = new DatagramSocket();Scanner sc =new Scanner(System.in);while (true) {System.out.println("请讲");String s = sc.nextLine();//输入的数据是886,发送数据结束if ("886".equals(s)) {byte[] bys = s.getBytes();//将广播地址封装进去DatagramPacket dp = new DatagramPacket(bys, bys.length,InetAddress.getByName("255.255.255.255"), 19900);//调用DatagramSocket对象的方法发送数据ds.send(dp);ds.close();break;}//创建数据,并把数据打包byte[] bys = s.getBytes();DatagramPacket dp = new DatagramPacket(bys, bys.length, InetAddress.getByName("127.0.0.1"), 19900);//调用DatagramSocket对象的方法发送数据ds.send(dp);}ds.close();}
}import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;/*** UDP接收数据:因为接收端不知道发送端什么时候停止发送,故采用死循环接收** @author Lenovo*/
public class ReceiveDemo {public static void main(String[] args) throws IOException {//创建接收对象,端口指定SendDemo的Packet端口DatagramSocket ds = new DatagramSocket(19900);while (true) {//创建一个数据包,用于接收数据byte[] bys = new byte[1024 * 64];DatagramPacket dp = new DatagramPacket(bys, bys.length);//调用DatagramSocket对象的方法接收数据ds.receive(dp);//解析数据包,并把数据在控制台显示// dp.getData返回0~最后的数组元素由new String解析String s = new String(dp.getData(), 0, dp.getLength());System.out.print(dp.getSocketAddress().toString());System.out.println(dp.getPort());System.out.println("数据是:" + s);if (s.equals("886")) {break;}}}
}

UDP的接收端可以接收很多发送端的消息,接收端只负责接收数据包,无所谓是哪个发送端的数据包。

5.3 TCP通信

Java对基于TCP协议的网络提供了良好的封装,使用Socket对象来代表两端的通信端口。TCP利用IO流实现数据的传输。

客户端:TCP协议基于请求响应模式,第一次主动发起的程序被称为客户端程序(Client)。

服务端:第一次通讯中等待连接的程序被称为服务器端(Sercer)程序。

5.3.1 TCP客户端实现

Java为客户端提供了Socket类,构造方法如下

方法名 说明
Socket(InetAddress address, int port) 创建流套接字并将其连接到指定IP指定端口号
Socket(String host,int port) 创建流套接字并将其连接到指定的主机上的指定端口号

相关方法

方法名 说明
InputStream getInputStream() 返回此套接字的输入流
OutputStream getOutputStream() 返回此套接字的输出流

示例代码

public class ClientDemo {public static void main(String[] args) {try {//创建socket对象,指定本机Ip,指定端口号9999Socket socket = new Socket("127.0.0.1",9999);//使用getOutputStream方法获得OutputStream对象OutputStream os = socket.getOutputStream();//将低级字节流封装成高级的打印流PrintStream out = new PrintStream(os);//打印流输出并刷新out.print("我来啦".getBytes());out.flush();} catch (Exception e) {e.printStackTrace();}}
}

5.3.2 TCP服务端实现

构造方法

方法名 说明
ServerSocket(int port) 创建绑定到指定端口的服务器套接字

相关方法

方法名 说明
Socket accept() 监听要连接到此的套接字并接受

注意事项

  1. accept方法是阻塞的,作用就是等待客户端链接
  2. 客户端创建对象并连接服务器,此时是通过三次握手协议,保证跟服务器之间的连接
  3. 针对客户端来讲,是往外写的,所以是输出流
    针对服务器来讲,是往里读的,所以是输入流
  4. read方法也是阻塞的
  5. 客户端在关流的时候,还多了一个往服务器写结束标记的动作
  6. 最后一步断开连接,通过四次挥手协议保证连接终止

示例代码

public class SeverDemo {public static void main(String[] args) {try {//创建绑定到指定端口的服务器对象ServerSocket ss = new ServerSocket(9999);//Socket accept() 侦听要连接到此套接字并接受它Socket socket = ss.accept();//获取输入流读数据InputStream is = socket.getInputStream();//将低级字节流封装成缓冲字符流BufferedReader br = new BufferedReader(new InputStreamReader(is));String s;if ((s = br.readLine()) != null) {System.out.println(socket.getRemoteSocketAddress()+"说了:"+s);}} catch (IOException e) {e.printStackTrace();}}
}

5.3.3 TCP通信程序练习

  • 案例需求

    客户端:发送数据,接受服务器反馈

    服务器:收到消息后给出反馈

  • 案例分析

    • 客户端创建对象,使用输出流输出数据
    • 服务端创建对象,使用输入流接受数据
    • 服务端使用输出流给出反馈数据
    • 客户端使用输入流接受反馈数据
  • 代码实现

    import java.io.OutputStream;
    import java.io.PrintStream;
    import java.net.Socket;
    import java.util.Scanner;
    /**客户端*/
    public class ClientDemo {public static void main(String[] args) {try {//创建socket对象,指定对方Ip为本机Ip,指定对方端口号9999Socket socket = new Socket("127.0.0.1", 9999);//使用getOutputStream方法获得OutputStream对象OutputStream os = socket.getOutputStream();//将低级字节流封装成高级的打印流PrintStream out = new PrintStream(os);Scanner sc = new Scanner(System.in);while (true) {System.out.println("请说");String s = sc.nextLine();//打印流输出并刷新out.print(s+"\n");out.flush();if (s.equals("88")) {break;}}} catch (Exception e) {e.printStackTrace();}}
    }import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.net.ServerSocket;
    import java.net.Socket;
    /**服务端*/
    public class SeverDemo {public static void main(String[] args) {try {//创建绑定到指定端口的服务器对象ServerSocket ss = new ServerSocket(9999);//Socket accept() 侦听要连接到此套接字并接受它Socket socket = ss.accept();//获取输入流读数据InputStream is = socket.getInputStream();//将低级字节流封装成缓冲字符流BufferedReader br = new BufferedReader(new InputStreamReader(is));String s;while ((s = br.readLine()) != null) {System.out.println(socket.getRemoteSocketAddress() + "说了:" + s);if (s.equals("88")) {break;}}} catch (IOException e) {e.printStackTrace();}}
    }
    

5.3.4 TCP程序上传文件

  • 案例需求

    客户端:数据来自于本地文件,接收服务器反馈

    服务器:接收到的数据写入本地文件,给出反馈

  • 案例分析

    • 创建客户端对象,创建输入流对象指向文件,每读一次数据就给服务器输出一次数据,输出结束后使用shutdownOutput()方法告知服务端传输结束
    • 创建服务器对象,创建输出流对象指向文件,每接受一次数据就使用输出流输出到文件中,传输结束后。使用输出流给客户端反馈信息
    • 客户端接受服务端的回馈信息
  • 相关方法

    方法名 说明
    void shutdownInput() 将此套接字的输入流放置在“流的末尾”
    void shutdownOutput() 禁止用此套接字的输出流
  • 代码实现

    /**客户端*/
    public class ClientDemo {public static void main(String[] args) {try {//创建客户端对象,目标IP为本机,端口设定9999Socket socket = new Socket("127.0.0.1",9999);//本地流读取文件BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\图片\\Saved Pictures\\壁纸.png"));//网络流OutputStream os = socket.getOutputStream();BufferedOutputStream bos = new BufferedOutputStream(os);//通过网络将数据写到服务器中byte[] bys = new byte[1024];int len;while ((len = bis.read(bys)) != -1) {bos.write(bys, 0, len);}bos.flush();//给服务器一个结束标记,告诉服务器文件已经传输完毕socket.shutdownOutput();//读取回传信息BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));String line;while ((line = br.readLine()) != null) {System.out.println(line);}//关闭IO流br.close();bos.close();bis.close();} catch (Exception e) {e.printStackTrace();}}
    }
    /**服务端*/
    public class SeverDemo {public static void main(String[] args) {try {//创建绑定到指定端口的服务器对象ServerSocket ss = new ServerSocket(9999);//Socket accept() 侦听要连接到此套接字并接受它Socket socket = ss.accept();//获取输入流读数据//将网络流封装成字节缓冲输入流BufferedInputStream br = new BufferedInputStream(socket.getInputStream());//创建字节缓冲输出流对象将文件写到本地BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\图片\\Saved Pictures\\copy.png"));byte[] bys = new byte[1024];int len;while ((len = br.read(bys)) != -1) {bos.write(bys, 0, len);}//写入完成回传信息BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));bw.write("上传成功");//关闭IO流br.close();bw.close();bos.close();} catch (Exception e) {e.printStackTrace();}}
    }
    

5.3.5 TCP程序服务器优化——循环

刚才的需求中,服务器只能处理一个客户端请求,接收完一个文件后就关闭了。可以采用循环解决这个问题。

public class SeverDemo {public static void main(String[] args) {try {//创建绑定到指定端口的服务器对象ServerSocket ss = new ServerSocket(9999);while (true) {//将accept方法放在循环内,可以重复接收多个文件//Socket accept() 侦听要连接到此套接字并接受它Socket socket = ss.accept();//获取输入流读数据//将网络流封装成字节缓冲输入流BufferedInputStream br = new BufferedInputStream(socket.getInputStream());//创建字节缓冲输出流对象将文件写到本地BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\图片\\Saved Pictures\\copy.png"));byte[] bys = new byte[1024];int len;while ((len = br.read(bys)) != -1) {bos.write(bys, 0, len);}//写入完成回传信息BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));bw.write("上传成功");//完成后关闭IO流bw.close();bos.close();br.close();}} catch (Exception e) {e.printStackTrace();}}
}

5.3.6 TCP程序服务器优化——UUID

使用循环优化后,再次上传文件会覆盖掉刚上传后的文件,可以使用UUID.randomUUID()方法生成随机的文件名。

public class SeverDemo {public static void main(String[] args) {try {//创建绑定到指定端口的服务器对象ServerSocket ss = new ServerSocket(9999);while (true) {//将accept方法放在循环内,可以重复接收多个文件//Socket accept() 侦听要连接到此套接字并接受它Socket socket = ss.accept();//获取输入流读数据//将网络流封装成字节缓冲输入流BufferedInputStream br = new BufferedInputStream(socket.getInputStream());//创建字节缓冲输出流对象将文件写到本地BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\图片\\Saved Pictures\\"+ UUID.randomUUID().toString()+".png"));byte[] bys = new byte[1024];int len;while ((len = br.read(bys)) != -1) {bos.write(bys, 0, len);}//写入完成回传信息BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));bw.write("上传成功");//完成后关闭IO流bw.close();bos.close();br.close();}} catch (Exception e) {e.printStackTrace();}}
}

5.3.7 TCP程序服务器优化——多线程

循环和UUID可以解决服务器处理多个客户端请求,但仍无法同时和多个客户端进行通信。可以开启多线程来处理。

/**线程任务*/
public class ThreadSocket implements Runnable {private Socket acceptSocket;ThreadSocket(Socket accept) {this.acceptSocket = accept;}@Overridepublic void run() {while (true) {BufferedOutputStream bos = null;try {BufferedInputStream bis = new BufferedInputStream(acceptSocket.getInputStream());bos = new BufferedOutputStream(new FileOutputStream("D:\\图片\\Saved Pictures\\"+ UUID.randomUUID().toString() + ".png"));byte[] bys = new byte[1024];int len;while ((len = bis.read(bys)) != -1) {bos.write(bys, 0, len);}BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(acceptSocket.getOutputStream()));bw.write("上传成功");bw.flush();//完成后关闭IO流bw.close();bis.close();} catch (Exception 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 SeverDemo {public static void main(String[] args) {try {ServerSocket ss = new ServerSocket(9999);while (true) {Socket accept = ss.accept();ThreadSocket ts = new ThreadSocket(accept);new Thread(ts).start();}} catch (IOException e) {e.printStackTrace();}}
}

5.3.8 TCP程序服务器优化——线程池

使用多线程资源消耗过大,优化使用线程池。

public class ClientDemo {public static void main(String[] args) {try {//创建客户端对象,目标IP为本机,端口设定9999Socket socket = new Socket("127.0.0.1",9999);//本地流读取文件BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\图片\\Saved Pictures\\原神壁纸1.png"));//网络流OutputStream os = socket.getOutputStream();BufferedOutputStream bos = new BufferedOutputStream(os);//通过网络将数据写到服务器中byte[] bys = new byte[1024];int len;while ((len = bis.read(bys)) != -1) {bos.write(bys, 0, len);}bos.flush();//给服务器一个结束标记,告诉服务器文件已经传输完毕socket.shutdownOutput();//读取回传信息BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));String line;while ((line = br.readLine()) != null) {System.out.println(line);}//关闭IO流br.close();bos.close();bis.close();} catch (Exception e) {e.printStackTrace();}}
}

六、 日志

6.1 概述

之前记录程序运行采用输出语句将信息打印在控制台,但不能永久保存而且不方便。程序中的日志可以用来记录程序在运行时的细节,并且可以永久保存。日志和输出语句的区别如下:

输出语句 日日志技术
取消日志 需要修改代码,灵活性较差 不需要修改代码,灵活性比较好
输出位置 只能是控制台 可以将日志信息写入到文件或者数据库中
多线程 和业务代码处于一个线程中,性能较差 多线程方式记录日志,不影响业务代码的性能

6.2 体系结构

日志规范:一些接口,提供给日志的实现框架设计的标准。

常见的日志规范有:

  • Commons Logging
  • Simple Logging Facade for Java

日志框架:第三方已经做好的日志记录实现代码,可以直接拿去用。

常见的日志实现框架有:

  • Log4J
  • Logback

6.3 Logback

Logback是由log4j创始人设计的另一个开源日志组件,性能更好,Logback是基于slf4j的日志规范实现的框架。

Logback主要分为三个技术模块

  • logback-core:是其他两个模块的基础
  • logback-classic:是log4j的改良版,完整实现了slf4j API
  • logback-access:模块与Tomcat和Jetty等Servlet容器集成,以提供HTTP访问日志功能。

通过使用logback,我们可以控制日志信息输送的目的地是控制台、文件等位置。

我们也可以控制每一条日志的输出格式。

通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。

这些可以通过一个配置文件来灵活地配置,不需要修改应用的代码。部分配置方法如下:

  • 日志级别

    级别程度依次是:TRACE<DEBUG<INFO<WARRN<ERROR,默认级别是debug(忽略大小写),对应其方法。

    作用:用于控制系统中哪些日志级别是可以输出的,只输出级别不低于设定级别的日志信息。一般只输出ERROR。

    ALLOFF分别是打开全部日志信息和关闭全部日志信息。

6.4 入门案例

  • 使用步骤

    1. 导入logback的相关jar包
    2. 编写logback配置文件
    3. 在代码中获取日志的对象
    4. 按照级别设置记录日志信息
  • 代码示例

    // 测试类
    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级别的日志");}
    }
    

七、 枚举

7.1 概述

枚举可以间接的表示一些固定的值比如星期、季节、月份等,枚举将变量的值一一列出来,变量的值只能限制在列举出来的值范围内。

7.2 定义格式

定义格式

public enum s{enum1,emum2,enum3;
}
//注意:定义枚举要用关键字enum

示例代码

public enum Season{SPRING,SUMMER,AUTUMN,WINTER;
}

7.3 特点

  • 所有枚举类都是Enum的子类
  • 我们可以通过“枚举类名.枚举项名”去访问指定的枚举项
  • 每一个枚举项其实就是该枚举的一个对象
  • 枚举也是一个类,也可以去定义成员变量
  • 枚举类的第一行上必须是枚举项,最后一个枚举项后的分号是可以省略的,但是如果枚举类有其他的东西,这个分号就不能省略。建议不省略
  • 枚举类可以有构造器,但必须是private的,它的默认也是private的。
  • 枚举项的用法比较特殊:枚举(“”);
  • 枚举类也可以有抽象方法,但是枚举项必须重写该方法

示例代码

public enum  Season {/*** 春天*/SPRING("春"){@Overridepublic void show() {System.out.println(this.name());}},/*** 夏天*/SUMMER("夏"){@Overridepublic void show() {System.out.println(this.name());}},/*** 秋天*/AUTUMN("秋"){@Overridepublic void show() {System.out.println(this.name());}},/*** 冬天*/WINTER("冬"){@Overridepublic void show() {System.out.println(this.name());}};Season(String name) {this.name = name;}private String name;public abstract void show();
}/**测试类*/
public class EnumDemo {public static void main(String[] args) {//我们可以通过"枚举类名.枚举项名称"去访问指定的枚举项//每一个枚举项其实就是该枚举的一个对象Season spring = Season.SPRING;spring.show();System.out.println(Season.SUMMER);}
}

7.4 方法

方法名 说明
String name() 获取枚举项的名称
int ordinal() 返回枚举项在枚举类中的索引值
int compareTo(E o) 比较两个枚举项,返回的是索引值的差
String toString() 返回枚举常量的名称
static T valueOf(Class type,String name) 获取指定枚举类中的指定名称的枚举值
values() 获得所有的枚举项

示例代码

public class EnumDemo {public static void main(String[] args) {//String name()获取枚举项的名称String name = Season.SPRING.name();System.out.println(name);// int ordinal()返回枚举项在枚举类中的索引值,从0开始计数
//        int index = Season.WINTER.ordinal();int index = Season.SPRING.ordinal();System.out.println(index);// int compareTo(E o)比较两个枚举项,返回的是索引值的差//compareTo左边减右边int i = Season.SPRING.compareTo(Season.AUTUMN);System.out.println(i);// String toString()返回枚举常量的名称String s = Season.SPRING.toString();System.out.println(s);// static<T> T valueOf(Class<T> type,String name)// 获取指定枚举类中的指定名称的枚举值Season season = Season.valueOf(Season.class, "SUMMER");System.out.println(season);// values()获得所有的枚举项Season[] values = Season.values();for (Season value : values) {System.out.println(value.toString());}}/*** 春天*/SPRING("春"){@Overridepublic void show() {System.out.println(this.name());}},/*** 夏天*/SUMMER("夏"){@Overridepublic void show() {System.out.println(this.name());}},/*** 秋天*/AUTUMN("秋"){@Overridepublic void show() {System.out.println(this.name());}},/*** 冬天*/WINTER("冬"){@Overridepublic void show() {System.out.println(this.name());}};Season(String name) {this.name = name;}private String name;public abstract void show();
}/**测试类*/
public class EnumDemo {public static void main(String[] args) {//我们可以通过"枚举类名.枚举项名称"去访问指定的枚举项//每一个枚举项其实就是该枚举的一个对象Season spring = Season.SPRING;spring.show();System.out.println(Season.SUMMER);}
}

7.4 方法

方法名 说明
String name() 获取枚举项的名称
int ordinal() 返回枚举项在枚举类中的索引值
int compareTo(E o) 比较两个枚举项,返回的是索引值的差
String toString() 返回枚举常量的名称
static T valueOf(Class type,String name) 获取指定枚举类中的指定名称的枚举值
values() 获得所有的枚举项

示例代码

public class EnumDemo {public static void main(String[] args) {//String name()获取枚举项的名称String name = Season.SPRING.name();System.out.println(name);// int ordinal()返回枚举项在枚举类中的索引值,从0开始计数
//        int index = Season.WINTER.ordinal();int index = Season.SPRING.ordinal();System.out.println(index);// int compareTo(E o)比较两个枚举项,返回的是索引值的差//compareTo左边减右边int i = Season.SPRING.compareTo(Season.AUTUMN);System.out.println(i);// String toString()返回枚举常量的名称String s = Season.SPRING.toString();System.out.println(s);// static<T> T valueOf(Class<T> type,String name)// 获取指定枚举类中的指定名称的枚举值Season season = Season.valueOf(Season.class, "SUMMER");System.out.println(season);// values()获得所有的枚举项Season[] values = Season.values();for (Season value : values) {System.out.println(value.toString());}}
}

day16多线程网络编程日志枚举相关推荐

  1. Linux多线程网络编程要义丨epoll与reactor原理

    linux多线程网络编程要义 1. epoll原理剖析 2. 单reactor原理以及应用 3. 多reactor原理以及应用 [Linux服务器系列]Linux多线程网络编程要义丨epoll与rea ...

  2. 【Linux服务器开发系列】详解多线程网络编程丨百分百干货分享丨学到就是赚到

    90分钟搞懂多线程网络编程模型 1. 网络编程关注的问题 2. 网络编程的几种模型reactor,one loop per thread及其变种 3. skynet,redis,nginx,memca ...

  3. Java高并发与多线程网络编程

    目录 一. 基础 1. 线程介绍 2. 创建并启动线程 3. 函数式接口编程 4. Thread 构造器 5. 守护线程 线程关系 6. join 7. interrupt 8. 优雅的结束线程 9. ...

  4. HSHA多线程网络编程模型介绍

     我在上一篇的分享<Leader/Follower多线程网络模型介绍>中详细介绍了LF多线程网络模型的原理和代码分析.它的思路是减少线程上下文切换和数据拷贝时间,通过角色转换,来提高处 ...

  5. python多线程网络编程_python网络编程之线程

    一 .背景知识 1.进程 之前我们已经了解了操作系统中进程的概念,程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程.程序和进程的区别就在于:程序是指令 ...

  6. python多线程网络编程_python之网络编程-多线程

    死锁现象,递归锁 所谓死锁:是指两个或两个以上的进程或线程在执行过程中,因为争夺资源而造成的一种互相等待的现象,若无外力作用,他们将无法推进下去,此时称系统处于死锁状态或系统产生了死锁,这些永远在相互 ...

  7. QT多线程网络编程程序崩溃问题与解决

    环境:Ubuntu14.04,Qt5.5 平台:QtCreator 场景:有以下三个线程:1.gstreamer循环取摄像头视频帧,25fps:2.HTTP循环请求消息并显示在QDoubleSpinB ...

  8. java为什么需要网络编程,2022最新

    跪求!阿里P9手写的这份1530页的Java核心编程技术手册的实现原理?否想知道对象关系映射(ORM)框架的实现原理?首线程池如何实现?先给大家展示一下这份手册的目录部分,需要获取的小伙伴可以直接转发 ...

  9. 多线程+SOCKET编程实现qq群聊的服务端和客户端

    多线程+SOCKET编程实现qq群聊的服务端和客户端 标签(空格分隔): 多线程 网络编程 线程同步 一.设计思路 1.服务端 每来一个客户端连接,服务端起一个线程维护: 将收到的消息转发给所有的客户 ...

最新文章

  1. Java文件流应用:剪切文件
  2. 【主题演讲】探索云、视频会议,编解码的奥妙
  3. 连接moogDB数据库
  4. LC67---删除有序链表中重复的元素---牛客---JAVA
  5. 使用阿里云对象存储OSS收藏老婆新垣结衣日常照
  6. vue 循环遍历 搜寻资料
  7. 二维数组名作为实参或者形参
  8. 数学建模--预测方法
  9. html swf转mp4,《swf转换成mp4及高清视频的方法》.docx
  10. sf授权php,授权系统全解源码(支持分子系统)【原完整版】
  11. OpenCL 简单概念
  12. html table相同值合并单元格,ElementUI表格列相同值自动合并单元格( 单列 )
  13. 软件测试计划模板--云闪付平台
  14. 电脑桌面便签小工具下载,专业桌面待办便签软件
  15. expdp/impdp 使用总结
  16. 运算符(一元 二元)
  17. 大小限制_微信传文件有大小限制怎么办?教你3秒把100MPPT压缩成10M
  18. 根据year年的第week周,查询第week周的起止时间
  19. java字符串长度(java字符串长度压缩)
  20. \t\tsizeof(char*)几个字节?

热门文章

  1. 【解决方案】聚焦两会-国标EasyGBS流媒体平台在2021年两会重点工作环保充电桩的视频监控应用
  2. 藏在GPT背后的治理分歧:那些赞同和反对的人们|AI百态(下篇)
  3. gulp-less解决遇到错误停止执行task
  4. UVA-10929-You can say 11(秦九昭算法+同余与模算术)
  5. 设置firefox背景为黑夜模式
  6. 【参赛作品14】Windows安装华为openGauss数据库——openGauss基于x86架构openEuler虚拟机的编译以及JDBC的连接
  7. 【来日复制粘贴】用高级筛选和函数公式拆分数据列表
  8. nofollow标签的作用 nofollow标签添加方法
  9. go实现时针分针夹角问题
  10. 车辆的纵向控制之标定