201871010105-曹玉中《面向对象程序设计(java)》第十七周学习总结

201871010105-曹玉中《面向对象程序设计(java)》第十七周学习总结

项目 内容
这个作业属于哪个过程 https://www.cnblogs.com/nwnu-daizh/
这个作业的要求在哪里 https://www.cnblogs.com/zyja/p/11963340.html
作业学习目标

(1) 掌握线程同步的概念及实现技术;

(2) 线程综合编程练习

第一部分:理论知识

一.进程与线程的区别      

        一般来说,把正在计算机中执行的程序叫做"进程"(Process),而不将其称为程序(Program).所谓线程(Thread),是"进程"中某个单一顺序的控制流.新兴的操作系统,如Mac,windows10等,大多采用多线程的概念,把线程视为基本执行单位.线程也是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();
这是错误的用法

三.线程同步

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

1.共享变量

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

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

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

3.受控访问的同步

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

synchronized有两个重要的含义

       一次只有一个线程可以执行代码的受保护部分.

       一个线程更改的数据对于其他线程是可见的.

       如果没有同步,数据就很容易处于不一致状态.例如,如果一个线程正在更新两个相关值,而另一个线程正在读取这两个值,有可能在第一个线程只写了一个值,还没有写另外一个值的时候,调度第二个线程运行,这样它就会看到一个旧值和一个新值.

4.确保共享数据更改的可见性.

同步可以让用户确保线程看到一致的内存视图.

       处理器可以使用高速缓存加速对内存的访问(或者编译器可以将值存储到寄存器中,以便进行更快的访问).这表示在这样的系统上,对于同一个变量,在两个不同的处理器上执行的两个线程可能会看到两个不同的值,如果没有正确的同步,线程可能会看到旧的变量值,或者引起其他形式的数据损坏.

       volatile 比同步简单,只适合于控制对基本变量(整数,布尔变量等) 的单个实例的访问.当一个变量被声明成volatile,任何对该变量的写操作都会绕过高速缓存,直接写入主内存,而任何对该变量的读取也都绕过高速缓存,直接取自主内存.这表示所有线程在任何时候看到的volatile变量值都相同.

5.用锁保护的原子代码块

       volatile对于确保每个线程看到最新的变量值非常有用,但实际上经常需要保护代码片段,同步使用监控器(monitor)或锁的概念,以协调对特定代码块的访问.

       每个Java对象都有一个相关的锁,同一时间只能有一个线程持有Java锁.当线程进入synchronized 代码块时,线程会阻塞并等待,直到锁可用.当线程处于就绪状态时,并且获得锁后,将执行代码块,当控制退出受保护的代码块,即到达了代码块末尾或者抛出了没有在synchronized 块中捕获的异常时,它就会释放该锁.

       这样,每次只有一个线程可以执行受给监控器保护的代码块.从其他线程的角度看,该代码块可以看做是原子的,它要么全部执行,要么不执行.

四.1.线程的调度和优先级

       线程调用的意义在于JVM应对运行的多个线程进行系统级的协调,以避免多个线程争用有限资源而导致应用系统死机或者崩溃.

       Java定义了线程的优先级策略,Java将线程的优先级分为了10个等级,分别用1~10之间的数字表示.数字越大表明线程的等级越高.相应的,在Thread类中定义了表示线程最低,最高,和普通优先级的成员变量MIN_PRIORITY,MAX_PRIORITY和NORMAL_PRIORITY,代表的优先级分别是1,10,5.当一个线程对象被创建时,其默认的线程优先级是5.

       为了控制线程的运行策略,Java定义了线程调度器来监控系统中处就绪状态的所有线程.线程调度器按照线程的优先级决定哪个线程投入处理器运行.在多个线程处于就绪状态下,具有高优先级的线程会在低优先级的线程之前得到执行.线程调度器同样采用"抢占式"策略来调度线程执行.即当前线程执行过程中,有较高优先级的线程进入就绪状态,则高优先级的线程立即被调度执行.具有相同优先级的所有线程采用轮转的方式来共同分配CPU时间片.

       在应用程序中设置线程优先级的方法很简单,在创建线程对象之后,可以调用线程对象的setPriority()方法改变该线程的运行优先级,同样可以调用getPriority()方法获取当前线程的优先级.

       在Java中比较特殊的线程是被称为(Daemon)线程的低级别线程.这个线程具有最低的优先级,用于为系统中的其他对象和线程提供服务.将一个用户线程设置为守护线程的方式是在线程对象创建之前调用线程对象的setDaemon方法.典型的守护线程是,JVM中的系统资源自动回收线程,它始终在低级别的状态中运行,用于实时监控和管理系统的可回收资源.

2.运行和挂起

        线程通过start()方法调用后,线程就进入了准备运行状态,进入准备运行阶段后,线程才能够获得运行的资格,即获得CPU的时间.

        注意:系统线程调度器来决定哪个线程可以运行,并确定其运行的时间,也就是说,线程具有不可预测性.

yield()方法

       调用线程Thread类的yield()方法将会导致当前线程从运行状态转换为准备运行状态.使用yield方法的好处就是可以让出CPU的时间,供其他线程使用,典型的例子就是用户调到需要进行长时间计算的线程(例如计算PI值),通过"取消"操作来获得快速的系统反应,从而预防假的死机现象,一个使用yield()方法的示例如下:

import java.io.*;
public class TestYield{
public static void main(String args[]){
MyThread3 t1 = new MyThread3("t1");
MyThread3 t2 = new MyThread3("t2");
t1.start();
t2.start();
}
}

class MyThread3 extends Thread{
MyThread3(String s){
super(s);
}
public void run(){
for (int i=0;i<100;i++){
System.out.println(getName()+": "+i);
if (i%10 == 0){
yield();
}
}
}
}

 

sleep()方法

       sleep()使当前线程(即调用该方法的线程)暂停执行一段时间,让其他线程有机会继续执行,但它并不释放对象锁,也就是说有Synchronized同步块,其他线程仍然不能访问共享数据.主要该方法要捕获异常.

       例如有两个线程同时执行(没有Synchronized),一个线程优先级为MAX_PRIORITY,另一个为MIN_PRIORITY.如果没有sleep()方法,只有高优先级的线程执行完成后,低优先级的线程才能执行.但当高优先级的线程调用sleep(5000)后,低优先级就有机会执行了.

       总之,sleep()可以使低优先级的线程得到执行的机会,也可以让同优先级,高优先级的线程有执行的机会.

3.wait()和notify()

       通常,多线程之间需要协调工作,例如两个人共用一个卫生间(每次只能一个人用),一个人必须等待另外一个人用完,得知没有人使用的时候才能使用卫生间.

       以上逻辑简单地说就是:如果条件不满足,则等待.当条件满足时,等待该条件的线程将被唤醒.在Java中,这个机制的实现依赖于wait()和notify().等待机制与锁机制是密切关联的.例如:

synchronized (obj){
while (!condition){
obj.wait();
} 
obj.doSomething();
}

      当线程A获得了obj锁之后,发现条件condition不满足,无法继续下一处理,于是线程A就等待(调用wait()方法).

       在另一线程B中,如果B更改了某些条件,使得线程A的condition条件满足了,就可以唤醒线程A:

synchronized (obj){
condition = true;
obj.notify();
}

4.线程调度规则

如果两个或是两个以上的线程都修改一个对象,那么把执行修改的方法定义为同步的(Synchronized),如果对象更新影响到只读方法,那么只读方法也应该定义为同步的.

如果一个线程必须等待一个对象状态发生变化,那么它应该在对象内部等待,而不是在外部等待,它可以调用一个被同步的方法,并让这个方法调用wait()方法.

每当一个方法改变某个对象的状态时,它应该调用notifyAll()方法,这给等待队列的线程提供机会,来看一看执行环境是否已发生改变.

记住wait(),notify(),notifyAll()方法都属于Object类,而不是Thread类,仔细检查看看是否每次都有相应的notify()或notifyAll()方法,且它们作用于相同的对象.

说明:在Java中每一个类都有一个主线程,要执行一个程序,那么这个类当中,一定要有main()方法,main()方法是Java类中的主线程.自己创建线程有两种方法,一种是继承Thread类,另一种是实现Runnable接口.一般情况下,最好避免继承,因为Java中是单继承的,如果继承Thread类,则无法再继承其他的类.
第二部分:实验

实验1:测试程序并进行代码注释。

测试程序1:

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

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

代码如下:

package synch;

import java.util.*;
import java.util.concurrent.locks.*;

/**
 * A bank with a number of bank accounts that uses locks for serializing access.
 * @version 1.30 2004-08-01
 * @author Cay Horstmann
 */
public class Bank
{
   private final double[] accounts;
   private Lock bankLock;
   private Condition sufficientFunds;

   /**
    * Constructs the bank.
    * @param n the number of accounts
    * @param initialBalance the initial balance for each account
    */
   public Bank(int n, double initialBalance)
   {
      accounts = new double[n];
      Arrays.fill(accounts, initialBalance);
      bankLock = new ReentrantLock();
      sufficientFunds = bankLock.newCondition();
   }

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

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

   /**
    * Gets the number of accounts in the bank.
    * @return the number of accounts
    */
   public int size()
   {
      return accounts.length;
   }
}
package synch;

/**
 * This program shows how multiple threads can safely access a data structure.
 * @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,结合程序运行结果理解程序;

掌握synchronized在多线程同步中的应用。

代码如下:

 

package synch2;

import java.util.*;

/**
 * A bank with a number of bank accounts that uses synchronization primitives.
 * @version 1.30 2004-08-01
 * @author Cay Horstmann
 */
public class Bank
{
   private final double[] accounts;

   /**
    * Constructs the bank.
    * @param n the number of accounts
    * @param initialBalance the initial balance for each account
    */
   public Bank(int n, double initialBalance)
   {
      accounts = new double[n];
      Arrays.fill(accounts, initialBalance);
   }

   /**
    * 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;
   }

   /**
    * Gets the number of accounts in the bank.
    * @return the number of accounts
    */
   public int size()
   {
      return accounts.length;
   }
}

 

package synch2;

/**
 * This program shows how multiple threads can safely access a data structure,
 * using synchronized methods.
 * @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;
     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 ThreadTest
{
 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张票

 

package cyz;
public class Demo {
    public static void main(String[] args) {
        Mythread mythread = new Mythread();
        Thread ticket1 = new Thread(mythread);
        Thread ticket2 = new Thread(mythread);
        Thread ticket3 = new Thread(mythread);
        ticket1.start();
        ticket2.start();
        ticket3.start();
    }
}
 
class Mythread implements Runnable {
    int ticket = 1;
    boolean flag = true;
 
    @Override
    public void run() {
        while (flag) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
 
            synchronized (this) {
                if (ticket <= 10) {
                    System.out.println(Thread.currentThread().getName() + "窗口售:第" + ticket + "张票");
                    ticket++;
                }
                if (ticket > 10) {
                    flag = false;
                }
            }
        }
    }
 
}

运行结果如下:

 

  

 

 

 

 

 

 第三部分:实验总结

        通过本章的学习,掌握了一些关于线程的剩下的相关知识,在学习的时候学的一片混乱,好在课下通过看书

和在Mooc上 看翁凯老师的课才对本章内容有了一定的了解,但还需要再做一些练习才能够掌握本章内容

的精髓,在做验证性实验部分内容的时候,并没有遇到很多困难,同时体验到了本章实验内容的重要性,

我会在课下多多学习,加强巩固基础知识。

posted on 2019-12-23 20:13  曹玉中-201871010105  阅读(197)  评论(1编辑  收藏  举报