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)使用方法- 同步方法:在方法上修饰,此时该方法变为一个同步方法。
- 同步块:可以更准确的锁定需要排队的代码片段
- 语法
/* 同步块使用时需要指定一个同步监视器对象,即:上锁的对象。该对象从语法的角度来讲可以是任意引用类型的实例。 但是必须同时满足多个需要同步(排队)执行该代码片段的线程看到的是同一个对象才可以! */ synchronized (this) { Thread.sleep(5000); System.out.println("同步执行的任务"); }
- 同步监视器对象:上锁的对象,要想保证同步块中的代码被多个线程同步运行,则要求多个线程看到的同步监视器对象是同一个,可以是java中任何引用类型的实例。一般用当前对象this即可。
- 语法
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。
- 静态方法加上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线程也要以这样的持有顺序进行。