1. 本周学习总结
1.1 以你喜欢的方式(思维导图或其他)归纳总结多线程相关内容。
2. 书面作业
本次PTA作业题集多线程
1. 源代码阅读:多线程程序BounceThread
1.1 BallRunnable类有什么用?为什么代码中需要调用Thread.sleep进行休眠?
分析: BallRunnable类实现了Runnable接口,其run方法包含线程执行体,该方法根据STEPS的值来控制绘制小球的次数,通过move方法来确定小球的位置。而使用sleep让线程进入阻塞状态,即,小球暂时停留,让人得以观察其运动轨迹。
1.2 Ball.java只做了两件事,这两件事分别是什么?BallComponent对象是干什么的?其内部的ArrayList有什么用?程序运行过程中,生成了几个BallComponent对象?该程序使用了多线程技术,每个小球是分别在不同的线程中进行绘制吗?
分析:
-
Ball.java:该类有两个方法:
-
public void move(Rectangle2D bounds)
:重绘小球时确定其位置。 -
public Ellipse2D getShape()
:得到一个Ellipse2D.Double对象,存储2D圆对象。 -
BallComponent对象:
-
加小球,画小球
-
ArrayList用于存放加入的小球
-
生成了一个BallComponent对象
-
每点击一次start按钮就产生一个线程。每个小球在自己所属线程内进行绘制
1.3 选做:程序改写:程序运行时,每个小球都是从固定位置出发。如何改写该程序,使得当点击start时,每个小球可以从不同位置出发、以不同的步进移动?
对Ball中的move方法进行修改。
x=Math.random()*bounds.getMaxX();
y=Math.random()*bounds.getMaxY();
dx=Math.random()*5;
dy=Math.random()*5;
1.4 选做:不同小球的移动轨迹一模一样。改造程序,使得每个小球运行轨迹不完全一样,比如有的可以走余弦轨迹、有的可以走方波轨迹、有的走布朗运动、有的走五角星,等等。
2. 实验总结:题集(多线程)
2.1 题目:Thread、PrintTask、Runnable与匿名内部类。并回答:a)通过定义Runnable接口的实现类来实现多线程程序比通过继承自Thread类实现多线程程序有何好处?b) 6-1,6-3,6-11实验总结。
分析:若我们创建的类继承自Thread那么该类就是一个Thread,若我们通过实现Runnable接口,则我们创建的类还可以再继承自其他类,总的来说操作Runnable接口的好处就是比较有弹性。
总结:
6-1:通过继承Thread并重写run方法来创建线程,Thread本身就实现了Runnable接口
6-3:使用Thread.currentThread()
获得当前执行线程对象,从而获得线程名
6-11:通过实现Runnable接口,并重写run方法来创建线程。
2.2 使用Lambda表达式改写6-3
Thread t1 = new Thread(()-> {
System.out.println(mainThreadName);
System.out.println(Thread.currentThread().getName());
System.out.println(Arrays.toString(Thread.class.getInterfaces()));
});
2.3 题目:6-2(Runnable与停止线程)。回答:需要怎样才能正确地停止一个运行中的线程?
当run或call方法执行完后线程正常死亡,由于stop方法易产生错误,故可以在run或call方法中依据标志位进行循环控制实现线程的停止。
2.4 选做:6-8(CountDownLatch)实验总结
总结:
-
CountDownLatch:一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
-
ExecutorService:提交任务,并进行调度执行。通过 Executors 类来创建的线程池的类型:拥有固定线程数的线程池,如果没有任务执行,那么线程会一直等待,
Executors.newFixedThreadPool()
2.5 选做:6-9(集合同步问题)实验总结
总结: 使用Collections.synchronizedList(new ArrayList<Integer>());
将传入的操作对象打包,返回线程安全的对象
2.6 选做:较难:6-10(Callable),并回答为什么有Runnable了还需要Callable?实验总结。
总结: 其实Callable和Runnable都是将方法包装成线程执行体,区别就是Callable中的call方法具有返回值,可以声明抛出异常对象。正是利用这个性质我们可以通过call方法返回n对应的斐波那契值,关联Fututr得到返回值,继而计算斐波那契数列的前n项和。
3. 互斥访问
3.1 修改TestUnSynchronizedThread.java源代码使其可以同步访问。(关键代码截图,需出现学号)
对Counter类中的方法进行了修改
修改后的代码
class Counter {
private volatile static int id = 0;
public synchronized static void addId() {
id++;
}
public synchronized static void subtractId() {
id--;
}
public static int getId() {
return id;
}
}
运行结果
3.2 选做:进一步使用执行器改进相应代码(关键代码截图,需出现学号)
参考资料:Java多线程之Executor、ExecutorService、Executors、Callable、Future与FutureTask
/**
*
* @author 周文华
*
*/
public class TestUnSynchronizedThread {
public static void main(String[] args) throws InterruptedException {
List<Callable<Integer>> taskList = new ArrayList<>();
ExecutorService exec = Executors.newSingleThreadScheduledExecutor();
for (int i = 0; i < 6; i++) {
if (i < 3)
taskList.add(new Adder());
else
taskList.add(new Subtracter());
}
List<Future<Integer>>list=exec.invokeAll(taskList);
exec.shutdown();
try {
System.out.println(list.get(list.size()-1).get());
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("main end");
}
}
class Adder implements Callable<Integer> {
@Override
public Integer call() {
for (int i = 0; i < 10000; i++)
Counter.addId();
System.out.println(Thread.currentThread().getName() + " end");
return Counter.getId();
}
}
class Subtracter implements Callable<Integer> {
@Override
public Integer call() {
for (int i = 0; i < 10000; i++)
Counter.subtractId();
System.out.println(Thread.currentThread().getName() + " end");
return Counter.getId();
}
}
class Counter {
private volatile static int id = 0;
public synchronized static void addId() {
id++;
}
public synchronized static void subtractId() {
id--;
}
public static int getId() {
return id;
}
}
运行结果
4. 互斥访问与同步访问
完成题集6-4(互斥访问)与6-5(同步访问)
4.1 除了使用synchronized修饰方法实现互斥同步访问,还有什么办法可以使用synchronized实现互斥同步访问,使用代码说明(请出现相关代码及学号)?
分析:还可以通过同步代码块实现互斥访问
代码:
package Test02;
import javax.swing.plaf.SliderUI;
public class SynchronizedTest {
//锁定方法,加锁对象SynchronizedTest实例
public synchronized void method1() {
System.out.println("method 1 ...");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//锁定SynchronizedTest对象
public void method2() {
synchronized (this) {
System.out.println("method 2 ...");
}
}
//方法内部进行锁定,锁定对象为str
public void method3() {
String str = "mehtod 3 ...";
synchronized (str) {
System.out.println(str);
}
}
public static void main(String[] args) {
SynchronizedTest test = new SynchronizedTest();
Thread th1 = new Thread(new Runnable() {
@Override
public void run() {
test.method1();
}
});
Thread th2 = new Thread(new Runnable() {
@Override
public void run() {
test.method2();
}
});
Thread th3 = new Thread(new Runnable() {
@Override
public void run() {
test.method3();
}
});
th1.start();
th2.start();
th3.start();
}
}
4.2 同步代码块与同步方法有何区别?
分析:由上例三个method可以看出使用同步代码块可以更实现精细的锁定,我们可以锁定只会发生竞速状况的区块。
4.3 实现互斥访问的原理是什么?请使用对象锁概念并结合相应的代码块进行说明。当程序执行synchronized同步代码块或者同步方法时,线程的状态是怎么变化的?
分析:我们知道在线程是并发进行的,即在同一时刻只能有一条指令执行,但多个线程指令快速轮换执行,CPU执行速度很快,使得在宏观上有同时执行的效果。线程同步最经典的例子是存取取钱的问题,若采用并发线程,进行取钱操作,很可能由于线程调度的不确定性,使的取出的钱数大于账户余额,这时我们就需要一个同步监视器了,在我们进行取钱操作之前得先获得同步监视器的锁定,即获得一个共有资源的使用权,使用期间其他线程无法对该资源进行操作,使用完之后再把锁交出,让其他线程去竞争使用权,从而保证了线程安全性。
4.4 Java多线程中使用什么关键字实现线程之间的通信,进而实现线程的协同工作?
分析:可以使用Object类的wait(),notify(),notifyAll(),但是这些方法必须由同步监视器对象来调用。
wait():导致当前线程等待,直到其他线程调用该同步监视器的notify(),notifyAll()方法。
notify():唤醒在此同步监视器上等待的单个线程,注意,只有在当前线程放弃了同步监测器的锁定之后(使用wait()方法后),才可以执行被唤醒的线程,选择随意。
notifyAll():唤醒所有在此监视器上等待的线程其他与notify()类似。
5. 线程间的合作:生产者消费者问题
5.1 运行MyProducerConsumerTest.java。正常运行结果应该是仓库还剩0个货物。多运行几次,观察结果,并回答:结果正常吗?哪里不正常?为什么?
分析:分析代码可知,该程序功能是放入先100个货物再取出100个货物,结果应当为0,执行几次后发现结果并不是正确的,应为仓库的容量为10,当仓库满时就应该停止添加操作,唤醒取出操作,而程序没有做这些操作,只是一个线程只负责添加,一个线程只负责取出,两线程之间没有实现通信。
5.2 使用synchronized, wait, notify解决该问题(关键代码截图,需出现学号)
public synchronized void add(String t) {
if (repo.size() >= capacity) {
System.out.println("仓库已满!无法添加货物。");
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
repo.add(t);
notify();
}
}
public synchronized void remove() {
if (repo.size() <= 0) {
System.out.println("仓库无货!无法从仓库取货");
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
repo.remove(0);
notify();
}
}
5.3 选做:使用Lock与Condition对象解决该问题。
class Repository {// 存放字符串的仓库
private int capacity = 10;// 仓库容量默认为10
private List<String> repo = new ArrayList<String>();// repo(仓库),最多只能放10个
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void add(String t) {
try {
lock.lock();
while(repo.size() >= capacity) {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
repo.add(t);
condition.signal();
} finally {
lock.unlock();}
}
public void remove() {
try {
lock.lock();
while (repo.size() <= 0) {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
repo.remove(0);
condition.signal();
} finally {
lock.unlock();
}
}
public int size() {
try {
lock.lock();
return repo.size();
} finally {
lock.unlock();
}
}
}
6. 面向对象设计作业-图书馆管理系统
6.1 系统的功能模块表格,表格中体现出每个模块的负责人。
6.2 运行视频
6.3 讲解自己负责的模块,并粘贴自己负责模块的关键代码(出现学号及姓名)。
系统的基本类及类图
给出关键类截图
代码写的很烂,改进中,希望大家给我一些建议。
public class Entry {
/**
* 职业选择,返回对应的实例变量。
*
* @param profession
* @return
*/
public static User professionJudge(String profession) {
if (profession.equalsIgnoreCase("Administrator")) {
Administrator administrator = new Administrator();
administrator.setAdmit(true);
return administrator;
}
if (profession.equalsIgnoreCase("Student"))
return new Student();
if (profession.equalsIgnoreCase("Teacher"))
return new Teacher();
else
return null;
}
/**
* 填写注册前的基本信息。 返回一个user
*
* @param userNo
* @param name
* @param userId
* @param password
* @return
*/
public static <T extends User> T writePersonalInformation(String profession, Long userNo, String name,
String userId, String password) {
User user = professionJudge(profession);
if (user != null) {
LoginInformation info = new LoginInformation();
info.setId(userId);
info.setPassword(password);
user.setUserNo(userNo);
user.setUserName(name);
user.setLoginInformation(info);
user.setLibraly(new Library());
return (T) user;
}
return null;
}
/**
* 将user加入users 只是先将user加入到user仓库
*
* @param user
* @param set
* @return
*/
public static <T extends User> boolean register(User user, UserStorage storage) {
boolean flag = false;
ObjectInputStream input = null;
ObjectOutputStream output = null;
if (user != null) {
// 先判断user仓库是否有user信息,若没有则去文件中查找
if (storage.isExit(user.getLoginInformation().getId()) == null) {
try {
// 打开文件成功,说明文件存在,不注册
input = new ObjectInputStream(
new BufferedInputStream(new FileInputStream(user.getLoginInformation().getId())));
} catch (FileNotFoundException e) {
// 打文件开不成功,说明文件不存在那就注册。
storage.addUser(user);
flag = true;
/*
* try { output = new ObjectOutputStream( new BufferedOutputStream(new
* FileOutputStream(user.getLoginInformation().getId())));
* output.writeObject(user); System.out.println("已经注册"); flag = true; } catch
* (FileNotFoundException e1) { e1.printStackTrace(); } catch (IOException e1) {
* e1.printStackTrace(); } finally { try { output.close(); } catch (IOException
* e1) { e1.printStackTrace(); } }
*/
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (input != null)
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
return flag;
/*
* boolean result = storage.isExit(user.getLoginInformation().getId()); if
* (result) { return false; } else { storage.addUser(user); return true; }
*/
}
/**
* 登录 直接从文件读取内容
*
* @param name
* @param password
* @param users
* @return
*/
public static int login(String userId, String password, UserStorage users) {
ObjectInputStream input = null;
User user = null;
int flag = 0;
if (users.isExit(userId) == null) {
try {
input = new ObjectInputStream(new BufferedInputStream(new FileInputStream(userId)));
try {
user = (User) input.readObject();
if (user.getLoginInformation().getPassword().equals(password)) {
// 登录成功,將user加入Users最后退出系统时自动写入文件
users.addUser(user);
flag = 1;
} else {
// 帐号密码不匹配
flag = 0;
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} catch (FileNotFoundException e) {
// 该帐号没有注册
flag = -1;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
try {
if (input != null)
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
} else {
user = users.isExit(userId);
if (password.equals(user.getLoginInformation().getPassword())) {
flag = 1;
} else {
flag = 0;
}
}
return flag;
}
/*
* boolean result = users.isExit(name); if (result) { int flag = 0;
* Iterator<User> iterator = users.getUserSet().iterator(); while
* (iterator.hasNext()) { if
* (password.equals(iterator.next().getLoginInformation().getPassword())) flag =
* 1; // 登录成功 else flag = 0; // 帐号密码不匹配 } return flag;
*
* } else { return -1; // 帐号不存在
*
* }
*
* }
*/
/**
* 程序开始时恢复书库信息
*
* @return
*/
public static StackRoom restoreStackRoom() {
StackRoom stackRoom = null;
try {
ObjectInputStream input = new ObjectInputStream(new BufferedInputStream(new FileInputStream("StackRoom")));
try {
stackRoom = (StackRoom) input.readObject();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
;
return stackRoom;
}
/**
* 程序结束时使用,更新书库信息
* 由于用户借书,管理员添加,删除书造成书本数量上的变化
* @param stackRoom
*/
public static void saveOrUpdateStackRoom(StackRoom stackRoom) {
ObjectOutputStream output = null;
try {
output = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream("StackRoom")));
output.writeObject(stackRoom);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (output != null) {
try {
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
public class Library implements Universal, Serializable {
private Map<String, BookItem> libraryMap = new TreeMap<>();
public <T> void print(Collection<T> value) {
Iterator iterator = value.iterator();
while (iterator.hasNext())
System.out.println(iterator.next());
}
public void showBooks() {
Collection<BookItem> BooksItems = libraryMap.values();
Iterator<BookItem> iterator = BooksItems.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
;
}
}
private boolean isExit(String bookId) {
return libraryMap.containsKey(bookId);
}
/**
* 借书,借书成功书库中该书数量减一
*
* @param bookId
* @param stackRoom
* @return
*/
public int borrowBooks(String bookId, StackRoom stackRoom) {
Collection<Set<BookItem>> setBooksItems = stackRoom.getBooksMap().values();
Iterator<Set<BookItem>> iterator = setBooksItems.iterator();
while (iterator.hasNext()) {
Set<BookItem> setBookItem = iterator.next();
Iterator setBooks = setBookItem.iterator();
while (setBooks.hasNext()) {
BookItem bookItem = (BookItem) setBooks.next();
Book book = bookItem.getBook();
if (book.getBookId().equals(bookId)) {
boolean result = isExit(bookId);
if (result) {
BookItem a = libraryMap.get(bookId);
System.out.println(a);
if (a.getCount() >= bookItem.getCount()) {
return 0; // 该书以及全部借出
} else {
a.setCount(a.getCount() + 1);
// 书库的书数量减一
bookItem.setCount(bookItem.getCount() - 1);
return 1; // 借书成功
}
} else {
libraryMap.put(bookId, new BookItem(book, 1));
return 1;
}
}
}
}
return -1; // bookId有误,不存在该书
}
/**
* 还书,还书成功书库中该书数量加一
*
* @param bookId
* @return
*/
public boolean returningBooks(String bookId, StackRoom stackRoom) {
if (libraryMap.containsKey(bookId)) {
String category = libraryMap.get(bookId).getBook().getCategory();
libraryMap.remove(bookId);
Set<BookItem> bookItems = stackRoom.getBooksMap().get(category);
for (BookItem bookItem : bookItems) {
if (bookItem.getBook().getBookId().equals(bookId)) {
bookItem.setCount(bookItem.getCount() + 1);
break;
}
}
return true;
} else {
return false;
}
}
}
public class StackRoom implements Serializable{
private Map<String, Set<BookItem>> booksMap = new HashMap<>();
//已经填入数据,该方法废弃
/*static {
Set<BookItem> economicalBook = new HashSet<>();
economicalBook.add(new BookItem(
new EconomicalBook("001", "货币金融学", "弗雷德克里S·米什金", "经济", new GregorianCalendar(2001, 6, 1)), 5));
economicalBook.add(
new BookItem(new EconomicalBook("002", "博弈论与信息学", " 张维迎", "经济", new GregorianCalendar(2001, 6, 1)), 5));
economicalBook.add(
new BookItem(new EconomicalBook("003", "国富论", "亚当.斯密", "经济", new GregorianCalendar(2001, 6, 1)), 5));
economicalBook.add(new BookItem(
new EconomicalBook("004", "牛奶可乐经济学", "弗雷德克里S·米什金", "经济", new GregorianCalendar(2001, 6, 1)), 5));
booksMap.put("经济", economicalBook);
Set<BookItem> militaryBook = new HashSet<>();
militaryBook
.add(new BookItem(new MilitaryBook("005", "孙子兵法", "孙子", "军事", new GregorianCalendar(2001, 6, 1)), 5));
militaryBook.add(new BookItem(
new MilitaryBook("006", "第二次世界大战史", "马丁·吉尔伯特 ", "军事", new GregorianCalendar(2001, 6, 1)), 5));
militaryBook
.add(new BookItem(new MilitaryBook("007", "太平洋战争", "青梅煮雨", "军事", new GregorianCalendar(2001, 6, 1)), 5));
militaryBook
.add(new BookItem(new MilitaryBook("0010", "梦残干戈", "黄朴民", "军事", new GregorianCalendar(2001, 6, 1)), 5));
booksMap.put("军事", militaryBook);
}*/
/**
* 打印
*
* @param value
*/
public <T> void print(Collection<T> value) {
if (value == null)
System.out.println("图书馆无该种类书籍相关信息");
else {
Iterator iterator = value.iterator();
while (iterator.hasNext())
System.out.println(iterator.next());
}
}
public Map<String, Set<BookItem>> getBooksMap() {
return booksMap;
}
public void setBooksMap(Map<String, Set<BookItem>> booksMap) {
this.booksMap = booksMap;
}
/**
* 展示书库,配合搜索使用
*/
public void showBooks() {
Collection<Set<BookItem>> bookItemSet = booksMap.values();
Iterator<Set<BookItem>> iteratorSet = bookItemSet.iterator();
while (iteratorSet.hasNext()) {
print(iteratorSet.next());
}
}
/**
* 搜索:依据书的类别
* 返回Set,使用print()进行打印
* @param key
* @return
*/
public Set<BookItem> searchBooks(String key) {
Set<BookItem> bookItems = booksMap.get(key);
return bookItems;
}
}
7. 选做:使用其他方法解决题目5的生产者消费者问题。
7.1 使用BlockingQueue解决生产者消费者问题关键代码截图
关键代码
class Repository {// 存放字符串的仓库
BlockingQueue repo = new LinkedBlockingQueue(10);
public void add(String t) {
try {
repo.put(t);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void remove() {
try {
repo.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public int size(){
return repo.size();
}
}
7.2 说明为什么不需要显示的使用wait、notify就可以解决同步问题。这样解决相比较wait、notify有什么优点吗?
分析: BlockingQueue顾名思义,阻塞队列。当队列满了的时候进行出队列操作,当队列空了的时候进行入队列操作,我们可以设置阻塞队列的容量,入队操作采用put()方法时,若队列已满则调用此方法的线程被阻断,直到有空间再继续,出队操作采用take()方法时,若队列为空,则阻断进入等待状态直到队列有新的对象被加入为止。
优点:就是我们可以坐享其成,虽然API很强大,但我们也要了解其原理,不能当只会查API的程序员。
7.3 使用Condition解决生产者、消费者问题。
class Repository {// 存放字符串的仓库
private int capacity = 10;// 仓库容量默认为10
private List<String> repo = new ArrayList<String>();// repo(仓库),最多只能放10个
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void add(String t) {
try {
lock.lock();
while(repo.size() >= capacity) {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
repo.add(t);
condition.signal();
} finally {
lock.unlock();}
}
public void remove() {
try {
lock.lock();
while (repo.size() <= 0) {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
repo.remove(0);
condition.signal();
} finally {
lock.unlock();
}
}
public int size() {
try {
lock.lock();
return repo.size();
} finally {
lock.unlock();
}
}
}
8. 选做:编写一段代码,证明你会使用ForkJoinPool.
3.码云及PTA
题目集:多线程
3.1. 码云代码提交记录
- 在码云的项目中,依次选择“统计-Commits历史-设置时间段”, 然后搜索并截图
必须出现几个要素:提交日期-用户名(姓名与学号)-不提交说明
3.2 截图"多线程"PTA提交列表
- 需要有两张图(1. 排名图。2.PTA提交列表图)
3.3 统计本周完成的代码量
- 需要将每周的代码统计情况融合到一张表中
周次 | 总代码量 | 新增文件代码量 | 总文件数 | 新增文件数 |
---|---|---|---|---|
1 | 665 | 20 | 20 | 20 |
2 | 1705 | 23 | 23 | 23 |
3 | 1834 | 30 | 30 | 30 |
4 | 1073 | 1073 | 17 | 17 |
5 | 1073 | 1073 | 17 | 17 |
6 | 2207 | 1134 | 44 | 27 |
7 | 3292 | 1085 | 59 | 15 |
8 | 3505 | 213 | 62 | 3 |
9 | 8043 | 1246 | 153 | 16 |
10 | 8606 | 543 | 167 | 14 |
11 | 9203 | 597 | 191 | 24 |
12 | 9203 | 0 | 191 | 0 |