学习笔记4:JavaSE & API(网络编程 & 多线程)

发布时间 2023-12-02 16:03:36作者: 执着的怪味豆

1、java.net.Socket:

(1)定义:Socket(套接字)封装了TCP协议的通讯细节,是的我们使用它可以与服务端建立网络链接,并通过它获取两个流(一个输入一个输出),然后使用这两个流的读写操作完成与服务端的数据交互。

(2)方法

  • getInputStream():获取输入流,返回值是InputStream的一个子类实例。
  • getOutputStream():获取输出流,返回值是OutputStream的一个子类实例。
  • close():断开与远端的连接,同时自动关闭通过它获取的输入输出流。
  • getInetAddress(): 获取地址信息,然后紧接着再通过getHostAddress()获取ip。

2、java.net.ServerSocket:

(1)定义:ServerSocket运行在服务端。

(2)作用:

  • 向系统申请服务端口,客户端的Socket就是通过这个端口与服务端建立连接的。
  • 监听服务端口,一旦一个客户端通过该端口建立连接则会自动创建一个Socket,并通过该Socket与客户端进行数据交互。

(3)方法:

  • accept():等待客户端连接。该方法是阻塞方法,等待客户端链接,一旦建立连接,返回一个Socket实例。
  • close():关闭

(4)端口占用:申请端口号被使用了,抛出异常:  java.net.BindException:address already in use

3、客户端、服务端通信示例(聊天室)

(1)连接与通信过程

  • 建立连接

   

  • 客户端与服务端完成第一次通讯(发送一行字符串) 

    

(2)聊天室客户端

public class Client {
    private Socket socket;
    public static void main(String[] args) {
        Client client = new Client();
        client.start();
    }
    public Client(){
        try {
            System.out.println("正在连接服务端...");
            socket = new Socket("localhost",8088);
            System.out.println("与服务端建立连接了!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public void start(){
        try {
            //启动用于读取服务端发送过来消息的线程:避免处理消息,卡住当前线程的执行
            ServerHandler handler = new ServerHandler();
            Thread t = new Thread(handler);
            t.start();
            //通过socket获取的字节输出流,写出的字节会通过网络发送给远端计算机
            OutputStream out = socket.getOutputStream();
            //转换流,字符转字节
            OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
            //缓冲流
            BufferedWriter bw = new BufferedWriter(osw);
            //自动行刷新-字符缓冲输出流
            PrintWriter pw = new PrintWriter(bw,true);
            Scanner scanner = new Scanner(System.in);
            while(true) {
                String line = scanner.nextLine();
                if("exit".equalsIgnoreCase(line)){
                    break;
                }
                pw.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    //该线程任务负责处理服务端发送过来的消息
    private class ServerHandler implements Runnable{
        public void run(){
            try{
                //通过socket获取输入流读,取服务端发送过来的消息
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);
                String line;
                //循环读取服务端发送过来的每一行字符串
                while((line = br.readLine())!=null){//readLine方法是阻塞方法,返回如果是null,表示对方close了。
                    System.out.println(line);
                }
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }
}
客户端

(3)聊天室服务端

public class Server {
    private ServerSocket serverSocket;
    private Collection<PrintWriter> allOut = new ArrayList<>();
    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }
    public Server(){
        try {
            System.out.println("正在启动服务端...");
            serverSocket = new ServerSocket(8088);//可能抛出java.net.BindException:address already in use
            System.out.println("服务端启动完毕!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public void start(){
        try {
            while(true) {
                System.out.println("等待客户端连接");
                Socket socket = serverSocket.accept();
                System.out.println("一个客户端连接了!");
                //启动一个线程来处理该客户端的交互
                ClientHandler clientHandler = new ClientHandler(socket);
                Thread thread = new Thread(clientHandler);
                thread.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    // 该线程任务是用一个线程来处理一个客户端的交互工作
    private class ClientHandler implements Runnable{
        private Socket socket;
        private String host;//记录远端计算机的地址信息
        public ClientHandler(Socket socket){
            this.socket = socket;
            host = socket.getInetAddress().getHostAddress();
        }
        public void run(){
            PrintWriter pw = null;
            try {
                //通过socket获取输入流读取对方发送过来的消息
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);

                //通过socket获取输出流用于给对方发送消息
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                pw = new PrintWriter(bw,true);

                //将该输出流存入共享数组allOut中
                synchronized (Server.this) {//加锁,并且使用外部类对象加锁
                    allOut.add(pw);
                }

                //通知所有客户端,该用户上线了!
                sendMessage(host+"上线了,当前在线人数:"+allOut.size());
                String line;
                /*
                    这里的BufferedReader读取时,底下连接的流是通过Socket获取的输入流,当远端计算机还处于连接状态,
                    但是暂时没有发送内容时,readLine方法会处于阻塞状态,直到对方发送过来一行字符串为止。
                    如果返回值为null,则表示对方断开了连接(对方调用了socket.close())。
                    对于windows的客户端而言(Mac不会),如果是强行杀死的进程,服务端这里readLine方法
                    会抛出下面异常: java.net.SocketException: connection reset
                    服务端无法避免这个异常。
                 */
                while ((line = br.readLine()) != null) {
                    //将读取到的内容,广播给所有客户端
                    sendMessage(host+"说:" + line);
                }
            }catch(IOException e){
                e.printStackTrace();
            }finally {
                //处理客户端断开连接后的操作
                synchronized (Server.this) {
                    allOut.remove(pw);
                }
                //通知所有客户端,该用户下线了!
                sendMessage(host+"下线了,当前在线人数:"+allOut.size());
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        //将消息群发给所有客户端(广播)
        private void sendMessage(String line){
            synchronized (Server.this) {
                System.out.println(line);
                //遍历allOut数组,将消息发送给所有客户端
                for (PrintWriter pw : allOut) {
                    pw.println(line);
                }
            }
        }
    }
}
服务端

4、多线程定义

(1)线程:一个顺序的单一的程序执行流程就是一个线程。代码一句一句的有先后顺序的执行。

(2)多线程:多个单一顺序执行的流程并发运行。造成"感官上同时运行"的效果。

(3)并发:线程调度程序会将CPU运行时间划分为若干个时间片段并尽可能均匀的分配给每个线程,拿到时间片的线程被CPU执行这段时间。当超时后线程调度程序会再次分配一个时间片段给一个线程使得CPU执行它,如此反复。

(4)用途:

  • 当出现多个代码片段执行顺序有冲突时,希望它们各干各的时就应当放在不同线程上"同时"运行
  • 一个线程可以运行,但是多个线程可以更快时,可以使用多线程运行

(5)线程的生命周期

    

5、创建线程 

(1)主线程:执行main方法的线程称为"主线程"。

(2)方式一:定义一个类继承Thread并重写run方法,在其中定义线程要执行的任务。启动线程调用start()。
  • 优点:结构简单,便于匿名内部类形式创建
  • 缺点:
    • 直接继承线程,会导致不能在继承其他类去复用方法,这在实际开发中是非常不便的。
    • 定义线程的同时重写了run方法,会导致线程与线程任务绑定在了一起(强耦合),不利于线程的重用。
  • 示例:
public class My {
    public static void main(String[] args){
        //创建两个线程
        Thread t1 = new MyThread1();
        Thread t2 = new MyThread2();
        t1.start();
        t2.start();
    }
}
class MyThread1 extends Thread{
    public void run(){
        for (int i=0;i<100;i++){
            System.out.println("执行任务一");
        }
    }
}
class MyThread2 extends Thread{
    public void run(){
        for (int i=0;i<100;i++){
            System.out.println("执行任务二");
        }
    }
}

(3)方式二:实现Runnable接口单独定义线程任务,再用任务实例化线程对象。

  • 优点:任务和线程实现解耦
  • 示例:
public class My {
    public static void main(String[] args){
        //实例化任务
        Runnable r1 = new MyRunnable1();
        Runnable r2 = new MyRunnable2();
        //创建线程并指派任务
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);

        t1.start();
        t2.start();
    }
}
class MyRunnable1 implements Runnable{
    public void run() {
        for (int i=0;i<100;i++){
            System.out.println("执行任务一");
        }
    }
}
class MyRunnable2 implements Runnable{
    public void run() {
        for (int i=0;i<100;i++){
            System.out.println("执行任务二");
        }
    }
}
  • 匿名内部类实现
public class My {
    public static void main(String[] args) {
        //方式一创建线程,方便使用匿名内部类
        Thread t1 = new Thread(){
            public void run(){
                for(int i=0;i<1000;i++){
                    System.out.println("执行任务一");
                }
            }
        };
        //方式二创建线程,任务和线程解耦
        Runnable r2 = ()->{
            for(int i=0;i<1000;i++){
                System.out.println("执行任务二");
            }
        };
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
    }
}

6、线程常用方法

(1)Thread.currentThread():静态方法,获取“执行当前方法”的线程。

(2)getName():获取线程的名字

(3)getId():获取该线程的唯一标识

(4)getPriority():获取该线程的优先级

(5)isAlive():该线程是否活着

(6)isDaemon():是否为守护线程

(7)isInterrupted():是否被中断

(8)Thead.sleep():静态方法,阻塞线程,超时后线程会自动回到RUNNABLE状态等待再次获取时间片并发运行。

(9)interrupt():当一个线程调用sleep方法处于睡眠阻塞的过程中,interrupt()被调用时,sleep方法会抛出该异常InterruptedException,从而打断睡眠阻塞。

7、线程优先级

(1)时间片:线程start后会纳入到线程调度器中统一管理,线程只能被动的被分配时间片并发运行,而无法主动索取时间片。线程调度器尽可能均匀的将时间片分配给每个线程。

(2)优先级:线程有10个优先级,使用整数1-10表示

  • 1为最小优先级,10为最高优先级。5为默认值
  • 调整线程的优先级可以最大程度的干涉获取时间片的几率。优先级越高的线程获取时间片的次数越多,反之则越少。

(3)设置优先级: 

  • setPriority(Thread.MIN_PRIORITY):MIN_PRIORITY、NORM_PRIORITY、MAX_PRIORITY是常量 

8、守护线程

 

(1)定义:守护线程也称为后台线程,就是一个普通线程,但是会随着进程结束时自动结束。

(2)特点:

  • 设置:守护线程是通过普通线程调用setDaemon(boolean on)方法设置而来的,因此创建上与普通线程无异。
  • 结束:守护线程的结束时机与普通线程不同,即:进程的结束。
  • 进程结束:当一个进程中的所有普通线程都结束时,进程就会结束,此时会杀掉所有正在运行的守护线程。
  • 场景:不关心某个线程什么时候停下来,但是当程序结束时自动结束,可以设置为守护线程,比如,GC垃圾回收。

(3)方法:

  • setDaemon():将一条进程设置成守护进程,注意, 但必须在start方法之前调用。

9、线程安全问题

(1)问题:当多个线程并发操作同一临界资源,由于线程切换时机不确定,导致操作临界资源的顺序出现混乱严重时可能导致系统瘫痪。
(2)临界资源:操作该资源的全过程同时只能被单个线程完成。
(3)解决:使用synchronized 加锁。

10、synchronized

(1)使用方法
  • 同步方法:在方法上修饰,此时该方法变为一个同步方法。
  • 同步块:可以更准确的锁定需要排队的代码片段
 (2)同步块:
  • 语法

/*
    同步块使用时需要指定一个同步监视器对象,即:上锁的对象。该对象从语法的角度来讲可以是任意引用类型的实例。
    但是必须同时满足多个需要同步(排队)执行该代码片段的线程看到的是同一个对象才可以!
*/
synchronized (this) {
    Thread.sleep(5000);
    System.out.println("同步执行的任务");
}

 

  • 同步监视器对象:上锁的对象,要想保证同步块中的代码被多个线程同步运行,则要求多个线程看到的同步监视器对象是同一个,可以是java中任何引用类型的实例。一般用当前对象this即可。
(3)同步方法:
  • 语法
    public synchronized void method() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + ":正在执行A方法");
        Thread.sleep(5000);
        System.out.println(Thread.currentThread().getName() + ":执行A方法完毕");
    }
  • 锁对象:同步方法也有同步监视器对象,“锁”的是this。静态方法,锁的是类对象this。
(4)静态方法:
  • 静态方法加上synchronized,该方法是一个同步方法,由于静态方法所属类,所以一定具有同步效果。
  • 锁对象:静态方法使用的同步监视器对象为当前类的类对象(Class的实例)。JVM在加载一个类时,会同时实例化一个Class实例与之对应,因此每个被加载的类都有且只有一个Class实例,而这个实例就称为这个加载类的类对象。

(5)互斥锁:

  • 定义:当多个线程执行不同的代码片段,但是这些代码片段之间不能同时运行时就要设置为互斥的。
  • 实现:使用synchronized锁定多个代码片段,并且指定的同步监视器是同一个时,这些代码片段之间就是互斥的。
  • 示例
public class MySyncDemo {
    public static void main(String[] args) {
        Foo foo = new Foo();
        Thread t1 = new Thread(){
            public void run(){
                foo.methodA();
            }
        };
        Thread t2 = new Thread(){
            public void run(){
                foo.methodB();
            }
        };
        t1.start();
        t2.start();
    }
}
class Foo{
    public synchronized void methodA(){
        Thread t = Thread.currentThread();
        try {
            System.out.println(t.getName()+":正在执行A方法...");
            Thread.sleep(5000);
            System.out.println(t.getName()+":执行A方法完毕!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public synchronized void methodB(){
        Thread t = Thread.currentThread();
        try {
            System.out.println(t.getName()+":正在执行B方法...");
            Thread.sleep(5000);
            System.out.println(t.getName()+":执行B方法完毕!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

(6)死锁

  • 产生:两个线程各自持有一个锁对象的同时等待对方先释放锁对象,此时会出现僵持状态。
    //定义两个锁对象,"筷子"和"勺"
    public static Object chopsticks = new Object();
    public static Object spoon = new Object();
    public static void main(String[] args) {
        Thread np = new Thread(() -> {
            System.out.println("北方人开始吃饭.");
            System.out.println("北方人去拿筷子...");
            synchronized (chopsticks){//锁住了筷子
                System.out.println("北方人拿起了筷子开始吃饭...");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                }
                System.out.println("北方人吃完了饭,去拿勺...");
                //去拿勺子(此时自己拿着筷子),发现勺子被锁住了,等待勺子释放;但是勺子释放的前提,是自己先放下筷子给对方用,死锁了。
                synchronized (spoon){
                    System.out.println("北方人拿起了勺子开始喝汤...");
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                    }
                    System.out.println("北方人喝完了汤");
                }
                System.out.println("北方人放下了勺");
            }
            System.out.println("北方人放下了筷子,吃饭完毕!");
        });
        
        Thread sp = new Thread(() -> {
            System.out.println("南方人开始吃饭.");
            System.out.println("南方人去拿勺...");
            synchronized (spoon){//锁住了勺子
                System.out.println("南方人拿起了勺开始喝汤...");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                }
                System.out.println("南方人喝完了汤,去拿筷子...");
                //去拿筷子(此时自己拿着勺子),发现筷子被锁住了,等待筷子释放;但是筷子释放的前提,是自己先放下勺子给对方用,死锁了。
                synchronized (chopsticks){
                    System.out.println("南方人拿起了筷子开始吃饭...");
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                    }
                    System.out.println("南方人吃完了饭");
                }
                System.out.println("南方人放下了筷子");
            }
            System.out.println("南方人放下了勺,吃饭完毕!");
        });
        np.start();
        sp.start();
    }
  • 解决死锁:
    • 尽量避免在持有一个锁的同时去等待持有另一个锁(避免synchronized嵌套)
    • 当无法避免synchronized嵌套时,就必须保证多个线程锁对象的持有顺序必须一致。即:A线程在持有锁1的过程中去持有锁2时,B线程也要以这样的持有顺序进行。