2022年06月27日讲到了线程控制 同学们
目录
1.线程同步
我们的线程在共享数据时容易出现一些问题
比如我们去银行取钱,ATM机先判断钱够不够,然后再吐出钞票
单线程肯定没有问题,但多线程可能有问题
比如我们账户里有800块
我们在ATM机上登录,同时登录手机银行
同时用ATM机取800,用手机转800
多线程情况下可能两边都成功了,我们就赚了800
public class Account {
// 这是我们的用户类
private String password; // 银行卡号
private double account; // 账户余额
public Account(String password,double account){
this.password=password;
this.account=account;
}
// 省略了get() set()方法
}
public class ATMThread extends Thread{
// 这是我们的取款机线程类
private String atmNO;
private Account account; //要登录的账号
private double money; //要取多少钱
public ATMThread(String atmNO,Account account,double money){
this.atmNO=atmNO;
this.account=account;
this.money=money;
}
@Override
public void run() {
if (money<=account.getAccount()){
// 执行取钱操作
System.out.println(atmNO+"号取款机取钱成功,吐出金额"+money);
try {
// 这个休眠1秒是模拟网络延迟
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.setAccount(account.getAccount()-money);
System.out.println("余额为:"+account.getAccount());
}else {
System.out.println("余额不足!");
}
}
public static void main(String[] args) {
Account account=new Account("872425",800);
// 创建两台取款机同时取800
new ATMThread("0001",account,800).start();
new ATMThread("0002",account,800).start();
}
}
这样银行就亏了800是不是
这就是线程安全问题
下面是解决方案:
① 同步代码块
上面这个问题是因为一个线程在另一个线程休眠的时候修改了对象的信息 (就是同时取了800块)
我们需要在同一时间只允许一个线程操作这个对象
synchronized (obj){ // 里面是要执行的东西 }
这是一个同步代码块, 那个obj对象就是同步监视器
如果有一个线程正在操作这个对象,那另一个也想操作的线程就要等它结束
才能获得这个对象的使用权
只要把 run()方法中的东西放到synchronized (){}里面就可以了
@Override
public void run() {
synchronized (account) {
if (money <= account.getAccount()) {
// 执行取钱操作
System.out.println(atmNO + "号取款机取钱成功,吐出金额" + money);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.setAccount(account.getAccount() - money);
System.out.println("余额为:" + account.getAccount());
} else {
System.out.println("余额不足!");
}
}
}
②同步方法
就是把一个方法用synchronized修饰一下就成了同步方法
比如我们这个例子 如果要用同步方法实现
我们需要把取款操作 作为一个方法写到 账户类里面,
我们写成 drawMoney()方法
这样在调用这个方法的时候,我们需要一个账户对象
account.drawMoney(money); // money 是要取多少钱
这样这个account对象就作为同步监视器,一次只能被一个线程持有
效果和同步代码块是一样的
public class Account {
// 这是刚才的用户类
private String password; // 银行卡号
private double account; // 账户余额
public Account(String password,double account){
this.password=password;
this.account=account;
}
// 加上的drawMoney()方法
public synchronized void drawMoney(double money){
if (account>=money){
System.out.println(Thread.currentThread().getName()+"号取款机取款成功,吐出金额"+money+"元");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account=account-money;
System.out.println("余额:"+account+"元");
}else {
System.out.println("余额不足!!!");
}
}
}
public class ATMThread extends Thread {
// 取款机线程类就变成了这样
private String atmNO;
private Account account; //要登录的账号
private double money; //要取的钱
public ATMThread(String atmNO, Account account, double money) {
this.atmNO = atmNO;
this.account = account;
this.money = money;
}
@Override
public void run() {
account.drawMoney(money);
}
public static void main(String[] args) {
Account account = new Account("872425", 800);
// 创建两台取款机同时取800
new ATMThread("0001", account, 800).start();
new ATMThread("0002", account, 800).start();
}
}
③同步锁
在使用同步锁的时候要创建一个 lock对象
private ReentrantLock lock=new ReentrantLock(); // 创建一个 lock对象
在要加锁的地方调用 lock.lock()方法,解锁的地方调用 lock.unlock()方法
public void drawMoney(double money){
// 刚才的 drawMoney()方法就变成了这样
lock.lock();
try {
if (account>=money){
System.out.println(Thread.currentThread().getName()+"号取款机取款成功,吐出金额"+money+"元");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account=account-money;
System.out.println("余额:"+account+"元");
} else {
System.out.println("余额不足!!!");
}
} finally {
lock.unlock();
}
}
把方法要执行的部分放在了try块儿里, lock.unlock()放在了finally块儿里
2.传统的线程通讯
①轮流 存钱 取钱
假设我们有两个线程,存款者线程 和 取款者线程
现在要求它们存一次 取一次 存一次 取一次
由于我们线程的执行是由随机性的 它们两个完全有可能连续抢到执行机会
我们可以使用wait() notify() notifyAll() 这三个方法 它们需要用同步监视器对象调用
wait() 让一个线程进入等待
notify() 唤醒一个线程
notifyAll() 唤醒所有线程
public class BankAccount {
// 这是账户类 里面有存款方法和取款方法
// flag 为 false表示账户里没有钱
private boolean flag = false;
// 存款操作的方法
public synchronized void depositMoney() throws Exception {
if (flag) {
this.wait();
} else {
System.out.println("存入了800元,当前账户余额为800元!");
flag = true;
Thread.sleep(1000);
this.notifyAll();
}
}
// 取款操作的方法
public synchronized void drawMoney() throws Exception {
if (flag) {
System.out.println("取走了800元,当前余额为0!");
flag = false;
Thread.sleep(1000);
this.notifyAll();
} else {
this.wait();
}
}
}
public class DepositMoney extends Thread{
// 这是存款线程类
private BankAccount account=null;
public DepositMoney(BankAccount account){
this.account=account;
}
@Override
public void run() {
try {
while (true){
// 死循环调用存钱方法
account.depositMoney();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class DrawMoney extends Thread{
// 这是取款线程类
private BankAccount account=null;
public DrawMoney(BankAccount account){
this.account=account;
}
@Override
public void run() {
try {
while (true){
// 死循环调用取钱方法
account.drawMoney();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class TestThread extends Thread{
// 这是用来测试的类
public static void main(String[] args) {
BankAccount account=new BankAccount();
new DepositMoney(account).start();
new DrawMoney(account).start();
}
}
这样就可以不断重复存钱取钱操作了
现在我们把这个题目升级一下
有三个存款线程 A B C , 一个取款线程
A存一次钱 取款者取一次, B存一次钱 取款者取一次, C存一次钱 取款者取一次
然后我们的 BankAccount类 就变成了这样
public class BankAccount {
// 账户类 里面有存款方法和取款方法
// 定义这个 num来判断是谁要存钱
public static int num=0;
// flag 为 false表示账户里没有钱
private boolean flag = false;
// 存款操作的方法
public synchronized void depositMoney() throws Exception {
if (flag) {
this.wait();
} else {
if (num%3==0){ // num=0 3 6...时 A存钱
System.out.print("A");
}else if((num-1)%3==0){ // num=1 4 7...时 B存钱
System.out.print("B");
}else if ((num-2)%3==0){ // num=2 5 8...时 C存钱
System.out.print("C");
}
System.out.println("存入了800元,当前账户余额为800元!");
flag = true;
num++; // 每存完一次钱 num+1
Thread.sleep(1000);
this.notifyAll();
}
}
// 取款操作的方法
public synchronized void drawMoney() throws Exception {
if (flag) {
System.out.println("取走了800元,当前余额为0!");
flag = false;
Thread.sleep(1000);
this.notifyAll();
} else {
this.wait();
}
}
}
我们再写一个题目:
②分行读取文件
我们定义 甲乙丙 三个线程, 读取一个文件
要求甲读第一行 乙读第二行 丙读第三行, 再甲读第四行 乙读第五行 以此类推
读到的东西在控制台输出一下
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class Reader {
// 这是我们的阅读器类
private boolean isOver = false;
private String operator = "甲"; // 这是指定当前应该由哪个线程来读
private BufferedReader reader = null;
public boolean isOver() {
return isOver;
}
public void setOver(boolean over) {
isOver = over;
}
public Reader() {
try {
// 创建了一个缓冲流 这里指定了读取哪个文件
reader = new BufferedReader(new FileReader("./file/e82.txt"));
} catch (FileNotFoundException fileNotFoundException) {
fileNotFoundException.printStackTrace();
}
}
public synchronized void readFile() {
// name 获取当前的线程名称
String name = Thread.currentThread().getName();
try {
// 如果应该由该线程读取, 则打印读到的内容
if (name.equals(operator)) {
String line = reader.readLine();
// 如果读完了 就把 idOver赋值为 true
if (line == null) {
this.isOver = true;
reader.close();
} else {
// 如果没有读完,则打印读到的内容
System.out.println(name + "读到了:" + line);
}// 每读到一行 把指定的线程变一次
switch (operator) {
case "甲":
operator = "乙";
break;
case "乙":
operator = "丙";
break;
case "丙":
operator = "甲";
break;
}
Thread.sleep(1000);
notifyAll(); //把其他线程唤醒
} else {
// 如果不该这个线程读 则这个线程需要等待
wait();
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
public class ReaderThread extends Thread{
// 这是我们的阅读器线程类
private Reader reader=null;
public ReaderThread(Reader reader,String name){
super(name); // 指定一下线程名称
this.reader=reader;
}
@Override
public void run() {
// 当isOver 为false时一直读取
while (!reader.isOver()){
reader.readFile();
}
}
}
public class ReaderDemo {
public static void main(String[] args) {
// 这是我们的阅读器线程类 创建 甲乙丙三个线程 一同执行
Reader reader=new Reader();
new ReaderThread(reader,"甲").start();
new ReaderThread(reader,"乙").start();
new ReaderThread(reader,"丙").start();
}
}
THE END (^_−)☆