JavaSE02_Day01(下)-互斥锁、聊天室案例重构(三、四)

一、互斥锁

1.1互斥锁

概念:如果使用synchronized关键字进行锁定多个代码片段时,并且指定的同步监视器对象是同一个监视器对象,那么当前代码进行执行的时候是有互斥效果,多个代码片段不能够同时进行执行。

问题:定义两个方法,方法A和方法B,这两个方法一个是进行人的呼吸,另一个方法是进行人的吞咽,然后对这两个方法的代码片段进行“上锁”,最终进行测试,查看执行代码以后,控制台的输出语句是否两个线程可以进行同时执行?

答案:不能同时执行的。

 package cn.tedu.thread02;
 /**
   * 互斥锁案例演示
  * @author cjn
  *
  */
 public class SyncDemo05 {
 
     public static void main(String[] args) {
         Person person = new Person();
         
         Thread t1 = new Thread() {
             @Override
             public void run() {
                 person.methodA();
            }          
        };
         
         Thread t2 = new Thread() {
             @Override
             public void run() {
                 person.methodB();
            }
        };
         t1.setName("包老师的气管");
         t2.setName("包老师的食管");
         //模拟人的食管是否可以同时进行呼吸和吞咽
         t1.start();
         t2.start();
         
    }
 
 }
 
 /**
   * 外部类Person
  */
 class Person{
     /**
       * 模拟人进行呼吸的方法
      */
     public synchronized void methodA() {
         Thread t1 = Thread.currentThread();
         System.out.println(t1.getName() + ":开始吸气...");
         try {
             Thread.sleep(2000);
        } catch (InterruptedException e) {
             e.printStackTrace();
        } catch (Exception e) {
             e.printStackTrace();
        }
         System.out.println(t1.getName() + ":吸气完成...");
    }
     
     /**
      * 模拟人进行吞咽的方法
      */
     public synchronized void methodB() {
         Thread t = Thread.currentThread();
         System.out.println(t.getName() + ":开始吃东西...");
         try {
             Thread.sleep(2000);
        } catch (InterruptedException e) {
             e.printStackTrace();
        } catch (Exception e) {
             e.printStackTrace();
        }
         System.out.println(t.getName() + ":吃完东西了...");
    }
     
 }

控制台结果:两个线程没有同时执行,存在互斥。

 包老师的气管:开始吸气...
 包老师的气管:吸气完成...
 包老师的食管:开始吃东西...
 包老师的食管:吃完东西了...

1.2 互斥锁常见的面试题

问题:如果在一个类中有两个方法,一个方法是静态方法,另一个方法是非静态方法,当进行使用线程,分别把两个方法作为任务序列进行调用执行,是否存在互斥关系?(这两个方法都添加了synchronized)

答案:静态方法和非静态方法(对象方法)如果同时声明了方法中带有synchronized关键字,它们之间是不存在互斥关系的,也就是非互斥关系。原因:静态方法使用synchronized关键字锁的对象是类对象,而非静态方法(对象方法)使用synchronized关键字锁的对象是当前方法所属的对象,也就意味着锁对象并不是同一个对象,所以不存在互斥关系。

代码测试

 package cn.tedu.thread;
 /**
  * 互斥锁案例面试题演示
  * @author cjn
  *
  */
 public class SyncDemo05 {
 
  public static void main(String[] args) {
  Person p = new Person();
  Thread t1 = new Thread(){
  @Override
  public void run() {
  Person.methodA();
  }
  };
  Thread t2 = new Thread(){
  @Override
  public void run() {
  p.methodB();
  }
  };
  t1.setName("气道");
  t2.setName("食道");
  t1.start();
  t2.start();
  }
 }
 /**
  * 外部类
  * @author cjn
  *
  */
 class Person{
  /**
  * 模拟人的呼吸
  */
  public synchronized static void methodA(){
  Thread t = Thread.currentThread();
  System.out.println(t.getName()+":开始吸气...");
  try {
  Thread.sleep(1000);
  } catch (InterruptedException e) {
  e.printStackTrace();
  } catch (Exception e) {
  e.printStackTrace();
  }
  System.out.println(t.getName()+":吸气完毕!");
  }
 
  /**
  * 模拟人的吞咽
  */
  public synchronized void methodB(){
  Thread t = Thread.currentThread();
  System.out.println(t.getName()+":开始吃东西...");
  try {
  Thread.sleep(1000);
  } catch (InterruptedException e) {
  e.printStackTrace();
  } catch (Exception e) {
  e.printStackTrace();
  }
  System.out.println(t.getName()+":吃东西完毕!");
  }
 }
 

控制台输出结果:观察结果可以发现,结果是两条两条同时出现的,即:不存在互斥问题。

 食道:开始吃东西...
 气道:开始吸气...
 食道:吃东西完毕!
 气道:吸气完毕!

 

二、聊天室案例重构

2.1 聊天室案例三

业务:可以创建多个客户端,并且不同的客户端都可以向服务器端发送消息数据。

一个客户端和一个服务端连接的原理图

多个客户端和一个服务端连接的原理图

实现步骤:

  • 在服务器端需要创建一个内部类(外部类也可以),这个内部类专门用于处理客户端的请求,可以理解成这个类是线程任务序列类,获取输入流对象,并进行循环判断,进而可以读取并输出客户端发送过来的数据消息。

  • 在服务器端的start()方法中创建线程实例,并且指定任务序列,然后开启线程。

 package cn.tedu.socket3;
 
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.net.ServerSocket;
 import java.net.Socket;
 
 /**
  * 聊天室服务器端案例三
  * @author cjn
  *
  */
 public class Server {
    /*
      * 声明ServerSocket属性
      * 服务端使用的socket对象是ServerSocket,
      * 需要通过该对象进行申请服务的端口号,如果
      * 服务器端需要获取客户端发送的数据消息,
      * 需要和客户端书写的端口号一致,只有这样
      * 才可以进行监听到客户端所发送的消息
      */
    private ServerSocket serverSocket;
     
    /**
      * 添加构造器方法,用于对属性的初始化操作
      * @param args
      */
    public Server() {
        try {
            //启动类以后,进行对ServerSocket进行初始化
            System.out.println("服务器端正在启动...");
            //创建ServerSocket实例以后需要指定端口号
            serverSocket = new ServerSocket(8888);
            System.out.println("服务器端启动成功!!!");
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
         
    }
 
    /**
      * 用于获取输入流对象,并且接收客户端发送的消息
      * @param args
      * @throws IOException
      */
    public void start() {
        try {
            /*
              * 循环等待客户端连接,每当有客户端连接,
              * 都会得到当前连接客户端的socket对象,
              * 将当前获取到的某个客户端的socket,
              * 传入新的线程,然后进行处理
              */    
            while (true) {
                System.out.println("等待客户端连接...");
                /*
                  * 连接客户端,ServerSocket提供了一个accept接收客户端连接的方法
                  * accept方法本身也是一个阻塞方法,没有客户端连接时会一直等待客户端连接
                  */
                Socket socket = serverSocket.accept();
                System.out.println("一个客户端连接完毕!!!");
                 
                //创建线程,在创建线程时,指定对客户端操作的任务序列
                ClientHandler clientHandler = new ClientHandler(socket);
                Thread thread = new Thread(clientHandler);
                thread.start();
            }
         
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
      * 内部类,用于处理客户端发送的请求
      * 可以作为线程的任务序列
      */
    class ClientHandler implements Runnable{
        //客户端发送过来的Socket对象
        private Socket socket;
        //查看客户端地址使用
        private String host;
        /**
          * 内部类构造器,用于对内部类中声明的属性进行初始化操作
          * @param socket 客户端传入的socket对象
          */
        public ClientHandler(Socket socket) {
            this.socket = socket;
            this.host = socket.getInetAddress().getHostAddress();
        }
        /**
          * 任务序列
          */
        @Override
        public void run() {
            try {
                //获取客户端发送过来的输入流对象
                InputStream is = socket.getInputStream();
                //流连接
                InputStreamReader isr = new InputStreamReader(is);
                BufferedReader br = new BufferedReader(isr);
                //使用缓冲输入流对象按行读取客户端发送的数据
                String message;
                while ((message = br.readLine()) != null) {
                    //如果读取的数据不为null,代表没有读取到末尾
                    System.out.println(host + "说" + message);
                }    
            } catch (IOException e) {
                e.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            }
             
        }
         
    }
     
    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }
 
 }

测试步骤:

       先启动服务器端,然后再启动客户端1,启动以后,控制台切换到客户端1,输入相关信息,查看服务端控制台是否有显示。再启动客户端2,启动以后,切换控制台到客户端2,输入相关信息,查看服务端控制台是否有显示。

结果结果

服务端正在启动...
服务端启动成功!
等待客户端连接...
一个客户端连接完毕!
等待客户端连接...
127.0.0.1说张三说:111
一个客户端连接完毕!
等待客户端连接...
127.0.0.1说李四说:222
一个客户端连接完毕!
等待客户端连接...
127.0.0.1说王五说:333

2.2 聊天室案例四

群聊业务:将当前多个客户端向服务器端发送消息以后,可以让所有客户端发送的消息不光在服务器端有显示,也可以让这些消息在不同的客户端中都显示。

  1. 客户端需要有读取服务端发送数据的功能,需要在原有输出流的基础上,再拥有输入流进行读取服务器端发送数据。

  1. 服务器端需要有向客户端写出数据的功能,需要在原有输入流的基础上,再拥有输出流进行向客户端发送数据。

实现步骤:

  • 在服务器端定义输出流类型的数组对象,用于保存各个客户端发送的数据的输出流对象。

  • 服务器之前只是拥有输入流对象,用于读取客户端发送过来的数据消息,现在需要将服务器端接收到客户端发送的数据再依次发送给每个客户端实现群聊效果,所以,需要在服务器端中添加输出流,并将这个输出流对象保存到上一步定义的数组中。

  • 客户端之前只是拥有输出流对象,用于向服务器端写出相关数据,但是通过图示和步骤二,客户端也需要进行读取服务端发送的数据消息,所以需要在客户端中添加输入流对象,用于读取服务器端发送过来的群聊数据。

Server

 package cn.tedu.socket4;
 
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.net.ServerSocket;
 import java.net.Socket;
 import java.util.Arrays;
 
 /**
  * 聊天室服务器端案例四
  * @author cjn
  *
  */
 public class Server {
     /*
      * 声明ServerSocket属性
      * 服务端使用的socket对象是ServerSocket,
      * 需要通过该对象进行申请服务的端口号,如果
      * 服务器端需要获取客户端发送的数据消息,
      * 需要和客户端书写的端口号一致,只有这样
      * 才可以进行监听到客户端所发送的消息
      */
     private ServerSocket serverSocket;
     //定义用于向各个客户端写出数据的输出流数组对象
     private PrintWriter[] allout = {};
     
     /**
      * 添加构造器方法,用于对属性的初始化操作
      * @param args
      */
     public Server() {
         try {
             //启动类以后,进行对ServerSocket进行初始化
             System.out.println("服务器端正在启动...");
             //创建ServerSocket实例以后需要指定端口号
             serverSocket = new ServerSocket(8888);
             System.out.println("服务器端启动成功!!!");
        } catch (IOException e) {
             e.printStackTrace();
        } catch (Exception e) {
             e.printStackTrace();
        }
         
    }
 
     /**
      * 用于获取输入流对象,并且接收客户端发送的消息
      * @param args
      * @throws IOException
      */
     public void start() {
         try {
             /*
               * 循环等待客户端连接,每当有客户端连接,
               * 都会得到当前连接客户端的socket对象,
               * 将当前获取到的某个客户端的socket,
               * 传入新的线程,然后进行处理
              */    
             while (true) {
                 System.out.println("等待客户端连接...");
                 /*
                  * 连接客户端,ServerSocket提供了一个accept接收客户端连接的方法
                  * accept方法本身也是一个阻塞方法,没有客户端连接时会一直等待客户端连接
                  */
                 Socket socket = serverSocket.accept();
                 System.out.println("一个客户端连接完毕!!!");
                 
                 //创建线程,在创建线程时,指定对客户端操作的任务序列
                 ClientHandler clientHandler = new ClientHandler(socket);
                 Thread thread  = new Thread(clientHandler);
                 thread.start();
            }
         
        } catch (IOException e) {
             e.printStackTrace();
        } catch (Exception e) {
             e.printStackTrace();
        }
    }
     /**
       * 内部类,用于处理客户端发送的请求
       * 可以作为线程的任务序列
      */
     class ClientHandler implements Runnable{
         //客户端发送过来的Socket对象
         private Socket socket;
         //查看客户端地址使用
         private String host;
         /**
          * 内部类构造器,用于对内部类中声明的属性进行初始化操作
          * @param socket 客户端传入的socket对象
          */
         public ClientHandler(Socket socket) {
             this.socket = socket;
             this.host = socket.getInetAddress().getHostAddress();
        }
         /**
          * 任务序列
          */
         @Override
         public void run() {
             
             try {
                 //获取客户端发送过来的输入流对象
                 InputStream is = socket.getInputStream();
                 //流连接
                 InputStreamReader isr = new InputStreamReader(is);
                 BufferedReader br = new BufferedReader(isr);
                 
                 //获取发送给客户端相关信息的输出流
                 OutputStream os = socket.getOutputStream();
                 OutputStreamWriter osw = new OutputStreamWriter(os);
                 BufferedWriter bw = new BufferedWriter(osw);
                 PrintWriter pw = new PrintWriter(bw,true);
                 
                 //将获取的输出流对象添加到输出流数组中
                 synchronized (allout) {
                     //1.对缓冲输出流数组进行扩容
                     allout = Arrays.copyOf(allout, allout.length + 1);
                     //2.将获取的输出流对象赋值给数组中的最后一个元素
                     allout[allout.length - 1] = pw;
                }
                 
                 
                 //使用缓冲输入流对象按行读取客户端发送的数据
                 String message;
                 while ((message = br.readLine()) != null) {
                     //如果读取的数据不为null,代表没有读取到末尾
                     System.out.println(host + "说" + message);
                }
                 
            } catch (IOException e) {
                 e.printStackTrace();
            } catch (Exception e) {
                 e.printStackTrace();
            }
             
        }
         
    }
     
     public static void main(String[] args) {
         Server server = new  Server();
         server.start();
    }
 
 }

        当前聊天室案例中的第四个版本并没有重构完,目前重构到了服务器端,在服务端定义了缓冲输出流数组,并且从客户端传入的socket中获取了输出流对象,并将输出流对象添加到了缓冲输出流数组中。剩下的重构逻辑,周六完成。

 

 

posted @ 2021-07-01 23:23  Coder_Cui  阅读(66)  评论(0编辑  收藏  举报