网络编程基础

发布时间 2023-10-06 13:45:51作者: 顺风顺水heng

网络编程

InetAddress类

表示IP对象的一个类

public static void main(String[] args) throws UnknownHostException {
    //获取本机的ip对象
    // InetAddress ip = InetAddress.getLocalHost();
    //获取域名
    // System.out.println(ip.getHostName());
    //获取真实ip地址
    // System.out.println(ip.getHostAddress());

    //getByName(域名)  得到域名对应的ip对象
    //localhost域名表示本机,对应的ip地址为127.0.0.1
    InetAddress ip = InetAddress.getByName("localhost");
    //获取域名
    System.out.println(ip.getHostName());
    //获取ip地址
    System.out.println(ip.getHostAddress());
}

Socket类和ServerSocket类

都属于Socket(套接字)对象,表示网络中的某个端点

  • Socket指普通端
  • ServerSocket指服务器端

使用套接字对象实现两个端点(Socket和ServerSocket)之间发送文件

服务器端

package com.hqyj.uploadTest;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;

/*
 * 使用套接字对象,实现客户端向服务端发送文件
 *
 * 定义服务端套接字对象
 * */
public class Server {
    public static void main(String[] args) throws IOException {
        //以本机创建服务端套接字对象
        ServerSocket server = new ServerSocket(8899, 100, InetAddress.getLocalHost());
        //等待客户端连接,返回连接的客户端套接字对象
        Socket client = server.accept();

        //定义要将读取到的数据写入到本地的文件字节输出流对象
        FileOutputStream fos = new FileOutputStream("上传文件.md");

        //获取客户端与服务端的输入流对象,读取发送的数据
        InputStream is = client.getInputStream();

        //定义读取的字节数组
        byte[] bytes = new byte[1024 * 1024 * 8];

        int count = is.read(bytes);
        while (count != -1) {
            //将读取到的数据写入到本地
            fos.write(bytes, 0, count);
            count = is.read(bytes);
        }

        fos.close();
        is.close();
    }
}

客户端

package com.hqyj.uploadTest;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

/*
 * 定义客户端套接字对象
 * */
public class Client {
    public static void main(String[] args) throws IOException {
        //创建客户端套接字对象,连接指定的服务端套接字对象
        Socket client = new Socket("192.168.31.39", 8899);
        //获取客户端与服务端的输出流对象
        OutputStream os = client.getOutputStream();

        //成功连接后,将某个文件发送给服务端
        //定义要发送的文件对象
        File file = new File("F:\\221001\\笔记\\面向对象部分回顾.md");

        //读取要发送的文件
        FileInputStream fis = new FileInputStream(file);

        //定义字节数组
        byte[] bytes = new byte[1024 * 1024 * 8];
        //循环读取要发送的文件
        int count = fis.read(bytes);
        while (count != -1) {
            //将读取到的数据写入到客户端套接字与服务端套接字的通道中
            os.write(bytes,0,count);
            count = fis.read(bytes);
        }

        fis.close();
        os.close();
    }
}

进程和线程

进程Process

进程就是操作系统中执行的程序。一个程序就是一个执行的进程实体。

每个运行中的进程,都有属于它独立的内存空间,各个进程互不影响。

线程Thread

线程是一个进程中的执行单元,一个进程中可以有多个线程。

多个线程,可以访问同一个进程中的资源。

每个线程都有一个独立的栈空间,这些线程所在的栈空间位于同一个进程空间中。

多线程

如果一个进程中,同时在执行着多个线程,就称为多线程。

多线程可以提高程序执行效率。如多个窗口卖票,可以加快卖票的效率。

其实每个执行的Java程序,都是多线程执行,main方法称为主线程,还有gc线程(守护线程)在同时运行。

如有一个工厂,工厂中有很多车间,每个车间有很多流水线。

工厂就是内存,车间就是各个进程,每个流水线都是一个进程中的一个线程。

并行和并发

并行

各个进程同时执行,称为并行。

并发

多个线程同时执行,称为并发。

同步和异步

同步

所有的任务排队执行,称为同步执行。

异步

在执行任务A的同时,执行任务B,称为异步执行。

Java中的线程Thread类

Java中,线程以对象的形式存在。

Thread类表示线程类

获取线程对象

  • 获取当前正在运行的线程对象

    Thread ct = Thread.cuurentThread();
    
  • 创建一个线程对象

    构造方法

    常用构造方法说明
    Thread() 创建一个默认的线程对象
    Thread(String name) 创建一个指定名称的线程对象
    Thread(Runnable target) 将一个Runnable对象包装为线程对象
    Thread(Runnable target,String name) 将一个Runnable对象包装为线程对象同时设置线程名

线程常用方法

方法作用
getId() 获取线程id
getName() 获取线程名,默认Thread-n
getPriority() 获取线程优先级,默认为5
getState() 获取线程状态
setName(String str) 设置线程名
setPriority(int priority) 设置线程优先级,范围在1-10,值越大越优先执行
isDaemon() 判断线程是否为守护线程
setDaemon(boolean f) 参数为true表示设置线程为守护线程
start() 让线程进入就绪状态
run() 线程获得执行权时执行的方法(线程要做的事情)
Thread.sleep(long m) 设置当前线程休眠m毫秒
Thread.currentThread() 获取当前执行的线程对象
Thread.yield() 线程让步

实现多线程

方式一:继承Thread类

  • 1.创建一个类,继承Thread类
  • 2.重写Thread类中的run()方法
  • 3.创建自定义的线程子类对象后,调用start()方法

自定义Thread线程的子类

package com.hqyj.ThreadTest;

/*
 * 实现多线程步骤
 * 1.成为Thread的子类
 * 2.重写run()方法
 * 3.创建当前类对象后,调用start()方法
 * */
public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            //让该线程输出0-99
            System.out.println(getName() + ":" + i);
        }
    }

    public MyThread(String name) {
        super(name);
    }

    public MyThread() {
    }
}

main类

package com.hqyj.ThreadTest;

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

        //创建无参数的自定义线程对象
        MyThread t1 = new MyThread();
        t1.setName("线程A");
        //创建自定义线程对象,参数为线程名
        MyThread t2 = new MyThread("线程B");

        //让两个线程自动执行,必须调用start()
        t1.start();
        t2.start();
    }
}

方式二:实现Runnable接口(建议使用)

由于Java中是单继承,如果某个类已经使用了extends关键字去继承了另一个类,这时就不能再通过extends继承Thread实现多线程。

就需要实现Runnable接口的方式实现多线程。

  • 1.自定义一个类,实现Runnable接口
  • 2.重写run()方法,将多线程要执行的内容写在该方法中
  • 3.创建Runnable接口的实现类对象
  • 4.使用构造方法Thread(Runnable target)或Thread(Runnable target,String name)将上一步创建的Runnable实现类对象包装为Thread对象

自定义Runnable接口的实现类

package com.hqyj.ThreadTest;
/*
 * 实现多线程步骤
 * 1.成为Runnable的实现类
 * 2.重写run()方法
 * 3.创建该类对象
 * 4.将其包装为Thread对象
 * */
public class MyThread2 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            //让该线程输出0-99
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

main类

package com.hqyj.ThreadTest;

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

        //创建Runnable接口的实现类
        Runnable target = new MyThread2();
        //由于启动多线程必须要通过Thread的start()方法,所以一定要创建Thread对象
        Thread mt = new Thread(target,"线程A");//这里使用Thread(Runnable target)构造方法创建Thread对象
        //让线程就绪
        mt.start();
        //创建另一个线程对象,让线程就绪
        new Thread(new MyThread2(),"线程B").start();
    }
}

方式三:使用匿名内部类

如果不想创建一个Runnable接口的实现类,就可以使用匿名内部类充当Runnable接口的实现类

package com.hqyj.ThreadTest;

/*
 * 实现多线程的方式三:
 * 使用匿名内部类
 * */
public class Test3 {
    public static void main(String[] args) {


        //使用Thread(Runnable target ,String name)构造方法创建线程对象
        //此时new Runnable() { @Override public void run() {}}就是一个匿名内部类
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                }
            }
        }, "自定义线程").start();

        //如果main方法当做一个线程时,需要先启动其他线程后,在执行main方法中的内容,否则依然是按顺序执行
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }

}

线程的生命周期

线程的初始化到终止的整个过程,称为线程的生命周期。

img

新生状态

当线程对象被创建后,就进入了新生状态。

就绪状态

当某个线程对象调用了start()方法后,就进入了就绪状态。

在这个状态下,线程对象不会做任何事情,只在等他CPU调度。

运行状态

当某个线程对象得到CPU时间片(CPU执行这个线程的机会所给的时间),则进入运行状态,开始执行run()方法。

不会等待run()方法执行完毕,只会在指定的时间内尽可能地执行run()方法。只要调用玩run()方法后,就会再进入就绪状态。

阻塞状态

如果某个线程遇到了sleep()方法或wait()方法时,就会进入阻塞状态。

sleep()方法会在指定时间后,让线程重新就绪。

wait()方法只有在被调用notify()或notifyAll()方法唤醒后才能重新就绪。

终止状态

当某个线程的run()方法中的所有内容都执行完,就会进入终止状态,意味着该线程的使命已经完成。

守护线程

如果将一个线程设置setDeamon(true),表示该线程为守护线程。

守护线程会随着其他非守护线程终止而终止。

package com.hqyj.DaemonTest;
/*
* Test类是一个自定义线程类,死循环输出
* */
public class Test implements Runnable {
    public static void main(String[] args) {

        Thread thread = new Thread(new Test());
        //将自定义线程类设置为守护线程
        thread.setDaemon(true);
        thread.start();

        //main线程终止,守护线程也会终止
        for (int i = 0; i < 100; i++) {
            System.out.println("main方法中的循环执行中");
        }


    }

    @Override
    public void run() {
        while (true) {
            System.out.println("守护线程执行中。。。");
        }
    }
}

多线程访问同一个资源

可能出现的问题

如银行存款100,同一时刻在手机和ATM一起取出,如果用多线程模拟,可能会出现两个线程都取出100的情况。要避免这种情况发生。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-23VpVYO6-1670159770115)(F:\221001\笔记\JavaAdv07.assets\image-20221202145400278.png)]

本应该大于售出后再减,再打印剩余,由于线程A在打印"售出一张"后,还没来得及执行后续内容,其他线程就开始执行了。

出现问题的原因

由于线程调用start()方法后,就进入就绪状态。如果获得了CPU时间片,就开始调用run()方法,调用run()方法后,就会再次进入就绪状态,不会等待run()方法执行完毕,所以在线程A执行run()方法的时候,线程B也开始执行了,这样就会出现数据共享的问题。

因为现在所有的线程都是异步(同时)执行。

如何解决

让线程同步(排队)执行即可。这样一来,某个线程执行run()方法的时候,让其他线程等待run()方法的内容执行完毕。

synchronized关键字

这个关键字可以修饰方法或代码块

修饰方法

写在方法的返回值之前,这时该方法就称为同步方法。

public synchronized void fun(){
    //会排队执行的代码
}
修饰代码块

写在一个独立的{}前,这时该段内容称为同步代码块。

synchronized(要同步的对象或this){
    //会排队执行的代码
}
原理

每个对象默认都有一把"锁",当某个线程运行到被synchronized修饰的方法时,该对象就会拥有这把锁,在拥有锁的过程中,其他线程不能同时访问该方法,只有等待其结束后,才会释放这把锁。

使用synchronized修饰后的锁称为"悲观锁"。

方法被synchronized修饰后,称为同步方法,就会让原本多线程变成了单线程(异步变为同步)。

多线程相关面试题

  • 实现多线程的方式

    • 继承Thread类
    • 实现Runnable接口后,包装为Thread对象
    • 匿名内部类
  • 为什么说StringBuilder或ArrayList、HashMap是非线程安全的

    package com.hqyj.ThreadSafe;
    
    public class Test {
        public static void main(String[] args) throws InterruptedException {
            // StringBuilder sb = new StringBuilder();
            StringBuffer sb = new StringBuffer();
            //循环10次创建10个线程对象
            for (int i = 0; i < 10; i++) {
                //创建线程对象
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        //每个线程都向StringBuilder对象中添加100次字符串
                        try {
                            Thread.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        for (int j = 0; j < 10; j++) {
                            sb.append("hello");
                        }
                    }
                }).start();
            }
            Thread.sleep(5000);
            //如果正常,应该长度为10线程*10次添加*每次5个字母  长度为500
            System.out.println(sb.length());
            //如果用StringBuilder,最终的长度可能不为500
            //如果用StringBuffer,最终的长度一定为500
            //所有StringBuffer是线程安全的,适用于多线程
            //所有StringBuilder是非线程安全的,适用于单线程
        }
    }
    
  • 什么叫死锁?怎么产生?如何解决?

    如果有两个人吃西餐,必须有刀和叉,此时只有一副刀叉。

    如果A拿到了刀,B拿到了叉,互相都在等待另一个工具,但都不释放自己拥有的,这时就会造成僵持的局面,这个局面就称为死锁,既不结束,也不继续。

模拟死锁出现的情况

定义两个线程类,线程A先获取资源A后,在获取资源B;线程B先获取资源B后,再获取资源A。

如果对资源A和资源B使用了synchronized进行同步,就会在线程A获取资源A的时候,线程B无法获取资源A,相反线程B在获取资源B的时候,线程A无法获取资源B,所以两个线程都不会得到另一个资源。

PersonA线程

package com.hqyj.deadlock;

public class PersonA implements Runnable {
    //定义两个共享的成员变量,刀、叉
    private Object knife;
    private Object fork;


    public PersonA(Object knife, Object fork) {
        this.knife = knife;
        this.fork = fork;

    }

    /*
 * 该线程执行run方法时,先获取knife对象,等待3s后获取fork对象
 *
 * */
    @Override
    public void run() {

        synchronized (knife) {
            System.out.println(Thread.currentThread().getName() + "获取了knife,3s后获取fork");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (fork) {
                System.out.println(Thread.currentThread().getName() + "获取了fork,可以吃饭了");
            }
        }
    }
}

PersonB线程

package com.hqyj.deadlock;

public class PersonB implements Runnable {
    //定义两个共享的成员变量,刀、叉
    private Object knife;
    private Object fork;


    public PersonB(Object knife, Object fork) {
        this.knife = knife;
        this.fork = fork;
    }

    /*
     * 该线程执行run方法时,先获取fork对象,等待3s后获取对象knife
     *
     * */
    @Override
    public void run() {

        synchronized (fork) {
            System.out.println(Thread.currentThread().getName() + "获取了fork,3s后获取knife");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (knife) {
                System.out.println(Thread.currentThread().getName() + "获取了knife,可以吃饭了");
            }
        }
    }

}

死锁的解决方式

方式一

让两个线程获取资源的顺序保持一致。

如两个线程都先获取knife,再获取fork

@Override
public void run() {
    synchronized (knife) {
        System.out.println(Thread.currentThread().getName() + "获取了knife,3s后获取fork");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (fork) {
            System.out.println(Thread.currentThread().getName() + "获取了fork,可以吃饭了");
        }
    }
}

方式二

让两个线程在获取资源A和B之前,再获取第三个资源,对第三个资源使用synchronized进行同步,这样某个线程在获取第三个资源后,将后续内容执行完毕,其他线程才能开始执行。

如在获取knife和fork之前,先获取paper对象

@Override
public void run() {
    //先获取paper,再进行后续操作
    synchronized (paper) {
        synchronized (knife) {
            System.out.println(Thread.currentThread().getName() + "获取了knife,3s后获取fork");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (fork) {
                System.out.println(Thread.currentThread().getName() + "获取了fork,可以吃饭了");
            }
        }
    }
}