前面我们学习了进程的两种实现方式,第一种:继承Thread类,第二种:实现Runnable接口,首先我们来对比一下两种实现方式:
第一种方式:
1)自定义MyThread 类继承自Thread
2)重写Thread类中的run()方法
3)创建MyThread类对象,分别去启动线程
注意:启动线程的时候不调用run()方法,因为run()不能作为启动线程的方法,该方法的调用相当于调用一个普通方法,并不会出现线程执行的一种随机性!所以启动线程用的是start()方法,start方法的执行是通过JVM调用run方法,但一个进程不要连续启动,否则会出现非法线程状态异常!
第二种方式:
1)自定义MyRunnable类实现Runnable接口
2)实现接口的run()方法
3)创建MyRunnable类对象,创建Thread类对象,将MyRunnable类对象作为参数进行传递,分别启动线程。
那么你可能会问:既然已经有了第一种实现方式,为什么还会有第二种呢?因为第二种方式实际上是优于第一种的
1)避免了Java单继承的一种局限性
2)更符合Java面向对象的一种设计原则:面向接口编程。将代码的实现和资源对象(MyRunnable)有效地分离开来(数据分离原则)
接下来说一下线程的生命周期:线程从开始创建的时候,一直到线程的执行,最后到线程的终止!
新建线程:此时线程没有执行执行资格,没有执行权。
线程就绪:线程有执行资格了,但是没有执行权,一旦该线程抢到了CPU的执行权,线程就开始执行了。
在执行线程之前,线程还可能会阻塞,如sleep()和wait()方法,此时线程处于阻塞状态,睡眠时间到了或执行notify()方法来唤醒进程,线程对象.start();
线程执行:线程有执行资格,并且有执行权,此时该线程如果被别的线程抢占到了CPU的执行权,线程就处于就绪的状态。
线程死亡:线程执行完毕,会被垃圾回收线程中的垃圾回收器及时从内存中释放掉!
这次我们就通过这两种方式来实现一个电影院售票的小案例:
需求:某电影院出售某些电影的票(复联3,红高粱....),有三个窗口同时进行售票(100张票),请您设计一个程序,模拟电影院售票
两种方式:
继承
接口
方式一:
//SellTicket线程
public class SellTicket extends Thread {
//为了不让外界更改这个类中的数据,用private修饰
//要让每一个线程都使用同一个数据,应该用static修饰
private static int tickets = 100 ;
@Override
public void run() {
//为了模拟电影卖票(模拟一直有票)
//死循环
//st1,st2,st3都有执行这个里面的方法
while(true) {
if(tickets>0) {
System.out.println(getName()+"正在出售第"+(tickets--)+"张票");
}
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
//创建三个子线程,分别代码三个窗口
SellTicket st1 = new SellTicket() ;
SellTicket st2 = new SellTicket() ;
SellTicket st3 = new SellTicket() ;
//设置线程名称
st1.setName("窗口1");
st2.setName("窗口2");
st3.setName("窗口3");
//启动线程
st1.start();
st2.start();
st3.start();
}
}
注意:这里的票数必须被static修饰,否则进程1、进程2和进程3会各自打印100张票,被static修饰之后可以实现数据的共享,3个进程共同打印100张票。
方式二:
public class SellTicket implements Runnable {
//定义100张票
private int tickets = 100 ;
@Override
public void run() {
while(true) {
if(tickets>0) {
System.out.println(Thread.currentThread().getName()
+"正在出售第"+(tickets--)+"张票");
}
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
//创建资源类对象(共享资源类/目标对象)
SellTicket st = new SellTicket() ;
//创建线程类对象
Thread t1 = new Thread(st, "窗口1") ;
Thread t2 = new Thread(st ,"窗口2") ;
Thread t3 = new Thread(st, "窗口3") ;
//启动线程
t1.start();
t2.start();
t3.start();
}
}
这里的Runnable接口实现的方法中之所以票数不用被static修饰,是因为设计该接口的目的是为希望在活动时执行代码的对象提供一个公共协议,也就是说这里的SellTicket作为Runnable的子实现类,其中的数据就已经是共享的了,所以不用再被static修饰。
为了模拟更真实的场景,加入延迟操作(让线程睡100毫秒)
public class SellTicket implements Runnable {
//定义100张票
private int tickets = 100 ;
@Override
public void run() {
while(true) {
try {
//t1睡 t2睡
Thread.sleep(100); //t2睡
} catch (InterruptedException e) {
e.printStackTrace();
}
if(tickets>0) {
//t1,t2,t3 三个线程执行run里面代码
//为了模拟更真实的场景(网络售票有延迟的),稍作休息
System.out.println(Thread.currentThread().getName()
+"正在出售第"+(tickets--)+"张票");//0
}
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
//创建资源类对象(共享资源类/目标对象)
SellTicket st = new SellTicket() ;
//创建线程类对象
Thread t1 = new Thread(st, "窗口1") ;
Thread t2 = new Thread(st ,"窗口2") ;
Thread t3 = new Thread(st, "窗口3") ;
//启动线程
t1.start();
t2.start();
t3.start();
}
}
程序的设计是好的,但是结果有一些问题:
1)同一张票被卖了多次
CPU的执行有一个特点(具有原子性操作:最简单最基本的操作)
t1线程进来,睡完了,100张票
原子性操作:记录以前的值
接着tickets-- :票变成99张票
在马上输出99张票之前,t2/t3进来,直接输出记录的以前那个tickets的值
出现:
窗口1正在出售第100张票
窗口3正在出售第99张票
窗口2正在出售第99张票
(延迟操作+线程的执行随机性)
理想状态:
t1正在出售第3张票
t3正在出售第2张票
t2正在出售第1张票
...
负票
t1出售第0张票 (延迟操作+线程的执行随机性)
t3正在出售-1张票
通过刚才的这个程序,有安全问题(同票还有负票问题)
如何解决多线程的安全问题?校验一个多线程程序是否有安全问题的隐患的前提条件:
1)当前程序是否是多线程环境
2)是否有共享数据
3)是否有多条语句对共享数据进行操作
看当前案例是否有多线程的安全问题:
1)是否是多线程环境 是
2)是否有共享数据 是
3)是否有多条语句对共享数据进行操作 是
现在就需要解决安全问题:
1)多线程环境 不能解决
2)对共享数据进行优化 不能解决
3)解决将多条语句对共享数据这一环进行解决
解决方案:就是将多条语句对共享数据操作的代码,用一个代码包起来---->代码--->同步代码块
格式:
synchronized(锁对象){
针对多条语句对共享数据操作代码;
}
锁对象:肯定一个对象,随便创建一个对象(匿名对象)
给刚才的这个程序加入了同步代码块,但是锁对象使用的匿名对象(每一个线程进来都有自己的锁),还是没有解决!
锁对象:每一个线程最终使用的锁对象,只能是同一把锁
public class SellTicket implements Runnable {
//定义100张票
private int tickets = 100 ;
private Object obj = new Object() ;
@Override
public void run() {
while(true) {
//new Object():锁对象 (门和关),使用匿名对象的方式,有问题!
//t1,t2,t3
/*synchronized(new Object()) {//t1
if(tickets>0) {
try {
//睡眠:延迟
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+"正在出售第"+(tickets--)+"张票");//0,-1
}
}*/
synchronized(obj) {//t1进来,门一关,t2,t3进不来了
if(tickets>0) {
try {
//睡眠:延迟
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+"正在出售第"+(tickets--)+"张票");//0,-1
}
}
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
//创建资源类对象(共享资源类/目标对象)
SellTicket st = new SellTicket() ;
//创建线程类对象
Thread t1 = new Thread(st, "窗口1") ;
Thread t2 = new Thread(st ,"窗口2") ;
Thread t3 = new Thread(st, "窗口3") ;
//启动线程
t1.start();
t2.start();
t3.start();
}
}
举例:
火车上上厕所(锁对象:将它看成是门的开和关)
synchronized(锁对象){
多条语句对共享数据操作的代码;
}
注意:
锁对象:一定要同一个锁(每个线程只能使用同一把锁)
锁对象:任何的Java类(引用类型)
public class SellTicket implements Runnable {
//定义100张票
private int tickets = 100 ;
private Object obj = new Object() ;
private Demo d = new Demo() ;
@Override
public void run() {
while(true) {
//t1,t2,t3
synchronized(d) { //门的开和关
//t1进来,门会关掉
//t2进来,门关掉
//t3进来,门关掉
if(tickets>0) {
try {
//0.1
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+"正在出售第"+(tickets--)+"张票");
//窗口1正在出售第100张票
//窗口2正在出售第99张票
//窗口3正在出售98张票
//....
//虽然加入延迟操作,就是synchronized,不会存在0或者负票了
}
}
}
}
}
class Demo{
}
下来我们通过一个例子来学习一下同步方法:
public class SellTicket implements Runnable {
//定义100张票
private static int tickets = 100 ;
private Object obj = new Object() ;
//定义一个变量
private int x = 0 ;
@Override
public void run() {
while(true) {
if(x %2==0) {
synchronized(SellTicket.class) { //门的开和关 :静态方法的锁对象:类名.class
if(tickets>0) {
try {
//0.1
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+"正在出售第"+(tickets--)+"张票");
}
}
}else {
/*synchronized(d) { //门的开和关
if(tickets>0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+"正在出售第"+(tickets--)+"张票");
}
}*/
//将其写成方法
sellTicket() ;
}
x ++ ;
}
}
//如果一个方法一进来就是同步代码块,那么可不可以将同步放到方法来进行声明呢? 可以
/*private void sellTicket() {
synchronized(d) {
if(tickets>0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+"正在出售第"+(tickets--)+"张票");
}
}
}*/
//非静态的方法:同步方法(需要底层源码,一些方法会声明synchronized)的锁对象:this
/*private synchronized void sellTicket() {
if(tickets>0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+"正在出售第"+(tickets--)+"张票");
}
}*/
//静态的同步方法:和反射有关 (静态同步方法的锁对象:类名.class)
private synchronized static void sellTicket() {
if(tickets>0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+"正在出售第"+(tickets--)+"张票");
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
//创建资源类对象(共享资源类/目标对象)
SellTicket st = new SellTicket() ;
//创建线程类对象
Thread t1 = new Thread(st, "窗口1") ;
Thread t2 = new Thread(st ,"窗口2") ;
Thread t3 = new Thread(st, "窗口3") ;
//启动线程
t1.start();
t2.start();
t3.start();
}
}