Java多线程通信、同步卖票实例--线程安全、详细注释
2017-06-29 14:36
323 查看
实现线程的有继承 thread类和实现runnable接口两种方式,一般没人会说实现callable接口这个方式,所以,这就暂不考虑这个方法。
下面分别以这2种方式实现线程安全的卖票的例子。
1,继承thread类来实现多线程卖票。
先是票的代码
下面是main方法
然后就是这个代码的执行结果。
![](https://img-blog.csdn.net/20170629141135650)
先不要惊慌:我都说了线程安全了,怎么还是输出了0,-1,-2,这些个不合法的票呢。
其实,这是上面ticket类里面的代码把那些同步的代码都给注释了,就是要示范一下,怎么个线程不安全。
也就是带有这个的(//【①】)代码,打开的话(标记过的是错误的就表打开啦 ),就不会看到卖出0,-1,-2的票啦
上面几种情况,我都把详细的注释,写在代码里了。跟着代码走,应该可以很好的理解这个多线程是怎么运行的吧。
这有个前提:就是你知道cpu是轮询执行程序的。这个是最基本的概念啦,每个线程,他是说停就停的,知道这个就好说啦。
2,再是实现runnable接口来实现多线程卖票的实例。
还是先看票的代码
线程1走同步代码块,线程2走同步方法。都可以实现多线程同步的效果。
然后就是看main的代码
在main方法里面看到了2个测试方法。
那么再看看这另一个票的代码实现。
这2个一起的原因,就是测试一下一般的同步函数和静态的同步函数,他们加锁的对象是谁。
经过测试,一般的普通同步函数,可以对this加锁,因为从代码可以看出来,这2个线程操作的都是同一个对象。
在上面最开始的以继承的方式实现多线程的第一个例子里面,是四个线程都各自操作各自的对象。
但是,那个票,是静态的,那么,静态的东西是属于类的,所以,虽然有四个对象,但是他们都操作的是一个共同的数据,那就得考虑线程安全问题,那就得考虑如何同步。
在这个实现runnable接口来实现多线程卖票的2个例子。
差别在这个地方。
一个是静态的,一个是非静态的,静态方法只能操作静态变量,所以,在第二个类里面,票变量被声明为静态的。第一个则不用。
而且,对于实现runnable接口的第一个票的例子,
发现2个线程,加锁的对象要相同,才能实现线程安全。
对于实现runnable接口的第二个票的例子,
发现,对于静态的方法而言,加锁的对象,就应该是类.class,这样才能线程安全。
有兴趣的小伙伴 可以把代码直接拿出来,自己测试一下,看看具体执行效果。更好的加深下理解。
下面分别以这2种方式实现线程安全的卖票的例子。
1,继承thread类来实现多线程卖票。
先是票的代码
package com.lxk.threadTest.ticket.extend; /** * Created by lxk on 2017/6/25 */ public class Ticket extends Thread { //private int ticket = 100;//创建一个对象就有100张票。错误一:几个线程都打印一次100-1。不合适。所以,如下操作,换成静态变量。 private static int ticket = 100;//静态变量是所有对象都共享,100张票。几个线程,卖的都是一个票。但是,一般都不这么干,静态变量,生命周期太长。 /** * 实现自定义线程的名称 */ public Ticket(String name) { super(name); } /** * 这地方就是需要注意的地方,如果不加[synchronized],就会发生线程安全问题。 * 奇怪了, * 怎么还是线程不安全,还是会执行出0,-1,-2。的结果出来。 * 错误原因的分析: * 可以看到添加的锁的对象是this,但是在main方法中有4个对象,每个对象都对自己加锁,锁不同,所以,还是不安全的。 * 比如:换成都对Ticket.class(内存中就有一份字节码文件存在)加锁,那就安全啦。 */ @Override //public synchronized void run() {//【①】 错误二:即使添加了同步方法(此处注释的代码),锁的是this,是线程不安全的。 public void run() { while (true) { //synchronized (this) {//【①】 错误二:即使添加了同步代码块,锁的也是this,是线程不安全的。 //synchronized (Ticket.class) {//【①】正确同步方式: 必须所有实例化的对象都锁相同的一个家伙,那就安全啦。 if (ticket > 0) { //睡一下,好实现线程不安全的现象,前提是这个方法,没有添加synchronized,同步函数。 try { Thread.sleep(10); } catch (Exception ignored) { } //错误三:不添加同步(即注释掉标记:①的所有代码),多线程操作则会打印出0,-1,-2 //分析: //假设1线程运行到下行代码处,还未执行,此时ticket的值仍然为1,那么其他线程继续执行还是都会进到这个判断 //假设其他几个线程都恰好停到此处,那么依次执行完之后,四个线程的结果就是,1,0,-1,-2. System.out.println(this.getName() + " sale:" + ticket--); } //} //【①】 } } }
下面是main方法
package com.lxk.threadTest.ticket.extend; /** * 卖票例子(继承Thread类,实现多线程) * <p> * Created by lxk on 2017/6/25 */ public class Main { public static void main(String[] args) { Ticket ticket1 = new Ticket("ticket1"); Ticket ticket2 = new Ticket("ticket2"); Ticket ticket3 = new Ticket("ticket3"); Ticket ticket4 = new Ticket("ticket4"); ticket1.start(); ticket2.start(); ticket3.start(); ticket4.start(); } }
然后就是这个代码的执行结果。
先不要惊慌:我都说了线程安全了,怎么还是输出了0,-1,-2,这些个不合法的票呢。
其实,这是上面ticket类里面的代码把那些同步的代码都给注释了,就是要示范一下,怎么个线程不安全。
也就是带有这个的(//【①】)代码,打开的话(标记过的是错误的就表打开啦 ),就不会看到卖出0,-1,-2的票啦
上面几种情况,我都把详细的注释,写在代码里了。跟着代码走,应该可以很好的理解这个多线程是怎么运行的吧。
这有个前提:就是你知道cpu是轮询执行程序的。这个是最基本的概念啦,每个线程,他是说停就停的,知道这个就好说啦。
2,再是实现runnable接口来实现多线程卖票的实例。
还是先看票的代码
package com.lxk.threadTest.ticket.implement; /** * Created by lxk on 2017/6/25 */ public class Ticket implements Runnable { private int tick = 100; boolean flag = true; //Object object = new Object(); public void run() { if (flag) { while (true) { //synchronized (object) {//这个同步代码块使用的锁是object,而下面的同步函数使用的是锁是this,所以,这么干就线程不安全。 synchronized (this) {//换成this就变得安全啦。说明下面同步函数使用的锁是this if (tick > 0) { try { Thread.sleep(10); } catch (Exception ignore) { } System.out.println(Thread.currentThread().getName() + "....sale...代码块 : " + tick--); } } } } else { while (true) { show(); } } } private synchronized void show() {//使用的锁就是this if (tick > 0) { try { Thread.sleep(10); } catch (Exception ignore) { } System.out.println(Thread.currentThread().getName() + "....sale...函数 : " + tick--); } } }可以看到上面,因为第一个继承的例子中,使用了同步代码块,或者同步函数,来解决同步问题。那么在下面这个例子中,直接把2个给弄到一起,看效果。
线程1走同步代码块,线程2走同步方法。都可以实现多线程同步的效果。
然后就是看main的代码
package com.lxk.threadTest.ticket.implement; /** * 卖票例子(实现Runnable接口,实现多线程) * <p> * Created by lxk on 2017/6/25 */ public class Main { public static void main(String[] args) { //test(); testStatic(); } /** * 测试普通的同步代码块和同步函数的同步效果。 * 结论:同步函数和同步代码块使用的锁都是this */ private static void test() { Ticket t = new Ticket(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); //现在修改为只有2个线程,1使用同步代码块,2使用同步函数。 //测试发现:两者使用的锁是不同的,因为使用的不是同一个锁,所以,线程还是不安全的。(下面分析为什么这要sleep()) t1.start(); //这时候,在没有添加下面的sleep的时候,代码一运行,所有执行结果是:全走的是同步函数, //因为线程1启动完之后,瞬间,主线程已经把flag置成false啦,所以,2个线程都走的是false结果。 //所以,要在这1线程启动完之后,主线程休息一下,就剩1线程在跑,才能看到2个线程分别的效果。 try { Thread.sleep(10); } catch (Exception ignore) { } t.flag = false; t2.start(); //运行结果:打印出0的错票。不安全。(此时,同步代码块使用的锁,是自己new的一个obj) //对错误代码进行分析如下: //两个前提。1,两个或以上的线程;2,用的是否是同一个锁。 // 后面修改同步代码块中的同步对象由object变成this,然后就安全啦。 // //这个修改完之后,就可以看到,没有输出0啦,而且2个线程,确实都执行了不同的同步实现。一个同步代码块,一个同步函数。 //Thread t3 = new Thread(t); //Thread t4 = new Thread(t); //t3.start(); //t4.start(); } private static void testStatic() { TicketStatic t = new TicketStatic(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t1.start(); //这时候,没有添加下面的sleep的时候,代码一运行,所有执行结果全走的是同步函数, //因为线程1启动完之后,瞬间,主线程已经把flag置成false啦,所以,都走的是false结果。 //所以,要在这1线程启动完之后,主线程休息一下才能看到2个线程分别的效果。 try { Thread.sleep(10); } catch (Exception ignore) { } t.flag = false; t2.start(); //运行结果:打印出0的错票。不安全。(这个时候,同步代码块使用的锁,是自己this) //静态方法使用的锁和同步代码块使用的锁不一样。静态同步函数使用的锁是类.class //对错误代码进行分析如下: //两个前提。1,两个或以上的线程;2,用的是否是同一个锁。 // 后面修改同步函数中的同步对象由this变成.class,然后就安全啦。 // //这个修改完之后,就可以看到,没有输出0啦,而且2个线程,确实都执行了不同的同步实现。一个同步代码块,一个同步函数。 } }
在main方法里面看到了2个测试方法。
那么再看看这另一个票的代码实现。
package com.lxk.threadTest.ticket.implement; /** * 测试:静态同步函数和非静态的差别 * <p> * Created by lxk on 2017/6/25 */ public class TicketStatic implements Runnable { private static int tick = 100; boolean flag = true; public void run() { if (flag) { while (true) { //synchronized (this) {//静态同步函数使用的是类对象。 synchronized (TicketStatic.class) {//内存中没有本类对象,但是一定有该类对应的字节码文件对象。类名.class if (tick > 0) { try { Thread.sleep(10); } catch (Exception ignore) { } System.out.println(Thread.currentThread().getName() + "....sale...代码块 : " + tick--); } } } } else { while (true) { show(); } } } private static synchronized void show() {//静态方法的时候,使用的锁就不是this,经测试,可发现使用的是类.class if (tick > 0) { try { Thread.sleep(10); } catch (Exception ignore) { } System.out.println(Thread.currentThread().getName() + "....sale...函数 : " + tick--); } } }
这2个一起的原因,就是测试一下一般的同步函数和静态的同步函数,他们加锁的对象是谁。
经过测试,一般的普通同步函数,可以对this加锁,因为从代码可以看出来,这2个线程操作的都是同一个对象。
在上面最开始的以继承的方式实现多线程的第一个例子里面,是四个线程都各自操作各自的对象。
但是,那个票,是静态的,那么,静态的东西是属于类的,所以,虽然有四个对象,但是他们都操作的是一个共同的数据,那就得考虑线程安全问题,那就得考虑如何同步。
在这个实现runnable接口来实现多线程卖票的2个例子。
差别在这个地方。
一个是静态的,一个是非静态的,静态方法只能操作静态变量,所以,在第二个类里面,票变量被声明为静态的。第一个则不用。
而且,对于实现runnable接口的第一个票的例子,
发现2个线程,加锁的对象要相同,才能实现线程安全。
对于实现runnable接口的第二个票的例子,
发现,对于静态的方法而言,加锁的对象,就应该是类.class,这样才能线程安全。
有兴趣的小伙伴 可以把代码直接拿出来,自己测试一下,看看具体执行效果。更好的加深下理解。
相关文章推荐
- javadoc-Java注释详细介绍
- 跟着实例学习java多线程8-同步容器类的问题
- 二叉排序树的建立和各种遍历方法-java(附详细注释和单元测试)
- Vue组件开发实例(详细注释)
- java 最简易记事本,带详细注释,仅供参考!
- java 线程之对象的同步和异步(实例讲解)
- 22个Java经典实例 带完美注释
- 如何:对制造者线程和使用者线程进行同步(C# 编程指南)的详细注释
- opencv学习实例(一)---camshift 色块追踪(详细注释)
- Java自带的线程池ThreadPoolExecutor详细介绍说明和实例应用
- 微信开发获取地理位置实例(java,非常详细,附工程源码)
- Java总结(十)—实现Runnable接口创建线程,线程安全同步,死锁(哲学家进餐问题),读写锁
- 基于red5的在线视频录制实例和详细注释
- Java自带的线程池ThreadPoolExecutor详细介绍说明和实例应用
- 在 Java 的多线程中,如何去判断给定的一个类是否是线程安全的(另外:synchronized 同步是否就一定能保证该类是线程安全的。)
- java线程的同步中notify和wait方法之生产消费实例讲解
- 【详细注释】Java->Socket 服务器与客户端的半双工通讯。拥有详细注释,完美诠释。
- Java 同步块(synchronized)详细说明
- java5的Semaphore同步工具简单实例
- 微信开发获取地理位置实例(java,非常详细,附工程源码)