201871010112-梁丽珍《面向对象程序设计(java)》第十七周学习总结

 

项目

内容

这个作业属于哪个课程

https://www.cnblogs.com/nwnu-daizh/

这个作业的要求在哪里

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.就绪状态

       线程虽然有资格运行,但调度程序还没有把它选为运行线程所处的状态.此时,线程具备了运行的条件,一旦被选中,马上就能运行.
       线程创建后,调用了start()方法,线程不处于运行状态,但能通过isAlive()测试.而且在线程运行之后,或者被从阻塞,等待或者睡眠状态回来之后,马上就能进入就绪状态.
3.运行状态
  从就绪状态池(注意不是队列,是池)中被选择为当前执行的线程所处的状态.
 4.等待,阻塞,或者睡眠状态
    线程依然是活的,但是缺少运行的条件.一旦具备了条件,就可以转为就绪状态,不能直接转为运行状态.另外,suspend()和stop()方法已经被废弃了,比较危险,不要在使用了.
 5.死亡状态
   一个线程的run()方法运行结束,那么该线程完成其使命,它的栈结构将解散,也就是死亡了.但是它依然是一个Thread对象,仍可以被引用,这一点与其他对象一样,而且被引用的对象也不会被垃圾回收器回收.
   一旦线程死去,它就永远不能重新启动了,也就是说,不能再用start()方法让它运行.下面的实例会抛出IllegalThreadStateException异常
    t.start();
    t.start();

这是错误的用法

注意:线程的启动要调用start()方法,只有这样才能创建新的调用栈.而直接调用run()方法的话,就不会创建新的调用栈,也就不会创建新的线程,run()方法就普通方法没什么两样了.

Thread类实现了Runnable

线程同步

  线程共享了相同的资源,但是在某些重要的情况下,一次只能让一个线程来访问共享资源,例如,作为银行账户这样的共享资源,如果多个线程可以同时使用该资源,就会出出现银行账户数据安全上的问题.

1.共享变量

       要使多个线程在一个程序中有用,必须有某种方法实现线程间的互相通信或共享结果,最简单的方法是使用共享变量.使用同步来确保值从一个线程正确传播到另一个线程,以及防止当一个线程正在更新一些相关数据项时,另一个线程看到不一致的中间结果.

2.存在于同一内存空间中的所有线程

       线程与进程有许多共同点,不同的是线程与同一进程中的其他线程共享相同进程上的下文,包括内存空间,只要访问共享变量(静态或者实例变量),线程就可以方便地互相交换数据,但必须确保线程以受控的方式访问共享变量,以免它们互相干扰对方的更改.

3.受控访问的同步

       为了确保可以在线程之间以受控方式共享数据,Java语言提供了两个关键字:synchronizedvolatile.

synchronized有两个重要的含义

  一次只有一个线程可以执行代码的受保护部分.
       一个线程更改的数据对于其他线程是可见的.
       如果没有同步,数据就很容易处于不一致状态.例如,如果一个线程正在更新两个相关值,而另一个线程正在读取这两个值,有可能在第一个线程只写了一个值,还没有写另外一个值的时候,调度第二个线程运行,这样它就会看到一个旧值和一个新值.
synchronized(对象) {  // 任意对象都可以。这个对象就是锁。
	需要被同步的代码;
}

public synchronized void transfer() {

}
4.确保共享数据更改的可见性.
同步可以让用户确保线程看到一致的内存视图.
       处理器可以使用高速缓存加速对内存的访问(或者编译器可以将值存储到寄存器中,以便进行更快的访问).这表示在这样的系统上,对于同一个变量,在两个不同的处理器上执行的两个线程可能会看到两个不同的值,如果没有正确的同步,线程可能会看到旧的变量值,或者引起其他形式的数据损坏.
       volatile 比同步简单,只适合于控制对基本变量(整数,布尔变量等) 的单个实例的访问.当一个变量被声明成volatile,任何对该变量的写操作都会绕过高速缓存,直接写入主内存,而任何对该变量的读取也都绕过高速缓存,直接取自主内存.这表示所有线程在任何时候看到的volatile变量值都相同.
private volatile double balance;
5.用锁保护的原子代码块
       volatile对于确保每个线程看到最新的变量值非常有用,但实际上经常需要保护代码片段,同步使用监控器(monitor)或锁的概念,以协调对特定代码块的访问.
       每个Java对象都有一个相关的锁,同一时间只能有一个线程持有Java锁.当线程进入synchronized 代码块时,线程会阻塞并等待,直到锁可用.当线程处于就绪状态时,并且获得锁后,将执行代码块,当控制退出受保护的代码块,即到达了代码块末尾或者抛出了没有在synchronized 块中捕获的异常时,它就会释放该锁.
       这样,每次只有一个线程可以执行受给监控器保护的代码块.从其他线程的角度看,该代码块可以看做是原子的,它要么全部执行,要么不执行.
6.Java锁定

   Java锁定可以保护许多代码块或方法,每次只有一个线程可以持有锁.

       反之,仅仅因为代码块由锁保护并不代表两个线程不能同时执行该代码块.它只表示如果两个线程正在等待相同的锁,则它们不能同时执行该代码.

7.同步的方法

 创建synchronized 块的最简单方法是将方法声明成synchronized.这表示在进入方法主体之前,调用者必须获得锁.示例代码如下:

public class Point{
    public synchronized void setXY(int x,int y){
        this.x = x;
        this.y = y;
    }
}
对于普通的synchronized方法,这个锁是一个对象,将针对它调用方法,对于静态的synchronized方法,这个锁是与Class对象相关的监控器,在该对象中声明了方法.
       setXY()方法被声明成了synchronized ,并不表示两个不同的线程不能同时执行setXY()方法,只要它们调用不同Point实例的setXY()方法,就可同时执行.对于一个Point实例,一次只能有一个线程执行setXY()方法,或Point的任何其他synchronized方法.
8.同步的方法
   创建synchronized 块的最简单方法是将方法声明成synchronized.这表示在进入方法主体之前,调用者必须获得锁.示例代码如下:
public class Point{
    public synchronized void setXY(int x,int y){
        this.x = x;
        this.y = y;
    }
}
对于普通的synchronized方法,这个锁是一个对象,将针对它调用方法,对于静态的synchronized方法,这个锁是与Class对象相关的监控器,在该对象中声明了方法.
       setXY()方法被声明成了synchronized ,并不表示两个不同的线程不能同时执行setXY()方法,只要它们调用不同Point实例的setXY()方法,就可同时执行.对于一个Point实例,一次只能有一个线程执行setXY()方法,或Point的任何其他synchronized方法.
9.同步的块
 synchronized块的语法比synchronized方法稍微复杂一点,因为还需要显式地指定锁要保护哪个块.
public class Point{
    public synchronized void setXY(int x,int y){
        synchronized (this) {
            this.x = x;
            this.y = y;
        }
    }
}
使用this引用作为锁很常见,但这不是必须的.使用this引用作为锁表示该代码与这个类中的synchronized方法使用一个锁.
       由于同步防止了多个线程同时执行一个代码块,因此性能上就有问题,即使是在单处理器的系统上,也最好在尽可能小的需要保护的代码上使用同步.
       访问基于堆栈的局部变量从来不需要受到保护,因此它们只能受到自己所属的线程访问.

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:

Elipse环境下调试教材651页程序14-7,结合程序运行结果理解程序;

掌握利用锁对象和条件对象实现的多线程同步技术。

程序:

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

Elipse环境下调试教材655页程序14-8,结合程序运行结果理解程序;

掌握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

Elipse环境下运行以下程序,结合程序运行结果分析程序存在问题;

尝试解决程序中存在问题。

 

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();
	}
}

 

运行结果:

实验编程练习

利用多线程及同步方法,编写一个程序模拟火车票售票系统,共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在多线程同步中的应用。

 

 

 

 

posted @ 2019-12-23 21:39  201871010112-梁丽珍  阅读(152)  评论(1编辑  收藏  举报