网络通信和线程安全【23/11/24】

目录

一 网络通信

        1.网络通信三要素

        2.UDP通信

        3.TCPC通信


一 网络通信

        1.网络通信三要素

                IP地址:设备在网络中的地址,是唯一的标识

                端口:应用程序在设备中唯一的标识

                协议:数据在网络中传输的规则,常见的协议有UDP协议和TCP协议

        2.UDP通信

               1. UDP通信全称:用户数据报协议。是一种无连接不可靠的协议.

                2.如何建立UDP通信:

                //接收端
class service{
    public static void main(String[] args) throws IOException {
        
        System.out.println("服务端启动...");
        
        // 1 : 创建接收端对象(人),(与发送端不同的是,它需要注册端口,因为这样发送端就知道数据发送到哪了)
        DatagramSocket socket = new DatagramSocket(8575) ;   //注意:端口号必须要和发送端写的端口一样,不然发送端就找不到接受端了
        
        // 2 : 创建一个对象接收数据(韭菜盘子)
        byte[] buffer = new byte[1024*64] ; //先创建一个盘子,大小为1024kb,因为UDP发送数据每次都是扔一捆韭菜过来(1024kb数据)
        
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length) ; //第一个参数表示接受的数据,第二个参数表示需要读取的数据大小
        
        while (true) {
            //3 : 等待接受数据
        socket.receive(packet);  //它会一直等到接收到代码
        
        // 4: 取出数据
        String st = new String(buffer,"UTF-8") ; 
        System.out.println("收到了:"+st);
        
        }
        
    }
}

//客户端

public class Main {
    public static  Scanner sc = new Scanner(System.in) ; 
    public static void main(String[] args) throws IOException {

        /** ===============UDP通信快速入门=======================
         *  
         *  UDP通信是一个可能会丢失数据的不是很可靠的通信协议
         *  
         *  在使用的时候,我们可以把它看成一个韭菜盒子
         *  */
        
        System.out.println("客户端启动...");
        
        // 1 : 创建发送端对象:发送端自带默认的端口号(扔韭菜的人)
        DatagramSocket socket = new DatagramSocket() ; //括号里面本来是要填端口号的,但发送端可以默认不填(socket-->插口) ,另外需要注意的是,涉及到文件传输的话,这相当于一个管道,最后要清理管道
     
        while (true) {
        System.out.println("请说:"); //先创建一个我要发送的数据
        
        String st = sc.nextLine() ; //先接受字符串数据
        
        if(st.equals("exit")) {
            System.out.println("客户端已结束运行");
            break ; 
        }
        
        byte[] buffer = st.getBytes() ; //数据包装类的构造器接受的参数是一个byte类型的数组,所以要把字符串先转换成字符然后在装进字符数组里面去
        
        // 2 : 创建一个数据包对象封装数据(韭菜盘子)
        DatagramPacket pack = new DatagramPacket(buffer, buffer.length , InetAddress.getByName("10.91.6.55"),8515) ; 
        /**
         * 参数一,buffer:  封装要发送的数据
         * 参数二,buffer.length:  选择要发送数据的大小(虽然你写了这么多到数组里面,但是你可能只想发送一部分出去)
         * 参数三,InetAddress.getByName("10.65.64.219") : 服务器(即接收端)的主机IP地址,这些数据你肯定要选择发送到哪个电脑去吧。
         * 参数四,8888 : 服务器的端口,既然已经定位到哪一台电脑了,那么肯定还要知道你的数据是发送到哪个应用上面去的吧。
         */
        
        // 3 : 发送数据出去
        socket.send(pack) ; 
        
        }
        
        /**  发送端完成,接下来要完成接收端*/
    }
}

 

      3.TCP通信

                1.TCP通信是一种有连接,可靠的传输协议,它在建立连接时会进行三次握手确认信息能否收到,而在断开连接时也会进行四次确认。

                2.TCP通信实现方式:

import java.util.*;
import java.net.*;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.*;

/**            TCP通信
 * 1,TCP 是一种面向连接,安全,可靠的传输数据的协议
 * 2,传输前,采用“三次握手”方式,点对点通信,是可靠的
 * 3,在连接中可进行大量数据的传输
 * 
 *   简单来说,就是客户端和服务端之间互相连接一条传输管道,通过管道进行数据传输
 * 
 * @author 黄佳乐
 *
 */
public class Main{
    
    private static Scanner sc = new Scanner(System.in) ;
    
    public static void main(String[] args) {
        
        /**            客户端           */
        
        System.out.println("====客户端启动=====");
        
        
        try {
            
            try (// 1: 创建Socket通信管道请求与服务端进行连接
            Socket socket = new Socket("10.65.67.114" , 2585)) {
                // 2: 与服务端连接完成之后,就要在通信管道中创建字节输出流,用于发送数据
                OutputStream os = socket.getOutputStream() ; 
                /**
                 * 这个并不是创建一个字节输出流管道,而是在通信管道socket中加上一个字节输出流
                 */
                
                // 3: 前面学过io流,所以我们知道字节输出流输出数据很低效,而我们学过高级流,所以要对这个普通的字节输出流加工一下。输出数据的话,最好的就是用打印流了。
                PrintStream ps = new PrintStream(os) ; //将字节输出流os加工成打印流
                
                
                    // 4: 现在可以发送数据了
                    System.out.println("请说:");
                    String msg = sc.nextLine();

                    ps.println(msg);
                    /**   重点!
                     * 这里如果你只按照系统默认给你的ps.print(msg)的话,是会报错的。
                     * 因为服务端接收数据用的是接收整行,而你这里不加换行的话,系统会以为你并没有写完一整行,所以无论你写多少服务端都不会接收数据
                     */
                    
                    // 5: 记得刷新数据
                    ps.flush();
        
            }
            
            // 6; 客户端完成后,接下来当然要完成服务端了。
            
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } 
        
    }
}

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 {

    public static void main(String[] args) {
        
        /**         服务端       */
        
        System.out.println("=====服务端启动=====");
        
        
        try {
            
            try (// 1: 服务端第一步必须要先创建一个端口,这样客户端才能找到服务端
            ServerSocket serverSocket = new ServerSocket(2585)) {
                // 2: 端口完成,确定连接之后,我就需要接收传过来的数据了,那么就要调用一个accept方法,等待接收客户端管道传来的数据,建立socket通信管道
                Socket socket = serverSocket.accept() ; //启动后,服务端会在这里等待接收数据
                
                // 3: 管道连接后,就需要获取客户端传过来的数据了,而数据是已字节形式传过来,刚好socket管道有提供获取字节输入流的方法,所以我们只需要用字节输入流来提取就行了。
                
                InputStream is = socket.getInputStream() ; 
                
                // 4: 但是普通的字节输入流接收数据显然是很低效的,所以我们需要把字节输入流加工一下,而字节输入流一般都包装成缓冲字符输入流。
                BufferedReader br = new BufferedReader(new InputStreamReader(is,"UTF-8")) ; 
                /**
                 * 这里是两步写在一起写的,分开写就是:
                 *   首先,把字节输入流is包装成字符输入流,
                 *   然后,再把字符输入流包装成缓冲字符输入流
                 */
                
                // 5:按行读取数据
                  String msg ;
                
                while((msg = br.readLine()) != null) {
                    /**   
                     * 
                     */
                    System.out.println(socket.getRemoteSocketAddress()+"说:"+msg);
                }
            }
            
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }

}
 

 

        2. TCP通信实现多发多收

              上面的代码,每次只能发送和接收一句话,那么我们想要一直发送和接收数据怎么办呢?这里我们可以用一个死循环在客户端不断的等待输入信息,然后发送,服务端也用一个死循环,不断的去读取管道里面的内容,实现代码如下:

客户端:

import java.util.*;
import java.net.*;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.*;

/**            TCP通信
 * 1,TCP 是一种面向连接,安全,可靠的传输数据的协议
 * 2,传输前,采用“三次握手”方式,点对点通信,是可靠的
 * 3,在连接中可进行大量数据的传输
 * 
 *   简单来说,就是客户端和服务端之间互相连接一条传输管道,通过管道进行数据传输
 * 
 * @author 黄佳乐
 *
 */
public class Main{
	
	private static Scanner sc = new Scanner(System.in) ;
	
	public static void main(String[] args) {
		
		/**            客户端           */
		
		System.out.println("====客户端启动=====");
		
		
		try {
			
			try (// 1: 创建Socket通信管道请求与服务端进行连接
			Socket socket = new Socket("10.65.67.114" , 2585)) {
				// 2: 与服务端连接完成之后,就要在通信管道中创建字节输出流,用于发送数据
				OutputStream os = socket.getOutputStream() ; 
				/**
				 * 这个并不是创建一个字节输出流管道,而是在通信管道socket中加上一个字节输出流
				 */
				
				// 3: 前面学过io流,所以我们知道字节输出流输出数据很低效,而我们学过高级流,所以要对这个普通的字节输出流加工一下。输出数据的话,最好的就是用打印流了。
				PrintStream ps = new PrintStream(os) ; //将字节输出流os加工成打印流
				
				
					while (true) {
						// 4: 现在可以发送数据了
						System.out.println("请说:");
						String msg = sc.nextLine();

						ps.println(msg);
						/**   重点!
						 * 这里如果你只按照系统默认给你的ps.print(msg)的话,是会报错的。
						 * 因为服务端接收数据用的是接收整行,而你这里不加换行的话,系统会以为你并没有写完一整行,所以无论你写多少服务端都不会接收数据
						 */

						// 5: 记得刷新数据
						ps.flush();
					}
					/**
					 * 想要实现循环发送的话,只需要在关键的等待传输数据的地方加上循环就可以了。
					 * 现在在没有加线程的情况下,只有一条主线程在执行,所以这里加上循环结构后,主线程就一直在这里传输数据
					 * 而前面创建的管道以及字节输出流,都只需要创建一个即可,所以只需要一条主线程就能完成。
					 * 只不过需要注意的是:当需要接收多个客户端的消息的时候,一条主线程就不行了,
					 * 为什么呢?
					 * 因为服务端只有一条主线程,当主线程进入到接收管道消息的循环之后,就会一直在这个循环里面
					 * 但是,此时你的服务端之和一个客户端通过管道建立了联系,而当第二个客户端也想通过管道和你建立联系的时候,服务端前面建立管道的联系的代码没有线程来执行了。
					 * 但是,如果只是单单在服务端前面建立管道联系的代码上也加上一个循环,这样显然还是不行的,因为当主线程执行到后面的等待消息的循环处后,前面的代码还是没法执行
					 * 所以,解决方案只能加上线程池了,可以将服务端建立管道联系的代码加上循环,让主线程去不断执行,
					 * 而后面的接收消息循环处,则可以做成一个线程任务,然后丢到线程池里,让子线程去执行
					 */
					
				
			}
			
			// 6; 客户端完成后,接下来当然要完成服务端了。
			
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} 
		
	}
}

 服务端:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Server {

	public static void main(String[] args) {
		
		         多发多收消息       
		
		System.out.println(=====服务端启动=====);
		
		
		try {
			
			try ( 1 服务端第一步必须要先创建一个端口,这样客户端才能找到服务端
			ServerSocket serverSocket = new ServerSocket(2585)) {
				 2 端口完成,确定连接之后,我就需要接收传过来的数据了,那么就要调用一个accept方法,等待接收客户端管道传来的数据,建立socket通信管道
				Socket socket = serverSocket.accept() ; 启动后,服务端会在这里等待接收数据
				
				 3 管道连接后,就需要获取客户端传过来的数据了,而数据是已字节形式传过来,刚好socket管道有提供获取字节输入流的方法,所以我们只需要用字节输入流来提取就行了。
				
				InputStream is = socket.getInputStream() ; 
				
				 4 但是普通的字节输入流接收数据显然是很低效的,所以我们需要把字节输入流加工一下,而字节输入流一般都包装成缓冲字符输入流。
				BufferedReader br = new BufferedReader(new InputStreamReader(is,UTF-8)) ; 
				
				  这里是两步写在一起写的,分开写就是:
				    首先,把字节输入流is包装成字符输入流,
				    然后,再把字符输入流包装成缓冲字符输入流
				 
				
				 5按行读取数据
				  String msg ;
				
				while((msg = br.readLine()) != null) {
					   
					  msg = br.readLine() 在这里等待接收!
					 
					System.out.println(socket.getRemoteSocketAddress()+说:+msg);
				}
			}
			
		} catch (IOException e) {
			 TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

}

============如果只是想实现与一个客户端之间进行多发多收消息,上面已经实现了,但是我要实现多个客户端的连接==========

public class Server{
	public static void main(String[] args) {
		
		
         多发多收消息---可以同时接收多个客户端发的消息      
		
		按照这个思路:
		              因为服务端只有一条主线程,当主线程进入到接收管道消息的循环之后,就会一直在这个循环里面
					  但是,此时你的服务端之和一个客户端通过管道建立了联系,而当第二个客户端也想通过管道和你建立联系的时候,服务端前面建立管道的联系的代码没有线程来执行了。
					  但是,如果只是单单在服务端前面建立管道联系的代码上也加上一个循环,这样显然还是不行的,因为当主线程执行到后面的等待消息的循环处后,前面的代码还是没法执行
					  所以,解决方案只能加上线程池了,可以将服务端建立管道联系的代码加上循环,让主线程去不断执行,
					  而后面的接收消息循环处,则可以做成一个线程任务,然后丢到线程池里,让子线程去执行
		 
		
		System.out.println(=====服务端启动=====);
		
		=========服务端启动就创建一个线程池,线程池一个就可以了,所以不需要放入循环里面============
		ExecutorService pool = new ThreadPoolExecutor(3,5,6,TimeUnit.SECONDS,new ArrayBlockingQueue(5),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy()) ; 
		
		try {
			
			try ( 1 服务端第一步必须要先创建一个端口,这样客户端才能找到服务端
			ServerSocket serverSocket = new ServerSocket(2585)) {
				
				while (true) {  ====需要在建立管道联系开始创建循环体,让主线程不断与客户端之间建立管道联系====
					
					 2 端口完成,确定连接之后,我就需要接收传过来的数据了,那么就要调用一个accept方法,等待接收客户端管道传来的数据,建立socket通信管道
					Socket socket = serverSocket.accept(); 启动后,服务端会在这里等待接收管道
					System.out.println(socket.getRemoteSocketAddress()+上线了!);
					====从这里开始,后面的代码都需要交给子线程去做了,因为主线程在这里已经接收到客户端请求的管道连接了=====

					======主线程在这里接收到客户端的管道请求,并与客户端建立联系,那么剩下读取数据的任务就要交给一个子线程去做了。我们可以选择创建一个线程,但这样不安全,所以我们直接用线程池======
					new MyThread(socket).start();  将socket管道交给一个刚创建的子线程,并且开始执行任务
					 如果想直接创建一个子线程这样就完成了!不不过这样的话,就可能造成子线程创建过多,cpu就会被占用,有高风险,所以我们不用这个方法,我们选择用线程池
					
					======将任务交给线程池去执行=======
					pool.execute(new MyThread(socket)) ; 
				}
			}
			
		} catch (Exception e) {
			 TODO Auto-generated catch block
			e.printStackTrace();
		}

	}
}

创建一个线程类(记住,继承Thread这是一个线程池,不是任务,而实现Runnnable接口是封装成一个任务,而不是线程。)
class MyThread implements Runnable{
	因为线程需要执行接收管道里面的数据,所以肯定需要把上面主线程创建的管道传给线程类,所以这里需要创建一个有参构造器来接收一下
	Socket socket ; 
	public MyThread(Socket socket) {
		this.socket = socket ; 
	}
	@Override
	public void run() {
		 run方法里面就可以执行我管道里的接收数据的任务了
		 3 管道连接后,就需要获取客户端传过来的数据了,而数据是已字节形式传过来,刚好socket管道有提供获取字节输入流的方法,所以我们只需要用字节输入流来提取就行了。

		try {
			InputStream is = socket.getInputStream();

		 4 但是普通的字节输入流接收数据显然是很低效的,所以我们需要把字节输入流加工一下,而字节输入流一般都包装成缓冲字符输入流。
		BufferedReader br = new BufferedReader(new InputStreamReader(is, UTF-8));
		
		  这里是两步写在一起写的,分开写就是:
		    首先,把字节输入流is包装成字符输入流,
		    然后,再把字符输入流包装成缓冲字符输入流
		 

		 5按行读取数据
		String msg;

		while ((msg = br.readLine()) != null) {
			   
			  msg = br.readLine() 在这里等待接收!
			 
			System.out.println(socket.getRemoteSocketAddress() + 说: + msg);
		} 
		
		} catch (Exception e) {
			 TODO handle exception
			System.out.println(socket.getRemoteSocketAddress()+下线了!);
		}
		super.run();
	}
}

  补充一点重要的:你们可能会觉得,上面学过线程,它的主要任务就是里面的线程可以重复使用,节约线程数量,所以这里的作用也是这个。错!这里的作用完全不是这个。
  这个通信任务不同于普通的任务,从代码可以看出来,runnable任务是来到循环体里面一直等待连接管道里传来的数据,并且把它读出来。
  所以,线程池每给一个子线程安排任务后,子线程就会一直困在这个循环里等待数据,所以每个子线程都会一直被占用,当任务数量达到线程池最大子线程数量后,这个任务也就执行不了了。
  
 那么,它的主要作用是什么呢?
 就是第二个作用,控制创建的线程数量,因为如果你不用线程池的话,就可能有无数个客户端与服务端连接,这样,服务端就会创建相应的线程,而线程是会占用cpu的,从而占用电脑资源,太多会崩的。
 这就是用线程池来完程tcp通信任务的主要作用
 

二 多线程

  1. 什么是多线程

        线程就是执行我们写的程序的就是一个线程,我们平常写的main方法就是有main线程去执行的,可是很多时候一个线程不能满足我们的编程需求,比如上面的的通信,如果我作为服务端既要接收信息又要发送信息的话,当我只有一个主线程,我在没有接收到信息之前就不能去执行发送信息,可这两个模块功能因该是独立的,所以这个时候就需要另外一个线程转门去执行发送信息的功能。

     2.多线程的创建方式

方式一:继承Thread类

import java.util.* ;
import java.lang.*;

/**               多线程
 * 
 * 一,多线程的实现方案一:继承Thread类
 *     1:定义一个子类MyThread继承线程类java.lang.Thread,重写run方法
 *     2:创建一个MyThread类的对象
 *     3: 调用线程对象的start()方法启动线程(重启后还是执行run方法的)
 * @author 黄佳乐
 *
 */
public class Main{
	public static void main(String[] args) {
	
		// 3 : new一个线程对象
		Thread t = new MyThread() ; // 因为MyThread类是继承了Thread的,所以这样是多态的写法
		
		// 4 : 通过调用start方法启动线程(执行的还是run方法)
		t.start();
		
		/**    这里为什么不直接调用run,来执行run方法呢?
		 * 
		 * 因为start是多线程的启动标志,会告诉cpu新执行一个线程
		 * 
		 * 如果你直接写成t.run() ; 那么这就不是多线程了,系统会认为你只是调用了一个普通的run方法,所以这样还是一个单线程
		 * 
		 */
		
		// 5 : 写一个main程序做对比
		for(int i = 0 ; i<5 ; ++i) {
			System.out.println("主线程执行输出: "+ i);
		}
		
		/**     执行过程:
		 * 我们如果调用的是一个普通类中的方法,那么按照程序的执行顺序,当执行到第4步的时候,成序会直接跳到哪个类里执行完run方法,然后再继续执行后行后面的主线程,
		 *     但是,我们现在使用多线程后,程序是同时执行run方法和后面的主线程的,也就是说,程序不会按照顺序先执行完前面的代码后再执行后面的代码,而是多线程一起执行的
		 * 这,就是多线程的主要作用!
		 */
	}
}

//1 ;定义一个线程类继承Thread
class MyThread extends Thread{
	
	// 2 : 重写run方法
	public void run() {
		for(int i = 0 ; i<4 ; ++i) {
			System.out.println("子线程执行输出:"+i);
		}
	}
}

/**  这个多线程创建的有缺点
* 优点: 编码简单
* 
* 缺点: 线程类已经继承Thread类,无法继承其它类,不利于扩展,功能变得单一
*/

方式二:实现Runnable接口

import java.util.* ;
import java.lang.*;

/**               多线程二
 * 
 * 一,多线程的实现方案二:实现Runnable接口
 *     1:定义一个线程任务类MyRunnable实现Runnable接口,重写run方法
 *     2:创建MyRunnable任务对象
 *     3:把MyRunnable任务交给Thread多线程处理
 *     4:调用线程对象statrt方法启动线程
 * @author 黄佳乐
 *
 */

//public class Main{
//	public static void main(String[] args) {
//		
//		// 3 : new一个任务类对象
//		 Runnable ru = new MyRunnable();//多态
//		
//		// 4 : 把任务类对象交给Thread多线程处理
//		Thread st = new Thread(ru) ; 
//		/**这一步是必须要的,因为我们上面创建的任务类并不是一个多线程,所以我们要多线程执行的话,还是要把它放进Thread多线程中去启动。
//		 *    另外之所以可以这样子去写,因为thread多线程的构造器中有人物类变量,所以可以把任务类直接交给Thread
//		 * 当然这3,4,5步也可以合并起来写成:new Thread(new MyRunnable()).start() ;
//		 */
//		
//		// 5 :启动线程
//		st.start();
//		
//		// 6 : 创建一个主线程任务做对比
//		for(int i = 0 ; i<5 ; ++i) {
//			System.out.println("主线程任务执行输出:"+ i);
//		}
//		
//	}
//}
//
//
 1:定义一个线程任务类,实现Runnable接口
//class MyRunnable implements Runnable{
//	
//	// 2 : 重写run方法v,定义线程的执行任务
//	@Override
//	public void run() {
//		// TODO Auto-generated method stub
//		for(int i = 0 ; i<5 ; ++i){
//			System.out.println("子线程执行输出:"+ i);
//		}
//		
//	}
//}


/**====================简便写法=============
 * 上面的代码可以使用简洁的写法。
 * 因为Runnable是接口,所以可以用匿名内部类加上landam表达式来简写
 * 简写代码如下:
 */

public class Main{
	public static void main(String[] args) {
		
		 new Thread(() -> {  for(int i = 0 ; i<5  ;++i)   System.out.println("子线程执行输出:"+i);  }) .start(); 
				// TODO Auto-generated method stub
				
		for(int i = 0 ; i<5 ; ++i)    System.out.println("主线程任务执行输出:"+ i);
		
		/**就是用来匿名内部类和landam表达式,两行代码搞定*/
	}
}




/**方式二创建多线程的优缺点:
 * 优点: 线程任务只是实现接口,所以可以继承类和实现其它接口,扩展性强(因为它没有继承,所以还可以继承一个类,而且还可以继续实现其它接口。类只可以继承一个,但接口可以多实现)
 * 
 * 缺点: 编程多一层对象包装(要多包装一个任务类)
 *        如果线程有执行结果是不可以直接返回的(这个方案一也一样不行,因为他们的线程执行的任务run方法的返回值都是void,所以如果执行需要有返回值的任务的话,它是不能返回的)
 *     
 */

方式三: 多线程创建方式三:利用Callable,FutureTask接口实现

import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.lang.*;

/**        多线程三
 * 多线程的实现方案三:利用Callable,FutureTask接口实现
 *   1:得到任务对象:
 *      ① : 定义类实现Callable接口,重写call方法,封装要做的事情。
 *      ② : 用FutureTask把Callable对象封装成线程任务对象。
 *   
 *   2 ;把线程任务对象交给Thread处理
 *   
 *   3 :调用Thread的start方法启动线程,执行任务
 *   
 *   4 :线程执行完毕后,通过FutureTask的get方法去获取任务执行的结果 
 * @author 黄佳乐
 *
 */
public class Main{
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		
		// 2 : 用FutureTask把Callable对象封装成线程任务对象。
		Callable<String> ca = new MyCallable(100) ; 
		
		FutureTask<String> fu = new FutureTask<>(ca) ;  //把Callable类对象用FutureTask封装成任务对象
		/**这里为什么不直接把Callable类对象用Thread类封装成线程对象呢?要多此一举,先用FutureTask封装,再用Thread封装线程任务呢?
		 * 因为,Thread类的构造器只能接受Runnable类型的对象,所以不能直接接受Callable类的对象,而FutureTask是继承Runnable类的,并且它有返回值,所以要先用TutureTask包装,这样Thread才能接收
		 */
		
		// 3: 把线程任务对象交给Thread处理
		Thread th = new Thread(fu) ; 
		
		// 4: 调用Thread的start方法启动线程,执行任务
		th.start() ; 
		
		// 5: 线程执行完毕后,通过FutureTask的get方法去获取任务执行的结果 
		String  st = fu.get() ; 
		System.out.println("第一个结果"+st);
		
		for(int i = 0 ; i<10 ; ++i)     System.out.println("主线程任务执行:"+ i);
	}
}

// 1 : 定义类实现Callable接口,重写call方法,封装要做的事情。
class MyCallable implements Callable<String>{   //Callable类有返回值,所以这里应该申明好最后线程执行完毕后返回的数据类型

	private int n ; 
	public MyCallable(int n) {
		this.n = n ; 
	}
	
	@Override
	public String call() {
		int sum = 0  ;
		for(int i = 1  ; i <= n ; ++i) {
			sum+=i ; 
		}
		return "子线程执行任务前n项和为:" + sum;
	}
	
}

/** 方式三的优缺点
优点: 1:线程任务类只是实现接口,可以继承类和实现接口,扩展性强。
      2:可以线程执行完毕后去获取线程执行的结果。
缺点:编码复杂,三层套娃
*/

 

     3.线程安全解决

        什么是线程安全?举个简单的例子,如果我和我老婆(没有。。。)都拿着同一张银行卡去取钱,把我和我老婆看作两个线程,正常情况下应该是,我取完钱后,银行将我的卡余额减去去的钱,可是如果在我刚取完钱,银行系统的代码还没执行到扣去余额时,我老婆刚好也进去去了,那么这个时候就会导致银行不安全。而我们解决的方法因该时,如果有一个线程进来取钱了,那么其他的线程就进不来,直到里面的线程取完钱。

线程安全解决方式一:同步代码块


import java.util.*;
import java.lang.*;
import java.io.*;

/**          如何解决线程安全问题?
 *   方法一:同步代码块
 *   方法二:同步方法
 *   方法三:上锁和解锁
 * @author 黄佳乐
 *
 *          模拟线程安全问题案例---取钱业务
 * 需求:
 *    小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟两人同是去取款10万元
 * 分析:
 *    1,需要提供一个账户类,创建一个账户对象代表2个人的共享账户
 *    2,需要定义一个线程类,线程类可以处理账户对象
 *    3,创建2个线程对象,传入同一个账户对象
 *    4,启动2个线程,去同一个账户对象中取款10万元
 */
public class Main{
	public static void main(String[] args) {
		
		/**=========== 方法一:同步代码块==============
		 * 我们的主要问题就是出现在两个线程同时进行余额判断的代码,从而导致判断错误。
		 * 那么,我们只需要将判断余额的这段核心代码进行限制,只允许一个线程单个执行,等其中一个执行完了这段核心代码,其他的线程才能进来,那么这个问题就解决了
		 */
		
		//1: 创建账户
		account acc = new account(100000.0,"ICBC-001") ; 
		
		//2: 创建两个线程分别代表小明和小红的
		new myThread(acc,"小明").start(); ; //小明的线程启动
		
		new myThread(acc,"小红").start(); ; //小红的线程启动
	}
}


//创建一个账户类,表示小明和小红的共享账户
class account{
	double restMoney ; //余额
	String idCard ; //账号
	
	public account() {
		super();
		// TODO Auto-generated constructor stub
	}
	public account(double restMoney, String idCard) {
		super();
		this.restMoney = restMoney;
		this.idCard = idCard;
	}
	public double getRestMoney() {
		return restMoney;
	}
	public void setRestMoney(double restMoney) {
		this.restMoney = restMoney;
	}
	public String getIdCard() {
		return idCard;
	}
	public void setIdCard(String idCard) {
		this.idCard = idCard;
	}
	
	//取款方法
	public void drwMoney(double money) throws InterruptedException {
		// TODO Auto-generated method stub
	
		//先获取前来取款的线程的名称,这样比较明了
		String name = Thread.currentThread().getName() ; //调用父类中的currentThread()来获取当前线程的名称
		
//	    if(name.equals("小红"))  Thread.sleep(3000);
		
		/**这个算是一个自己写的解决方案吧,因为两个线程几乎同时到这里,但是我把小红这条线程给拦截下来,暂停3秒后再执行,这样,小明那条线程就能先跑完后面更新余额的那一步,这样再轮到小红这条线程全判断的时候,就不会出现这样的bug了。*/
		
		//判断余额是否足够
		/**=====================================核心代码=====================================*/
		synchronized (this) //主要需要理解的就是这个this,这个this是个对象,代表的是账户对象acc。因为这个方法调用的是账户对象里面的方法,所以this就是acc。只要记住this这个锁,其实都是唯一的共享资源对象就行
		/**这一段就是核心代码
		 * 所以就用synchronized将这段代码锁起来
		 * 这个的作用就是当两个线程来到这里的时候,系统会通过一些比较算法只允许一条线程进来,那么其它的线程这能等,只有进来的线程把这段锁起来的代码都执行完了,其他的线程才能进来
		 */
		
		{
			
			if (restMoney >= money) {

				System.out.println(name + "取款成共!!吐出现金:" + money);

				//更新余额
				restMoney -= money;

				//剩余余额
				System.out.println("账户剩余余额:" + restMoney + "元"); //账户剩余余额:-100000.0元

				/**账户剩余余额:-100000.0元。证明银行倒贴100000元*/
			}
			else System.out.println(name+"想要取款"+money+",但是余额不足!");
			
		}
		/**====================================核心代码====================================*/
	}
	
}

//创建一个线程类
class myThread extends Thread{
	
	account acc ; //需要接受到acc账户类
	
	public myThread(account acc,String name) {
		// TODO Auto-generated constructor stub
		super(name) ; //将自定义的线程名称传给父类
		
		this.acc = acc ; //将账户传给自己的成员变量
		
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		
		//取款线程代码
		try {
			acc.drwMoney(100000) ;
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}   //java是严格按照对象的思想设计的,所以取款业务应该是属于account账户类中的方法,所以这里掉acc的取款方法,以实现将取款方法转移到account类进行1
	}
}

线程安全解决方式二:同步方法


import java.util.*;
import java.lang.*;
import java.io.*;

/**          如何解决线程安全问题?
 *   方法一:同步代码块
 *   方法二:同步方法
 *   方法三:上锁和解锁
 * @author 黄佳乐
 *
 *          模拟线程安全问题案例---取钱业务
 * 需求:
 *    小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟两人同是去取款10万元
 * 分析:
 *    1,需要提供一个账户类,创建一个账户对象代表2个人的共享账户
 *    2,需要定义一个线程类,线程类可以处理账户对象
 *    3,创建2个线程对象,传入同一个账户对象
 *    4,启动2个线程,去同一个账户对象中取款10万元
 */
public class Main{
	public static void main(String[] args) {
		
		/**=========== 方法一:同步代码块==============
		 * 我们的主要问题就是出现在两个线程同时进行余额判断的代码,从而导致判断错误。
		 * 那么,我们只需要将判断余额的这段核心代码进行限制,只允许一个线程单个执行,等其中一个执行完了这段核心代码,其他的线程才能进来,那么这个问题就解决了
		 */
		
		//1: 创建账户
		account acc = new account(100000.0,"ICBC-001") ; 
		
		//2: 创建两个线程分别代表小明和小红的
		new myThread(acc,"小明").start(); ; //小明的线程启动
		
		new myThread(acc,"小红").start(); ; //小红的线程启动
	}
}


//创建一个账户类,表示小明和小红的共享账户
class account{
	double restMoney ; //余额
	String idCard ; //账号
	
	public account() {
		super();
		// TODO Auto-generated constructor stub
	}
	public account(double restMoney, String idCard) {
		super();
		this.restMoney = restMoney;
		this.idCard = idCard;
	}
	public double getRestMoney() {
		return restMoney;
	}
	public void setRestMoney(double restMoney) {
		this.restMoney = restMoney;
	}
	public String getIdCard() {
		return idCard;
	}
	public void setIdCard(String idCard) {
		this.idCard = idCard;
	}
	
	//取款方法
	public void drwMoney(double money) throws InterruptedException {
		// TODO Auto-generated method stub
	
		//先获取前来取款的线程的名称,这样比较明了
		String name = Thread.currentThread().getName() ; //调用父类中的currentThread()来获取当前线程的名称
		
//	    if(name.equals("小红"))  Thread.sleep(3000);
		
		/**这个算是一个自己写的解决方案吧,因为两个线程几乎同时到这里,但是我把小红这条线程给拦截下来,暂停3秒后再执行,这样,小明那条线程就能先跑完后面更新余额的那一步,这样再轮到小红这条线程全判断的时候,就不会出现这样的bug了。*/
		
		//判断余额是否足够
		/**=====================================核心代码=====================================*/
		synchronized (this) //主要需要理解的就是这个this,这个this是个对象,代表的是账户对象acc。因为这个方法调用的是账户对象里面的方法,所以this就是acc。只要记住this这个锁,其实都是唯一的共享资源对象就行
		/**这一段就是核心代码
		 * 所以就用synchronized将这段代码锁起来
		 * 这个的作用就是当两个线程来到这里的时候,系统会通过一些比较算法只允许一条线程进来,那么其它的线程这能等,只有进来的线程把这段锁起来的代码都执行完了,其他的线程才能进来
		 */
		
		{
			
			if (restMoney >= money) {

				System.out.println(name + "取款成共!!吐出现金:" + money);

				//更新余额
				restMoney -= money;

				//剩余余额
				System.out.println("账户剩余余额:" + restMoney + "元"); //账户剩余余额:-100000.0元

				/**账户剩余余额:-100000.0元。证明银行倒贴100000元*/
			}
			else System.out.println(name+"想要取款"+money+",但是余额不足!");
			
		}
		/**====================================核心代码====================================*/
	}
	
}

//创建一个线程类
class myThread extends Thread{
	
	account acc ; //需要接受到acc账户类
	
	public myThread(account acc,String name) {
		// TODO Auto-generated constructor stub
		super(name) ; //将自定义的线程名称传给父类
		
		this.acc = acc ; //将账户传给自己的成员变量
		
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		
		//取款线程代码
		try {
			acc.drwMoney(100000) ;
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}   //java是严格按照对象的思想设计的,所以取款业务应该是属于account账户类中的方法,所以这里掉acc的取款方法,以实现将取款方法转移到account类进行1
	}
}

 线程安全解决方式三:同步锁


import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.lang.*;
import java.io.*;

/**          如何解决线程安全问题?
 *   方法一:同步代码块
 *   方法二:同步方法
 *   方法三:上锁和解锁
 * @author 黄佳乐
 *
 *          模拟线程安全问题案例---取钱业务
 * 需求:
 *    小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟两人同是去取款10万元
 * 分析:
 *    1,需要提供一个账户类,创建一个账户对象代表2个人的共享账户
 *    2,需要定义一个线程类,线程类可以处理账户对象
 *    3,创建2个线程对象,传入同一个账户对象
 *    4,启动2个线程,去同一个账户对象中取款10万元
 */
public class Main{
	public static void main(String[] args) {
		
		/**=========== 方法三:同步锁==============
		 
		 直接把核心代码;上一把锁,结束的话,再解锁。跟同步代码块有点像
		 */
		
		 
			
			
		//1: 创建账户
		account acc = new account(100000.0,"ICBC-001") ; 
		
		//2: 创建两个线程分别代表小明和小红的
		new myThread(acc,"小明").start(); ; //小明的线程启动
		
		new myThread(acc,"小红").start(); ; //小红的线程启动
	}
}


//创建一个账户类,表示小明和小红的共享账户
class account{
	double restMoney ; //余额
	String idCard ; //账号
	
	private final Lock lock = new ReentrantLock() ;  //创建一个同步锁对象
	
	public account() {
		super();
		// TODO Auto-generated constructor stub
	}
	public account(double restMoney, String idCard) {
		super();
		this.restMoney = restMoney;
		this.idCard = idCard;
	}
	public double getRestMoney() {
		return restMoney;
	}
	public void setRestMoney(double restMoney) {
		this.restMoney = restMoney;
	}
	public String getIdCard() {
		return idCard;
	}
	public void setIdCard(String idCard) {
		this.idCard = idCard;
	}
	
	
	//取款方法
	/**=========================方法二:同步方法的核心方法========================*/
	public  void  drwMoney(double money) throws InterruptedException {
		/**直接在核心方法加上synchronized就行了,虽然性能上不如同步代码块,但是比较方便,系统会自动帮你加上锁*/
		
		// TODO Auto-generated method stub
	
		lock.lock();  //  从这里开始上锁
		
		//先获取前来取款的线程的名称,这样比较明了
		String name = Thread.currentThread().getName() ; //调用父类中的currentThread()来获取当前线程的名称
		
//	    if(name.equals("小红"))  Thread.sleep(3000);
		
		/**这个算是一个自己写的解决方案吧,因为两个线程几乎同时到这里,但是我把小红这条线程给拦截下来,暂停3秒后再执行,这样,小明那条线程就能先跑完后面更新余额的那一步,这样再轮到小红这条线程全判断的时候,就不会出现这样的bug了。*/
		
		//判断余额是否足够
		/**=====================================核心代码=====================================*/
//		synchronized (this) //主要需要理解的就是这个this,这个this是个对象,代表的是账户对象acc。因为这个方法调用的是账户对象里面的方法,所以this就是acc。只要记住this这个锁,其实都是唯一的共享资源对象就行
		
		/**这一段就是核心代码
		 * 所以就用synchronized将这段代码锁起来
		 * 这个的作用就是当两个线程来到这里的时候,系统会通过一些比较算法只允许一条线程进来,那么其它的线程这能等,只有进来的线程把这段锁起来的代码都执行完了,其他的线程才能进来
		 */
		
//		{
			
			if (restMoney >= money) {

				System.out.println(name + "取款成共!!吐出现金:" + money);

				//更新余额
				restMoney -= money;

				//剩余余额
				System.out.println("账户剩余余额:" + restMoney + "元"); //账户剩余余额:-100000.0元

				/**账户剩余余额:-100000.0元。证明银行倒贴100000元*/
			}
			else System.out.println(name+"想要取款"+money+",但是余额不足!");
			
			lock.unlock();   //从这里开始解锁
//		}
		/**====================================核心代码====================================*/
	}
	
}

//创建一个线程类
class myThread extends Thread{
	
	account acc ; //需要接受到acc账户类
	
	public myThread(account acc,String name) {
		// TODO Auto-generated constructor stub
		super(name) ; //将自定义的线程名称传给父类
		
		this.acc = acc ; //将账户传给自己的成员变量
		
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		
		//取款线程代码
		try {
			acc.drwMoney(100000) ;
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}   //java是严格按照对象的思想设计的,所以取款业务应该是属于account账户类中的方法,所以这里掉acc的取款方法,以实现将取款方法转移到account类进行1
	}
}

下周见!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值