五、线程同步
1. 概念
即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多种。
1、为什么要创建多线程?
在一般情况下,创建一个线程是不能提高程序执行效率的,所以要创建多个线程。
2、为什么要线程同步?
多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使线程同步。
3、线程同步是什么意思?
同步就是协同步调,按预定的先后次序进行运行。如:你做完,我再做。
错误理解 :“同”字从字面上容易理解为一起动作,其实不是。“同”字应是指协同、协助、互相配合。
正确理解 :所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,同时其他线程也不能调用这个方法。
线程同步的作用 :
- 线程有可能和其他线程共享一些资源,比如:内存、文件、数据库等
- 当多个线程同时读写同一份共享资源的时候,可能会引起冲突。这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到,不能一窝蜂挤上去抢作一团。
- 线程同步的真是意思其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。
2. 同步互斥访问(synchronized)
基本上所有解决线程安全问题的方式都是采用“序列化临界资源访问”的方式,即在同一时刻只有一个线程操作临界资源,操作完了才能让其他线程进行操作,也称同步互斥访问。
在Java中一般采用synchronized和Lock来实现同步互斥访问。
3. Synchronized关键字
首先了解一下互斥锁 :就是能达到互斥访问目的的锁
- 如果对一个变量加上互斥锁,那么在同一时刻,该变量只能有一个线程能访问,即当一个线程访问临界资源时,其他线程只能等待。
- 在Java中,每一个对象都有一个锁标记(monitor),也被称为监视器,当多个线程访问对象时,只有获取了对象的锁才能访问。
- 在我们编写代码的时候,可以使用synchronized修饰对象的方法或者代码块,当某个线程访问这个对象synchronized方法或者代码块时,就获取到了这个对象的锁,这个时候其他对象是不能访问的,只能等待获取到锁的这个线程执行完该方法或者代码块之后,才能执行对象的方法。
4. synchronized同步代码块实现
- 第一种:synchronized代码块方式(灵活)
- 第二种:在实例方法上使用synchronized
- 第三种 : 在静态方法上使用synchronized
package com.thread; /** * synchronized同步代码块实现 * * @author 云村小威 * * @2023年7月21日 下午12:37:53 */ public class Text07 { // 第一种 public void myThread1(Thread thread) { // this代表当前对象 synchronized (this) { for (int i = 0; i < 5; i++) { System.out.println(thread.getName() + ":" + i); } } } // 第二种 public synchronized void myThread2(Thread thread) { // this代表当前对象 for (int i = 0; i < 5; i++) { System.out.println(thread.getName() + ":" + i); } } // 第三种 public static synchronized void myThread3(Thread thread) { // this代表当前对象 for (int i = 0; i < 5; i++) { System.out.println(thread.getName() + ":" + i); } } }
5. 什么是类锁和对象锁
5.1 概念
对象锁:在java中每个对象都有一个唯一的锁,对象锁用于对象实例方法或者一个对象实例上面的 —— 一个对象一把锁,100个对象100把锁。
类锁:是用于一个类静态方法或者class对象的,一个类的实例对象可以有多个,但是只有一个class对象 —— 100个对象,也只是1把锁。
注意上述第三种方式synchronized同步代码块实现:在静态方法添加synchronized这把锁属于类了,所有这个类的对象都共享这把锁,这就叫类锁。
5.2 释放锁时机
如果一个方法或者代码块被synchronized关键字修饰,当线程获取到该方法或代码块的锁,其他线程是不能继续访问该方法或代码块的。而其他线程想访问该方法或代码块,就必须要等待获取到锁的线程释放这个锁,而在这里释放锁只有两种情况 :
- 线程执行完代码块,自动释放锁;
- 程序报错,JVM让线程自动释放锁;
举例:这里我调用了上述的第一种和第二种实现同步方式
public static void main(String[] args) { new Text07().myThread1(new Thread()); new Text07().myThread2(new Thread()); }
6. 线程等待与线程唤醒
Object类 | 作用 |
Void wait() | 让活动在当前对象的线程无限等待(释放之前占有的锁) |
Void notify() | 唤醒当前对象正在等待的线程(只提示唤醒,不会释放锁) |
Void notifyAll() | 唤醒在此对象监视器上等待的所有线程。 |
方法详解 :
wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方法是object类自带的。wait和notify方法不是通过线程对象调用的;
六、线程安全问题(生产者和消费者模式)
1、什么是 “ 生产者和消费者模式 ” ?
- 生产线负责生产,消费线程负责消费。
- 生产线程和消费线程要达到均衡
- 这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法。
2、模拟一个业务需求
- 仓库我们采用List集合
- List集合中假设只能存储1个元素
- 1个元素就表示仓库满了
- 如果List集合中元素个数是0,就表示仓库空了
- 保证List集合永远都是最多存储1个元素
- 必须做到这种效果:生产1个消费1个。
模拟生产者与消费者案例:
首先我创建了一个产品类来定义我们需要生产的对象,接着创建一个工厂类写了生产和出售两个方法并实现同步锁,通过put方法将生产的鸡脚保存到容器里如果容器小于10就消费者等待(wait() 方法),如果大于就唤醒( notifyAll()方法 )所有等待的消费者。然后再定义生产者类它需要不停做事,它什么时候休息根据工厂类生产的鸡脚是否大于20,否则不可休息得一直做事。最后是消费者调用工厂类的sale方法只要保存鸡脚的容器大于10就开始购买对容器进行减少,如果没有了就让消费者进行等待。
package com.thread; import java.util.Vector; /** * 生产者与消费者案例 * * @author 云村小威 * * @2023年7月22日 上午10:51:47 */ public class Text08 { public static void main(String[] args) { // 店铺运营 // 创建一个工厂 Factory f = new Factory(); // 招三个成产员工 Producer p1 = new Producer(f); Producer p2 = new Producer(f); Producer p3 = new Producer(f); new Thread(p1, "工作人员A").start(); new Thread(p2, "工作人员B").start(); new Thread(p3, "工作人员C").start(); // 引入三个消费者 Sale s1 = new Sale(f); Sale s2 = new Sale(f); Sale s3 = new Sale(f); new Thread(s1, "消费者A").start(); new Thread(s2, "消费者B").start(); new Thread(s3, "消费者C").start(); } } /** * 消费者 */ class Sale implements Runnable { Factory f = null; public Sale(Factory f) { this.f = f; } public void run() { while (true) { // 让消费者延迟购买的时间 try { Thread.sleep(1500); } catch (InterruptedException e) { e.printStackTrace(); } f.sale(); } }; } /** * 生产类 */ class Producer implements Runnable { Factory f = null; public Producer(Factory f) { this.f = f; } @Override public void run() { // 需要不停的做事 while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 调用生产数据的方法 f.put(new Product(1, "鸡脚")); } } } /** * 工厂类 */ class Factory { // 准备储存产品的集合容器,多线程优先使用Vector集合,该集合类中的方法大部分都是带有同步的概念机制。 Vector<Product> vc = new Vector<Product>(); // 专门定义一个生产数据方法 public synchronized void put(Product product) { // 判断产品于目标数量是否相同 if (vc.size() >= 20) { // 产品数量满足让工作人员处于等待状态 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } else { // 让等待的工作人员生产产品 this.notifyAll(); System.out.println("正在生产中...当前货架的鸡脚数目是:" + vc.size()); vc.add(product); System.out.println("货架鸡脚总数目是:" + vc.size()); } } // 专门定义一个负责消费数据方法 public synchronized void sale() { // 只要容器存在货物就可以进行购买 if (vc.size() > 10) { this.notifyAll(); // 唤醒所有等待的消费者 System.out.println("当前货架上的鸡脚数目是:" + vc.size() + ",正在出售鸡脚。"); vc.remove(0); System.out.println("\t目前货架上的鸡脚数目是:" + vc.size()); } else { // 没有鸡脚,让消费者处于等待状态 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } /** * 产品类 */ class Product { private int pid; private String name; public Product() { // TODO Auto-generated constructor stub } public int getPid() { return pid; } public void setPid(int pid) { this.pid = pid; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Product(int pid, String name) { super(); this.pid = pid; this.name = name; } }
七、多线程小案例
1. 老虎机案例
package com.thread; import java.util.Random; import javafx.application.Application; import javafx.application.Platform; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.Pane; import javafx.stage.Stage; /** * 老虎机游戏程序 * @author 云村小威 * * @2023年7月22日 上午11:27:56 */ public class Game extends Application { Random rd = new Random(); // 随机数对象 boolean flag = true; // 控制开关默认打开 int timer = 50; // 控制事件变量 // 通过数组保存没有规律的多张图片 String[] images = { "lib\\7.png", "lib\\橙子.png", "lib\\柠檬.png", "lib\\苹果.png", "lib\\西瓜.png", "lib\\香蕉.png", }; // 总布局 Pane pane = new Pane(); private Label a1 = new Label("", new ImageView(new Image("lib\\苹果.png", 150, 150, false, false))); private Label a2 = new Label("", new ImageView(new Image("lib\\西瓜.png", 150, 150, false, false))); private Label a3 = new Label("", new ImageView(new Image("lib\\柠檬.png", 150, 150, false, false))); private Button btn1 = new Button("开始游戏"); private Button btn2 = new Button("点击停止"); { a1.setStyle("-fx-border-color:#ccc;"); a1.setLayoutX(60); a1.setLayoutY(40); pane.getChildren().add(a1); a2.setStyle("-fx-border-color:#ccc;"); a2.setLayoutX(225); a2.setLayoutY(40); pane.getChildren().add(a2); a3.setStyle("-fx-border-color:#ccc;"); a3.setLayoutX(390); a3.setLayoutY(40); pane.getChildren().add(a3); btn1.setLayoutX(180); btn1.setLayoutY(220); btn1.setPrefSize(100, 40); pane.getChildren().add(btn1); btn2.setLayoutX(340); btn2.setLayoutY(220); btn2.setPrefSize(100, 40); pane.getChildren().add(btn2); } @Override public void start(Stage stage) throws Exception { // 创建一个场景 Scene scene = new Scene(pane, 600, 300); stage.setScene(scene); stage.show(); /** * 开始游戏点击事件 */ btn1.setOnAction(e -> { flag = true; new Thread() { public void run() { while (flag) { try { Thread.sleep(timer); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } Platform.runLater(new Runnable() { @Override public void run() { int sj1 = rd.nextInt(6); a1.setGraphic(new ImageView(new Image(images[sj1]))); int sj2 = rd.nextInt(6); a2.setGraphic(new ImageView(new Image(images[sj2]))); int sj3 = rd.nextInt(6); a3.setGraphic(new ImageView(new Image(images[sj3]))); } }); timer += 6; if (timer >= 300) { timer = 50; break; } } } }.start(); }); /** * 停止点击事件 */ btn2.setOnAction(e -> { flag = false; }); } public static void main(String[] args) { launch(); } }
运行效果:
2. FX版无限聊天
1. 服务器
package com.net; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.ServerSocket; import java.net.Socket; import javafx.application.Application; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.stage.Stage; /** * 服务器 * * @author 云村小威 * * @2023年7月19日 下午9:40:54 */ public class ServerFx extends Application { /** * 总布局 BorderPane */ BorderPane bor = new BorderPane(); // 上 HBox hb = new HBox(); TextField t = new TextField(); Button btn = new Button("启动服务"); { hb.getChildren().addAll(t, btn); hb.setAlignment(Pos.CENTER); hb.setPadding(new Insets(20)); hb.setSpacing(20); btn.setPrefSize(100, 40); bor.setTop(hb); } // 中 TextArea ta = new TextArea(); { ta.setEditable(false); // 文本域不可修改 ta.setStyle("-fx-border-color: #ccc;-fx-border-width: 10 10 10 10;"); bor.setCenter(ta); } // 下 HBox hb2 = new HBox(); TextField text = new TextField(); Button btn1 = new Button("发送"); { hb2.getChildren().addAll(text, btn1); hb2.setAlignment(Pos.CENTER); hb2.setPadding(new Insets(20)); hb2.setSpacing(20); bor.setBottom(hb2); } /** * 将通信对象声明在start方法外面 */ Socket s = null; ServerSocket ss = null; /** * 定义所需的流对象 扩大权限方便关闭流 */ InputStream inputStream = null; BufferedReader br = null; OutputStream outputStream = null; BufferedWriter bw = null; @Override public void start(Stage stage) throws Exception { stage.setTitle("服务器"); Scene scene = new Scene(bor, 800, 700); stage.setScene(scene); stage.show(); /** * 启动服务点击事件 */ btn.setOnAction(e -> { // 创建服务器 try { ss = new ServerSocket(Integer.parseInt(t.getText())); } catch (IOException e1) { e1.printStackTrace(); } ta.appendText("服务已启动,等待客户端连接...\n"); // 开启一个线程 自动接受客户端发送的信息 new Thread() { public void run() { // 多线程控制接受客户端信息 try { s = ss.accept(); ta.appendText("客户端已连接...\n"); } catch (IOException e) { e.printStackTrace(); } while (true) { // 读取 获取输入流 try { inputStream = s.getInputStream(); br = new BufferedReader(new InputStreamReader(inputStream)); String count = br.readLine(); ta.appendText("\t客户端:" + count + "\n"); } catch (IOException e) { e.printStackTrace(); } } } }.start(); }); /** * 发送信息点击事件 */ btn1.setOnAction(e -> { try { outputStream = s.getOutputStream(); bw = new BufferedWriter(new OutputStreamWriter(outputStream)); // 获取文本框内容 String count = text.getText(); bw.write(count); bw.newLine(); bw.flush(); ta.appendText("服务器:" + count + "\n"); text.setText(""); } catch (IOException e1) { e1.printStackTrace(); } }); } public static void main(String[] args) { launch(); } }
2. 客户端
package com.net; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.Socket; import javafx.application.Application; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.text.Text; import javafx.stage.Stage; /** * 客户端 * * @author 云村小威 * * @2023年7月19日 下午9:41:46 */ public class ClientFx extends Application { /** * 总布局 BorderPane */ BorderPane bor = new BorderPane(); // 上 HBox hb = new HBox(); Text ip = new Text("IP"); TextField t = new TextField("127.0.0.1"); Text tcp = new Text("端口号"); TextField t2 = new TextField(); Button btn = new Button("连接"); { t.setDisable(true);// 设置ip地址不可编辑 hb.getChildren().addAll(ip, t, tcp, t2, btn); hb.setAlignment(Pos.CENTER); hb.setPadding(new Insets(20)); hb.setSpacing(20); btn.setPrefSize(100, 40); bor.setTop(hb); } // 中 TextArea ta = new TextArea(); { ta.setEditable(false); // 文本域不可修改 ta.setStyle("-fx-border-color: #ccc;-fx-border-width: 10 10 10 10;"); bor.setCenter(ta); } // 下 HBox hb2 = new HBox(); TextField text = new TextField(); Button btn1 = new Button("发送"); { hb2.getChildren().addAll(text, btn1); hb2.setAlignment(Pos.CENTER); hb2.setPadding(new Insets(20)); hb2.setSpacing(20); bor.setBottom(hb2); } /** * 定义所需的流对象 扩大权限方便关闭流 */ InputStream inputStream = null; BufferedReader br = null; OutputStream outputStream = null; BufferedWriter bw = null; // 定义客户端对象 Socket s = new Socket(); @Override public void start(Stage stage) throws Exception { stage.setTitle("客户端"); stage.setResizable(false); // 设置窗口不可动 Scene scene = new Scene(bor, 800, 700); stage.setScene(scene); stage.show(); /** * 连接点击事件 */ btn.setOnAction(e -> { String ip = t.getText(); // 获取ip地址 int port = Integer.parseInt(t2.getText()); try { s = new Socket(ip, port); ta.appendText("客户端成功连接服务器...\n"); } catch (IOException e1) { e1.printStackTrace(); } // 开启一个线程 (让它自动接受数据) new Thread() { public void run() { while (true) { try { inputStream = s.getInputStream(); br = new BufferedReader(new InputStreamReader(inputStream)); String count = br.readLine(); ta.appendText("\t服务器:" + count + "\n"); } catch (IOException e) { e.printStackTrace(); } } } }.start(); }); /** * 发送信息点击事件 */ btn1.setOnAction(e -> { try { outputStream = s.getOutputStream(); bw = new BufferedWriter(new OutputStreamWriter(outputStream)); // 获取文本框内容 String count = text.getText(); bw.write(count); bw.newLine(); bw.flush(); ta.appendText("客户端:" + count + "\n"); text.setText(""); } catch (IOException e1) { e1.printStackTrace(); } }); } public static void main(String[] args) { launch(); } }
运行效果: