【JAVA CORE_API】Day20 互斥、Socket高级、在线聊天室v2.0

同步的静态方法

同步的静态方法

同步的静态方法在Java中通过static synchronized声明,这意味着该方法对类的所有实例共享一个锁。只有当一个线程持有类的锁时,其他线程才能访问该方法。

创建两条线程测试方法独立性

 class Boo {
     public static synchronized void doSome() {
         System.out.println(Thread.currentThread().getName() + " is executing doSome()");
         try {
             Thread.sleep(2000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println(Thread.currentThread().getName() + " finished executing doSome()");
     }
 }
 
 public class Main {
     public static void main(String[] args) {
         Thread t1 = new Thread(Boo::doSome, "Thread 1");
         Thread t2 = new Thread(Boo::doSome, "Thread 2");
 
         t1.start();
         t2.start();
     }
 }
 
  • 由于doSome()是一个静态同步方法,t1t2线程在执行时会串行化,只有当一个线程完成后,另一个线程才能进入该方法。

静态方法使用同步块

静态方法使用同步块是为了对部分代码块进行同步,而不是对整个方法进行同步。这样可以提高效率,因为只有关键部分需要锁定,而其他部分可以并发执行。

 class Boo {
     public static void doSome() {
         System.out.println(Thread.currentThread().getName() + " is entering doSome()");
 
         synchronized (Boo.class) {  // 锁定Boo类对象
             System.out.println(Thread.currentThread().getName() + " is executing synchronized block");
             try {
                 Thread.sleep(2000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println(Thread.currentThread().getName() + " finished synchronized block");
         }
 
         System.out.println(Thread.currentThread().getName() + " is exiting doSome()");
     }
 }
 
 public class Main {
     public static void main(String[] args) {
         Thread t1 = new Thread(Boo::doSome, "Thread 1");
         Thread t2 = new Thread(Boo::doSome, "Thread 2");
 
         t1.start();
         t2.start();
     }
 }
 
  • 在上面的代码中,Thread 1Thread 2可以同时进入doSome()方法的同步块外的部分,但只能一个线程进入同步块,确保了同步块内的操作是线程安全的。

  • 锁定对象synchronized (Boo.class) 语句锁定了Boo类的Class对象。由于这是一个静态方法,锁定的是整个类,而非某个实例。

  • 同步块效果:当一个线程进入同步块时,其他线程需要等待该线程释放锁才能进入同步块。但同步块外的代码可以并发执行。

  • 提高效率:如果方法中的其他部分不需要同步,可以只对关键部分加锁,减少等待时间,提高并发性能。

互斥锁

互斥锁概述

  • 互斥锁是什么:互斥锁(Mutex)是指几个线程执行几个不一样的代码,但是代码又不能一起执行,使用互斥锁;

  • 如何实现互斥锁:在Java中,通过synchronized关键字为像创建互斥锁的两端代码加上本关键字来实现互斥锁,可以用在方法或同步块上。

 package day20;
 
 public class MutexDemo {
     public static void main(String[] args) {
         Aoo aoo = new Aoo();
         new Thread(aoo::methodA).start();
         new Thread(aoo::methodB).start();
     }
 }
 
 class Aoo{
     public void methodA() {
         synchronized (this) {
             Thread thread = new Thread();
             System.out.println(thread.getName() + ":methodA");
             try {
                 Thread.sleep(5000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println(thread.getName() + ":methodA完毕");
         }
     }
 
     public void methodB() {
         synchronized (this) {
             Thread thread = new Thread();
             System.out.println(thread.getName() + ":methodB");
             try {
                 Thread.sleep(5000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println(thread.getName() + ":methodB完毕");
         }
     }
 }
 

并发安全API

StringBuilder & StringBuffer

StringBuilder
  • 定义StringBuilder是一个可变的字符序列,可以在不生成新的对象的情况下对字符串进行修改。

  • 线程安全性StringBuilder 不是线程安全的。它在单线程环境中表现更好,因为它不需要在每个方法调用时处理同步,因此速度较快。

  • 性能:由于没有同步机制,StringBuilder通常比StringBuffer更快,在没有并发访问的情况下,StringBuilder是更好的选择。

StringBuffer
  • 定义StringBuffer也是一个可变的字符序列,与StringBuilder类似,但它在每个方法上都进行了同步。

  • 线程安全性StringBuffer 是线程安全的。它通过在方法上加锁,确保多线程环境下的安全操作。多个线程可以同时访问StringBuffer对象,而不会导致数据不一致。

  • 性能:由于每次操作都要进行同步,StringBuffer的性能会比StringBuilder稍低。但在需要线程安全的环境中,StringBuffer是更好的选择。

选择使用
  • 单线程环境StringBuilder更适合,因为它更快。

  • 多线程环境StringBuffer更适合,因为它是线程安全的。

 package day20;
 
 /**
  * StringBuilder不是并发安全的,不能在多线程场景下使用(不能同时被多个线程操作)
  * StringBuffer是并发安全的,支持多个线程同时操作
  */
 public class SyncAPIDemo1 {
     public static void main(String[] args) {
 //        StringBuilder builder = new StringBuilder();
         StringBuffer builder = new StringBuffer();
         Thread t1 = new Thread(){
             public void run(){
                 for (int i = 0; i < 1000; i++) {
                     builder.append("a");
                 }
             }
         };
         Thread t2 = new Thread(){
             public void run(){
                 for (int i = 0; i < 1000; i++) {
                     builder.append("b");
                 }
             }
         };
         t1.start();
         t2.start();
 
         try {
             Thread.sleep(500);
         } catch (InterruptedException e) {
         }
         System.out.println(builder.length());
     }
 }
 

非并发安全的集合

  • ArrayList

  • LinkedList

  • HashSet

这些集合类在默认情况下都不是线程安全的,多线程并发操作可能会导致数据不一致或异常。

转换为线程安全集合的方法

  • 静态方法Collections.synchronizedXXX()

    • 例如:Collections.synchronizedList()Collections.synchronizedSet()
 package day20;
 
 import java.util.*;
 
 /**
  * 集合实现:
  * ArrayList,LinkedList,HashSet他们都不是并发安全的!
  */
 public class SyncAPIDemo2 {
     public static void main(String[] args) {
 //        List<Integer> c = new ArrayList<>();
 //        List<Integer> c = new LinkedList<>();
 
         // Collections可以将现有的集合转换为并发安全的
 //        List<Integer> c = Collections.synchronizedList(new ArrayList<>());
 //        List<Integer> c = Collections.synchronizedList(new LinkedList<>());
         Set<Integer> c = Collections.synchronizedSet(new HashSet<>());
         Thread t1 = new Thread(){
             public void run(){
                 for (int i = 0; i < 1000; i++) {
                     c.add(i);
                 }
             }
         };
         Thread t2 = new Thread(){
             public void run(){
                 for (int i = 1000; i < 2000; i++) {
                     c.add(i);
                 }
             }
         };
         t1.start();
         t2.start();
         try {
             Thread.sleep(500);
         } catch (InterruptedException e) {
         }
         System.out.println(c.size());
     }
 }

在线聊天室v2.0

Client.java

package day20;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

/**
 * 客户端
 */
public class Client {
    /**
     * java.net.Socket:套接字
     * 它封装了TCP协议的通信细节,使用它可以和服务器建立连接并进行交互
     * 可以理解为是电话
     */
    private Socket socket;
    // 处理回复消息,创建回复接口:该线程任务负责读取来自服务端发送过来的消息并输出到控制台上
    private class ServerHandler implements Runnable {
        @Override
        public void run() {
            try {
                // 通过Socket提供的方法InputStream getInputStream(),获取输入流,将服务端发送的消息读取出来
                InputStream inputStream = socket.getInputStream();
                InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
                BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

                // 循环读取来自服务端发送过来的消息并输出到客户端控制台上
                String message;
                while ((message = bufferedReader.readLine()) != null) {
                    System.out.println(message);
                }

            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public Client() {
        // 实例化Socket对象,就是与远端计算机建立连接的过程
        // 需要传递两个对数:
        // 1.远端计算机的IP地址,用于找到网络上对方的计算机
        // 2.远端计算机的端口号,用于找到对方计算机中的应用程序
        try {
            System.out.println("正在连接服务端......");
            /**
             * 如何查找本机IP地址:
             * Windows:Win+R,输入cmd,回车,输入ipconfig,回车
             * Mac:触控板上五指向中间抓,选择"终端"程序打开,输入/sbin/ifconfig查看IP地址
             */
            socket = new Socket("localhost", 8888);
            System.out.println("连接成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 该方法用于启动客户端程序的执行
    public void start() {
        // 通过Socket获取输出流用于向服务端发送消息
        try {
            OutputStream outputStream = socket.getOutputStream();
            OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
            BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
            PrintWriter printWriter = new PrintWriter(bufferedWriter,true);  // 自动行刷新

            // 输入昵称
            Scanner scanner01 = new Scanner(System.in);
            while (true) {
                System.out.println("请输入昵称:");
                // 要求:昵称不能为空或者只有空格都不可以,如果发生则提示用户重新输入
                String nickname = scanner01.nextLine();
                if (nickname.trim().isEmpty()){
                    System.out.println("昵称不能为空,请重新输入!");
                }else {
                    printWriter.println(nickname);
                    System.out.println("昵称设置成功!欢迎您:" + nickname);
                    break;
                }
            }

            // 启动用于读取服务端消息的线程
            ServerHandler serverHandler = new ServerHandler();
            Thread thread = new Thread(serverHandler);
            // 如果我们输入exit想结束聊天,这个线程依然活着,所以我们想让他和主线程一起死,于是我们设计这个线程为守护线程
            thread.setDaemon(true);
            thread.start();

            Scanner scanner = new Scanner(System.in);
            while (true) {
                String line = scanner.nextLine();
                if ("exit".equalsIgnoreCase(line)) {
                    break;
                }
                printWriter.println(line);  // 发送消息给服务端
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();  // 进行四次挥手
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Client client = new Client();
        client.start();
    }
}

server.java

package day20;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
 * 服务端
 */
public class Server {
    /**
     * java.net.ServerSocket:
     * 运行在服务端,主要作用有两个:
     * 1.像系统申请服务端口,客户端通过该端口与服务端建立连接
     * 2.监听服务端口,一旦有客户链接了,就会立即创建一个Socket对象与客户端进行交互
     *      如果将Socket比喻为“电话”,那ServerSocket就相当于客户中心的“总机”。
     * 解决方法:
     * 1.更换端口号;
     * 2.杀死占用该端口的进行(通常由于服务端启动了两次导致)
     */
    private ServerSocket serverSocket;
    private List<PrintWriter> allOut = new ArrayList<>();
    private String nickname;
    public Server(){
        try {
            System.out.println("正在启动服务端......");
            serverSocket = new ServerSocket(8888);
            System.out.println("服务端启动完成!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 启动服务端方法:
    /**
     * accept():
     * 用于接收客户端连接,并返回一个Socket对象与所连接的客户端进行交互
     * 该方法是一个阻塞方法,调用后程序会“卡住”,直到一个客户端连接为止
     */
    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();
        }
    }
    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }

    /**
     * 该线程任务用于负责与指定的客户端通信
     */
    private class ClientHandler implements Runnable{
        private Socket socket;
        private String host;  // 记录客户端的IP地址
        public ClientHandler(Socket socket) {
            this.socket = socket;
            // 通过socket获取客户端ip地址
            host = socket.getInetAddress().getHostAddress();
        }
        @Override
        public void run() {
            PrintWriter printWriter = null;
            try {
                // 接收客户端发送过来的消息
                InputStream inputStream = socket.getInputStream();
                InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
                BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
                // 读取客户端发送过来的第一行字符串一定是客户端的昵称
                nickname = bufferedReader.readLine();

                // 通过Socket获取输出流,用于将消息发送给该客户端
                OutputStream outputStream = socket.getOutputStream();
                OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
                BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
                printWriter = new PrintWriter(bufferedWriter,true);

                // 将对应该客户端的输出流存入到共享集合allOut中
                synchronized (allOut) {
                    allOut.add(printWriter);
                }

                // 对应该客户端的输出流共享到allOut中
                allOut.add(printWriter);

                // 广播该客户端上线了
                broadcast(nickname + "["+host+"]上线了!当前在线人数:" + allOut.size());

                String message;
                while ((message = bufferedReader.readLine()) != null) {    // 读取客户端发送过来的消息
                    broadcast(host + "说:" + message);
                }
            } catch (IOException e) {

            } finally {
                // 处理该客户端断开连接后的操作
                // 将该客户端的输出流从共享集合allOut中删除
                synchronized (allOut) {
                    allOut.remove(printWriter);
                }
                // 广播消息,告知所有客户端该用户下线了
                broadcast(host + "下线了!当前在线人数:" + allOut.size());

                try {    // 进行四次挥手
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        private void broadcast(String message){
            System.out.println(message);
            synchronized (allOut) {
                for (PrintWriter writer : allOut) {
                    writer.println(message);
                }
            }
        }
    }
}

  • 9
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值