Java多线程通信、同步卖票实例--线程安全、详细注释

来源:互联网 发布:未来日记 知乎 编辑:程序博客网 时间:2024/05/17 23:37

实现线程的有继承 thread类和实现runnable接口两种方式,一般没人会说实现callable接口这个方式,所以,这就暂不考虑这个方法。

下面分别以这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,这样才能线程安全。


有兴趣的小伙伴 可以把代码直接拿出来,自己测试一下,看看具体执行效果。更好的加深下理解。


原创粉丝点击