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

实现线程的有继承 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类,实现多线程)
 * 

* 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(); } }


然后就是这个代码的执行结果。

Java多线程通信、同步卖票实例--线程安全、详细注释_第1张图片

先不要惊慌:我都说了线程安全了,怎么还是输出了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接口,实现多线程)
 * 

* 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;

/**
 * 测试:静态同步函数和非静态的差别
 * 

* 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,这样才能线程安全。


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


你可能感兴趣的:(#,java,JUC)