【多线程:锁】卖票程序
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");==就是栈中创建了一个对象会随代码块的结束释放。