I've done this when I was learning Java, something around 10 years ago. It works:
Constantes.java: 
package jsc;
public interface Constantes {
    public static final String MULTICAST_IP = "224.0.0.1";
    public static final int     MULTICAST_PORTA = 3333;
    public static final String SEPARADOR = "[>>>]";
    public static final int TAMANHO_MENSAGEM = 1024;
    public static final long ESPERA = 3000;
    public static final String ESTOUONLINE = "EstouOnline";
    public static final String DESCONECTANDO = "Desconectando";
    public static final String PRIVADO = "Privado";
}
ControladorThread.java
package jsc;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.Iterator;
import java.util.StringTokenizer;
import java.util.Vector;
public class ControladorThread extends Thread implements Constantes{
    private MulticastSocket mcSocket;
    private Main main;
    private Vector<Usuario> listaUsuarios;                          // lista de usuários ativos
    public ControladorThread(Main main){
        super("ReceptoraThread_" + main.getNick());
        listaUsuarios = new Vector<Usuario>();
        listaUsuarios.add(new Usuario(main.getNick(), new Date().getTime()));
        this.main = main;
        try{
            mcSocket = new MulticastSocket(MULTICAST_PORTA);
            mcSocket.joinGroup(InetAddress.getByName(MULTICAST_IP));
        } catch(IOException e){
            e.printStackTrace();
        }
    }
    public void run(){
        while(true){
            try{
                byte[] buffer = receberPacote();
                processar(buffer);
                removerUsuariosOciosos();
                atualizarListaUsuarios();
            } catch(IOException e){
                e.printStackTrace();
            }
        }
    }   
    public byte [] receberPacote() throws IOException{
        byte[] buffer = new byte[TAMANHO_MENSAGEM];
        DatagramPacket pacote = new DatagramPacket(buffer, buffer.length);
        mcSocket.receive(pacote);
        return buffer;
    }
    public void processar(byte[] buffer){
        String mensagem = new String(buffer);
        mensagem = mensagem.trim();
        StringTokenizer tokens = new StringTokenizer(mensagem, SEPARADOR);
        String t1 = tokens.nextToken();
        String t2 = tokens.nextToken();
        if(t1.equals(ESTOUONLINE))
            atualizarEstadoUsuario(t2);
        else if(t1.equals(DESCONECTANDO))
            desconectarUsuario(t2);
        else if(t1.equals(PRIVADO)){
            String t3 = tokens.nextToken();
            String t4 = tokens.nextToken();
            if(t3.equals(main.getNick())){
                receberMensagemPrivada(t2, t4);
            }
        }
        else
            main.setTextoEntrada(t1 + " diz: " + t2);
    }
    public void receberMensagemPrivada(String deUsuario, String mensagem){
        main.abrirChatPrivado(main.getNick(), deUsuario, mensagem);
    }
    public boolean atualizarEstadoUsuario(String nomeUsuario){
        int pos;
        for(Iterator i = listaUsuarios.iterator(); i.hasNext(); ){
            Usuario uAux = (Usuario) i.next();
            if(uAux.getNome().equals(nomeUsuario)){
                pos = listaUsuarios.indexOf(uAux);
                listaUsuarios.remove(uAux);
                uAux.setTempoInicio(new Date().getTime());
                listaUsuarios.add(pos, uAux);
                return true;
            }
        }
        listaUsuarios.add(new Usuario(nomeUsuario, new Date().getTime()));
        return false;
    }
    public void removerUsuariosOciosos(){
        Usuario usuario = null;
        for(Iterator i = listaUsuarios.iterator(); i.hasNext(); ){
            usuario = (Usuario) i.next();
            if(new Date().getTime() - usuario.getTempoInicio() > ESPERA){
                desconectarUsuario(usuario.getNome());
                i = listaUsuarios.iterator();
            }
        }
    }
    public void desconectarUsuario(String nomeUsuario){
        for(Iterator i = listaUsuarios.iterator(); i.hasNext(); ){
            Usuario uAux = (Usuario) i.next();
            if(uAux.getNome().equals(nomeUsuario)){
                i.remove();
                break;
            }
        }
    }
    public void atualizarListaUsuarios(){
        Vector<String> sVector = new Vector<String>();
        Usuario uAux = null;
        System.out.println("\nOnline: ");
        for(Iterator i = listaUsuarios.iterator(); i.hasNext(); ){
            uAux = (Usuario) i.next();
            System.out.print( uAux.getNome() + " ");
            sVector.add(uAux.getNome());
        }
        main.setUsuariosOnline(sVector);
    }
    private class Usuario{
        private String nome;
        private long tempoInicio;
        public Usuario(){}
        public Usuario(String nome, long tempoInicio){
            this.nome = nome;
            this.tempoInicio = tempoInicio;
        }
        public String getNome() {
            return nome;
        }
        public void setNome(String nome) {
            this.nome = nome;
        }
        public long getTempoInicio() {
            return tempoInicio;
        }
        public void setTempoInicio(long tempoInicio) {
            this.tempoInicio = tempoInicio;
        }
    }
    public void sair(){
        try {
            mcSocket.leaveGroup(InetAddress.getByName(MULTICAST_IP));
            mcSocket.close();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
EstouOnlineThread.java
package jsc;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
public class EstouOnlineThread extends Thread implements Constantes{
    private MulticastSocket mcSocket;
    private String nick;
    private byte[] buffer;
    public EstouOnlineThread(String nick){
        super("EstouOnlineThread_" + nick);
        this.nick = nick;
        try {
            mcSocket = new MulticastSocket();
        } catch(IOException e) {
            e.printStackTrace();
        } 
    }
    public void run(){
        String saida = ESTOUONLINE + SEPARADOR + nick;
        buffer = saida.getBytes();
        while(true){
            try{
                DatagramPacket estouOnline = new DatagramPacket(buffer, buffer.length, InetAddress.getByName(MULTICAST_IP), MULTICAST_PORTA);
                mcSocket.send(estouOnline);
                System.out.println(saida);
                sleep(ESPERA);
            }
            catch(InterruptedException e){
                e.printStackTrace();
            }
            catch(IOException e){
                e.printStackTrace();
            }
        }
    }
}
MensagemPrivadaFrame.java
package jsc;
import java.awt.BorderLayout;
import java.awt.Frame;
import java.awt.TextArea;
import java.awt.TextField;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.UnknownHostException;
public class MensagemPrivadaFrame extends Frame implements Constantes{
    private static final long serialVersionUID = 1L;    
    private TextArea entrada;
    private TextField saida;
    private String nomeJanela;
    private String nick;
    private String paraUsuario;
    private MulticastSocket mcSocket;
    private ActionListener saidaListener;
    private WindowAdapter frameListener;
    private boolean estouVivo;      // indica que a janela ainda está ativa
    public MensagemPrivadaFrame(String nick, String paraUsuario){
        super("JSC - Chat com " + paraUsuario);
        setIconImage(Toolkit.getDefaultToolkit().getImage("icone.4"));
        this.nick = nick;
        this.paraUsuario = paraUsuario;
        this.nomeJanela = nick + paraUsuario;
        try {
            mcSocket = new MulticastSocket();
        } catch (IOException e) {
            e.printStackTrace();
        }
        iniciarComponentes();
        estouVivo = true;
    }
    public void setNomeJanela(String nomeJanela){
        this.nomeJanela = nomeJanela;
    }
    public String getNomeJanela(){
        return nomeJanela;
    }
    public String getNick() {
        return nick;
    }
    public void setNick(String nick) {
        this.nick = nick;
    }
    public boolean estouVivo(){
        return estouVivo;
    }
     public void iniciarComponentes(){
        saidaListener = new ActionListener(){
                public void actionPerformed(ActionEvent e){
                    TextField origem = (TextField) e.getSource();
                    enviarMensagem(origem.getText());
                    entrada.append("\n(" + nick + " diz) " + origem.getText());
                    origem.setText("");
                }
        };
        frameListener = new WindowAdapter(){
            public void windowClosing(WindowEvent e){
                estouVivo = false;
                dispose();
            }
        };
        entrada = new TextArea("[JSC] Bate papo privado entre " + nick + " e " + paraUsuario + "\n");
        entrada.setEditable(false);
        saida = new TextField();
        saida.addActionListener(saidaListener);
        addWindowListener(frameListener);
        setLayout(new BorderLayout());
        int x = (int) (Math.random() * 500);
        int y = (int) (Math.random() * 500);
        setBounds(x, y, 400, 300);
        System.out.println(x + " " + y);
        add("Center", entrada);
        add("South", saida);
        setVisible(true);
        saida.requestFocus();
    }
    public void setTextoEntrada(String texto){
        entrada.append("\n" + texto);
        entrada.setCaretPosition(entrada.getText().length());
    }
    public void enviarMensagem(String mensagem){
        try{
            mensagem = PRIVADO + SEPARADOR + nick + SEPARADOR + paraUsuario + SEPARADOR + mensagem;
            byte[] bMensagem = mensagem.getBytes();
            DatagramPacket pacote = new DatagramPacket(bMensagem, bMensagem.length, InetAddress.getByName(MULTICAST_IP), MULTICAST_PORTA);
            mcSocket.send(pacote);
        }
        catch(UnknownHostException e){
            e.printStackTrace();
        }
        catch(IOException e){
            e.printStackTrace();
        }
    }
}
Main.java
package jsc;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.ScrollPane;
import java.awt.TextArea;
import java.awt.TextField;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.UnknownHostException;
import java.util.Iterator;
import java.util.Vector;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
public class Main extends Frame implements Constantes{
    private static final long serialVersionUID = 1L;
    private TextArea entrada;
    private TextField saida;
    private JList usuariosOnline;
    private ScrollPane usuariosOnlineScroll;
    private WindowAdapter  mainListener;
    private ActionListener saidaListener;
    private MouseAdapter   listListener;
    private MulticastSocket mcSocket;                           // soquete para multicasting
    private Vector<String> listaUsuariosOnline;                 // lista com os nomes de usuários online
    private Vector<MensagemPrivadaFrame> listaJanelasAbertas;   // janelas de conversação privadas abertas
    private String nick;                                        // nome do usuário no chat
    public void setNick(String nick){
        this.nick = nick;
    }
    public String getNick(){
        return nick;
    }
    public Main(String nick){
        super("Java Socket Chat [" + nick + "]");
        setIconImage(Toolkit.getDefaultToolkit().getImage("icone.1"));  
        this.nick = nick;
        listaUsuariosOnline = new Vector<String>();
        listaUsuariosOnline.add(nick);
        listaJanelasAbertas = new Vector<MensagemPrivadaFrame>();
        try{
            mcSocket = new MulticastSocket();
        }
        catch(IOException e){
            e.printStackTrace();
        }
        iniciarComponentes();
        new EstouOnlineThread(nick).start();
        new ControladorThread(this).start();
    }
    public void iniciarComponentes(){
        mainListener = new WindowAdapter(){
            public void windowClosing(WindowEvent e){
                sair();
            }
        };
        saidaListener = new ActionListener(){
            public void actionPerformed(ActionEvent e){
                TextField origem = (TextField) e.getSource();
                enviarMensagem(origem.getText());
                origem.setText("");
            }
        };
        listListener = new MouseAdapter(){
            public void mouseClicked(MouseEvent e){
                if( e.getClickCount() >= 2 ){
                    // abrir a janela para mensagens privadas e passar o id do usuário
                    JList jlAux = (JList) e.getSource();
                    String paraUsuario = (String) jlAux.getSelectedValue();
                    abrirChatPrivado(nick, paraUsuario, null);
                }
            }
        };
        usuariosOnline = new JList(listaUsuariosOnline);
        usuariosOnline.setSize(new Dimension(60, 280));
        usuariosOnlineScroll = new ScrollPane();
        usuariosOnlineScroll.add(usuariosOnline);
        entrada = new TextArea("Olá " + nick);
        entrada.setEditable(false);
        entrada.setSize(300,280);
        saida   = new TextField();
        saida.addActionListener(saidaListener);
        usuariosOnline.addMouseListener(listListener);
        usuariosOnline.setMinimumSize(new Dimension(60, 250));
        addWindowListener(mainListener);
        setSize(400, 300);
        setLayout(new BorderLayout());
        add("North", new JLabel("Java Socket ChatO"));
        add("Center", entrada);
        add("South", saida);
        add("East", usuariosOnlineScroll);
        setVisible(true);
        requestFocus();
    }
    public void enviarMensagem(String mensagem){
        try{
            mensagem = nick + SEPARADOR + mensagem;
            byte[] bMensagem = mensagem.getBytes();
            DatagramPacket pacote = new DatagramPacket(bMensagem, bMensagem.length, InetAddress.getByName(MULTICAST_IP), MULTICAST_PORTA);
            mcSocket.send(pacote);
        }
        catch(UnknownHostException e){
            e.printStackTrace();
        }
        catch(IOException e){
            e.printStackTrace();
        }
    }
    private void desconectando(){
        try{
            String mensagem = "Desconectando" + SEPARADOR + nick;
            byte[] bMensagem = mensagem.getBytes();
            DatagramPacket pacote = new DatagramPacket(bMensagem, bMensagem.length, InetAddress.getByName(MULTICAST_IP), MULTICAST_PORTA);
            mcSocket.send(pacote);
        }
        catch(UnknownHostException e){
            e.printStackTrace();
        }
        catch(IOException e){
            e.printStackTrace();
        }
    }
    public void abrirChatPrivado(String nick, String paraUsuario, String mensagem){
        removerJanelasInativas();   
        if(nick.equals(paraUsuario)){
            JOptionPane.showMessageDialog(null, "Você não pode abrir um janela de conversação para você mesmo!", "Burro!", JOptionPane.ERROR_MESSAGE);
            return;
        }
        String nome = nick + paraUsuario;
        MensagemPrivadaFrame janela = null;
        for(Iterator i = listaJanelasAbertas.iterator(); i.hasNext();){
            janela = (MensagemPrivadaFrame) i.next();
            if(nome.equals(janela.getNomeJanela())){
                System.out.println(nick + " - " + janela.getNomeJanela() + " - " + janela.toString());
                janela.setTextoEntrada("(" + paraUsuario + " diz) " + mensagem);
                //janela.requestFocus();
                return;
            }
        }
        janela = new MensagemPrivadaFrame(nick, paraUsuario);
        if(mensagem != null)
            janela.setTextoEntrada("(" + paraUsuario + " diz) " + mensagem);
        listaJanelasAbertas.add(janela);
        //janela.requestFocus();
    }
    public void removerJanelasInativas(){
        MensagemPrivadaFrame janela = null;
        for(Iterator i = listaJanelasAbertas.iterator(); i.hasNext(); ){
            janela = (MensagemPrivadaFrame) i.next();
            if( !janela.estouVivo()){
                i.remove();
            }
        }
    }
    public void setTextoEntrada(String texto){
        entrada.append("\n" + texto);
        entrada.setCaretPosition(entrada.getText().length());
    }
    public void setUsuariosOnline(Vector<String> listaUsuariosOnline){
        usuariosOnline.setListData(listaUsuariosOnline);
    }
    public void sair(){
        desconectando();
        dispose();
        System.exit(0);
    }
    public static void main(String args[]){
        String nick = JOptionPane.showInputDialog("Digite seu nome (max. 20 caracteres): ");
        if(nick != null && !nick.equals("")){
            if(nick.length() > 20)
                nick = nick.substring(0, 20);
            new Main(nick);
        }
        else
            JOptionPane.showMessageDialog(null, "É necessário informar um nome para entrar no bate-papo");
        //System.exit(0);
    }
}
Nowadays I'm not proud of the code, but it really works. 
Edit:
As some have suggested, I've made some code improvements (refactoring) and post the project on GitHub: https://github.com/jaumzera/javasocketchat