本课程将模拟支付平台上买家付款到支付平台,卖家从支付平台取款的简单业务流程,最终结果以控制台输出形式展现.
前置知识: 1 基本的'Java'语法 2 基本的'进程'与'线程'概念 你将学会: 1 多线程的基本使用方法 2 多线程的'安全问题' 3 Java中多线程实现 4 Java中的'并发包' 5 如何使用'JUnit'来通过测试题 参考资料: 1 http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html
本实验环境采用带桌面的 Ubuntu Linux 环境,实验中会用到的环境或软件:
本课程难度一般,属于中级课程,适合具有 Java 基本语法和核心API基础的同学学习实践多线程编程。
在讲解线程之前,首先要了解一下 进程
。
进程
和 线程
是操作系统的基本概念。
在计算机中,内存,网络,文件等都可以看作是某种资源。
每个程序如果需要运行起来,都必须申请使用相关的资源。
在windows中,应该都使用过 进程管理器
,结束某个进程可能电脑就不那么卡了.
这是因为每个进程要运行都会申请相关的资源,资源使用完了就会归还.
运行中的程序才能称为进程。进程 可以认为是 运行中的程序(程序 + 资源)
如果把进程看作工厂,线程就是里面的工人
每个线程都有自己的职能。有的线程负责接受外来输入,有的线程负责计算,有的线程负责发送信息。所有的线程一起工作,能够提高效率。
并发与并行
对于单核计算机来说,它只有一个CPU,每次只能执行一个程序,为什么我们的电脑
能同时运行这么多程序呢?
操作系统给每个线程分配不同的 CPU 时间片( 运行时间
),在这个时间片内, CPU 只执行一个时间片内的线程,多个时间片中的相应线程在 CPU 内 轮流
执行,由于每个时间片时间很短(这里的时间片短就是时间间隔很短),所以对用户来说,仿佛各个线程在计算机中是并行( 同时
)处理的(实际上多线程之间是并发执行的,即某一时间点上只有一个线程在执行)。操作系统是根据线程的优先级来安排 CPU 的时间,优先级高的线程优先运行,优先级低的线程则继续等待。
线程总体分两类:用户线程和守候线程。当所有用户线程执行完毕的时候, JVM 自动关闭。但是守候线程却不独立于 JVM ,守候线程一般是由操作系统或者用户自己创建的。
有两种方法来创建新的执行线程。一是要声明一个类 Thread
的子类。这个子类应重写 Thread
类的 run
方法。子类的实例可以被分配和启动。例如,一个计算素数比规定值大的线程可以写为如下形式:
class PrimeThread extends Thread { long minPrime; PrimeThread(long minPrime) { this.minPrime = minPrime; } public void run() { // compute primes larger than minPrime . . . } }
然后下面的代码将创建一个线程并启动它:
PrimeThread p = new PrimeThread(143); p.start();
Java 语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象 object
中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问 object
的一个加锁代码块时,另一个线程仍然可以访问该 object
中的非加锁代码块。
synchronized
关键字,代表这个方法加锁,相当于不管哪一个线程(例如线程 A ),运行到这个方法时,都要检查有没有其它线程 B (或者 C 、 D 等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用 synchronized 方法的线程 B (或者 C 、 D )运行完这个方法后再运行此线程 A ,没有的话,锁定调用者,然后直接运行。它包括两种用法: synchronized 方法和 synchronized 块。
synchronized
方法声明时使用,放在范围操作符( public
等)之后,返回类型声明( void
等)之前。这时,线程获得的是成员锁,即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候,当前线程(就是在 synchronized
方法内部的线程)执行完该方法后,别的线程才能进入。例如:
public synchronized void synMethod(){ //方法体 }
如在线程 t1 中有语句 obj.synMethod()
那么由于 synMethod
被 synchronized
修饰,在执行该语句前, 需要先获得调用者 obj
的对象锁, 如果其他线程(如 t2 )已经锁定了 obj
(能是通过 obj.synMethod
,也可能是通过其他 synchronized
修饰的方法 obj.otherSynMethod
锁定的 obj
), t1 需要等待直到其他线程( t2 )释放 obj
, 然后 t1 锁定 obj
, 执行 synMethod
方法。 返回之前之前释放 obj
锁。
下载代码到实验楼环境中,解压以后,使用Eclipse打开.完成里面的功能需求 $ wget http://labfile.oss.aliyuncs.com/courses/576/PayPlatform.tar.gz
需求:
只需要实现相关的类和相关的方法
实现 Caculator
类:具体要求如下
1 已知Caculator有如下功能: 给一个整数增加1(inc) `需求1:返回正确的数值 需求2:静态方法` 怎么算是通过挑战: 完成代码以后,直接在Eclipse中运行test目录下的TestCaculator。 如果控制台显示:"pass"证明你完成了挑战,点击提交。如果是"fail",证明你的实现有问题 看"fail"后面的其他提示。修改代码,直到通过挑战 注意不必整个类实现以后才开始提交。 例如需要实现inc函数,只需要找到TestCaculator这个类。 运行其中的testInc()。如果显示"pass",证明这个函数通过了测试. 这样更容易帮助你找到错误.
示例代码
我们已经提供的代码如下: class Caculator{ //完成需求函数 } 完成的代码应该是这样的: class Caculator{ public static int inc(int arg){ return arg + 1; } } 然后运行test/shiyanlou/TestCaculator 控制台会显示"pass" 挑战成功
需求:
实现 Bank
类:具体要求如下。
完成代码以后运行test/shiyanlou/TestBank类系统会自行判题
1 已知Bank类有如下属性 银行总余额(totalBalance) `double [.00](两位小数)` 所有的用户的余额信息(usersBalanceInfo) `HashMap` 银行名称(name) `String` 银行地址(address) `String` 当前利息(percent) `double [.00](如上)` 2 已知Bank类有如下功能 存款(deposit)(int userId,double value) `需求1:对应用户的余额增加 需求2:银行总余额增加` 取款(withdraw)(int userId,double value) `需求1:对应用户的余额减少 需求2:银行总余额缉拿少` 查看某个用户的余额(query)(int userId) `需求1:返回对应用户的余额` 注册新的用户(register)(String userName,String password) `需求1:注册新的用户` 每笔消费需要记录(log) `[2017-10-01 12:00:53 用户姓名 动作 金额]` `提示 1 是否按格式实现属性 2 是否正确实实现函数 3 是否返回正确的格式 完成 public class Bank { #完成需求 } 没有通过的时候,查看控制台信息 注意一个一个的通过挑战。
package com.shiyanlou.java; public class Account { //余额 浮点类型 private double balance; public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } //构造方法 public Account() { balance = 0; } /* * 付款,此处有关键字synchronized修饰。 */ public synchronized void deposit(double amount) { double tmp=balance; try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } tmp+=amount; balance=tmp; } /* * 取款此处有关键字synchronized修饰。 */ public synchronized void withdraw(double amount) { double tmp=balance; try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } tmp-=amount; balance=tmp; } public void printInfo() { System.out.println("Balance on "+ " account = " + balance); } }
Buyer
类继承了 Runnable
,实现买家汇款,具体代码如下。 。
package com.shiyanlou.java; public class Buyer implements Runnable { //买家基本信息 private String name; private String address; private String email; private String phone; private Account account; //构造方法 public Buyer(String name, String address, String email, String phone,Account account) { this.name = name; this.address = address; this.email = email; this.phone = phone; this.account=account; } //打印用户信息(基本信息+用户名下账户) public void printCustomerInfo() { System.out.println(" Information about a Buyer"); System.out.println(" Name - " + name); System.out.println(" address - " + address); System.out.println(" email - " + email); System.out.println(" phone # - " + phone); if(account != null){ account.printInfo(); } else{ System.out.println("The Buyer has no accounts"); } } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } @Override public void run() { for (int i=1; i<10; i++){ account.deposit(1000); System.out.println("The Balance of account after No:"+i +"deposite is :"+account.getBalance()); } } }
这里的 run()
方法内是 for
循环 10 次汇款 1000 元,并打印的操作。
在 Java 中可有两种方式实现多线程,一种是继承 Thread
类,一种是实现 Runnable
接口。在实际开发中一个多线程的操作很少使用 Thread
类,而是通过 Runnable
接口完成。但是在使用 Runnable
定义的子类中没有 start()
方法,只有 Thread
类中才有。此时观察 Thread
类,有一个构造方法: public Thread(Runnable targer)
此构造方法接受 Runnable
的子类实例,也就是说可以通过 Thread
类来启动 Runnable
实现的多线程。实现该接口需要覆盖 run
方法,然后将需要以多线程方式将执行的代码书写在 run
方法内部或在 run
方法内部进行调用。
Seller
类继承 Runnable
,实现买家取款,具体代码如下。
已知买家有如下属性,姓名(name),地址(address),邮箱(email),电话(phone),账号(account)
package com.shiyanlou.java; public class Seller implements Runnable { private String name; private String address; private String email; private String phone; private Account account; //构造方法 public Seller(String name, String address, String email, String phone,Account account) { this.name = name; this.address = address; this.email = email; this.phone = phone; this.account= account; } //打印用户信息(基本信息+用户名下账户) public void printCustomerInfo() { System.out.println(" Information about a customer"); System.out.println(" Name - " + name); System.out.println(" address - " + address); System.out.println(" email - " + email); System.out.println(" phone # - " + phone); if(account != null){ account.printInfo(); } else{ System.out.println("The customer has no accounts"); } } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } @Override public void run() { for (int i=1; i<10; i++){ account.withdraw(1000); System.out.println("The Balance of account after No:"+i +"withdraw is :"+account.getBalance()); } } }
这里的 run()
方法内是 for
循环 10 次取 款 1000 元,并打印的操作。
Business
是测试类,创建线程实现存取款操作,具体代码如下。
package com.shiyanlou.java; public class Business { public static void main(String[] args){ Account midAccount;//中间账户 Buyer buyer; Seller seller; midAccount = new Account(); buyer = new Buyer("Buyer", "shiyanlou,sichuan", "buyer@gmail.com", "8888888",midAccount);//买家基本信息以汇款的目标账户 seller = new Seller("Seller", "aliyun,sichuan", "Seller@gmail.com", "6666666",midAccount);//卖家基本信息以取款的目标账户 Thread accountThread1 = new Thread(buyer);//新建线程1 买家存款 Thread accountThread2 = new Thread(seller);//新建线程2 卖家取款 accountThread1.start();//线程1开始 accountThread2.start();//线程2开始 System.out.printf("Account : start Balance: %f/n",midAccount.getBalance()); } }
在 Business.java 文件中,包含了 main()
方法。该方法中创建了一个中间账户,用于接收买家付款;同时,卖家从中取款。对余额为 0 时可做欠款记账处理,可显示取款成功。
新建买家卖家对象,其中的账户为同一中间账户。创建两个线程,分别传入买家和卖家,开启线程。接下来,两个线程将分别使用 deposite
和 withdraw
方法,对同一账户进行付/取款操作。
点击工具栏上的绿色运行按钮 Run
:
运行程序,观察控制台 Console,实验结果如图。
输出结果可能有多种情况:
第一种情况:
第二种情况(输出结果的顺序请以实际为准):
10 次汇款 1000 ,10 次取款 1000 ,则最终余额应为 0 。说明实验成功。
最终的输出结果 2 显示, deposite
和 withdraw
可能并不是有序交叉执行的,其中的原因之一是由于在 Account.java 中设置的
Thread.sleep(long millis)
不同导致的。可以看到 deposite()
方法设置的: Thread.sleep(50)
是 50 毫秒, withdraw()
方法设置的: Thread.sleep(20)
是 20 毫秒。 thread
的 sleep()
方法使当前执行线程休眠(暂停执行)指定的毫秒数。它就会放弃 CPU ,转到阻塞状态。在休眠期间,所有其他具有执行资格的程序都可以获取 CPU 资源,进行执行。具体情况还需要考虑操作系统的队列调度算法,即需要获取 CPU 资源的线程会进入操作系统的就绪队列,休眠的进程被挂起,进入阻塞状态,处于阻塞状态的进程,若其等待的事件已经发生,则进程由阻塞状态转变为就绪状态(内容繁多,不再赘述,详情请参考: 操作系统——进程的状态及转换 )。
由于对临界资源的 midAccount
中间账户的访问方法有 synchronized
关键字修饰,使得在 accountThread1
线程访问此资源时, accountThread2
线程因得不到资源只能等候,不能读写已被 accountThread1
线程占有的资源。避免了读脏数据现象的发生。所以最后我们的得到的第 10 次结果 The Balance of account after No:10 deposite is :0
。
本次课程,通过编写简单的 PayPlatform 程序模拟支付平台流程,其中主要涉及到 Java 线程与同步。为了了解线程同步的概念,对操作体统进程并发与并行、状态转换等相关知识点进行了介绍。知识由点及面,涉及范围较广,需要配合参考文档进一步理解消化。
sleep()
方法与 wait()
方法的区别。