黑马程序员 Java基础<九>---> 多线程
多线程
一、概述:
1、线程是什么
说到线程,我们就得先说说进程。所谓进程,就是一个正在执行(进行)中的程序,每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。如我们常用的QQ,打开运行它时它就是一个进程,在windows下我们通常都可以通过任务管理器中的进程来查看正在运行的进程有哪些。线程,就是进程中的一个独立的控制单元,线程在控制着进程的执行,一个进程至少有一个线程。比方说,办一批东西,搬东西整体就是一个进程,而搬东西的每个人就是一个线程。
2、java中的线程:
在java中,JVM虚拟机启动时,会有一个进程为java.exe,该程序中至少有一个线程负责java程序的执行;而且该程序运行的代码存在于main方法中,该线程称之为主线程。其实,JVM启动时不止有一个线程(主线程),还有负责垃圾回收机制的线程。
3、多线程的意义:
简单的说,就是提高程序的执行效率。
二、自定义线程:
第一种:创建线程的第一种方式:继承Thread类。
1、步骤:
1)定义类继承Thread。
2)复写Thread类中的run方法。目的:将自定义代码存储在run方法。让线程运行。
3)调用线程的start方法,该方法两个作用:启动线程,调用run方法。
class Demo extends Thread
{
public void run()
{
for(int x=0; x<60; x++)
System.out.println("demo run----"+x);
}
}
class ThreadDemo
{
public static void main(String[] args)
{
//for(int x=0; x<4000; x++)
//System.out.println("Hello World!");
Demo d = new Demo();//创建好一个线程。
d.start();//开启线程并执行该线程的run方法。
//d.run();//仅仅是对象调用方法。而线程创建了,并没有运行。
for(int x=0; x<60; x++)
System.out.println("Hello World!--"+x);
}
}
通过结果发现运行结果每一次都不同。因为多个线程都获取cpu的执行权。cpu执行到谁,谁就运行。明确一点,在某一个时刻,只能有一个程序在运行。(多核除外)cpu在做着快速的切换,以达到看上去是同时运行的效果。我们可以形象把多线程的运行行为在互相抢夺cpu的执行权。这就是多线程的一个特性:随机性。谁抢到谁执行,至于执行多长,cpu说的算。
2、为什么要覆盖run方法呢?
Thread类用于描述线程,该类就定义了一个功能,用于存储线程要运行的代码。该存储功能就是run方法。也就是说Thread类中的run方法,用于存储线程要运行的代码。
3、线程的状态:
1)被创建:线程被创建,进入阻塞状态,cpu执行它的时候他才正式进入运行状态
2)运行:运行状态,线程被运行
3)冻结(放弃了执行资格):冻结分为sleep和wait,sleep(time)时间结束后进入阻塞状态,当cpu执行它的时候它才正式进入运行状态。还有一种是wait,当wait的时候必须被notify()唤醒之后才能具备执行资格,也就是唤醒后就进入阻塞状态
4)阻塞(也称为临时状态,这个状态是具有执行资格,但是没有执行权):当线程具备运行资格,但是没有执行权的时候就出于阻塞状态
5)消亡:stop和run方法结束,线程就消亡了。
第二种:实现Runnable接口
1、步骤:
第一、定义类实现Runnable接口。
第二、覆盖Runnable接口中的run方法。
第三、通过Thread类建立线程对象。要运行几个线程,就创建几个对象。
第四、将Runnable接口的子类对象作为参数传递给Thread类的构造函数。为什么要将Runnable接口的子类对象传递给Thread的构造函数。因为,自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程去执行指定对象的run方法。就必须明确该run方法所属对象。
第五、调用Thread类的start方法开启线程,并调用Runnable接口子类的run方法。
class Ticket implements Runnable//extends Thread
{
private int tick = 100;//多窗口共卖100张票
public void run()
{
while(true)//让循环多执行,目的看出执行的效果,这样线程就必须手动停止,可以加条件让它停止
{
if(tick>0)
{
System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);//创建了一个线程;
Thread t2 = new Thread(t);//创建了一个线程;
Thread t3 = new Thread(t);//创建了一个线程;
Thread t4 = new Thread(t);//创建了一个线程;
t1.start();
t2.start();
t3.start();
t4.start();
/*
Ticket t1 = new Ticket();
//Ticket t2 = new Ticket();
//Ticket t3 = new Ticket();
//Ticket t4 = new Ticket();
t1.start();
t1.start();
t1.start();
t1.start();
*/
}
}
2、实现方式和继承方式有什么区别呢?
1)实现方式好处:避免了单继承的局限性。在定义线程时,建议使用实现方式。
2)两种方式区别:
继承Thread:线程代码存放Thread子类run方法中。
实现Runnable,线程代码存在接口的子类的run方法。
需要注意的是:局部变量在每一个线程中都独有一份。
三、多线程安全问题:
1、通过上面卖票的例子运行结果出现了0,-1,-2等错票。
2、问题的原因:当多条语句在操作同一个线程共享数据是,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误。
3、解决办法:对多条操作共享数据的语句,只能让一个线程都执行完后才让其他线程执行,这样就不会造成共享数据的错乱了。、、
4、Java对于多线程的安全问题提供了专业的解决方式。就是同步代码块。
synchronized(对象)
{
需要被同步的代码(操作共享数据的代码)
}
对象如持有锁,持有锁的线程可以在同步代码块中执行。没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。
5、同步的前提:
1)必须有两个或两个以上的线程
2)必须是多个线程使用同一个锁
6、同步作用:保证同步中只能有一个线程在运行。
7、同步的好处:解决了多线程的安全问题。
8、同步的弊端:多个线程需要判断锁,较为消耗资源。
9、同步代码块解决安全问题
synchronized(对象)//对象称为锁旗标
{
需要被同步的代码
}
卖票的实例:
class Ticket implements Runnable
{
private int tick = 1000;
Object obj = new Object();
public void run()
{
while(true)//让循环多执行,目的看出执行的效果,这样线程就必须手动停止,可以加条件让它停止
{
synchronized(obj)//任意对象
{
if(tick>0)
{
//try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
}
}
}
}
}
class TicketDemo2
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
10、同步函数
同步函数就是将修饰符synchronized放在返回类型的前面,一般函数内代码较少,下面通过同步函数给出多线程安全问题的具体解决方案:
/*
* 需求: 银行有一个金库。 有两个储户分别存300员,每次存100,存3次。
*
* 目的:该程序是否有安全问题,如果有,如何解决?
*
*
* 如何找问题: 1,明确哪些代码是多线程运行代码。 2,明确共享数据。 3,明确多线程运行代码中哪些语句是操作共享数据的。
*/
class Bank {
private int sum;
// Object obj = new Object();
public synchronized void add(int n)// 同步函数
{
// synchronized(obj)//同步代码块
// {
sum = sum + n;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sum=" + sum);
// }
}
}
class Cus implements Runnable {
private Bank b = new Bank();
public void run() {
for (int x = 0; x < 3; x++) {
b.add(100);
}
}
}
class BankDemo {
public static void main(String[] args) {
Cus c = new Cus();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
11、同步中的锁:
1)非静态同步函数中的锁---> this
函数需要被对象调用。那么函数都有一个所属对象引用。就是this。所以同步函数使用的锁是this。
锁的验证,原理只有使用的是同一个锁才能进行同步:
class Ticket implements Runnable
{
private int tick = 100;
Object obj = new Object();
boolean flag = true;
public void run()
{
if(flag)
{
while(true)
{
synchronized(this)
{
if(tick>0)
{
try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"....code : "+ tick--);
}
}
}
}
else
while(true)
show();
}
public synchronized void show()//this
{
if(tick>0)
{
try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"....show.... : "+ tick--);
}
}
}
class ThisLockDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try{Thread.sleep(10);}catch(Exception e){}
t.flag = false;
t2.start();
// Thread t3 = new Thread(t);
// Thread t4 = new Thread(t);
// t3.start();
// t4.start();
}
}
2)静态的同步方法,使用的锁是该方法所在类的字节码文件对象。 类名.class
通过验证,发现不在是this。因为静态方法中也不可以定义this。静态进内存是,内存中没有本类对象,但是一定有该类对应的字节码文件对象。因为,static是随着类的加载而加载的,类在加载的时候会差生一个字节码class对象。类名.class 该对象的类型是Class
锁的验证,原理只有使用的是同一个锁才能进行同步:
class Ticket implements Runnable
{
private static int tick = 100;
//Object obj = new Object();
boolean flag = true;
public void run()
{
if(flag)
{
while(true)
{
synchronized(Ticket.class)
{
if(tick>0)
{
try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"....code : "+ tick--);
}
}
}
}
else
while(true)
show();
}
public static synchronized void show()
{
if(tick>0)
{
try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"....show.... : "+ tick--);
}
}
}
class StaticMethodDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try{Thread.sleep(10);}catch(Exception e){}
t.flag = false;
t2.start();
}
}
12、单利设计模式:
1)解决问题:解决一个类在内存中只存在一个对象的问题
2)如何保证对象的唯一性:
①、为避免建立过多的该类对象,应首先禁止其他应用程序创建该类对象。
②、为让其他应用程序访问到该对象,在本类中自定义一个对象,为避免直接访问该对象,要对其进行私有化。
③、提供访问方式,便于其他程序对自定义对象的访问,提供的访问方法是公有的。
3)具体实现:
class Single1 {
private static Single1 s1 = new Single1();//饿汉式,上来就创建对象
private Single1() {
}
public static Single1 getInstance() {
return s1;
}
}
class Single2 {
private static Single2 s2 = null;//懒汉式,需要的时候才创建对象,称为对象的延迟加载
private Single2() {
}
public static Single2 getInstance() {
if (s2 == null) {
synchronized (Single2.class) {
if (s2 == null) {
s2 = new Single2();
}
}
}
return s2;
}
}
懒汉式因为操作共享数据的代码有多条,所以在多线程时存在安全隐患。所以给出了以上双重循环的方式,这样做也提高了代码的执行效率。
比如说,当A调用时,当读到if(s1==null) 时,可能就停在这了,然后cpu再调用B,B也读到if(s1==null)这停下了,cpu再切换到A,接着创建一个对象,A就执行完了;之后B也向下执行,又创建一个对象;此时,对象就不唯一了,就破坏了对象的唯一性的初衷。那么解决方案是这样的:
这利用了锁的机制。synchronized是java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。这涉及到了多线程的问题。在这个例子中,比如说,当A调用时,当读到第二个if(s1==null) 时,可能就停在这了,然后cpu再调用B,B读到第一个if(s1==null)这停下了,因为加上synchronized后,A进去就相当于将其他的调用锁在外面的语句上了,要先执行完A,那么A执行完后,就已经创建了一个对象;当B再读到第二个if(s1==null)的时候不符合就直接结束了。如果再有其他C或D等调用的时候,就直接不符合第一个(s1==null)的条件,所以直接返回s。
四、死锁:
1、造成原因:同步中嵌套同步。而且锁不相同
实例一:
class Ticket implements Runnable
{
private int tick = 1000;
Object obj = new Object();
boolean flag = true;
public void run()
{
if(flag)
{
while(true)
{
synchronized(obj)
{
show();
}
}
}
else
while(true)
show();
}
public synchronized void show()//this
{
synchronized(obj)
{
if(tick>0)
{
try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"....code : "+ tick--);
}
}
}
}
class DeadLockDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try{Thread.sleep(10);}catch(Exception e){}
t.flag = false;
t2.start();
}
}
实例二:
class Test implements Runnable
{
private boolean flag;
Test(boolean flag)
{
this.flag = flag;
}
public void run()
{
if(flag)
{
while(true)
{
synchronized(MyLock.locka)
{
System.out.println(Thread.currentThread().getName()+"...if locka ");
synchronized(MyLock.lockb)
{
System.out.println(Thread.currentThread().getName()+"..if lockb");
}
}
}
}
else
{
while(true)
{
synchronized(MyLock.lockb)
{
System.out.println(Thread.currentThread().getName()+"..else lockb");
synchronized(MyLock.locka)
{
System.out.println(Thread.currentThread().getName()+".....else locka");
}
}
}
}
}
}
class MyLock
{
static Object locka = new Object();
static Object lockb = new Object();
}
class DeadLockTest
{
public static void main(String[] args)
{
Thread t1 = new Thread(new Test(true));
Thread t2 = new Thread(new Test(false));
t1.start();
t2.start();
}
}
死锁是在开发中应该避免的地方,首先知道了它,我们就可以去避免它的出现啦!
五、多线程间的通信:
1、概述:
线程间通讯:其实就是多个线程在操作同一个资源,但是操作的动作不同。
看看下面的实例:
class Res
{
String name;
String sex;
boolean flag = false;
}
class Input implements Runnable
{
private Res r ;
Input(Res r)
{
this.r = r;
}
public void run()
{
int x = 0;
while(true)
{
synchronized(r)
{
if(r.flag)
try{r.wait();}catch(Exception e){}//等待的线程进入线程池,而且在线程池中是有序存放的
if(x==0)
{
r.name="mike";
r.sex="man";
}
else
{
r.name="丽丽";
r.sex = "女女女女女";
}
x = (x+1)%2;
r.flag = true;
r.notify();//通常唤醒的是线程池中最先等待的线程
}
}
}
}
class Output implements Runnable
{
private Res r ;
Output(Res r)
{
this.r = r;
}
public void run()
{
while(true)
{
synchronized(r)
{
if(!r.flag)
try{r.wait();}catch(Exception e){}
System.out.println(r.name+"...."+r.sex);
r.flag = false;
r.notify();
}
}
}
}
class InputOutputDemo
{
public static void main(String[] args)
{
Res r = new Res();
Input in = new Input(r);
Output out = new Output(r);
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
t1.start();
t2.start();
}
}
因为上面的代码是两个线程对共享数据的不同操作,而他们的执行权却是不一定的,也就是说可能input执行了多次,每一次执行都会覆盖掉上一次的值。轮到output执行的时候也可能是执行多次,所以就会出现同一个值被打印多次的现象。所以在上面就用到了等待唤醒机制来解决这个问题。
2、等待唤醒机制:
等待wait,是让一个线程出于被冻结的状态,就是放弃了执行权,出于被挂起的状态,出于这种状态的线程需要被notify方法唤醒。notify唤醒的都是最先在线程池中被挂起的线程,而如果需要唤醒对方的线程就需要用到notifyAll方法,当然这个方法连自己放的线程也唤醒了。
看下下面的实例:
class ProducerConsumerDemo
{
public static void main(String[] args)
{
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(pro);
Thread t3 = new Thread(con);
Thread t4 = new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
/*
对于多个生产者和消费者。
为什么要定义while判断标记。
原因:让被唤醒的线程再一次判断标记。
为什么定义notifyAll,
因为需要唤醒对方线程。
因为只用notify,容易出现只唤醒本方线程的情况。导致程序中的所有线程都等待。
*/
class Resource
{
private String name;
private int count = 1;
private boolean flag = false;
public synchronized void set(String name)
{
while(flag)//while,每次notify后都会回来判断条件,而if不会,它会直接往下执行。
try{this.wait();}catch(Exception e){}//t1(放弃资格) t2(获取资格)
this.name = name+"--"+count++;
System.out.println(Thread.currentThread().getName()+"...生产者.."+this.name);
flag = true;
this.notifyAll();//唤醒所有的,这样就避免只唤醒自己一方,造成全部冻结的情况
}
public synchronized void out()
{
while(!flag)
try{wait();}catch(Exception e){}//t3(放弃资格) t4(放弃资格)
System.out.println(Thread.currentThread().getName()+"...消费者........."+this.name);
flag = false;
this.notifyAll();
}
}
class Producer implements Runnable
{
private Resource res;
Producer(Resource res)
{
this.res = res;
}
public void run()
{
while(true)
{
res.set("+商品+");
}
}
}
class Consumer implements Runnable
{
private Resource res;
Consumer(Resource res)
{
this.res = res;
}
public void run()
{
while(true)
{
res.out();
}
}
}
在上面这个例子中就使用到了notifyAll方法避免了多线程时notify出现只唤醒自己一方的线程,但是notifyAll也唤醒了自己一方的其他线程,这并不是我们想要的,那么怎么才能到达只唤醒对方的线程呢?
JDK1.5 中提供了多线程升级解决方案。
将同步Synchronized替换成显示Lock操作。
将Object中的wait,notify notifyAll,替换成了Condition对象。
该对象可以Lock锁 进行获取。
该示例中,实现了本方只唤醒对方操作。
Lock:替代了Synchronized
lock 锁
unlock 解锁
newCondition() 创建一个condition对象
Condition:替代了Object wait notify notifyAll
await();等待,线程被挂起
signal();唤醒
signalAll();唤醒全部
看实例:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ProducerConsumer2 {
public static void main(String[] args) {
Resource2 res = new Resource2();
new Thread(new Producer2(res)).start();
new Thread(new Producer2(res)).start();
new Thread(new Consumer2(res)).start();
new Thread(new Consumer2(res)).start();
}
}
class Resource2 {
private String name;
private int count = 1;
private boolean flag;
private Lock lock = new ReentrantLock();
private Condition condition_pro = lock.newCondition();
private Condition condition_con = lock.newCondition();
public void set(String name) {
lock.lock();
try {
while (flag) {
condition_pro.await();
}
this.name = name + "--" + count++;
System.out.println(Thread.currentThread().getName() + "...生产者..."
+ this.name);
flag = true;
condition_con.signal();// 只唤醒消费者
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void out() {
lock.lock();
try {
while (!flag) {
condition_con.await();
}
System.out.println(Thread.currentThread().getName() + ".消费者"
+ this.name);
flag = false;
condition_pro.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
class Producer2 implements Runnable {
private Resource2 res;
public Producer2(Resource2 res) {
this.res = res;
}
@Override
public void run() {
while (true)
res.set("-------生产奶--------");
}
}
class Consumer2 implements Runnable {
private Resource2 res;
Consumer2(Resource2 res) {
this.res = res;
}
@Override
public void run() {
while (true)
res.out();
}
}
这里使用的是signal方法,而不是signalAll方法。是因为通过Condition的两个对象,分别唤醒对方,这就体现了Lock锁机制的灵活性。可以通过Contidition对象调用Lock接口中的方法,就可以保证多线程间通信的流畅性了。
六、Thread类中的方法简介:
1、停止线程:
在java 1.5之后,就不再使用stop方法停止线程了。那么该如何停止线程呢?只有一种方法,就是让run方法结束。开启多线程运行,运行代码通常为循环结构,只要控制住循环,就可以让run方法结束,也就可以使线程结束。
注:特殊情况:当线程处于冻结状态,就不会读取标记,那么线程就不会结束。如下:
class StopThread implements Runnable
{
private boolean flag =true;
public void run()
{
while(flag)
{
System.out.println(Thread.currentThread().getName()+"....run");
}
}
public void changeFlag()
{
flag = false;
}
}
class StopThreadDemo
{
public static void main(String[] args)
{
StopThread st = new StopThread();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
//t1.setDaemon(true);//守护线程,在启动前调用,也就是后台线程,前台线程结束,后台线程也就结束了,不用刻意去控制结束
//t2.setDaemon(true);
t1.start();
t2.start();
int num = 0;
while(true)
{
if(num++ == 60)
{
st.changeFlag();
t1.interrupt();
t2.interrupt();
break;
}
System.out.println(Thread.currentThread().getName()+"......."+num);
}
System.out.println("over");
}
}
Thread类提供该方法 interrupt()让冻结的线程恢复到运行状态是,这时需要对冻结进行清除。强制让线程恢复到运行状态中来。这样就可以操作标记让线程结束。
2、守护线程:---setDaemon()
可将一个线程标记为守护线程,直接调用这个方法。此方法需要在启动前调用守护线程在这个线程结束后,会自动结束,则Jvm虚拟机也结束运行。
//守护线程(后台线程),在启动前调用。后台线程自动结束
t1.setDaemon(true);
t2.setDaemon(true);
t1.start();
t2.start();
3、临时加入线程:--join()
特点:当A线程执行到B线程join()方法时,A线程就会等待,B线程都执行完,A才会执行。join可用来临时加入线程执行。
class Demo implements Runnable{
public void run(){
for(int x=0;x<90;x++){
System.out.println(Thread.currentThread().getName() + "----run" + x); //Thread.currentThread().getName()获取当前线程名字
}
}
}
class JoinDemo{
public static void main(String[] args)throws Exception{
Demo d = new Demo();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
t2.start();
t1.join();//等t1执行完了,主线程才从冻结状态恢复,和t2抢执行权。t2执不执行完都无所谓。
int n = 0;
for(int x=0;x<80;x++){
System.out.println(Thread.currentThread().getName() + "----main" + x);
}
System.out.println("Over");
}
}
4、优先级:
在Thread中,存在着1~10这十个执行级别,最高的是 MAX_PRIORITY 为10,最低是 MIN_PRIORITY 为1,默认优先级是 NORM_PRIORITY 为5;但是并不是优先级越高,就会一直执行这个线程,只是说会优先执行到这个线程,此后还是有其他线程会和此线程抢夺cpu执行权的。
优先级是可以设定的,可通过setPriority()设定,如:setPriority(Thread.MAX_PRIORITY)设优先级为最大。
yield():此方法可暂停当前线程,而执行其他线程。通过这个方法,可稍微减少线程执行频率,达到线程都有机会平均被执行的效果。如下:
class Demo implements Runnable{
public void run(){
for(int x=0;x<90;x++){
System.out.println(Thread.currentThread().toString() + "----run" + x);
Thread.yield();//稍微减少线程执行频率。可达到线程都有机会达到平均运行的效果
}
}
}
class YieldDemo{
public static void main(String[] args)throws Exception{
Demo d = new Demo();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
t1.setPriority(Thread.MAX_PRIORITY);//设置线程优先级最大
t2.start();
System.out.println("Over");
}