一道编程题以下:java
实例化三个线程,一个线程打印a,一个线程打印b,一个线程打印c,三个线程同时执行,要求打印出10个连着的abc。编程
题目分析:安全
经过题意咱们能够得出,本题须要咱们使用三个线程,三个线程分别会打印6次字符,关键是如何保证顺序必定是abc...呢。因此此题须要同步机制来解决问题!多线程
令打印字符A的线程为ThreadA,打印B的ThreadB,打印C的为ThreadC。问题为三线程间的同步唤醒操做,主要的目的就是使程序按ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程,所以本人整理出了三种方式来解决此问题。ide
最初给个错误示例:函数
public class MyABC { public static void main(String[] args) { MyRun run = new MyRun(); Thread a = new Thread(run, "A"); Thread b = new Thread(run, "B"); Thread c = new Thread(run, "C"); a.start(); b.start(); c.start(); } } class MyRun implements Runnable { private int total = 1; private boolean goA = true; private boolean goB = false; private boolean goC = false; @Override public void run() { while (total <= 10) { if (Thread.currentThread().getName().equals("A") && goA) { System.out.print(Thread.currentThread().getName()); // 可试着用sleep来模拟CPU分配时间,看看是什么结果 // Thread.currentThread().sleep(100); // synchronized (this) { goB = true; goA = false; // } } else if (Thread.currentThread().getName().equals("B") && goB) { System.out.print(Thread.currentThread().getName()); // 可试着用sleep来模拟CPU分配时间,看看是什么结果 // Thread.currentThread().sleep(100); goC = true; goB = false; } else if (Thread.currentThread().getName().equals("C") && goC) { System.out.print(Thread.currentThread().getName()); // 可试着用sleep来模拟CPU分配时间,看看是什么结果 // Thread.currentThread().sleep(100); goA = true; goC = false; total++; } } } }
解析:性能
该代码严格依赖各个标志,一旦标志变换顺序被打乱,程序就崩了。线程的执行是由系统随机调度的,也就是当线程A执行完gob=true时,颇有可能CPU被系统收回,此时B执行,C执行,当C执行到goa=true时,假设系统把C的CPU收回,分配给A,而A获得CPU权限后继续下一条代码goa=false;此时标志位乱了,程序没法继续正常执行。由于线程是随机的,全部还有不少可能。。。别的不说,最少要在两个状态那里加上synchronized 包裹this
下面给出正确的作法:atom
1、经过两个锁(不推荐,可读性和安全性比较差)spa
(1)解法1:
/** * 基于两个lock实现连续打印abcabc.... * @author lixiaoxi * */ public class TwoLockPrinter implements Runnable { // 打印次数 private static final int PRINT_COUNT = 10; // 前一个线程的打印锁 private final Object fontLock; // 本线程的打印锁 private final Object thisLock; // 打印字符 private final char printChar; public TwoLockPrinter(Object fontLock, Object thisLock, char printChar) { this.fontLock = fontLock; this.thisLock = thisLock; this.printChar = printChar; } @Override public void run() { // 连续打印PRINT_COUNT次 for (int i = 0; i < PRINT_COUNT; i++) { // 获取前一个线程的打印锁 synchronized (fontLock) { // 获取本线程的打印锁 synchronized (thisLock) { //打印字符 System.out.print(printChar); // 经过本线程的打印锁唤醒后面的线程 // notify和notifyall都可,由于同一时刻只有一个线程在等待(由于每一个线程只有自身的锁和上一个锁,不会出现a,b,c同时抢一个锁的状况) thisLock.notify(); } // 不是最后一次则经过fontLock等待被唤醒 // 必需要加判断,否则虽然可以打印10次,但10次后就会直接死锁 //最后一次打印的时候线程A唤醒线程B,A线程等待;线程B唤醒线程C,线程B等待;线程C唤醒线程A,线程C等待(此时只有A线程不会被阻塞,线程BC会被阻塞) if(i < PRINT_COUNT - 1){ try { // 经过fontLock等待被唤醒 fontLock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //若是不加if(i < PRINT_COUNT - 1)的条件能够这么作:由于线程A不阻塞,而线程B在等待A锁,线程C在等待B锁,因此这里能够经过线程A的notify()释放A锁,来唤醒B线程 synchronized (thisLock) { thisLock.notify(); } } } } public static void main(String[] args) throws InterruptedException { // 打印A线程的锁 Object lockA = new Object(); // 打印B线程的锁 Object lockB = new Object(); // 打印C线程的锁 Object lockC = new Object(); // 打印a的线程 Thread threadA = new Thread(new TwoLockPrinter(lockC, lockA, 'A')); // 打印b的线程 Thread threadB = new Thread(new TwoLockPrinter(lockA, lockB, 'B')); // 打印c的线程 Thread threadC = new Thread(new TwoLockPrinter(lockB, lockC, 'C')); // 依次开启a b c线程 threadA.start(); Thread.sleep(100); // 确保按顺序A、B、C执行 threadB.start(); Thread.sleep(100); threadC.start(); Thread.sleep(100); } }
打印结果:
ABCABCABCABCABCABCABCABCABCABC
分析:
此解法为了为了肯定唤醒、等待的顺序,每个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是fontLock,就是前一个线程所持有的对象锁,还有一个就是自身对象锁thisLock。主要的思想就是,为了控制执行的顺序,必需要先持有fontLock锁,也就是前一个线程要释放掉前一个线程自身的对象锁,当前线程再去申请自身对象锁,二者兼备时打印,以后首先调用thisLock.notify()释放自身对象锁,唤醒下一个等待线程,再调用fontLock.wait()释放prev对象锁,暂停当前线程,等待再次被唤醒后进入循环。运行上述代码,能够发现三个线程循环打印ABC,共10次。程序运行的主要过程就是A线程最早运行,持有C,A对象锁,后释放A锁,唤醒B。线程B等待A锁,再申请B锁,后打印B,再释放B锁,唤醒C,线程C等待B锁,再申请C锁,后打印C,再释放C锁,唤醒A。看起来彷佛没什么问题,但若是你仔细想一下,就会发现有问题,就是初始条件,三个线程按照A,B,C的顺序来启动,按照前面的思考,A唤醒B,B唤醒C,C再唤醒A。可是这种假设依赖于JVM中线程调度、执行的顺序,因此须要手动控制他们三个的启动顺序,即Thread.Sleep(100)。
通俗点说:就是每一个对象只有一把锁,即lockA 一把锁,lockB一把锁,lockC一把锁,其中想进入同步代码块,执行打印,必需要具备两把锁,即threadA要执行打印,要具有lockC和lockA,threadB要执行打印,必须具有lockA和lockB,threadC要具有lockB和lockC,并且获取两把锁的顺序要一致,即获取到第一把锁以后,才能去争取第二把锁(若是不能获取第一把锁,那么取争取第二把锁的机会也没有,就不能执行run方法),线程A启动以后,在main主线程执行sleep休眠以后,得到cpu时间片开始执行run方法,此时得到lockC以后,再得到lockA,进入同步代码块,此时执行threadA执行thisLock.notify();唤醒外面等待的lockA锁的线程(这个时候B线程尚未执行,因此外面没有线程等待lockA锁,若是B线程启动以后,在lockA锁池中等待,那么执行notify()方法,会当即唤醒threadB线程,去参与lockA锁的争夺,可是notify()只是唤醒,并不会释放锁(只有在执行完synchronized (thisLock)的代码以后,才能释放thisLock的锁,很明显执行了thisLock.notify(),synchronized (thisLock)锁住的代码就已经结束,因此thisLock.notify()会当即唤醒B线程,同时释放lockB的锁),只有在threadA调用fontLock.wait()以后(线程执行完同步代码块也会释放锁),会当即释放自身持有的lockC锁(可是此时lockA锁并无释放),并进入等待,等待持有lockC锁的线程的唤醒,即threadC线程,其余代码执行过程分析省略...
注意:一个线程经过synchronized嵌套锁住多个对象,而后在最里层调用wait()函数,只释放wait()函数关联的锁对象,而不是释放线程当时持有的所有锁(对象的wait()函数,只会释放该对象的锁,不影响其余的锁的状态)。
(2)解法2:
/** * 基于两个lock实现连续打印abcabc.... * @author lixiaoxi * */ public class MyThreadPrinter2 implements Runnable { private String name; private Object prev; private Object self; private MyThreadPrinter2(String name, Object prev, Object self) { this.name = name; this.prev = prev; this.self = self; } @Override public void run() { int count = 10; while (count > 0) { synchronized (prev) { synchronized (self) { System.out.print(name); count--; //通知等待self锁的线程能够抢锁了,即唤醒等待self锁的线程,即后一个线程,但此时并无释放self的锁,只有执行完synchronized (self) {...}代码后才会释放self的锁 self.notify(); } try { prev.wait();//当前线程持有的prev的锁被释放,同时当前线程进入等待被唤醒 } catch (InterruptedException e) { e.printStackTrace(); } } } //若是缺失下面这段代码,那么会有两个线程没法退出,由于输出A的线程打印10次后,没办法调用notify()通知打印B的线程,以此内推 // synchronized (self) { // /** // *线程A被唤醒,进入最后一次打印A的时候,self.notify()会唤醒等待A锁(self)的线程(即B线程),经过prev.wait()会释放线程A持有的C锁,当前线程A等待被唤醒 // *线程B被唤醒,进入最后一次打印B的时候,self.notify()会唤醒等待B锁(self)的线程(即C线程),经过prev.wait()会释放线程B持有的A锁,此时线程B等待被唤醒 // *线程C被唤醒,进入最后一次打印C的时候,self.notify()会唤醒等待C锁(self)的线程(即A线程),经过prev.wait()会释放线程C持有的B锁,此时线程C等待被唤醒 // *总结:若是缺失这段代码,线程A会被唤醒,可是BC会阻塞,造成死锁 // *加入这段代码的效果是:最后一次线程C在执行了prev.wait()以后,会释放自身持有的A锁,此时线程A被唤醒(B,C在等待被唤醒),继续执行,进入到while(){}下面的synchronized (self) {} // *此时经过self.notify();会唤醒等待A锁的线程(B线程),在线程A执行完run()方法后,A线程会释放自身持有的A锁,此时B线程抢到A锁,继续执行while后的同步代码块,同理B线程会唤醒C线程 // */ // self.notify(); // } // System.out.print(Thread.currentThread().getName()+"---执行完毕"); } public static void main(String[] args) throws Exception { Object a = new Object(); Object b = new Object(); Object c = new Object(); MyThreadPrinter2 pa = new MyThreadPrinter2("A", c, a); MyThreadPrinter2 pb = new MyThreadPrinter2("B", a, b); MyThreadPrinter2 pc = new MyThreadPrinter2("C", b, c); new Thread(pa).start(); Thread.sleep(10); new Thread(pb).start(); Thread.sleep(10); new Thread(pc).start(); } }
先来解释一下其总体思路,从大的方向上来说,该问题为三线程间的同步唤醒操做,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。
为了控制线程执行的顺序,那么就必需要肯定唤醒、等待的顺序,因此每个线程必须同时持有两个对象锁,才能继续执行。
一个对象锁是prev,就是前一个线程所持有的对象锁。还有一个就是自身对象锁。主要的思想就是,为了控制执行的顺序,必需要先持有prev锁,也就前一个线程要释放自身对象锁,再去申请自身对象锁,二者兼备时打印,以后首先调用self.notify()释放自身对象锁,唤醒下一个等待线程,再调用prev.wait()释放prev对象锁,终止当前线程,等待循环结束后再次被唤醒。
运行上述代码,能够发现三个线程循环打印ABC,共10次。程序运行的主要过程就是A线程最早运行,持有C,A对象锁,后释放A,C锁,唤醒B。线程B等待A锁,再申请B锁,后打印B,再释放B,A锁,唤醒C,线程C等待B锁,再申请C锁,后打印C,再释放C,B锁,唤醒A。
看起来彷佛没什么问题,但若是你仔细想一下,就会发现有问题,就是初始条件,三个线程按照A,B,C的顺序来启动,按照前面的思考,A唤醒B,B唤醒C,C再唤醒A。
可是这种假设依赖于JVM中线程调度、执行的顺序。具体来讲就是,在main主线程启动ThreadA后,须要在ThreadA执行完,在prev.wait()等待时,再切回线程启动ThreadB,ThreadB执行完,在prev.wait()等待时,再切回主线程,启动ThreadC,只有JVM按照这个线程运行顺序执行,才能保证输出的结果是正确的。而这依赖于JVM的具体实现。
考虑一种状况,以下:若是主线程在启动A后,在执行A的同步代码块的过程当中又切回主线程(假设在代码self.notify以前就被切回主线程),启动了ThreadB,ThreadC,以后,因为ThreadA还没有执行self.notify,也就是ThreadB须要在synchronized(prev)处等待,而ThreadB由于等待prev的锁(A锁),而没法进入synchronized (self),因此线程B此时没有持有自身的B锁(self),而这时C却调用synchronized(prev)获取了对b的对象锁。这样,在A调用完后,同时ThreadB获取了prev也就是a的对象锁,而ThreadC持有B锁,ThreadA也释放了C锁,此时ThreadC的执行条件就已经知足了,会打印C,以后释放c,及b的对象锁,这时ThreadB具有了运行条件,会打印B,也就是循环变成了ACBACB了。这种状况,能够经过在run中主动释放CPU,来进行模拟(即在self.notify以前使用Thread.sleep(1)便可让出cpu时间片,让其余线程执行)。为了不这种与JVM调度有关的不肯定性。须要让A,B,C三个线程以肯定的顺序启动,在线程A和线程B启动以后,各加入:Thread.sleep(10);
(3)用concurrent包实现的一样的逻辑代码
/** * 基于两个lock实现连续打印abcabc.... concurrent的逻辑代码 * * @author lixiaoxi * */ public class MyThreadPrinter2 implements Runnable { private String name; private ReentrantLock prev; private ReentrantLock self; private Condition prevCondition; private Condition selfCondition; private MyThreadPrinter2(String name, ReentrantLock prev, Condition prevCondition, ReentrantLock self, Condition selfCondition) { this.name = name; this.prev = prev; this.self = self; this.prevCondition = prevCondition; this.selfCondition = selfCondition; } public void run() { int count = 10; while (count > 0) { prev.lock(); self.lock(); System.out.print(name); count--; selfCondition.signal(); self.unlock(); try { prevCondition.await(); } catch (InterruptedException e) { e.printStackTrace(); } finally { prev.unlock(); } } self.lock(); selfCondition.signal(); self.unlock(); } public static void main(String[] args) throws Exception { ReentrantLock a = new ReentrantLock(); Condition aCondition = a.newCondition(); ReentrantLock b = new ReentrantLock(); Condition bCondition = b.newCondition(); ReentrantLock c = new ReentrantLock(); Condition cCondition = c.newCondition(); MyThreadPrinter2 pa = new MyThreadPrinter2("A", c, cCondition, a, aCondition); MyThreadPrinter2 pb = new MyThreadPrinter2("B", a, aCondition, b, bCondition); MyThreadPrinter2 pc = new MyThreadPrinter2("C", b, bCondition, c, cCondition); new Thread(pa).start(); Thread.sleep(10); new Thread(pb).start(); Thread.sleep(10); new Thread(pc).start(); Thread.sleep(10); } }
2、经过一个ReentrantLock和三个conditon实现(推荐,安全性,性能和可读性较高)
package com.demo.test; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; /** * 基于一个ReentrantLock和三个conditon实现连续打印abcabc... * @author lixiaoxi * */ public class RcSyncPrinter implements Runnable{ // 打印次数 private static final int PRINT_COUNT = 10; // 打印锁 private final ReentrantLock reentrantLock; // 本线程打印所需的condition private final Condition thisCondtion; // 下一个线程打印所须要的condition private final Condition nextCondtion; // 打印字符 private final char printChar; public RcSyncPrinter(ReentrantLock reentrantLock, Condition thisCondtion, Condition nextCondition, char printChar) { this.reentrantLock = reentrantLock; this.nextCondtion = nextCondition; this.thisCondtion = thisCondtion; this.printChar = printChar; } @Override public void run() { // 获取打印锁 进入临界区 reentrantLock.lock(); try { // 连续打印PRINT_COUNT次 for (int i = 0; i < PRINT_COUNT; i++) { //打印字符 System.out.print(printChar); // 使用nextCondition唤醒下一个线程 // 由于只有一个线程在等待,因此signal或者signalAll均可以 nextCondtion.signal(); // 不是最后一次则经过thisCondtion等待被唤醒 // 必需要加判断,否则虽然可以打印10次,但10次后就会直接死锁 if (i < PRINT_COUNT - 1) { try { // 本线程让出锁并等待唤醒 thisCondtion.await(); } catch (InterruptedException e) { e.printStackTrace(); } } } } finally { // 释放打印锁 reentrantLock.unlock(); } } public static void main(String[] args) throws InterruptedException { // 写锁 ReentrantLock lock = new ReentrantLock(); // 打印a线程的condition Condition conditionA = lock.newCondition(); // 打印b线程的condition Condition conditionB = lock.newCondition(); // 打印c线程的condition Condition conditionC = lock.newCondition(); // 实例化A线程 Thread printerA = new Thread(new RcSyncPrinter(lock, conditionA, conditionB, 'A')); // 实例化B线程 Thread printerB = new Thread(new RcSyncPrinter(lock, conditionB, conditionC, 'B')); // 实例化C线程 Thread printerC = new Thread(new RcSyncPrinter(lock, conditionC, conditionA, 'C')); // 依次开始A B C线程 printerA.start(); Thread.sleep(100); //printerA.join(); printerB.start(); Thread.sleep(100); printerC.start(); } }
打印结果:
ABCABCABCABCABCABCABCABCABCABC
分析:
仔细想一想本问题,既然同一时刻只能有一个线程打印字符,那咱们为何不使用一个同步锁ReentrantLock?线程之间的唤醒操做能够经过Condition实现,且Condition能够有多个,每一个condition.await阻塞只能经过该condition的signal/signalall来唤醒!这是synchronized关键字所达不到的,那咱们就能够给每一个打印线程一个自身的condition和下一个线程的condition,每次打印字符后,调用下一个线程的condition.signal来唤醒下一个线程,而后自身再经过本身的condition.await来释放锁并等待唤醒。
注意:这里不能使用join(),由于join()方法会让主线程等待printerA线程执行完毕,而线程中有await()方法,会一直阻塞...
下面分析下线程启动的过程:
调用start()方法后,线程printerA启动,这时此线程处于就绪(可运行)状态,并无运行,一旦获得cpu时间片,就开始执行run()方法;此时主线程继续向下执行, Thread.sleep(100)让主线程阻塞此时main线程让出CPU资源,printerA得到cpu时间片,开始执行run方法,调用reentrantLock.lock();获取锁资源,进入同步方法,在printerA执行到thisCondtion.await()的时候,释放锁资源并等待被唤醒;主线程main在sleep在休眠时间事后的某个点获取cpu资源后,继续向下执行,此时printerB线程启动(此时若是printerA若是尚未执行到await()方法,则在等待;若是printerA已经执行了await(),则printerB获取锁资源,进入同步方法),如下C线程同理..
nextCondtion.signal();唤醒是由于等待了thisCondtion.await();若是不写nextCondtion.signal(),那么只要等待了就没有唤醒的机会了...
3、经过一个锁和一个状态变量来实现(推荐)
package com.demo.test; /** * 基于一个锁和一个状态变量实现连续打印abcabc... * @author lixiaoxi * */ public class StateLockPrinter { //状态变量 private volatile int state=0; // 打印线程 private class Printer implements Runnable { //打印次数 private static final int PRINT_COUNT=10; //打印锁 private final Object printLock; //打印标志位 和state变量相关 private final int printFlag; //后继线程的线程的打印标志位,state变量相关 private final int nextPrintFlag; //该线程的打印字符 private final char printChar; public Printer(Object printLock, int printFlag,int nextPrintFlag, char printChar) { super(); this.printLock = printLock; this.printFlag=printFlag; this.nextPrintFlag=nextPrintFlag; this.printChar = printChar; } @Override public void run() { //获取打印锁 进入临界区 synchronized (printLock) { //连续打印PRINT_COUNT次 for(int i=0;i<PRINT_COUNT;i++){ //循环检验标志位 每次都阻塞而后等待唤醒 while (state!=printFlag) { try { printLock.wait(); } catch (InterruptedException e) { return; } } //打印字符 System.out.print(printChar); //设置状态变量为下一个线程的标志位 state=nextPrintFlag; //注意要notifyall,否则会死锁,由于notify只通知一个, //可是同时等待的是两个,若是唤醒的不是正确那个就会没人唤醒,死锁了 printLock.notifyAll(); } } } } public void test() throws InterruptedException{ //锁 Object lock=new Object(); //打印A的线程 Thread threadA=new Thread(new Printer(lock, 0,1, 'A')); //打印B的线程 Thread threadB=new Thread(new Printer(lock, 1,2, 'B')); //打印C的线程 Thread threadC=new Thread(new Printer(lock, 2,0, 'C')); //一次启动A B C线程 threadA.start(); Thread.sleep(1000); threadB.start(); Thread.sleep(1000); threadC.start(); } public static void main(String[] args) throws InterruptedException { StateLockPrinter print = new StateLockPrinter(); print.test(); } }
打印结果:
ABCABCABCABCABCABCABCABCABCABC
分析:
状态变量是一个volatile的整型变量,0表明打印a,1表明打印b,2表明打印c,三个线程都循环检验标志位,经过阻塞前和阻塞后两次判断能够确保当前打印的正确顺序,随后线程打印字符,而后设置下一个状态字符,唤醒其它线程,而后从新进入循环。
补充题
三个Java多线程循环打印递增的数字,每一个线程打印5个数值,打印周期1-75,一样的解法:
package com.demo.test; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; /** * 数字打印,三个线程同时打印数字,第一个线程打印12345,第二个线程打印678910 ......... * @author lixiaoxi * */ public class NumberPrinter { //打印计数器 private final AtomicInteger counter=new AtomicInteger(0); private class Printer implements Runnable{ //总共须要打印TOTAL_PRINT_COUNT次 private static final int TOTAL_PRINT_COUNT = 5; //每次打印PER_PRINT_COUNT次 private static final int PER_PRINT_COUNT = 5; //打印锁 private final ReentrantLock reentrantLock; //前一个线程的condition private final Condition afterCondition; //本线程的condition private final Condition thisCondtion; public Printer(ReentrantLock reentrantLock, Condition thisCondtion,Condition afterCondition) { super(); this.reentrantLock = reentrantLock; this.afterCondition = afterCondition; this.thisCondtion = thisCondtion; } @Override public void run() { //进入临界区 reentrantLock.lock(); try { //循环打印TOTAL_PRINT_COUNT次 for(int i=0;i<TOTAL_PRINT_COUNT;i++){ //打印操做 for(int j=0;j<PER_PRINT_COUNT;j++){ //以原子方式将当前值加 1。 //incrementAndGet返回的是新值(即加1后的值) System.out.println(counter.incrementAndGet()); } //经过afterCondition通知后面线程 afterCondition.signalAll(); if(i < TOTAL_PRINT_COUNT - 1){ try { //本线程释放锁并等待唤醒 thisCondtion.await(); } catch (InterruptedException e) { e.printStackTrace(); } } } } finally { reentrantLock.unlock(); } } } public void test() throws InterruptedException { //打印锁 ReentrantLock reentrantLock=new ReentrantLock(); //打印A线程的Condition Condition conditionA=reentrantLock.newCondition(); //打印B线程的Condition Condition conditionB=reentrantLock.newCondition(); //打印C线程的Condition Condition conditionC=reentrantLock.newCondition(); //打印线程A Thread threadA=new Thread(new Printer(reentrantLock,conditionA, conditionB)); //打印线程B Thread threadB=new Thread(new Printer(reentrantLock, conditionB, conditionC)); //打印线程C Thread threadC=new Thread(new Printer(reentrantLock, conditionC, conditionA)); // 依次开启a b c线程 threadA.start(); Thread.sleep(100); threadB.start(); Thread.sleep(100); threadC.start(); } public static void main(String[] args) throws InterruptedException { NumberPrinter print = new NumberPrinter(); print.test(); } }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75