201871010112-梁丽珍《面向对象程序设计(java)》第十七周学习总结
|
项目 |
内容 |
|
这个作业属于哪个课程 |
|
|
这个作业的要求在哪里 |
https://www.cnblogs.com/nwnu-daizh/p/12073034.html |
|
作业学习目标 |
(1) 理解和掌握线程的优先级属性及调度方法; (2) 掌握线程同步的概念及实现技术; (3) Java线程综合编程练习 |
第一部分:总结线程同步技术
什么是线程同步?
当使用多个线程来访问同一个数据时,非常容易出现线程安全问题(比如多个线程都在操作同一数据导致数据不一致),所以我们用同步机制来解决这些问题。
线程的状态
线程的状态表示线程在某时间段内进行的活动和将要进行的任务.线程有创建,就绪,运行,阻塞,死亡 5种状态.一个具有生命的线程,总是处于以下5种状态之一.


1.创建状态
实例化Thread对象,但没有调用start()方法时的状态.例如
ThreadTest test = new ThreadTest(); //或者 Thread t = new Thread(test);
此时虽然创建了Thread对象,但是它们暂时不能通过isAlive()测试.
2.就绪状态
3.运行状态
t.start();
t.start();
这是错误的用法
注意:线程的启动要调用start()方法,只有这样才能创建新的调用栈.而直接调用run()方法的话,就不会创建新的调用栈,也就不会创建新的线程,run()方法就普通方法没什么两样了.
Thread类实现了Runnable
线程同步
1.共享变量
要使多个线程在一个程序中有用,必须有某种方法实现线程间的互相通信或共享结果,最简单的方法是使用共享变量.使用同步来确保值从一个线程正确传播到另一个线程,以及防止当一个线程正在更新一些相关数据项时,另一个线程看到不一致的中间结果.
2.存在于同一内存空间中的所有线程
线程与进程有许多共同点,不同的是线程与同一进程中的其他线程共享相同进程上的下文,包括内存空间,只要访问共享变量(静态或者实例变量),线程就可以方便地互相交换数据,但必须确保线程以受控的方式访问共享变量,以免它们互相干扰对方的更改.
3.受控访问的同步
为了确保可以在线程之间以受控方式共享数据,Java语言提供了两个关键字:synchronized和volatile.
synchronized有两个重要的含义
synchronized(对象) { // 任意对象都可以。这个对象就是锁。
需要被同步的代码;
}
public synchronized void transfer() {
}
private volatile double balance;
6.Java锁定
Java锁定可以保护许多代码块或方法,每次只有一个线程可以持有锁.
反之,仅仅因为代码块由锁保护并不代表两个线程不能同时执行该代码块.它只表示如果两个线程正在等待相同的锁,则它们不能同时执行该代码.
7.同步的方法
创建synchronized 块的最简单方法是将方法声明成synchronized.这表示在进入方法主体之前,调用者必须获得锁.示例代码如下:
public class Point{
public synchronized void setXY(int x,int y){
this.x = x;
this.y = y;
}
}
public class Point{
public synchronized void setXY(int x,int y){
this.x = x;
this.y = y;
}
}
9.同步的块
public class Point{
public synchronized void setXY(int x,int y){
synchronized (this) {
this.x = x;
this.y = y;
}
}
}
10.大多数类并没有同步
因为同步会带来小小的性能损失,大多数通用类,如java.util中的Collection类,不在内部使用同步,这表示在没有附加同步的情况下,不能在多个线程中使用诸如HashMap这样的类.
Lock显示锁
java从1.5版本之后,提供了Lock接口。这时候,直接将锁封装成了对象。线程进入同步就是具备了锁,执行完,离开同步,就是释放了锁。在后期对锁的分析过程中,发现,获取锁,或者释放锁的动作应该是锁这个事物更清楚。所以将这些动作定义在了锁当中,并把锁定义成对象。所以,同步是隐式的锁操作,而Lock对象是显示的锁操作。
bankLock.lock(); //a ReentrantLock object
try
{
//临界区
}
finally
{
bankLock.unlock(); //如果在临界区抛出异常,必须保证锁被释放
}
另外,有一个需要注意的地方是,锁的唤醒机制的不同。
在用同步synchronized来实现加锁时,由于这时的锁用的是任意对象,所以如wait,notify,notifyAll等操作锁的等待唤醒的方法都定义在Object中。
而现在用Lock时,用的锁是Lock对象。所以查找等待唤醒机制方式需要通过Lock接口来完成。而Lock接口中并没有直接操作等待唤醒的方法,而是将这些方式又单独封装到了一个对象中。这个对象就是Condition,将Object中的三个方法进行单独的封装。并提供了功能一致的方法 await()、signal()、signalAll()体现新版本对象的好处。
下面是java核心技术卷1中关于显示锁核心代码:
/**
* Transfers money from one account to another.
* @param from the account to transfer from
* @param to the account to transfer to
* @param amount the amount to transfer
*/
public void transfer(int from, int to, double amount) throws InterruptedException
{
bankLock.lock();
try
{
while (accounts[from] < amount)
sufficientFunds.await();
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
sufficientFunds.signalAll();
}
finally
{
bankLock.unlock();
}
}
这里使用一个锁来保护Bank中的transfer方法,模拟现实中转账操作。
假定一个线程调用transfer时,在执行结束前被剥夺了运行权。假定第二个线程也调用transfer,由于第二个线程不能获得锁,将在调用lock方法时被阻塞。它必须等待第一个线程完成transfer方法的执行之后才能再度激活。当第一个线程释放锁时,那么第二个线程才可以运行。
锁是可重入的,因为线程可以重复地获得已经持有的锁。锁保持一个持有计数来跟踪对lock方法的嵌套调用。线程在每一次调用lock都要使用unlock来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。例如,当transfer方法调用getTotalBalance方法,这也会封锁bankLock对象,此时bankLock持有的计数为2.当getTotalBalance方法退出是,持有计数变为1.当transfer方法退出的时候,持有的计数变为0.线程释放锁。上面实例代码中调用的方法getTotalBalance与transfer方法使用的是同一个锁。
/**
* Gets the sum of all account balances.
* @return the total balance
*/
public double getTotalBalance()
{
bankLock.lock();
try
{
double sum = 0;
for (double a : accounts)
sum += a;
return sum;
}
finally
{
bankLock.unlock();
}
}
另外,上面的案列使用显示锁Lock,同样可以用同步的方法来实现,将transfer方法用synchronized修饰,同样在transfer中调用的getTotalBalance也需要用synchronized来修饰。
/**
* Transfers money from one account to another.
* @param from the account to transfer from
* @param to the account to transfer to
* @param amount the amount to transfer
*/
public synchronized void transfer(int from, int to, double amount) throws InterruptedException
{
while (accounts[from] < amount)
wait();
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
notifyAll();
}
/**
* Gets the sum of all account balances.
* @return the total balance
*/
public synchronized double getTotalBalance()
{
double sum = 0;
for (double a : accounts)
sum += a;
return sum;
}
二、实验内容和步骤
实验1:测试程序并进行代码注释。
测试程序1:
l 在Elipse环境下调试教材651页程序14-7,结合程序运行结果理解程序;
l 掌握利用锁对象和条件对象实现的多线程同步技术。
程序:
package synch;
import java.util.*;
import java.util.concurrent.locks.*;
/**
*一种拥有许多银行帐户的银行,它使用锁来序列化访问。
* @version 1.30 2004-08-01
* @author Cay Horstmann
*/
public class Bank
{
private final double[] accounts;//银行运转的基础数据
private Lock bankLock;//锁对象
private Condition sufficientFunds;
/**
* 构建银行。
* @param 账户数量
* @param 每个帐户的初始余额
*/
public Bank(int n, double initialBalance)
{
accounts = new double[n];
Arrays.fill(accounts, initialBalance);//调用initialBalance生成锁对象属性
bankLock = new ReentrantLock();
sufficientFunds = bankLock.newCondition();
}
/**
* 把钱从一个账户转到另一个账户。
* @param 从该账户转账
* @param 把钱转到账户上
* @param 转帐金额
*/
//transfer方法
public void transfer(int from, int to, double amount) throws InterruptedException
{//通过锁对象生成条件对象
bankLock.lock();//加锁
try
{
while (accounts[from] < amount)
sufficientFunds.await();//条件对象如果被注释会出现死锁现象不能实现线程的有效调用
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
sufficientFunds.signalAll();//调用signalAll()方法
}
finally
{
bankLock.unlock();
}
}
/**
* 获取所有帐户余额的总和。
* @return the total balance
*/
public double getTotalBalance()
{
bankLock.lock();//加锁
try
{
double sum = 0;
for (double a : accounts)
sum += a;
return sum;
}
finally
{
bankLock.unlock();//解锁
}
}
/**
* 获取银行帐户的数目。
* @return 账户数量
*/
public int size()
{
return accounts.length;
}
}
package synch;
/**
* 这个程序演示了多线程如何安全访问一个数据结构。
* @version 1.31 2015-06-21
* @author Cay Horstmann
*/
public class SynchBankTest
{
public static final int NACCOUNTS = 100;
public static final double INITIAL_BALANCE = 1000;
public static final double MAX_AMOUNT = 1000;
public static final int DELAY = 10;
public static void main(String[] args)
{
Bank bank = new Bank(NACCOUNTS, INITIAL_BALANCE);
for (int i = 0; i < NACCOUNTS; i++)
{
int fromAccount = i;
Runnable r = () -> {
try
{
while (true)
{
int toAccount = (int) (bank.size() * Math.random());
double amount = MAX_AMOUNT * Math.random();
bank.transfer(fromAccount, toAccount, amount);
Thread.sleep((int) (DELAY * Math.random()));
}
}
catch (InterruptedException e)
{
}
};
Thread t = new Thread(r);
t.start();
}
}
}
运行结果:

测试程序2:
l 在Elipse环境下调试教材655页程序14-8,结合程序运行结果理解程序;
l 掌握synchronized在多线程同步中的应用。
程序:
package synch2;
import java.util.*;
/**
* 使用同步原语的具有多个银行帐户的银行
* @version 1.30 2004-08-01
* @author Cay Horstmann
*/
public class Bank
{
private final double[] accounts;
/**
* 构建了银行。
* @param 账户数量
* @param 每个账户的初始余额
*/
public Bank(int n, double initialBalance)
{
accounts = new double[n];
Arrays.fill(accounts, initialBalance);
}
/**
* 把钱从一个账户转到另一个账户。
* @param 从账户转出
* @param 到账转到
* @param 转帐金额
*/
public synchronized void transfer(int from, int to, double amount) throws InterruptedException
{
while (accounts[from] < amount)
wait();//使线程处于等待集中
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
notifyAll();//唤醒所有等待的线程
}
/**
* 获取所有帐户余额的总和。
* @return 总平衡
*/
public synchronized double getTotalBalance()
{
double sum = 0;
for (double a : accounts)
sum += a;
return sum;
}
/**
* 获取银行中的帐户编号。
* @return 账户数量
*/
public int size()
{
return accounts.length;
}
}
package synch2;
/**
* 这个程序展示了多线程如何安全地访问一个数据结构,使用同步方法。
* @version 1.31 2015-06-21
* @author Cay Horstmann
*/
public class SynchBankTest2
{
public static final int NACCOUNTS = 100;
public static final double INITIAL_BALANCE = 1000;
public static final double MAX_AMOUNT = 1000;
public static final int DELAY = 10;
public static void main(String[] args)
{
Bank bank = new Bank(NACCOUNTS, INITIAL_BALANCE);//创建一个银行对象
for (int i = 0; i < NACCOUNTS; i++)
{
int fromAccount = i;
Runnable r = () -> {
try
{
while (true)
{
int toAccount = (int) (bank.size() * Math.random());//拿出一个随机账户
double amount = MAX_AMOUNT * Math.random();//设定随机一笔钱
bank.transfer(fromAccount, toAccount, amount);//转账操作
Thread.sleep((int) (DELAY * Math.random()));//随机休眠时间
}
}
catch (InterruptedException e)
{
}
};
Thread t = new Thread(r);//创建一个线程
t.start();//线程处于可运行状态
}
}
}
运行结果:

测试程序3:
l 在Elipse环境下运行以下程序,结合程序运行结果分析程序存在问题;
l 尝试解决程序中存在问题。
class Cbank
{
private static int s=2000;//当类加载时s赋值为2000
public static void sub(int m)
{
int temp=s;
temp=temp-m;
try {
Thread.sleep((int)(1000*Math.random()));
}
catch (InterruptedException e) { }//捕获中断异常
s=temp;
System.out.println("s="+s);
}
}
class Customer extends Thread//继承
{
public void run()//中值返回
{
for( int i=1; i<=4; i++)
Cbank.sub(100);
}
}
public class Thread3
{
public static void main(String args[])
{
Customer customer1 = new Customer();//把变量customer1的值设置为分配给新的Customer对象的内部地址
Customer customer2 = new Customer();
customer1.start();
customer2.start();
}
}
运行结果:

修改后代码:
package Thread;
class Cbank
{
private static int s=2000;
public synchronized static void sub(int m)
{
int temp=s;
temp=temp-m;
try {
Thread.sleep((int)(1000*Math.random()));
} catch (InterruptedException e) { }
s=temp;
System.out.println("s="+s);
}
}
class Customer extends Thread
{
public void run()
{
for(int i=1;i<=4;i++)
Cbank.sub(100);
}
}
public class Thread3 {
public static void main(String args[])
{
Customer customer1 = new Customer();
Customer customer2 = new Customer();
customer1.start();
customer2.start();
}
}
运行结果:

实验2 编程练习
利用多线程及同步方法,编写一个程序模拟火车票售票系统,共3个窗口,卖10张票,程序输出结果类似(程序输出不唯一,可以是其他类似结果)。
Thread-0窗口售:第1张票
Thread-0窗口售:第2张票
Thread-1窗口售:第3张票
Thread-2窗口售:第4张票
Thread-2窗口售:第5张票
Thread-1窗口售:第6张票
Thread-0窗口售:第7张票
Thread-2窗口售:第8张票
Thread-1窗口售:第9张票
Thread-0窗口售:第10张票
程序设计思路简述:
首先创建一个售票的一个线程组,其有3个线程售票,然后通过传递sellTicketThreadGroud参数给一个新的线程,重写run方法,执行sell方法,其中sell方法是使用 synchronized 关键字来修饰的,其确保在一个时刻只有一个线程可以进入sell方法执行代码。

结对编程者:吴丽丽
public class SellTickets3 {
private static int tickets = 1;
// 使用 synchronized 关键字修饰方法,确保某一时刻只有一个线程能够进入该方法中执行代码
protected synchronized static void sell() {
if (tickets <= 10) {
System.out.println(Thread.currentThread().getName() + " 窗口售:第 " + tickets++ + " 张票");
}
}
public static void startSell() {
// 售票线程所在线程组
ThreadGroup sellTicketThreadGroup = new ThreadGroup("sell ticket thread group");
// 开启 3 个线程售票
for (int i = 0; i < 3; i++) {
// 新建售票线程,并将其加入售票线程组中
new Thread(sellTicketThreadGroup, "Thread-" + (i)) {
@Override
public void run() {
while (tickets > 0) {
sell();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
}.start();
}
}
public static void main(String[] args) {
SellTickets3.startSell();
}
}
运行结果:

实验总结:
这周继续学习了有关线程的知识,主要学习了有关线程同步的问题。线程同步主要是为了解决多线程并发运行不确定性问题,使得多个线程中在一个线程使用某种方法时候,另一线程要使用该方法,就只能等待。在实验中学会了在Java中解决多线程同步问题。掌握利用锁对象和条件对象实现的多线程同步技术。掌握synchronized在多线程同步中的应用。

浙公网安备 33010602011771号