有三个窗口同时卖票à并行
共101张票,票号从1到101
在写代码之前我们先来复习一下线程的基本概念。
进程:是一个正在执行中的程序。每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者称为一个控制单元。当程序运行时,会开辟一段内存空间,进程就是用来定义和标识内存空间,并封装其中的控制单元。
线程:是真正执行的部分,是进程中的一个独立的控制单元。线程在控制着进程的执行。
(1) 一个进程中至少有一个线程。
(2) JAVA虚拟机进程至少有两个线程:主线程和垃圾回收线程。主线程执行的代码是在main中。
(3) 线程的创建有两种方式:继承Thread类并重写Thread类的run方法;实现Runable接口。
(4) CPU在线程之间切换。线程通过抢夺CPU执行权来执行。
(5) 多线程具有随机性:CPU执行权通过抢夺,具有随机性。至于执行多长时间,是有CPU决定。
线程演示:
通过继承Thread类并重写Thread类的run方法方式创建一个线程类,并在main方法中执行该线程。观察开启的自定义线程和主线程的并行执行效果。
自定义线程类:
public class DemoThread extends Thread{//java中提供的自定义线程类的方式 @Override public void run() {//将自定义线程的所有操作均写着run()中 for(int x = 0;x<60;x++) System.out.println("DemoThread-----"+x); } } |
定义主函数:
public class ThreadDemo { public static void main(String[] args) throws FileNotFoundException { DemoThread t = new DemoThread();//新建一个线程(实例化一个线程) t.start();//调用start()开启线程
for(int i=0;i<60;i++) System.out.println("mainThread++++++++++++++++++"+i); } } |
部分执行结果:
mainThread++++++++++++++++++0 DemoThread-----0 mainThread++++++++++++++++++1 DemoThread-----1 mainThread++++++++++++++++++2 DemoThread-----2 mainThread++++++++++++++++++3 DemoThread-----3 mainThread++++++++++++++++++4 mainThread++++++++++++++++++5 mainThread++++++++++++++++++6 |
可以看到主函数和自定义的线程类中run()函数交替执行。这就是cpu在线程的切换的反映,cpu执行一个线程一会,然后切换到另一个线程上。原理如下:
这里提到了线程争夺cpu的问题,我们来看一下线程的状态:
线程的状态包括:被创建,运行,冻结,消亡,临时状态。关系如下图:
其中最重要的三个状态是运行、冻结和阻塞状态。这三个状态的划分是基于两个概念:执行资格和cpu。线程要有执行资格,才能去排队【抢夺/竞争/申请】CPU。
可以这么理解:(自己看了一遍还不如上面那一句话好理解……)
线程T没来大姨妈,这时候就有了侍寝的资格,线程T的牌子会被大总管(jvm)放到盘子里面,等候cpu的决定(阻塞状态)。等啊等,cpu终于翻了线程T的牌子,你就获得了cpu,开始运动吧,你懂的(运行状态)。cpu这次不太给力啊,这么快就完了(CPU时间片结束),这个线程只能等着下次再被翻到牌子了(阻塞状态)。好不容易等到下一次,正运动着呢,谁把线程大姨妈请来了(sleep)?>_<!!线程T这段时间木有资格了(冻结状态),jvm很尽职的在这段时间不再把这个线程的牌子放进盘子。
有了上面的多线程的概念和基本用法,我们实现一下售票系统。
首先每个售票窗口都会同时卖票,那么应该将每个窗口看做是一个线程。我们首先定义售票窗口线程类。
public class TicketWindow extends Thread{ public TicketWindow(String name){ //调用父类的构造方法,可以再创建TicketWindow线程时指定线程名 super(name); } private static int ticketId = 0; //静态变量,所有售票窗口共享这一变量值 @Override public void run(){ while(true){ if(ticketId <= 100){ ticketId++; System.out.println(currentThread().getName()+"-----"+ticketId);//打印票号 } else break; } } } |
创建三个售票窗口线程(售票窗口线程类的实例化对象),并开启线程。
public class ThreadDemo1 { public static void main(String[] args) throws FileNotFoundException { TicketWindow tw1 = new TicketWindow("tw1");//创建线程tw1,表示售票窗口1 TicketWindow tw2 = new TicketWindow("tw2"); TicketWindow tw3 = new TicketWindow("tw3");
tw1.start();//开启线程 tw2.start(); tw3.start(); } } |
部分执行结果如下:可以看到三个售票线程交替卖票,结果符合我们的需求——三个窗口相互独立的卖票(结果中序号小的出现在后边是由于电脑的双核造成的,不用管)。
tw1-----1 tw1-----2 tw1-----3 tw1-----4 tw1-----5 tw1-----6 tw1-----7 tw1-----8 tw1-----9 tw2-----10 tw2-----12 tw2-----14 tw1-----11 tw2-----15 tw3-----14 tw3-----18 tw3-----19 tw2-----17 tw1-----16 tw2-----21 tw3-----20 |
其实售票系统v1程序是存在着线程安全问题的。例如
问题1:
这是会出现打印出102号票的现象,我们的票号范围是1-101.
问题2:
这时就会出现两个售票窗口都打印出来同一张票(12)的现象,而11号票没有被卖出。
这些现象都是线程安全问题。
我们可以人为的控制cpu切换来验证线程安全问题。
Java中在Thread类中提供了sleep方法,可以使线程交出cpu执行权,“睡一下”。
//线程不安全验证 public class TicketWindow extends Thread{ public TicketWindow(String name){//调用父类的构造方法,在实例化线程时可以指定它的名称 super(name); } private static int ticketId = 0;//由于三个窗口卖的票是同一套,因此应该将票号定义为共享的变量 @Override public void run(){ while(true){ if(ticketId <= 100){ try {// sleep方法有异常抛出,处理一下 Thread.sleep(100);//使线程等待100ms,此过程中释放cpu,300ms后再请求cpu } catch (InterruptedException e) {}
ticketId++; try {// sleep方法有异常抛出,处理一下 Thread.sleep(200);//使线程等待200ms,此过程中释放cpu,200ms后再请求cpu } catch (InterruptedException e) {} System.out.println(currentThread().getName()+"-----"+ticketId); }else break; } } } |
再如上面一样,创建三个线程同时卖票,部分执行结果如下:
tw2-----96 tw1-----96 tw3-----96 tw2-----99 tw3-----99 tw1-----99 tw2-----102 tw3-----102 tw1-----102 |
可以看到有些票有些票被卖了多次(96,99),没有被卖出(97,99,100,101),且出现了102票。
总结一下:
多线程安全问题出现的场景:
(1) 存在多个线程(废话。。。)
(2) 多个线程共享/操作同一个资源(例如票号)
可以看到线程安全问题是由于多个线程同时操作一个共享的资源造成的。那么我们如果在一个线程使用这个资源时,禁止其他线程再获取这个资源,也就是一次只能有一个线程操作这个资源,这种状态成为同步(我也不知道咋就叫这名了)。
java专门为多线程安全问题提供了解决机制:同步代码块。将操作共享资源的代码放到同步代码块中。yuju
…. synchronized(锁) { 需要被同步的代码 } |
一个线程进入同步代码块后,锁处于锁上的状态,该线程执行完这些代码后离开同步代码块时,锁被打开,其他线程才能够进入。所有使用同一个锁的代码块可以实现同步效果。例如下面的程序:当有线程进入同步代码1时,其他所有的线程都不能进入同步代码1和同步代码2,因为此时锁1处于锁上的状态。但可以进入同步代码3,因为同步代码3使用的是锁2,与锁1的状态没有关系。
synchronized(锁1) { 同步代码1 } synchronized(锁1) { 同步代码2 } synchronized(锁2) { 同步代码3 } |
那么锁到底是什么呢?java中使用对象作为锁,任意对象都可以作为锁。但是一定要保证相关的线程使用的是同一个锁。
public class LockThread extends Thread { private Object MyLock; public LockThread(String name, Object MyLock){ super(name); this.MyLock = MyLock; } public void run(){ System.out.println(currentThread().getName()+":我获得cpu了,准备进入同步代码块....."); synchronized(MyLock){ System.out.println(currentThread().getName()+":我进来了!"); System.out.println(currentThread().getName()+":歇会"); try{ Thread.sleep(100); }catch(Exception e){} System.out.println(currentThread().getName()+":走了!"); } } } |
主函数创建三个线程,并且传入同一个对象作为线程的锁,则这三个线程使用的是同一个锁。
public class MyThreadDemo { public static void main(String[] args) { String myLock = "hi";//任意定义一个对象,传给线程作为锁,可以是任意对象,例如int myLock = 1;也可以是内存中已经存在的对象,例如Object.class
Thread t1 = new LockThread("t1",myLock); Thread t2 = new LockThread("t2",myLock); Thread t3 = new LockThread("t3",myLock);
t1.start(); t2.start(); t3.start(); } } |
执行结果如下:可以看到当t1先进入同步代码块后,即使t3,t2获得了cpu,也没有办法进入到同步代码块中。知道t1出来,并开锁后t2才能进入。
t1:我获得cpu了,准备进入同步代码块..... t1:我进来了! t1:歇会 t3:我获得cpu了,准备进入同步代码块..... t2:我获得cpu了,准备进入同步代码块..... t1:走了! t2:我进来了! t2:歇会 t2:走了! t3:我进来了! t3:歇会 t3:走了! |
利用上面的知识,我们修改v1程序,消除线程安全问题。
找到所有【读取(使用)、修改】共享资源的语句,将他们放在同步代码块中。
public class TicketWindow extends Thread{ public TicketWindow(String name){//调用父类的构造方法,在实例化线程时可以指定它的名称 super(name); } private static int ticketId = 0;//由于三个窗口卖的票是同一套,因此应该将票号定义为共享的变量 @Override public void run(){ while(true){ synchronized(TicketWindow.class){ if(ticketId < 100){ try { Thread.sleep(100);//使线程等待300ms,此过程中释放cpu,300ms后再请求cpu } catch (InterruptedException e) { e.printStackTrace(); } ticketId++;
try { Thread.sleep(200);//使线程等待300ms,此过程中释放cpu,300ms后再请求cpu } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(currentThread().getName()+"-----"+ticketId); }else break; } } } } |
执行结果正常。
如果我们把整个run方法中的代码都放到同步代码块里会出现什么情况呢?
public void run(){ synchronized(TicketWindow.class){ while(true){ if(ticketId <= 100){ try { Thread.sleep(100);//使线程等待300ms,此过程中释放cpu,300ms后再请求cpu } catch (InterruptedException e) { e.printStackTrace(); } ticketId++;
try { Thread.sleep(100);//使线程等待300ms,此过程中释放cpu,300ms后再请求cpu } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(currentThread().getName()+"-----"+ticketId); }else break; } } } |
执行可以发现所有的票都是一个窗口卖出的:
tw1-----89 tw1-----90 tw1-----91 tw1-----92 tw1-----93 tw1-----94 tw1-----95 tw1-----96 tw1-----97 tw1-----98 tw1-----99 tw1-----100 tw1-----101 |
这是因为tw1进入同步代码块,锁上锁,执行while循环,循环过程中cpu切换到其他线程,例如tw2.tw执行run,发现锁处于锁定状态,不执行操作,cpu再切换,最后cpu只能继续执行tw1。实际上这就变成了单线程了。
从这里可以发现:
不应该将过多的代码放到同步代码块中。只将所有【读取(使用)、修改】共享资源的语句放在同步代码块中。
当同步中嵌套同步时,要注意是否会出现死锁情况。即都在等待对方线程正在占用的锁,导致程序无法继续执行。
public class DeadLockThread extends Thread{ private boolean flag; private String lockA; private String lockB;
DeadLockThread(boolean flag, String lockA, String lockB){ this.flag = flag; this. lockA = lockA; this. lockB = lockB; }
public void run(){ if (flag==true) while(true){ synchronized(lockA){ System.out.println(Thread.currentThread().getName()+"------Hold lockA,wating lcokB…."); synchronized(lockB){ System.out.println(Thread.currentThread().getName()+"=====Hold lockA and lcokB"); } } } else while(true){ synchronized(lockB){ System.out.println(Thread.currentThread().getName()+"------Hold lockB,wating lcokA…."); synchronized(lockA){ System.out.println(Thread.currentThread().getName()+"======Hold lockB and lcokA"); } } } } } |
public class DeadLockDemo { public static void main(String[] args) { String lockA = "la"; String lockB = "lb";
Thread t1 = new DeadLockThread(true, lockA, lockB); Thread t2 = new DeadLockThread(false, lockA, lockB);
t1.start(); t2.start(); } } |
执行输出如下信息,同时程序卡住:
Thread-0------Hold lockA,wating lcokB…. Thread-1------Hold lockB,wating lcokA….
|