目录
一 网络通信
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通信任务的主要作用
二 多线程
- 什么是多线程
线程就是执行我们写的程序的就是一个线程,我们平常写的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
}
}
下周见!