Java
day20_2023.9.21
Lock(锁)
从Jdk5开始,Java提供了更强大的线程同步机制,通过显式定义同步锁对象来实现同步,这里就是使用Lock对象
锁是用于通过多个线程控制对共享资源的访问的工具。
通常,锁提供对共享资源的独占访问:一次只能有一个线程可以获取锁,并且对共享资源的所有访问都要求首先获取锁。
这里,常用的是Lock接口的子实现类 ReentrantLock
一个ReentrantLock(可重入互斥锁)具有与使用synchronized方法和语句访问的隐式监视锁相同的基本行为和语义,但具有扩展功能。可以实现 显式的加锁、释放锁
语法:
class X {
private final ReentrantLock lock = new ReentrantLock(); // …
public void m() {
lock.lock(); //加锁
try { // … method body }
finally {
lock.unlock() //解锁
} } }
示例:多线程抢票通过Lock加锁
public class TicketLock implements Runnable {
private int ticketNum = 10;
boolean flag = true;
//声明一个显示的锁Lock对象
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (flag) {
buyTicket();
}
}
//写一个买票方法
public void buyTicket(){
try {
lock.lock();
//买票结束的判断
if (ticketNum <=0 ){
flag = false;
return;
}
Thread.sleep(20);
System.out.println(Thread.currentThread().getName()
+"---->抢到了第"+ticketNum-- +"张票");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
//在外面创建对象,将对象传入new Thread中,创建多个子线程
//这时候,多个子线程就会共享这个对象中的数据
TicketLock ticketLock = new TicketLock();
new Thread(ticketLock,"小明").start();
new Thread(ticketLock,"小红").start();
new Thread(ticketLock,"黄牛").start();
}
}
synchronized 和 ReentrantLock 区别是什么?
synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量
synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。
主要区别如下:
ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
普通同步方法,锁是当前实例对象
静态同步方法,锁是当前类的class对象
同步方法块,锁是括号里面的对象
Lock锁和synchronized锁,到低用哪个?
Lock所在刚出来的时候,很多的性能方面确实比synchronized要好,但是从JDK6开始,synchronized也被做了各种的优化
优化:适应自旋锁、锁消除、轻量级锁、偏向锁…
所以,现在Lock锁和synchronized锁,性能差别不是很大,synchronized使用起来更加简单,所以大部分时候还是使用synchronized,如果需要使用Lock锁的特有的特性,才会使用Lock锁
死锁
死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续了。
使用多线程的时候,要避免死锁的发生
示例:
使用死锁模拟生活中抢资源的案例: 茶杯和牙膏
/*
茶杯对象
*/
public class TeaCup {
}
/*
牙膏
*/
public class ToothPaste {
}
package com.iweb.airui369.deathlock;
/*
刷牙
*/
public class BrushTooth implements Runnable {
//线程的选择
int choice; // 0 是茶杯 1牙膏
String name; //线程名
static TeaCup teaCup = new TeaCup();
static ToothPaste toothPaste = new ToothPaste();
//构造方法
public BrushTooth(int choice, String name) {
this.choice = choice;
this.name = name;
}
//定义一个刷牙方法
public void brushTooth() throws InterruptedException {
//如果线程,先拿了茶杯,那就需要再拿牙膏
if (choice == 0){
//要把已有的对象锁住不放
synchronized (teaCup){
System.out.println(this.name + "拿到茶杯了,他在等牙膏刷牙!");
//去拿牙膏,并锁住
Thread.sleep(1000L);
//synchronized (toothPaste){
System.out.println(this.name + "拿到牙膏了,开始刷牙!");
//}
}
}else {
//先拿牙膏,在拿茶杯的情况
synchronized (toothPaste){
System.out.println(this.name + "拿到牙膏了,他在等茶杯刷牙!");
//去拿牙膏,并锁住
Thread.sleep(1000L);
synchronized (teaCup){
System.out.println(this.name + "拿到茶杯了,开始刷牙!");
}
}
}
}
@Override
public void run() {
try {
brushTooth();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Test {
public static void main(String[] args) {
BrushTooth t1 = new BrushTooth(0, "丁吉龙");
BrushTooth t2 = new BrushTooth(1, "文阶进");
new Thread(t1).start();
new Thread(t2).start();
}
}
形成死锁的四个必要条件是什么
**互斥条件:**线程(进程)对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放
**请求与保持条件:**一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
**不剥夺条件:**线程(进程)已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
**循环等待条件:**当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞
如何避免线程死锁
我们只要破坏产生死锁的四个条件中的其中一个就可以了。
破坏互斥条件
这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
破坏请求与保持条件
一次性申请所有的资源。
破坏不剥夺条件
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件
靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
volatile关键字
Java 提供了 volatile 关键字来保证可见性和禁止指令重排
volatile 修饰的变量,具有以下特点 :
1,volatile关键字保证可见性
2,不保证原子性,仍然存在线程安全问题
3,禁止指令重排,保证有序性
volatile关键字保证可见性
可见性,就是一个线程在操作完某个变量之后,这个值会对其他线程可见
public class Thread1 {
public int num = 0;
//volatile可以保证类属性的可见性。也就是一个线程修改完值之后,另一个线程马上就
//能拿到这个被修改后的值
//public volatile boolean flag = false;
//使用synchronized 实现可见性
public boolean flag = false;
public synchronized boolean getFlag() {
return flag;
}
public synchronized void setFlag() {
this.flag = true;
}
public int getNum(){
return num;
}
public void addNum(){
num++;
}
public static void main(String[] args) {
Thread1 t1 = new Thread1();
//创建多线程
//第一个多线程中,调用t1对象的addNum方法,完成num数字的++操作
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
t1.addNum();
System.out.println("addNum次数===" + i);
try {
Thread.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//t1.flag = true;
t1.setFlag();
System.out.println("flag已经设为true了");
}
}).start();
//第二个线程
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程2在执行....");
//无限循环
//while (!t1.flag){
while (!t1.getFlag()){
}
System.out.println("第二个线程获取到的num值:" + t1.getNum());
}
}).start();
}
}
Volatile不能保证原子性
public class Thread2 {
//volatile不能保证原子性
public volatile int num = 0;
public int getNum(){
return num;
}
//public synchronized void addNum(){
public void addNum(){
num++;
}
public static void main(String[] args) {
Thread2 t2 = new Thread2();
//循环创建10个线程,每个线程分别执行100次addNum方法的调用
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j <100 ; j++) {
t2.addNum();
try {
Thread.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
//如果是理想情况下,这里的num值应该是1000
//这个循环是主线程执行的内容
for (int i = 0; i < 10; i++) {
System.out.println("num值是:" + t2.getNum());
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
synchronized 和 volatile 的区别
synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。
volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。
区别
volatile 是变量修饰符;synchronized 可以修饰类、方法、变量。
volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。
通过实现Callable接口来创建线程
/*
通过实现Callable接口,创建线程
步骤:
1,创建实现类,实现Callable接口
2,以实现类为参数,创建FutureTask对象
3,将FutureTask作为参数,创建Thread对象
4,调用线程对象的start()方法
*/
public class CallableDemo implements Callable {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()
+"--->call()方法执行....");
return 1;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建FutureTask对象
FutureTask<Integer> futureTask
= new FutureTask<Integer>(new CallableDemo());
//通过FutureTask对象创建线程
Thread t = new Thread(futureTask);
t.start();
//线程运行后,通过futureTask对象获取值
System.out.println("返回值:" + futureTask.get());
System.out.println(Thread.currentThread().getName()
+"线程正在执行.....");
}
}
Callable和Runnable的区别?
相同点 :
都是接口
都可以编写多线程程序
都需要通过Thread.start()方法来启动线程
区别:
Runnable接口run()方法没有返回值,Callable接口call方法,有返回值,是个泛型,和Future、FutureTask配合可以获取异步执行的结果
Runnable接口run()方法只能捕获运行时异常,且无法抛出处理,Callable接口的call方法,允许抛出异常,可以获取异常信息
Callable接口支持返回执行结果,需要调用futureTask.get()得到返回值,这个方法会阻塞线程,不调用的话不会阻塞
Future接口
Futrue接口表示异步任务,是一个可能还没有完成的异步任务的结果,Callable用于产生结果,Future用于获取结果
线程池
线程池的基本思想,其实就是一种对象池的思想
开辟一块内存空间,里面存放了众多的(未死亡的)线程,池中的线程的调度,由池管理器来处理,当有线程任务的时候,从池子中取一个线程,执行完成后,线程对象归池
这样做,可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。
JDK5的线程池,分为: 固定尺寸的线程池,可变尺寸连接池,
相关的 API :Executors(线程池工具类) 和 ExecutorSevice(线程池对象接口)
Executors 面提供了一些静态工厂方法,生成一些常用的线程池:
**(1)newSingleThreadExecutor:**创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
**(2)newFixedThreadPool:**创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。如果希望在服务器上使用线程池,建议使用 newFixedThreadPool方法来创建线程池,这样能获得更好的性能。
(**3) newCachedThreadPool:**创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
**(4)newScheduledThreadPool:**创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
public class ExecutorsDemo extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+“正在执行…”);
//try {
// Thread.sleep(1000);
//} catch (InterruptedException e) {
// e.printStackTrace();
//}
}
public static void main(String[] args) {
//可变大小的线程池
//ExecutorService pool = Executors.newCachedThreadPool();
//线程池指定的大小为2的固定大小
ExecutorService pool = Executors.newFixedThreadPool(2);
//创建几个线程
Thread t1 = new ExecutorsDemo();
Thread t2 = new ExecutorsDemo();
Thread t3 = new ExecutorsDemo();
Thread t4 = new ExecutorsDemo();
//把线程加入到线程池中,并执行线程中的具体的实现
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
pool.execute(t4);
//关闭线程池
pool.shutdown();
}
}
线程池之ThreadPoolExecutor详解
Executors和ThreaPoolExecutor创建线程池的区别
《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors 各个方法的弊端:
newFixedThreadPool 和 newSingleThreadExecutor: 主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。
newCachedThreadPool 和 newScheduledThreadPool: 主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
ThreaPoolExecutor创建线程池方式只有一种,就是走它的构造函数,参数自己指定
你知道怎么创建线程池吗?
创建线程池的方式有多种,这里你只需要答 ThreadPoolExecutor 即可。
ThreadPoolExecutor() 是最原始的线程池创建,也是阿里巴巴 Java 开发手册中明确规范的创建线程池的方式。
ThreadPoolExecutor构造函数重要参数分析
ThreadPoolExecutor 3 个最重要的参数:
**corePoolSize :**核心线程数,线程数定义了最小可以同时运行的线程数量。
**maximumPoolSize :**线程池中允许存在的工作线程的最大数量
**workQueue:**当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中。
ThreadPoolExecutor其他常见参数:
**keepAliveTime:**线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
**unit :**keepAliveTime 参数的时间单位。
**threadFactory:**为线程池提供创建新线程的线程工厂
**handler :**线程池任务队列超过 maxinumPoolSize 之后的拒绝策略
一个简单的线程池Demo:Runnable+ThreadPoolExecutor
public class MyRunnable implements Runnable {
private String name;
public MyRunnable(String s) {
this.name = s;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()
+"开始时间" + new Date());
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+"结束时间" + new Date());
}
}
public class Test {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final long KEEP_ALIVE_TIME = 10L;
private static final int WORK_QUEUE = 100;
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(WORK_QUEUE));
for (int i = 0; i < 10; i++) {
MyRunnable thread = new MyRunnable("线程" + i);
threadPoolExecutor.execute(thread);
}
//终止线程池
threadPoolExecutor.shutdown();
}
}
线程交互
wait()方法 notify()方法 notifyAll()方法
这三个方法,都是Object类的方法,不是线程的方法
**wait()方法:**释放占有的对象锁,线程进入等待池,释放cpu,而其他正在等待的线程可以抢占此锁,获取到锁的线程即可运行程序。
它和sleep()方法不同,线程调用sleep()方法,会休眠一段时间,休眠期间会暂时释放cpu,但是并不释放对象锁,也就是说,在休眠期间,其他线程无法进入此代码内部,休眠结束,线程重新获得cpu,执行代码
wait()和sleep()方法的最大区别:wait()方法会释放对象锁,sleep()不会
**notify()方法:**该方法会唤醒因为调用对象的wait()方法而等待的线程,其实就是对 对象锁的唤醒,从而使得wait()方法的线程可以有机会获取对象锁,调用notify()方法后,不会立即释放锁,而是继续执行当前代码,直到synchronized中的代码全部执行完毕后,才会释放对象锁
注意:wait()方法和notify()方法都需要在synchronized代码块中调用
为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用?
当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify()方法。
同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。
由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。
示例:
package com.iweb.airui369.thread05;
public class WaitDemo implements Runnable {
int count = 0;
@Override
public void run() {
while (count < 10){
synchronized (Thread1.obj){ //锁一个对象
try {
//线程第一次执行的时候,不需要唤醒,所以排除掉count=0的情况
if (count != 0){
Thread1.obj.notify();
}
System.out.println("线程A" + count);
Thread.sleep(1000L);
Thread1.obj.wait();
count++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class WaitDemo1 implements Runnable {
int count = 0;
@Override
public void run() {
while (count < 10){
synchronized (Thread1.obj){
try {
Thread1.obj.notify(); //唤醒A
System.out.println("线程B" + count);
Thread.sleep(1000L);
if (count != 9){
Thread1.obj.wait(); //B进入等待
}
count++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class Thread1 {
//创建一个Object常量对象
public static final Object obj = new Object();
public static void main(String[] args) {
new Thread(new WaitDemo()).start();
new Thread(new WaitDemo1()).start();
}
}
面试题:sleep() 和 wait() 有什么区别?
两者都可以暂停线程的执行
类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
是否释放锁:sleep() 不释放锁;wait() 释放锁。
用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。
sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。
生产消费者模型(不是面向对象设计模式)
更准确说法,应该是 “生产者–消费者—仓库” 模型
1,生产者仅仅在仓储未满的时候,生产,仓储满了,停止生产
2,消费者,在仓储有产品的时候,才能消费,仓储空了,则等待
3,当消费者发现仓储没有产品,会通知生产者去生产
4,生产者生产出可消费的产品,通知消费者去消费
这个模型下,需要哪些对象(类)?
生产、消费、仓库、产品
生产:生产方法
消费:消费方法
产品
仓库:
添加数据,需要判断,仓库满了没有,满了等待消费,没有满,通知生产者生产
减少数据,判断,没有产品了,等待生产,消费完了,通知生产者生产
/*
消费者
*/
public class Consumer extends Thread{
//仓库对象,生产者生产的商品放入仓库中
Factory factory;
//构造方法
public Consumer(Factory factory) {
this.factory = factory;
}
@Override
public void run() {
//循环消费产品
for (int i = 1; i <= 50; i++) {
//每次消费产品,其实就是在从仓库中拿取商品
//拿取的只能生产者生成出来
Product product = factory.getProduct();
System.out.println("消费消费了:" + product.num + "号产品");
}
}
}
/*
生产者
*/
public class Producer extends Thread {
//仓库对象,生产者生产的商品放入仓库中
Factory factory;
//构造方法
public Producer(Factory factory) {
this.factory = factory;
}
@Override
public void run() {
//循环生成产品
for (int i = 1; i <= 50; i++) {
//每次生产产品,其实就是在往仓库中添加商品
//每次添加的商品都是新生产的,可以加个编号作为表示
factory.push(new Product(i));
System.out.println("生产者生产了:" + i + "号产品");
}
}
}
/*
仓库
*/
public class Factory {
//声明一个存放商品的空间 10个
Product[] products = new Product[10];
//计数器
int count = 0;
//生产方法
public synchronized void push(Product product){
//如果产品数量满了,就等待,停止生产,消费者消费了,才能继续生产
while (count >= products.length-1){
try {
this.wait(); //生产者等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果仓库没有满,继续生产
products[count] = product;
count++;
this.notifyAll(); //唤醒
}
public synchronized Product getProduct(){
//如果仓库中没有商品了,这个时候,消费者等待
while (count <= 0){
try {
this.wait(); //消费者等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--; //消费一次,就是数量少一个
//拿到仓库中的商品,并返回
try {
Thread.sleep(10L);
} catch (InterruptedException e) {
e.printStackTrace();
}
Product product = products[count];
this.notifyAll();
return product;
}
}
/*
商品
*/
public class Product {
int num;
public Product(int num) {
this.num = num;
}
}
public class Test {
public static void main(String[] args) {
Factory factory = new Factory();
//创建生产者和消费者线程,同时操作仓库
new Producer(factory).start();
new Consumer(factory).start();
}
}
银行取钱案例
/*
账户类
*/
public class Account {
int money; //余额
String name; //卡名
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
package com.iweb.airui369.thread07;
public class Bank extends Thread {
Account account; //账户对象
int drawMoney; //取钱的值
int haveMoney; //有多少钱了
//标记变量
boolean flag = true;
public Bank(String name,Account account, int drawMoney) {
super(name);
this.account = account;
this.drawMoney = drawMoney;
}
@Override
public void run() {
while (flag){
takeMoney();
}
}
//取钱
public void takeMoney(){
if (account.money <= 0){
flag = false;
return;
}
//锁定账户对象
synchronized (account){
//如果账户余额不足,就不能取了
if (drawMoney > account.money){
System.out.println(Thread.currentThread().getName()+
"想取钱,但是余额不足了");
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//新余额 = 旧余额-每次取的前
account.money = account.money - drawMoney;
//手里的钱 = 手里已有的钱 + 新取的钱
haveMoney = haveMoney + drawMoney;
System.out.println(
this.getName() + "本次取了"+ drawMoney + ",现在手里一共有:"
+haveMoney + ",目前的账户余额是:"+ account.money);
}
}
}
public class Test {
public static void main(String[] args) {
Account account = new Account(1000, "建行卡");
Bank t1 = new Bank("你", account, 50);
Bank t2 = new Bank("女朋友", account, 200);
t1.start();
t2.start();
}
}