【多线程:锁】卖票程序

简介: 【多线程:锁】卖票程序

【多线程:锁】卖票程序

01.介绍

什么是锁:锁是为了保证线程安全,即多线程运行过程中,保证某一部分只能让当前线程运行完,而不能在当前线程运行过程中切换其他线程。总的来说:多线程保证线程安全就是让多个线程执行的情况和单线程一样。

什么是卖票程序:卖票程序就是生产消费者模式,我们有很多的卖票点卖票可以卖给很多人,但所有票都在一个票池里,卖票分为两个过程:卖票,总票数减一。这样如果不用锁处理就会出现==线程安全问题==,比如某一个线程在卖票但票数还没有来得及减一,就切换到另一个线程了然后这个线程又把这张票卖了一次,显然不符合要求。解决这个问题我们就要靠锁即线程同步来解决。

02.卖票程序

原版

class A implements Runnable
{
    public static int tickets = 100;
    public void run()
    {
        while(true)
        {
            if(tickets>0)
            {
                System.out.printf("%s线程正在卖出第%d张票\n",Thread.currentThread().getName(),tickets);
                tickets--;                
            }
            else
                break;
        }    
    }    
}
public class TestTickets
{
    public static void main(String[] args)
    {
        A aa1=new A();
        Thread t1=new Thread(aa1);
        t1.start();    
        A aa2=new A();
        Thread t2=new Thread(aa2);
        t2.start();
    }    
    /*
此程序有问题,问题在于 多个线程共同争抢同一个资源,因为在线程运行的任意位置 线程都可以随意切换到另一个线程,这样就会导致
某一个线程需要运行一个整体 但运行到其中的某一步就被强制切换到另一个线程了 因为都用一个资源 所以导致另一个线程对于资源的改变
但这种改变并没有实时作用在当前线程上 使其运行结果出错。
拿卖票这个举例:
两个线程共同卖一个票池,对于每一个线程 只有当卖完票 并且票数减一后 才算当前线程的卖票程序运行成功一次,但会出现这样的问题 
当线程一运行到正在卖第n张票数时 立即被切换到线程二 此时线程一还没有执行票数减一操作,所有此时线程二依旧运行正在卖第n张票
但 实际上第n张票早已经卖完 应该卖的是第n-1张票,这就是问题所在。
    */
}

结果

Thread-1线程正在卖出第100张票
....
Thread-1线程正在卖出第82张票
Thread-0线程正在卖出第100张票
Thread-0线程正在卖出第80张票
Thread-0线程正在卖出第79张票
Thread-0线程正在卖出第78张票
Thread-0线程正在卖出第77张票

....

可以看到线程1已经卖到了第82张票但此时线程0又卖了一次之前的第100张票,它的逻辑是这样的:最开始线程1运行了==System.out.printf("%s线程正在卖出第%d张票\n",Thread.currentThread().getName(),tickets);==语句但是并没有减1,然后切换到了线程0,然后线程0已经加载了==System.out.printf("%s线程正在卖出第%d张票\n",Thread.currentThread().getName(),tickets);==语句但此时还没有来的及输出(注意:因为输出语句已经加载好 所以此时要输出的应该是Thread-0线程正在卖出第100张票 只是没有来得及输出),之后又切换到了线程1继续从上次线程1运行的地方运行 此时运行==tickets--;==,然后一直到==Thread-0线程正在卖出第100张票==之前都是线程1在正常运行 运行到==Thread-1线程正在卖出第82张票==,此时切换到线程0 因为之前线程0已经加载了==System.out.printf("%s线程正在卖出第%d张票\n",Thread.currentThread().getName(),tickets);==所以继续从这里运行 所以接下来运行==System.out.printf("%s线程正在卖出第%d张票\n",Thread.currentThread().getName(),tickets);==语句输出Thread-0线程正在卖出第100张票 但是此时的票数实际上是81 然后线程0运行==tickets--==后 此时票数是80,所以之后线程0才会输入Thread-0线程正在卖出第80张票,那么第81张呢?其实第81张也是和第100张的情况一样,第81张被线程1的输出语句加载但还没有输出 但是之后 再次切换到线程1时会再次输出81。

加锁(线程同步)

前提知识

线程同步实际上是通过synchronized关键字实现的,它的作用是规定一个锁 这个锁可以锁定一块代码段也可以锁定一个方法, 这个锁 锁住一个公共对象 只有拿到这个锁的线程才可以运行锁内部的代码且不可被打断 直到这块代码运行完毕 释放锁 之后所有线程再来争抢锁。这样做的目的是为了 公共资源部分的代码在同一时刻只能由一个线程运行,避免线程不安全。对于卖票程序来说 它们的公共资源的就是 票池 锁住的部分就是卖票和票数减一。实质上 锁 锁住的对象叫做监听器 锁住对象的动作叫做锁定。

代码

class A implements Runnable
{
    public static int tickets = 10000;//写法一的话 static可以不用加 但写法二必须加
    String str = new String("aa");//a  这样写 只有写法一正确
    //static String str = new String("aa"); //b  这样写 写法二写法一都正确
    public A()
    {
        System.out.println(this);    
    }
    public void run()
    {
        while(true)
        {
            //synchronized(this)//这样写 只有写法一正确
            synchronized(str)//这样写写法一一定正确,但写法二 只有在b的情况下才能正确
            {
                if(tickets>0)
                {
                    System.out.printf("%s线程正在卖出第%d张票\n",Thread.currentThread().getName(),tickets);
                    tickets--;                
                }
                else
                {
                    break;
                }
            }
        }    
    }    
}

public class TestTickets_2
{
    public static void main(String[] args)
    {
        A aa=new A();
        Thread t1=new Thread(aa);    
        Thread t2=new Thread(aa);
        t1.start();
        t2.start();//写法一
        
        /*A aa1=new A();
        Thread t1=new Thread(aa1);
        t1.start();    
        A aa2=new A();
        Thread t2=new Thread(aa2);
        t2.start();//写法二*/
    }    
}
/*
synchronized总结:
格式:
synchronized(类对象名aa)
{
    同步代码块
}
synchronized(类对象名 aa)的含义是:判断aa是否已经被其他线程霸占,如果发现已经被其他线程霸占,则当前线程陷入等待,如果发现aa
没有被其他线程霸占,则当前线程霸占注aa对象,并执行3行的代码同步块,在当前线程执行3行代码时,其他线程将无法再执行3行的代码
(因为当前线程以及霸占了aa对象),当前线程执行完3行的代码后,会自动释放对aa对象的霸占,此时其他线程会相互竞争对aa的霸占,最终
cpu会选择其中的某一个线程执行

补充:
霸占的专业术语叫做锁定,霸占住的那个对象专业术语叫做监听器,监听器可以是this指向的那个对象也可以是类里的其他对象。

锁定类似于 有n个人上厕所 有一个厕所牌 只有当某人拿到厕所牌才可以上厕所,关键点在于只有一个厕所牌 即厕所牌是公共的,区别在于
是哪个人拿了厕所牌 则锁定那个人的厕所牌 如果上完厕所则 其他人再争抢厕所牌。

当修饰代码块时需要(类对象名 aa),当修饰方法时 则不需要只需要在方法前加上 synchronized 不需要括号 默认监听器是this。

当在运行同步代码块时,线程是可以随机改变的 只不过因为线程的监听器被锁定 导致其他的线程的监听器无法被锁定 导致只有监听器被锁定
的那个线程运行完同步代码块后 把监听器释放 才能让其他线程的监听器取竞争。

当在创建线程时,只有写法一是对的,写法二虽然也创建了两个线程 但这两个线程的synchronized没有锁定的关系,依旧会出现代码不同步
的问题。

如果保证写法二也是正确的 则必须保证 有唯一的厕所牌 即 公共对象,这个公共对象不能是this 因为this会指向不同的对象,所以这种
情况下 必须用一个公共的类对象 才能保证写法二正确,但写法一则不用考虑 因为这个的公共对象就是aa

推荐使用写法一
*/

结果

测试了一万张票,没有出现错误。

解释

大家认真看上述代码即注释

我们可以发现写法一 实质上就是把A的实例对象aa或者aa对象的成员或者A类的成员作为监听器 把aa对象的tickets或者A类的tickets作为票池

写法二的实质是把A类的成员作为监听器 A类的tickets作为票池

实际中我们采用写法一

其他代码

class A extends Thread
{
    public static int tickets = 10000;
    static String str = new String("aa");
    public void run()
    {
        while(true)
        {
            synchronized(str)
            {
                if(tickets>0)
                {
                    System.out.printf("%s线程正在卖出第%d张票\n",Thread.currentThread().getName(),tickets);
                    tickets--;                
                }
                else
                {
                    break;
                }
                    
            }
        }    
    }    
}

public class TestTickets_3
{
    public static void main(String[] args)
    {
        A aa1=new A();
        A aa2=new A();
        aa1.start();
        aa2.start();
        /*这种写法类似于TestTickets_2.java里的写法二,必须创建一个公共对象,不推荐使用*/
    }    
}

解释:本质和写法二一样,只是之前的两种写法是用Runnable接口实现 这个写法是直接继承Thread类

错误代码1

class A implements Runnable
{
    public int tickets = 10000;
    public A()
    {
        System.out.println(this);    
    }
    public synchronized void run()
    //错误写法 对于此程序来说synchronized加在这里会导致 只有一个线程卖票 另一个线程完全不能卖
    //因为放在方法前是保证此方法完全运行 但此方法就是卖10000张票 会导致一个线程把这10000张票全部卖完
    //放在代码块是为了 保证 每卖一张票就必须减一张票 这个才是本程序的真正目的
    {
        while(true)
        {
            if(tickets>0)
            {
                System.out.printf("%s线程正在卖出第%d张票\n",Thread.currentThread().getName(),tickets);
                tickets--;                
            }
            else
            {
                break;
            }
                    
        }    
    }    
}

public class TestTickets_4
{
    public static void main(String[] args)
    {
        A aa=new A();
        Thread t1=new Thread(aa);    
        Thread t2=new Thread(aa);
        t1.start();
        t2.start();
    }    
}

解释:这里的错误很明显 此时锁定的是整个方法 这样会导致 某一个线程先得到锁 就会直接把票卖完 另一个线程完全陷入阻塞

错误代码2

class A implements Runnable
{
    public int tickets = 10000;
    public A()
    {
        System.out.println(this);    
    }
    public  void run()
    {
        //String str=new String("aa");//错误写法 此时str是局部变量 不再是属性对象
        String str="aa";//正确写法 因为字符串存放在统一的数据区
        while(true)
        {
            synchronized(str)//锁定的不是同一个str对象
            {
                if(tickets>0)
                {
                    System.out.printf("%s线程正在卖出第%d张票\n",Thread.currentThread().getName(),tickets);
                    tickets--;                
                }
                else
                {
                    break;
                }
            }    
        }    
    }    
}

public class TestTickets_5
{
    public static void main(String[] args)
    {
        A aa=new A();
        Thread t1=new Thread(aa);    
        Thread t2=new Thread(aa);
        t1.start();
        t2.start();
    }    
}

解释:这个错误也很容易理解 因为此时str变量不是对象的成员变量 只是一个局部变量而局部变量当前代码块运行完毕后就会释放 起不了监听器的作用,但是==String str="aa";==则可以 原因是这种创建方法的"aa"是在堆中不会随代码块释放 只有当程序运行结束由gc清除,但是==String str=new String("aa");==就是栈中创建了一个对象会随代码块的结束释放。

目录
相关文章
|
2月前
|
安全 Java 调度
Java编程时多线程操作单核服务器可以不加锁吗?
Java编程时多线程操作单核服务器可以不加锁吗?
44 2
|
28天前
|
Java 开发者
如何通过易语言多线程提升程序响应速度
如何通过易语言多线程提升程序响应速度
133 62
|
21天前
|
Java 开发者
如何通过易语言多线程提升程序响应速度?
如何通过易语言多线程提升程序响应速度?
|
2月前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
1月前
|
运维 API 计算机视觉
深度解密协程锁、信号量以及线程锁的实现原理
深度解密协程锁、信号量以及线程锁的实现原理
33 1
|
28天前
|
监控 Java API
|
1月前
|
Java 应用服务中间件 测试技术
Java21虚拟线程:我的锁去哪儿了?
【10月更文挑战第8天】
32 0
|
1月前
|
安全 调度 数据安全/隐私保护
iOS线程锁
iOS线程锁
26 0
|
1月前
|
Java API
【多线程】乐观/悲观锁、重量级/轻量级锁、挂起等待/自旋锁、公平/非公锁、可重入/不可重入锁、读写锁
【多线程】乐观/悲观锁、重量级/轻量级锁、挂起等待/自旋锁、公平/非公锁、可重入/不可重入锁、读写锁
31 0
|
1月前
|
安全 Java 程序员
【多线程-从零开始-肆】线程安全、加锁和死锁
【多线程-从零开始-肆】线程安全、加锁和死锁
43 0