项目 |
内容 |
这个作业属于哪个课程 |
|
这个作业的要求在哪里 |
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在多线程同步中的应用。