• 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在多线程同步中的应用。

  • 相关阅读:
    Ajax World 名人堂
    Chrome Flash插件的手动安装方法
    2009年郭红俊的工作
    动画缓冲或叫缓动函数(Animation Easing)
    2008年郭红俊的工作
    Android下调整多媒体音量方法
    游戏中按概率播放某个音效简单c++实现
    string转int 等(转)
    .NET简谈面“.NET技术”向接口编程 狼人:
    改善代码设计 —— 处理概括关系(Dealing w“.NET技术”ith Generalization) 狼人:
  • 原文地址:https://www.cnblogs.com/LZ-728672/p/12077103.html
Copyright © 2020-2023  润新知