栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

线程相关内容(聊天室案例)

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

线程相关内容(聊天室案例)

目录

多线程

创建线程有两种方式:

方式一:继承 Thread 并重写 run() 方法

方式二:实现 Runnable 接口单独定义线程任务

匿名内部类形式的线程创建

使用多线程实现多客户端连接服务端

线程API

获取线程相关信息的方法

线程优先级

sleep阻塞

守护线程

多线程并发安全问题

synchronized 关键字

同步方法

同步块

在静态方法上使用synchronized

静态方法中使用同步块

互斥锁

死锁

解决死锁

聊天室案例(续)


多线程

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

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

并发:

多个线程实际运行是走走停停的。线程调度程序会将 CPU 运行时间划分为若干个时间片段并

尽可能均匀的分配给每个线程,拿到时间片的线程被 CPU 执行这段时间。当超时后线程调度

程序会再次分配一个时间片段给一个线程使得 CPU 执行它。如此反复。由于 CPU 执行时间在

纳秒级别,我们感觉不到切换线程运行的过程。所以微观上走走停停,宏观上感觉一起运行

的现象成为并发运行!

用途:

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

线程的生命周期图

创建线程有两种方式:

方式一:继承 Thread 并重写 run() 方法

定义一个线程类,重写 run() 方法,在其中定义线程要执行的任务(希望和其他线程并发执行的任务)。

注意:启动该线程要调用该线程的 start() 方法,而不是 run() 方法

public class ThreadDemo1 {
    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<1000;i++){
            System.out.println("芜湖~");
        }
    }
}
class MyThread2 extends Thread{
    public void run(){
        for (int i=0;i<1000;i++){
            System.out.println("起飞啦~");
        }
    }
}

第一种创建线程的方式

优点:在于结构简单,便于匿名内部类形式创建。

缺点:

  • 直接继承线程,会导致不能在继承其他类去复用方法,这在实际开发中是非常不便的(java只支持单继承)。
  • 定义线程的同时重写了 run() 方法,会导致线程与线程任务绑定在了一起,不利于线程的重用。

方式二:实现 Runnable 接口单独定义线程任务
public class ThreadDemo2 {
    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<1000;i++){
            System.out.println("你是谁啊?");
        }
    }
}
class MyRunnable2 implements Runnable{
    public void run() {
        for (int i=0;i<1000;i++){
            System.out.println("开门!查水表的!");
        }
    }
}

匿名内部类形式的线程创建
public class ThreadDemo3 {
    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 = new Runnable() {
//            public void run() {
//                for(int i=0;i<1000;i++){
//                    System.out.println("我是查水表的!");
//                }
//            }
//        };
        //Runnable可以使用lambda表达式创建
        Runnable r2 = ()->{
                for(int i=0;i<1000;i++){
                    System.out.println("我是查水表的!");
                }
        };


        Thread t2 = new Thread(r2);

        t1.start();
        t2.start();
    }
}

java 中的代码都是靠线程运行的,执行main方法的线程称为"主线程"。

线程提供了一个方法:

  • static Thread currentThread(),该方法可以获取运行这个方法的线程
public class CurrentThreadDemo {
    public static void main(String[] args) {
        
        Thread main = Thread.currentThread();//获取执行main方法的线程(主线程)
        System.out.println("线程:"+main);

        dosome();//主线程执行dosome方法
    }

    public static void dosome(){
        Thread t = Thread.currentThread();//获取执行dosome方法的线程
        System.out.println("执行dosome方法的线程是:"+t);
    }
}

使用多线程实现多客户端连接服务端

流程图

服务端代码改造

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 Server {
    
    private ServerSocket serverSocket;

    
    public Server(){

        try {
            System.out.println("正在启动服务端...");            
            serverSocket = new ServerSocket(8088);
            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();
        }

    }

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

//---------------------- 新增开始 ----------------------    
    
    private class ClientHandler implements Runnable{

        private Socket socket;
        
        public ClientHandler(Socket socket){
            this.socket = socket;
        }
        
        @Override
        public void run() {
            try {
                
                InputStream is = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(is, "UTF-8");
                BufferedReader br = new BufferedReader(isr);

                String message = null;
                while ((message = br.readLine()) != null){
                    System.out.println("客户端说:" + message);
                }
                
            } catch (IOException e) {
                e.printStackTrace();
            }
            
        }
    }
//---------------------- 新增结束 ----------------------    
}

线程API

获取线程相关信息的方法
public class ThreadInfoDemo {
    public static void main(String[] args) {
        Thread main = Thread.currentThread();//获取主线程

        String name = main.getName();//获取线程的名字
        System.out.println("名字:"+name);

        long id = main.getId();//获取该线程的唯一标识
        System.out.println("id:"+id);

        int priority = main.getPriority();//获取该线程的优先级
        System.out.println("优先级:"+priority);

        boolean isAlive = main.isAlive();//该线程是否活着
        System.out.println("是否活着:"+isAlive);

        boolean isDaemon = main.isDaemon();//是否为守护线程
        System.out.println("是否为守护线程:"+isDaemon);

        boolean isInterrupted = main.isInterrupted();//是否被中断了
        System.out.println("是否被中断了:"+isInterrupted);

    }
}

线程优先级

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

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

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

sleep阻塞

线程提供了一个静态方法

  • static void sleep(long ms)
  • 使运行该方法的线程进入阻塞状态指定的毫秒,超时后线程会自动回到RUNNABLE状态等待再次获取时间片并发运行
public class SleepDemo {
    public static void main(String[] args) {
        System.out.println("程序开始了!");
        try {
            Thread.sleep(5000);//主线程阻塞5秒钟
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("程序结束了!");
    }
}

sleep方法处理异常:InterruptedException

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

public class SleepDemo2 {
    public static void main(String[] args) {
        Thread lin = new Thread(){
            public void run(){
                System.out.println("林:刚美完容,睡一会吧~");
                try {
                    Thread.sleep(9999999);
                } catch (InterruptedException e) {
                    System.out.println("林:干嘛呢!干嘛呢!干嘛呢!都破了像了!");
                }
                System.out.println("林:醒了");
            }
        };

        Thread huang = new Thread(){
            public void run(){
                System.out.println("黄:大锤80!小锤40!开始砸墙!");
                for(int i=0;i<5;i++){
                    System.out.println("黄:80!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                    }
                }
                System.out.println("咣当!");
                System.out.println("黄:大哥,搞定!");
                lin.interrupt();//中断lin的睡眠阻塞
            }
        };
        lin.start();
        huang.start();
    }
}

守护线程

守护线程也称为:后台线程

  • 守护线程是通过普通线程调用 setDaemon(boolean on) 方法设置而来的,因此创建上与普通线程无异
  • 守护线程的结束时机上有一点与普通线程不同,即:进程的结束
  • 进程结束:当一个进程中的所有普通线程都结束时,进程就会结束,此时会杀掉所有正在运行的守护线程
​public class DaemonThreadDemo {
    public static void main(String[] args) {
        Thread rose = new Thread(){
            public void run(){
                for(int i=0;i<5;i++){
                    System.out.println("rose:let me go!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                    }
                }
                System.out.println("rose:啊啊啊啊啊啊AAAAAAAaaaaa....");
                System.out.println("噗通");
            }
        };

        Thread jack = new Thread(){
            public void run(){
                while(true){
                    System.out.println("jack:you jump!i jump!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                    }
                }
            }
        };
        rose.start();
        jack.setDaemon(true);//设置守护线程必须在线程启动前进行
        jack.start();

    }
}

​

通常当我们不关系某个线程的任务什么时候停下来,它可以一直运行,但是程序主要的工作都结束时它应当跟着结束时,这样的任务就适合放在守护线程上执行,比如GC就是守护线程上运行的

多线程并发安全问题

当多个线程并发操作同一个临界资源,由于线程切换时机不确定,导致操作临界资源的顺序出现混乱,严重时可能导致系统瘫痪

临界资源:操作该资源的全过程同时只能被单个线程完成

public class SyncDemo {
    public static void main(String[] args) {
        Table table = new Table();
        Thread t1 = new Thread(){
            public void run(){
                while(true){
                    int bean = table.getBean();
                    Thread.yield();
                    System.out.println(getName()+":"+bean);
                }
            }
        };
        Thread t2 = new Thread(){
            public void run(){
                while(true){
                    int bean = table.getBean();
                    
                    Thread.yield();
                    System.out.println(getName()+":"+bean);
                }
            }
        };
        t1.start();
        t2.start();
    }
}

class Table{
    private int beans = 20;//桌子上有20个豆子

    public int getBean(){
        if(beans==0){
            throw new RuntimeException("没有豆子了!");
        }
        Thread.yield();
        return beans--;
    }
}

synchronized 关键字

synchronized有两种使用方式

  • 在方法上修饰,此时该方法变为一个同步方法
  • 同步块,可以更准确的锁定需要排队的代码片段

同步方法

当一个方法使用 synchronized 修饰后,这个方法称为“同步方法”,即:多个线程不能同时在方法内部执行,只能有先后顺序的一个一个进行,将并发操作同一临界资源的过程改为同步执行就可以有效的解决并发安全问题

public class SyncDemo {
    public static void main(String[] args) {
        Table table = new Table();
        Thread t1 = new Thread(){
            public void run(){
                while(true){
                    int bean = table.getBean();
                    Thread.yield();
                    System.out.println(getName()+":"+bean);
                }
            }
        };
        Thread t2 = new Thread(){
            public void run(){
                while(true){
                    int bean = table.getBean();
                    Thread.yield();
                    System.out.println(getName()+":"+bean);
                }
            }
        };
        t1.start();
        t2.start();
    }
}

class Table{
    private int beans = 20;//桌子上有20个豆子

    
    public synchronized int getBean(){
        if(beans==0){
            throw new RuntimeException("没有豆子了!");
        }
        Thread.yield();
        return beans--;
    }
}

同步块

有效的缩小同步范围可以在保证并发安全的前提下尽可能的挺高并发效率,同步块可以更准确的控制需要需要多个线程排队执行的代码片段

语法:

synchronized(同步监视器对象){

        需要多线程同步执行的代码片段

}

同步监视器对象即上锁的对象,想要保证同步块中的代码被多个线程同步运行,则要求多个线程看到的同步监视器对象是同一个 

public class SyncDemo2 {
    public static void main(String[] args) {
        Shop shop = new Shop();
        Thread t1 = new Thread(){
            public void run(){
                shop.buy();
            }
        };
        Thread t2 = new Thread(){
            public void run(){
                shop.buy();
            }
        };
        t1.start();
        t2.start();
    }
}

class Shop{
    public void buy(){
        
//    public synchronized void buy(){
        Thread t = Thread.currentThread();//获取运行该方法的线程
        try {
            System.out.println(t.getName()+":正在挑衣服...");
            Thread.sleep(5000);
            
            synchronized (this) {
//            synchronized (new Object()) {//没有效果!
                System.out.println(t.getName() + ":正在试衣服...");
                Thread.sleep(5000);
            }

            System.out.println(t.getName()+":结账离开");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

在静态方法上使用synchronized

当在静态方法上使用 synchronized 后,该方法是一个同步方法,由于静态方法所属类,所以一定具有同步效果

静态方法使用的同步监听器对象为当前类的类对象(Class 的实例,可参考Java反射机制篇)

public class SyncDemo3 {
    public static void main(String[] args) {
        Thread t1 = new Thread(){
            public void run(){
                Boo.dosome();
            }
        };
        Thread t2 = new Thread(){
            public void run(){
                Boo.dosome();
            }
        };
        t1.start();
        t2.start();
    }
}
class Boo{
    // 在JVM中,每个被加载的类都有且只有一个Class的实例与之对应
    public synchronized static void dosome(){
            Thread t = Thread.currentThread();
            try {
                System.out.println(t.getName() + ":正在执行dosome方法...");
                Thread.sleep(5000);
                System.out.println(t.getName() + ":执行dosome方法完毕!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

静态方法中使用同步块

指定的锁对象通常也是当前类的类对象

class Boo{
    public static void dosome(){
        
        synchronized (Boo.class) {
            Thread t = Thread.currentThread();
            try {
                System.out.println(t.getName() + ":正在执行dosome方法...");
                Thread.sleep(5000);
                System.out.println(t.getName() + ":执行dosome方法完毕!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

互斥锁

当多个线程执行不同的代码片段,但是这些代码片段之间不能同时运行时就要设置为互斥的

使用 synchronized 锁定多个代码片段,并且指定的同步监视器是同一个时,这些代码片段之间就是互斥的,即:多个线程不能同时访问这些方法

public class SyncDemo4 {
    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();
        }
    }
}

死锁

两个线程各自持有一个锁对象的同时等待对方先释放锁对象,此时会出现僵持状态,这个现象就是死锁

public class DeadLockDemo {
    //定义两个锁对象,"筷子"和"勺"
    public static Object chopsticks = new Object();
    public static Object spoon = new Object();

    public static void main(String[] args) {
        Thread np = new Thread(){
            public void run(){
                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(){
            public void run(){
                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 线程在持有锁 a 的过程中去持有锁 b 时,B 线程也要以这样的持有顺序进行
public class DeadLockDemo2 {
    //筷子
    private static Object chopsticks = new Object();
    //勺
    private static Object spoon = new Object();

    public static void main(String[] args) {
        //北方人
        Thread np = new Thread(){
            public void run(){
                try {
                    System.out.println("北方人:开始吃饭");
                    System.out.println("北方人去拿筷子...");
                    synchronized (chopsticks) {
                        System.out.println("北方人拿起了筷子,开始吃饭...");
                        Thread.sleep(5000);
                    }
                    System.out.println("北方人吃完了饭,放下了筷子");
                    System.out.println("北方人去拿勺子...");
                    synchronized (spoon){
                            System.out.println("北方人拿起了勺子,开始喝汤...");
                            Thread.sleep(5000);
                    }
                    System.out.println("北方人喝完了汤,北方人放下了勺子");
                    System.out.println("吃饭完毕。");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        //南方人
        Thread sp = new Thread(){
            public void run(){
                try {
                    System.out.println("南方人:开始吃饭");
                    System.out.println("南方人去拿勺...");
                    synchronized (spoon) {
                        System.out.println("南方人拿起了勺,开始喝汤...");
                        Thread.sleep(5000);
                    }
                    System.out.println("南方人喝完了汤,放下勺子...");
                    System.out.println("南方人去拿筷子...");
                    synchronized (chopsticks){
                        System.out.println("南方人拿起了筷子,开始吃饭...");
                        Thread.sleep(5000);
                    }
                    System.out.println("南方人吃完了饭,南方人放下了筷子");
                    System.out.println("吃饭完毕。");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        np.start();
        sp.start();
    }
}

聊天室案例(续)

实现服务端发送消息给客户端

在服务端通过 Socket 获取输出流,客户端获取输入流,实现服务端将消息发送给客户端

这里让服务端直接将客户端发送过来的消息再回复给客户端来进行测试

服务端代码

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;


public class Server {
 
    private ServerSocket serverSocket;

    
    public Server(){

        try {
            System.out.println("正在启动服务端..."); 
            serverSocket = new ServerSocket(8088);
            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();
        }

    }

    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 获取远端计算机地址信息
            host = socket.getInetAddress().getHostAddress();
        }
//---------------------- 新增结束 ----------------------
        
        @Override
        public void run() {
            try {
                InputStream is = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(is, "UTF-8");
                BufferedReader br = new BufferedReader(isr);
                
//---------------------- 新增开始 ----------------------
                OutputStream os = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8");
                BufferedWriter bw = new BufferedWriter(osw);
                PrintWriter pw = new PrintWriter(bw, true);

                String message = null;
                while ((message = br.readLine()) != null){
                    // 将消息回复给客户端
                    pw.println(host + "说:" + message);
                }
//---------------------- 新增结束 ----------------------
                
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }
}

客户端代码

import java.io.*;
import java.net.Socket;
import java.util.Scanner;


public class Client {

    private Socket socket;

    
    public Client(){
        System.out.println("正在链接服务端...");
 
        try {
            socket = new Socket("localhost", 8088);
            System.out.println("与服务端链接成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    
    public void start(){

        try {
           
            OutputStream os = socket.getOutputStream();
            OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8");
            BufferedWriter bw = new BufferedWriter(osw);
            PrintWriter pw = new PrintWriter(bw, true);

//---------------------- 新增开始 ----------------------
            // 通过 socket 获取输入流读取服务端发送过来的消息
            InputStream is = socket.getInputStream();
            InputStreamReader isr = new InputStreamReader(is, "UTF-8");
            BufferedReader br = new BufferedReader(isr);
//---------------------- 新增结束 ----------------------
            
            Scanner scan = new Scanner(System.in);
            while(true){
                String line = scan.nextLine();
                if("exit".equalsIgnoreCase(line)){
                    break;
                }
                pw.println(line);
                
//---------------------- 新增开始 ----------------------
                line = br.readLine();
                System.out.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 类上定义一个数组 allOut 可以被所有内部类ClientHandler 实例访问,从而这些 ClientHandler 实例之间想互访的数据存放在这个数组中达到共享数据的目的,对此只需要将所有 ClientHandler 中的输出流都存入到数组 allOut 中就可以达到互访输出流转发消息的目的了

 服务端代码

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;


public class Server {
 
    private ServerSocket serverSocket;

//---------------------- 新增开始 ----------------------
    // 存入所有客户端的输出流,用于广播消息
    private PrintWriter[] allOut = {};
//---------------------- 新增结束 ----------------------

    
    public Server(){

        try {
            System.out.println("正在启动服务端..."); 
            serverSocket = new ServerSocket(8088);
            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();
        }

    }

    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 获取远端计算机地址信息
            host = socket.getInetAddress().getHostAddress();
        }

        @Override
        public void run() {
            try {
                
                InputStream is = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(is, "UTF-8");
                BufferedReader br = new BufferedReader(isr);

                OutputStream os = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8");
                BufferedWriter bw = new BufferedWriter(osw);
                PrintWriter pw = new PrintWriter(bw, true);

//---------------------- 新增开始 ----------------------
                
                allOut = Arrays.copyOf(allOut, allOut.length + 1);
                // 2. 将输出流存入数组最后一个位置
                allOut[allOut.length - 1] = pw;

                String message = null;
                while ((message = br.readLine()) != null){
                    // 将消息回复给客户端
                    for(int i = 0; i < allOut.length; i++){
                        allOut[i].println(host + "说:" + message);
                    }
                }
//---------------------- 新增结束 ----------------------

            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }
}

客户端解决收发消息的冲突问题

由于客户端 start 方法中循环进行的操作顺序是先通过控制台输入一句话后,将其发送给服务端,然后再读取服务端发送回来的一句话,这导致如果客户端不输入内容就无法收到服务端发送过来的其他信息(其他客户端的聊天内容),因此要将客户端中接收消息的工作移动到一个单数的线程上执行,才能保证收发消息的互不打扰。

客户端代码

import java.io.*;
import java.net.Socket;
import java.util.Scanner;


public class Client {
 
    private Socket socket;

    
    public Client(){
        System.out.println("正在链接服务端...");

        try {
            socket = new Socket("localhost", 8088);
            System.out.println("与服务端链接成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    
    public void start(){

        try {
//---------------------- 新增开始 ----------------------
            // 启动读取服务端发送过来消息的线程
            ServerHandler serverHandler = new ServerHandler();
            Thread thread = new Thread(serverHandler);
            thread.setDaemon(true);
            thread.start();
//---------------------- 新增结束 ----------------------
            
            OutputStream os = socket.getOutputStream();
            OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8");
            BufferedWriter bw = new BufferedWriter(osw);
            PrintWriter pw = new PrintWriter(bw, true);
            
            Scanner scan = new Scanner(System.in);
            while(true){
                String line = scan.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{
        @Override
        public void run() {
            // 通过 socket 获取输入流读取服务端发送过来的消息
            try {
                InputStream is = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(is, "UTF-8");
                BufferedReader br = new BufferedReader(isr);

                String line;
                // 循环读取服务端发送过来的每一行字符串
                while((line= br.readLine()) != null){
                    System.out.println(line);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
//---------------------- 新增结束 ----------------------

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

服务端完成处理客户端断开连接后的操作

当一个客户端断开连接后,服务端处理该客户端交互的线程 ClientHandler 应当将通过 socket 获取的输出流从共享数组 allOut 中删除,防止其他的 ClientHandler 再将消息通过这个输出流发送给当前客户端

服务端代码 

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;


public class Server {

    private ServerSocket serverSocket;

    // 存入所有客户端的输出流,用于广播消息
    private PrintWriter[] allOut = {};

    
    public Server(){

        try {
            System.out.println("正在启动服务端...");
            serverSocket = new ServerSocket(8088);
            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();
        }

    }

    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 获取远端计算机地址信息
            host = socket.getInetAddress().getHostAddress();
        }

        @Override
        public void run() {

            PrintWriter pw = null;

            try {
                
                InputStream is = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(is, "UTF-8");
                BufferedReader br = new BufferedReader(isr);

                OutputStream os = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8");
                BufferedWriter bw = new BufferedWriter(osw);
                pw = new PrintWriter(bw, true);

                
                allOut = Arrays.copyOf(allOut, allOut.length + 1);
                // 2. 将输出流存入数组最后一个位置
                allOut[allOut.length - 1] = pw;
                sendMessage(host + "上线了,当前在线人数:" + allOut.length);


                String message = null;
                while ((message = br.readLine()) != null){
                    // 将消息回复给客户端
                    sendMessage(host + "说:" + message);
                }
            } catch (IOException e) {
                e.printStackTrace();

//---------------------- 新增开始 ----------------------
            } finally {
                // 处理客户端断开链接的操作
                // 将当前客户端的输出流从allOut中删除(数组缩容)
                for(int i = 0; i < allOut.length; i++){
                    if(allOut[i] == pw){
                        allOut[i] = allOut[allOut.length - 1];
                        allOut = Arrays.copyOf(allOut, allOut.length - 1);
                        break;
                    }
                }
                sendMessage(host + "下线了,当前在线人数:" + allOut.length);
                try {
                    socket.close(); // 与客户端断开连接
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }

    
    private void sendMessage(String message){
        for(int i = 0; i < allOut.length; i++){
            allOut[i].println(message);
        }
    }
//---------------------- 新增结束 ----------------------

}

服务端解决多线程并发安全问题

为了能将信息发给所有客户端,我们在 Server 上添加了一个数组类型的属性 allOut,并且共所有线程 ClientHandler 使用,这时对数组的操作要考虑并发安全问题

当两个客户端同时上线(橙、绿)

 两个 ClientHandler 启动后都会对数组扩容,将自身的输出流传入数组

此时 ClientHandler (橙)先拿到CPU时间,进行数组扩容

 扩容后发生 CPU 切换,ClientHandler(绿)拿到时间

 此时 ClientHandler(绿)进行数组扩容

ClientHandler(绿)扩容后,将输出流存入数组最后一个位置

 线程切换回 ClientHandler(橙)

ClientHandler(橙)将输出流存入数组最后一个位置,此时会将 ClientHandler(绿)存入的输入流覆盖,出现了并发安全问题!

选取合适的锁对象

this 不可以

allOut 不可以,大多数情况下可以选择临界资源作为锁对象,但是这里不行

ClientHandler(橙)锁定allOut

 ClientHandler(橙)扩容 allOut

由于数组是定长的,扩容实际是创建新数组,因此扩容后赋值给 allOut 时,ClientHandler(橙)之前锁定的对象就被 GC 回收了,而新扩容的数组并没有锁 

若此时发生线程切换,ClientHandler(绿)锁定 allOut 时,发现该 allOut 没有锁,因此可以锁定,并执行 synchronized 内部代码

ClientHandler(绿)也可以进行数组扩容,那么它之前锁定的数组也被GC回收了

 

 从上述代码可以看出,锁定 allOut并没有限制多个线程(ClientHandler)操作 allOut 数组,还是存在并发安全问题

 可以选取外部类对象作为锁对象,因为这些内部类 ClientHandler 都从属于这个外部类对象 Server.this

还要考虑对数组的不同操作之间的互斥问题,道理同上,因此,对allOut数组的扩容,缩容和遍历操作要进行互斥 

最终代码

服务器端

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;


public class Server {

    
    private ServerSocket serverSocket;

    // 存入所有客户端的输出流,用于广播消息
    private PrintWriter[] allOut = {};

    
    public Server(){

        try {
            System.out.println("正在启动服务端...");
            
            serverSocket = new ServerSocket(8088);
            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();
        }

    }

    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 获取远端计算机地址信息
            host = socket.getInetAddress().getHostAddress();
        }

        @Override
        public void run() {

            PrintWriter pw = null;

            try {
                
                InputStream is = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(is, "UTF-8");
                BufferedReader br = new BufferedReader(isr);

                OutputStream os = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8");
                BufferedWriter bw = new BufferedWriter(osw);
                pw = new PrintWriter(bw, true);

                
                synchronized (Server.this) { // 新增 --------
                    allOut = Arrays.copyOf(allOut, allOut.length + 1);
                    // 2. 将输出流存入数组最后一个位置
                    allOut[allOut.length - 1] = pw;
                    sendMessage(host + "上线了,当前在线人数:" + allOut.length);
                }

                String message = null;
                while ((message = br.readLine()) != null){
                    // 将消息回复给客户端
                    sendMessage(host + "说:" + message);
                }
            } catch (IOException e) {
                
            } finally {
                // 处理客户端断开链接的操作
                // 将当前客户端的输出流从allOut中删除(数组缩容)
                synchronized (Server.this) { // 新增 --------
                    for (int i = 0; i < allOut.length; i++) {
                        if (allOut[i] == pw) {
                            allOut[i] = allOut[allOut.length - 1];
                            allOut = Arrays.copyOf(allOut, allOut.length - 1);
                            break;
                        }
                    }
                }
                sendMessage(host + "下线了,当前在线人数:" + allOut.length);
                try {
                    socket.close(); // 与客户端断开连接
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }

    
    private void sendMessage(String message) {
        synchronized (Server.this) { // 新增 --------
            for (int i = 0; i < allOut.length; i++) {
                allOut[i].println(message);
            }
        }
    }
}

客户端

import java.io.*;
import java.net.Socket;
import java.util.Scanner;


public class Client {

    
    private Socket socket;

    
    public Client(){
        System.out.println("正在链接服务端...");

        
        try {
            socket = new Socket("localhost", 8088);
            System.out.println("与服务端链接成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    
    public void start(){

        try {
            // 启动读取服务端发送过来消息的线程
            ServerHandler serverHandler = new ServerHandler();
            Thread thread = new Thread(serverHandler);
            thread.setDaemon(true);
            thread.start();

            // 低级流,将字节通过网络发送给对方
            OutputStream os = socket.getOutputStream();
            // 高级流,负责衔接字节流与字符流,并将写出的字符按指定字符集转文字
            OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8");
            // 高级流,负责块写文本数据加速
            BufferedWriter bw = new BufferedWriter(osw);
            // 高级流,负责按行写出字符串,自动行刷新
            PrintWriter pw = new PrintWriter(bw, true);

            Scanner scan = new Scanner(System.in);
            while(true){
                String line = scan.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{
        @Override
        public void run() {
            // 通过 socket 获取输入流读取服务端发送过来的消息
            try {
                InputStream is = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(is, "UTF-8");
                BufferedReader br = new BufferedReader(isr);

                String line;
                // 循环读取服务端发送过来的每一行字符串
                while((line= br.readLine()) != null){
                    System.out.println(line);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

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

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1007316.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号