黑马程序员--多线程的应用(1)

第四讲 多线程的应用(1)

一、由一个实例引出的问题

假设我们有这样一个需求:某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。

1、继承Thread类实现

代码实现如下:

package cn.itcast_01;

public class SellTickets extends Thread {

    public SellTickets() {
        super();
    }

    public SellTickets(String name) {
        super(name);
    }

    @Override
    public void run() {
        int ticket=100;
        while(true){
            if(ticket>0){
                System.out.println(getName()+"正在出售第"+(ticket--)+"张票");
            }
        }
    }
}
package cn.itcast_01;

public class SellTicketsDemo {
    public static void main(String[] args) {
        SellTickets st1=new SellTickets("窗口1");
        SellTickets st2=new SellTickets("窗口1");
        SellTickets st3=new SellTickets("窗口1");

        st1.start();
        st2.start();
        st3.start();
    }
}

通过查看程序运行发现同一张票被出售了多次,这是为什么呢?
- 由于ticket这个变量被定义在了run()方法中,每一个线程都会执行这条语句,相当于买着各自的100张票
改进:将ticket这个变量定义为成员变量。
- 通过运行发现还是会出现不同的窗口卖同一张票,原因是每一个线程在新建线程对象是都会加载这个成员标量
改进:将ticket这个变量定义为静态成员变量。
- 通过运行发现上述问题出现的概率小了,但并不是完全杜绝了

通过阅读代码我们发现这100张票是三个窗口共有的,将ticket定义为静态的显然不合适,创建三个卖票的线程也不合适,所以我们用第二种方法改进。

2、实现Runnable接口实现

代码实现如下:

package cn.itcast_01;

public class SellTicketsImpl implements Runnable {
    private int ticket=100;
        @Override
    public void run() {
        while(true){
            if(ticket>0){
                System.out.println(Thread.currentThread().getName()+"正在出售第"+(ticket--)+"张票");
            }
        }       
    }

}
package cn.itcast_01;

public class SellTicketsImplDemo {
    public static void main(String[] args) {
        SellTicketsImpl sti=new SellTicketsImpl();

        Thread t1=new Thread(sti,"窗口1");
        Thread t2=new Thread(sti,"窗口2");
        Thread t3=new Thread(sti,"窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

通过这种方法我们实现了电影院售票程序,从表面上看不出什么问题,但是在真实场景中,售票时网络是不能实时传输的,总是存在延迟的情况,在出售一张票以后需要一点时间的延迟。所以我们改进以上代码,实现出售完一张票后延迟100毫秒,再出售下一张。
代码实现如下:

package cn.itcast_02;

public class SellTicketsImpl implements Runnable {
    private int ticket=100;
        @Override
    public void run() {
        while(true){
            if(ticket>0){
                //为了模拟更真实的应用场景,我们延时100毫秒
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"正在出售第"+(ticket--)+"张票");
            }
        }       
    }

}
package cn.itcast_02;

public class SellTicketsImplDemo {
    public static void main(String[] args) {
        SellTicketsImpl sti=new SellTicketsImpl();

        Thread t1=new Thread(sti,"窗口1");
        Thread t2=new Thread(sti,"窗口2");
        Thread t3=new Thread(sti,"窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

通过运行发现,加入延迟后出现了两个问题:
- 相同的票卖了多次
- 出现了0票和负数票

出现多个窗口卖同一张票的原因是CPU的每一次操作都是一个原子性(最简单、最基本)的操作,而ticket–这个操作可以分为以下几个步骤:a.记录以前的值。b.ticket减1操作。c:将以前的值输出。d.ticket变成99。而在这几个步骤中间如果其他线程抢到了CPU的执行权就会出现多个窗口卖同一张票的情况。
而出现出现了0票和负数票的原因是随机性和延迟性。三个窗口最坏情况是卖到-1张票,四个窗口最坏情况是卖到-2张票。

以上两个问题就是我们所说的线程安全问题。线程安全问题在理想状态下是不容易出现的,但是一旦出现对软件的影响是非常大的。

二、线程安全问题的解决

1、线程安全问题的判断标准

  • 是否是多线程环境。
  • 是否有共享数据。
  • 是否有多条语句操作共享数据。

而上述实例中同时具备了以上三个标准,所以会出现线程安全问题。而前两个标准必然是符合的,所以我们只能限制第三个标准来解决线程安全问题。

2、线程安全问题的解决方法

通过限制第三个标准来解决线程安全问题的思路是,将操作共享数据的代码包成一个整体,让某个线程执行这个整体时其他线程不能执行。Java提供了线程同步机制来实现上述思路。
- 同步代码块
格式:synchronized(对象){需要同步的代码;}
对象:任意对象,但多个线程必须使用同一个对象,即保证是“同一把锁”。同步能解决线程安全问题的关键就在于这个对象。
同步代码:操作共享数据的多条语句。
代码实现如下:

package cn.itcast_03;

public class SellTicketsImpl implements Runnable {
    private int ticket = 100;
    private Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (ticket > 0) {
                    // 为了模拟更真实的应用场景,我们延时100毫秒
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()
                            + "正在出售第" + (ticket--) + "张票");
                }
            }
        }
    }

}
package cn.itcast_03;

public class SellTicketsImplDemo {
    public static void main(String[] args) {
        SellTicketsImpl sti=new SellTicketsImpl();

        Thread t1=new Thread(sti,"窗口1");
        Thread t2=new Thread(sti,"窗口2");
        Thread t3=new Thread(sti,"窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}
  • 同步方法及锁对象代
    同步方法的锁对象是this,代码实现如下:
package cn.itcast_04;

public class SellTicketsImpl implements Runnable {
    private int ticket = 100;
    private int i = 0;

    @Override
    public void run() {
        while (true) {
            if (i % 2 == 0) {
                //同步方法的锁对象是this
                synchronized (this) {
                    if (ticket > 0) {
                        // 为了模拟更真实的应用场景,我们延时100毫秒
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()
                                + "正在出售第" + (ticket--) + "张票");
                    }
                }
            } else {
                sellTickets();

            }
            i++;

        }
    }

    private synchronized void sellTickets() {

        if (ticket > 0) {
            // 为了模拟更真实的应用场景,我们延时100毫秒
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在出售第"
                    + (ticket--) + "张票");
        }

    }

}
  • 静态方法及锁对象
    静态方法的锁对象是当前类的class文件对象,代码实现如下:
package cn.itcast_05;

public class SellTicketsImpl implements Runnable {
    private static int ticket = 100;
    private int i = 0;

    @Override
    public void run() {
        while (true) {
            if (i % 2 == 0) {
                //静态方法的锁对象是当前类的class文件对象
                synchronized (SellTicketsImpl.class) {
                    if (ticket > 0) {
                        // 为了模拟更真实的应用场景,我们延时100毫秒
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()
                                + "正在出售第" + (ticket--) + "张票");
                    }
                }
            } else {
                sellTickets();

            }
            i++;

        }
    }

    private  static synchronized void sellTickets() {

        if (ticket > 0) {
            // 为了模拟更真实的应用场景,我们延时100毫秒
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在出售第"
                    + (ticket--) + "张票");
        }

    }

}

3、同步线程的特点

  • 线程同步的前提
    a.多线程
    b.多个线程使用的是同一个所对象
  • 线程同步的好处
    同步的出现解决了多线程的安全问题
  • 线程同步的弊端
    当线程相对较多时,因为每个线程都回去判断同步上的锁对象,这是很耗费资源的,无形中会降低程序的执行效率。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值