欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

多线程基础 、 TCP通信

程序员文章站 2022-06-06 07:59:41
...

1. 多线程基础

1.1. 线程同步

1.1.1. 线程安全API与非线程安全API

之前学习的API中就有设计为线程安全与非线程安全的类:

StringBuffer 是同步的 synchronized append();

StringBuilder 不是同步的 append();

相对而言StringBuffer在处理上稍逊于StringBuilder,但是其是线程安全的。当不存在并发时首选应当使用StringBuilder。

同样的:

Vector 和 Hashtable 是线程安全的而ArrayList 和 HashMap则不是线程安全的。

对于集合而言,Collections提供了几个静态方法,可以将集合或Map转换为线程安全的:

例如:

Collections.synchronizedList() :获取线程安全的List集合

Collections.synchronizedMap():获取线程安全的Map

  1.     ...
  2.     List<String> list = new ArrayList<String>();
  3.     list.add("A");
  4.     list.add("B");
  5.     list.add("C");
  6.     list = Collections.synchronizedList(list);//将ArrayList转换为线程安全的集合
  7.     System.out.println(list);//[A,B,C] 可以看出,原集合中的元素也得以保留
  8.     ...
	...
	List<String> list = new ArrayList<String>();
	list.add("A");
	list.add("B");
	list.add("C");
	list = Collections.synchronizedList(list);//将ArrayList转换为线程安全的集合
	System.out.println(list);//[A,B,C]   可以看出,原集合中的元素也得以保留
	...

1.1.2. 使用ExecutorService实现线程池

当一个程序中若创建大量线程,并在任务结束后销毁,会给系统带来过度消耗资源,以及过度切换线程的危险,从而可能导致系统崩溃。为此我们应使用线程池来解决这个问题。

ExecutorService是java提供的用于管理线程池的类。

线程池有两个主要作用:

  1. 控制线程数量
  2. 重用线程

线程池的概念:首先创建一些线程,它们的集合称为线程池,当服务器接受到一个客户请求后,就从线程池中取出一个空闲的线程为之服务,服务完后不关闭该线程,而是将该线程还回到线程池中。

在线程池的编程模式下,任务是提交给整个线程池,而不是直接交给某个线程,线程池在拿到任务后,它就在内部找有无空闲的线程,再把任务交给内部某个空闲的线程,任务是提交给整个线程池,一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务

线程池有以下几种实现策略:

Executors.newCachedThreadPool()

创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。

Executors.newFixedThreadPool(int nThreads)

创建一个可重用固定线程集合的线程池,以共享的*队列方式来运行这些线程。

Executors.newScheduledThreadPool(int corePoolSize)

创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

Executors.newSingleThreadExecutor()

创建一个使用单个 worker 线程的 Executor,以*队列方式来运行该线程。

可以根据实际需求来使用某种线程池。例如,创建一个有固定线程数量的线程池:

  1.     ...
  2.     ExecutorService threadPool
  3.         = Executors.newFixedThreadPool(30);//创建具有30个线程的线程池
  4.     Runnable r1 = new Runable(){
  5.         public void run(){
  6.             //线程体
  7.         }
  8.     };
  9.     threadPool.execute(r1);//将任务交给线程池,其会分配空闲线程来运行这个任务。
  10.     ...
	...
	ExecutorService threadPool 
		= Executors.newFixedThreadPool(30);//创建具有30个线程的线程池
	Runnable r1 = new Runable(){
		public void run(){
			//线程体
		}
	};
	threadPool.execute(r1);//将任务交给线程池,其会分配空闲线程来运行这个任务。
	...

1.1.3. 使用BlockingQueue

BlockingQueue是双缓冲队列。

在多线程并发时,若需要使用队列,我们可以使用Queue,但是要解决一个问题就是同步,但同步操作会降低并发对Queue操作的效率。

BlockingQueue内部使用两条队列,可允许两个线程同时向队列一个做存储,一个做取出操作。在保证并发安全的同时提高了队列的存取效率。

双缓冲队列有一下几种实现:

ArrayBlockingDeque:规定大小的BlockingDeque,其构造函数必须带一个int参数来指明其大小.其所含的对象是以FIFO(先入先出)顺序排序的。

LinkedBlockingDeque:大小不定的BlockingDeque,若其构造函数带一个规定大小的参数,生成的BlockingDeque有大小限制,若不带大小参数,所生成的BlockingDeque的大小由Integer.MAX_VALUE来决定.其所含的对象是以FIFO(先入先出)顺序排序的。

PriorityBlockingDeque:类似于LinkedBlockDeque,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数的Comparator决定的顺序。

SynchronousQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成的。

例如:

  1.     public static void main(String[] args) {
  2.         BlockingQueue<String> queue
  3.             = new LinkedBlockingDeque<String>();
  4.         
  5.         try {
  6.             //queue.offer("A");//立即向队列末尾追加元素
  7.             
  8.             /*
  9.              * 向队列末尾追加元素,指定可延迟5秒。
  10.              * 若5秒钟内成功将元素加入队列返回true
  11.              * 若超时后元素仍然没有加入队列则返回flase
  12.              */
  13.             queue.offer("A",5,TimeUnit.SECONDS);
  14.         } catch (InterruptedException e) {
  15.             e.printStackTrace();
  16.         }
  17.         System.out.println(queue.poll());
  18.     }
	public static void main(String[] args) {
		BlockingQueue<String> queue
			= new LinkedBlockingDeque<String>();
		
		try {
			//queue.offer("A");//立即向队列末尾追加元素
			
			/*
			 * 向队列末尾追加元素,指定可延迟5秒。
			 * 若5秒钟内成功将元素加入队列返回true
			 * 若超时后元素仍然没有加入队列则返回flase
			 */
			queue.offer("A",5,TimeUnit.SECONDS);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(queue.poll());
	}

2. TCP通信

2.1. Socket原理

2.1.1. Socket简介

socket通常称作“套接字”,用于描述IP地址和端口,是一个通信链的句柄。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。

应用程序通常通过“套接字”向网络发出请求或者应答网络请求。Socket和ServerSocket类库位于java .net包中。ServerSocket用于服务端,Socket是建立网络连接时使用的。在连接成功时,应用程序两端都会产生一个Socket实例,操作这个实例,完成所需的会话。

2.1.2. 获取本地地址和端口号

java.net.Socket为套接字类,其提供了很多方法,其中我们可以通过Socket获取本地的地址以及端口号。

  1.         int getLocalPort()
		int getLocalPort()

该方法用于获取本地使用的端口号

  1.         InetAddress getLocalAddress()
		InetAddress getLocalAddress()

该方法用于获取套接字绑定的本地地址

使用InetAddress获取本地的地址方法:

  1.         String getCanonicalHostName()
		String getCanonicalHostName()

获取此 IP 地址的完全限定域名。

  1.         String getHostAddress()
		String getHostAddress()

返回 IP 地址字符串(以文本表现形式)。

代码如下:

  1.         public void testSocket()throws Exception {
  2.             Socket socket = new Socket("localhost",8088);
  3.             InetAddress add = socket.getLocalAddress();//获取本地地址信息
  4.             System.out.println(add.getCanonicalHostName());
  5.             System.out.println(add.getHostAddress());
  6.             System.out.println(socket.getLocalPort());
  7.         }
		public void testSocket()throws Exception {
			Socket socket = new Socket("localhost",8088);
			InetAddress add = socket.getLocalAddress();//获取本地地址信息
			System.out.println(add.getCanonicalHostName());
			System.out.println(add.getHostAddress());
			System.out.println(socket.getLocalPort());
		} 

2.1.3. 获取远端地址和端口号

Socket也提供了获取远端的地址以及端口号的方法:

  1.         int getPort()
		int getPort()

该方法用于获取远端使用的端口号 。

  1.         InetAddress .getInetAddress()
		InetAddress .getInetAddress()

该方法用于获取套接字绑定的远端地址 。

代码如下:

  1.         public void testSocket()throws Exception {
  2.             Socket socket = new Socket("localhost",8088);
  3.              InetAddress inetAdd = socket.getInetAddress();
  4.             System.out.println(inetAdd.getCanonicalHostName());
  5.             System.out.println(inetAdd.getHostAddress());
  6.             System.out.println(socket.getPort());
  7.         }
		public void testSocket()throws Exception {
			Socket socket = new Socket("localhost",8088);
		 	InetAddress inetAdd = socket.getInetAddress();
			System.out.println(inetAdd.getCanonicalHostName());
			System.out.println(inetAdd.getHostAddress());
			System.out.println(socket.getPort()); 
		} 

2.1.4. 获取网络输入流和网络输出流

通过Socket获取输入流与输出流,这两个方法是使用Socket通讯的关键方法。封装了TCP协议的Socket是基于流进行通讯的,所以我们在创建了双方连接后,只需要获取相应的输入与输出流即可实现通讯。

  1.     InputStream getInputStream()
	InputStream getInputStream()

该方法用于返回此套接字的输入流。

  1.     OutputStream .getOutputStream()
	OutputStream .getOutputStream()

该方法用于返回此套接字的输出流。

代码如下:

  1.     public void testSocket()throws Exception {
  2.         Socket socket = new Socket("localhost",8088);
  3.         InputStream in = socket.getInputStream();
  4.         OutputStream out = socket.getOutputStream();
  5.     }
	public void testSocket()throws Exception {
		Socket socket = new Socket("localhost",8088);
		InputStream in = socket.getInputStream();
		OutputStream out = socket.getOutputStream();
	} 

2.1.5. close方法

当使用Socket进行通讯完毕后,要关闭Socket以释放系统资源。

  1.     void close()
	void close()

当关闭了该套接字后也会同时关闭由此获取的输入流与输出流。

2.2. Socket通讯模型

2.2.1. Server端ServerSocket监听

java.net.ServerSocket是运行于服务端应用程序中。通常创建ServerSocket需要指定服务端口号,之后监听Socket的连接。监听方法为:

  1.         Socket accept()
		Socket accept()

该方法是一个阻塞方法,直到一个客户端通过Socket连接后,accept会封装一个Socket,该Socket封装与表示该客户端的有关的信息。通过这个Socket与该客户端进行通信。

代码如下:

  1.         …
  2.         //创建ServerSocket并申请服务端口8088
  3.         ServerSocket server = new ServerSocket(8088);
  4.         /*方法会产生阻塞,直到某个Socket连接,并返回请求连接的Socket*/
  5.         Socket socket = server.accept();
  6.         …

		//创建ServerSocket并申请服务端口8088
		ServerSocket server = new ServerSocket(8088);
		/*方法会产生阻塞,直到某个Socket连接,并返回请求连接的Socket*/
		Socket socket = server.accept();

2.2.2. Client端Socket连接

通过上一节我们已经知道,当服务端ServerSocket调用accept方法阻塞等待客户端连接后,我们可以通过在客户端应用程序中创建Socket来向服务端发起连接。

需要注意的是,创建Socket的同时就发起连接,若连接异常会抛出异常。 我们通常创建Socket时会传入服务端的地址以及端口号。

代码如下:

  1.     //参数1:服务端的IP地址,参数2:服务端的服务端口
  2.     Socket socket = new Socket(“localhost”,8088);
  3.     …
	//参数1:服务端的IP地址,参数2:服务端的服务端口 
	Socket socket = new Socket(“localhost”,8088);

2.2.3. C-S端通信模型

C-S的全称为(Client-Server):客户端-服务器端

客户端与服务端通信模型如下:

多线程基础 、 TCP通信

图- 1

  1. 服务端创建ServerSocket
  2. 通过调用ServerSocket的accept方法监听客户端的连接
  3. 客户端创建Socket并指定服务端的地址以及端口来建立与服务端的连接
  4. 当服务端accept发现客户端连接后,获取对应该客户端的Socket
  5. 双方通过Socket分别获取对应的输入与输出流进行数据通讯
  6. 通讯结束后关闭连接。

代码如下:

  1.     /**
  2.      *    Server端应用程序
  3.      */
  4.     public class Server {
  5.     public static void main(String[] args) {
  6.         ServerSocket server = null;
  7.         try {
  8.             //创建ServerSocket并申请服务端口为8088
  9.             server = new ServerSocket(8088);
  10.             
  11.             //侦听客户端的连接
  12.             Socket socket = server.accept();
  13.             
  14.             //客户端连接后,通过该Socket与客户端交互
  15.             //获取输入流,用于读取客户端发送过来的消息
  16.             InputStream in = socket.getInputStream();
  17.             BufferedReader reader
  18.                 = new BufferedReader(
  19.                     new InputStreamReader(
  20.                         in,"UTF-8"
  21.                     )
  22.                 );
  23.             
  24.             //获取输出流,用于向该客户端发送消息
  25.             OutputStream out = socket.getOutputStream();
  26.             PrintWriter writer
  27.                 = new PrintWriter(
  28.                     new OutputStreamWriter(
  29.                         out,"UTF-8"    
  30.                     ),true
  31.                 );
  32.             
  33.             //读取客户端发送的消息
  34.             String message = reader.readLine();
  35.             System.out.println("客户端说:"+message);
  36.             
  37.             //向客户端发送消息
  38.             writer.println("你好客户端!");
  39.         } catch (Exception e) {
  40.             e.printStackTrace();
  41.         } finally{
  42.             if(server != null){
  43.                 try {
  44.                     server.close();
  45.                 } catch (IOException e) {
  46.                 }
  47.             }
  48.         }
  49.     }
  50. }
  51.  
  52. /**
  53. * Client端应用程序
  54. */
  55. public class Client {
  56.     public static void main(String[] args) {
  57.         Socket socket = null;
  58.         try {
  59.             socket = new Socket("localhost",8088);
  60.             //获取输入流,用于读取来自服务端的消息
  61.             InputStream in = socket.getInputStream();
  62.             BufferedReader reader
  63.                 = new BufferedReader(
  64.                     new InputStreamReader(
  65.                         in,"UTF-8"
  66.                     )
  67.                 );
  68.             
  69.             //获取输出流,用于向服务端发送消息
  70.             OutputStream out
  71.                 = socket.getOutputStream();
  72.             OutputStreamWriter osw
  73.                 = new OutputStreamWriter(out,"UTF-8");
  74.             PrintWriter writer
  75.                 = new PrintWriter(osw,true);
  76.             
  77.             //向服务端发送一个字符串
  78.             writer.println("你好服务器!");
  79.             
  80.             //读取来自客户端发送的消息
  81.             String message = reader.readLine();
  82.             System.out.println("服务器说:"+message);
  83.         } catch (Exception e) {
  84.             e.printStackTrace();
  85.         } finally{
  86.             try {
  87.                 if(socket != null){
  88.                     //关闭Socket
  89.                     socket.close();
  90.                 }
  91.             } catch (IOException e) {
  92.                 e.printStackTrace();
  93.             }
  94.         }
  95.     }
  96. }
	/**
	 *	Server端应用程序
	 */
	public class Server {
	public static void main(String[] args) {
		ServerSocket server = null;
		try {
			//创建ServerSocket并申请服务端口为8088
			server = new ServerSocket(8088);
			
			//侦听客户端的连接
			Socket socket = server.accept();
			
			//客户端连接后,通过该Socket与客户端交互
			//获取输入流,用于读取客户端发送过来的消息
			InputStream in = socket.getInputStream();
			BufferedReader reader
				= new BufferedReader(
					new InputStreamReader(
						in,"UTF-8"
					)
				);
			
			//获取输出流,用于向该客户端发送消息
			OutputStream out = socket.getOutputStream();
			PrintWriter writer
				= new PrintWriter(
					new OutputStreamWriter(
						out,"UTF-8"	
					),true
				);
			
			//读取客户端发送的消息
			String message = reader.readLine();
			System.out.println("客户端说:"+message);
			
			//向客户端发送消息
			writer.println("你好客户端!");
		} catch (Exception e) {
			e.printStackTrace();
		} finally{
			if(server != null){
				try {
					server.close();
				} catch (IOException e) {
				}
			}
		}
	}
}

/**
 * Client端应用程序
 */
public class Client {
	public static void main(String[] args) {
		Socket socket = null;
		try {
			socket  = new Socket("localhost",8088);
			//获取输入流,用于读取来自服务端的消息
			InputStream in = socket.getInputStream();
			BufferedReader reader
				= new BufferedReader(
					new InputStreamReader(
						in,"UTF-8"
					)
				);
			
			//获取输出流,用于向服务端发送消息
			OutputStream out
				= socket.getOutputStream();
			OutputStreamWriter osw
				= new OutputStreamWriter(out,"UTF-8");
			PrintWriter writer
				= new PrintWriter(osw,true);
			
			//向服务端发送一个字符串
			writer.println("你好服务器!");
			
			//读取来自客户端发送的消息
			String message = reader.readLine();
			System.out.println("服务器说:"+message);
		} catch (Exception e) {
			e.printStackTrace();
		} finally{
			try {
				if(socket != null){
					//关闭Socket
					socket.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}