Java基础————线程安全售票案列

线程安全之售票案例

在本篇博客中我将讲述继承Thread的线程的三种上锁方式(同步代码块、同步方法、Lock锁),以及实现Runnable接口的三种上锁方式。

需求描述

电影院即将上线一部新电影,现先预售100张票,有三个窗口售卖这100张票。

继承Thread的线程

  1. 使用同步代码块
public class SellTicketExtendsThreadSynchronized {
	public static void main(String[] args) {
		
		SellTicketThread t1 = new SellTicketThread("窗口一");
		SellTicketThread t2 = new SellTicketThread("窗口二");
		SellTicketThread t3 = new SellTicketThread("窗口三");
		t1.start();
		t2.start();
		t3.start();
	}
}

class SellTicketThread extends Thread{
	
	private  static int ticket = 100;
	private static Object object = new Object();
	//加static关键字,使所有线程共用同一个资源,
	//没有则是每一个线程对象都有自己的ticket资源,即三个对象就有三个1~100张票
	public SellTicketThread(String name) {
		// TODO Auto-generated constructor stub
		super(name);
	}
	@Override
	public void run() {
		// TODO Auto-generated method stub
		
			while(0 < ticket) {
				//synchronized ("a") {
				//synchronized (Thread.class) {
				synchronized (object) {
					if(0 < ticket) {
						System.out.println(Thread.currentThread().getName()+"正在售卖第"+ticket+"张票。");
						ticket--;	
					}else {
						System.out.println(Thread.currentThread().getName()+"票已售完");
					}
				}
			}
	}
}

注意事项:
1、共享资源添加static关键字,使该属性成为所有对象共用的属性,否者创建多少个对象便有多少个属性,那时共享资源便不再是共享资源,而是每个对象各自有属于自己的资源,比如上面的ticket,如果不是静态成员,那么在创建的三个线程时,每个线程都会有100张ticket,这时售卖票便会每个线程售卖自己的100张票。
2、同步代码块尽量粒度小一些,这样多线程的效率会更高,在上面售票案列中将同步代码块放到了循环内,如果在同步代码块中 不对ticket进行判断的话,当一个线程售卖最后一张票时可能会有另外两个线程也进入了循环,这时便会出现卖票为0和票为-1的情况,所以要避免这种情况的发生。
3、确保锁对象的唯一性,如果锁对象不唯一,比如说上面所给代码如果在线程类中的Object成员对象变量没有使用static关键字修饰的话,那么每个线程对象自己便各自拥有一个Object对象,使用object当锁对象就是使用不同的锁,同步代码块就锁不住,就会卖出重票。所以一定要保证锁对象的唯一性。
4、锁对象可以有很多,只要是唯一的即可,比如存放在常量池的常量对象“abc”、“a”等等,因为在常量池中这些对象都只有一个,所以使用他们当对象时锁对象便是唯一的,还可以使用类的字节码文件,比如Object.class、Thread.class等,因为类的字节码文件也是唯一的,所以也可以使用。

  1. 同步方法
public class SellTicketExtendsThreadSynchronizedMethod {
	
	public static void main(String[] args) {
		SellTicketThread1 t1 = new SellTicketThread1("窗口1");
		SellTicketThread1 t2 = new SellTicketThread1("窗口2");
		SellTicketThread1 t3 = new SellTicketThread1("窗口3");
		t1.start();
		t2.start();
		t3.start();
	}
}

class SellTicketThread1 extends Thread{
	
	private  static int ticket = 100;
	public SellTicketThread1(String name) {
		// TODO Auto-generated constructor stub
		super(name);
	}
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(0 < ticket) {
			method1();
		}
	}
	//锁对象是调用该方法的对象,这里是三个线程对象调用,所以锁对象就是三个线程,故锁不住
	//解决办法给该方法增加一个Static关键字,使方法上升为类的共用方法,这样创建的对象便共享这一个方法,锁对象则是类的字节码文件
	private synchronized static void method1() {
			if(0 < ticket) {
				System.out.println(Thread.currentThread().getName()+"正在售卖第"+ticket+"张票。");
				ticket--;	
			}else {
				System.out.println(Thread.currentThread().getName()+"票已售完");
			}
	}
}

注意事项:
除了同步带码块的方式的注意事项外,这里我要额外讲的是,使用synchronized关键词的普通方法和静态方法的注意事项,如果只使用synchronized关键字修饰而不适用static,这时的锁对象是this,this代表什么呢,也就是调用该方法的对象,在上述代码中调用该方法的就是三个线程对象,所以锁对象就是三个线程对象,所以光使用synchronized修饰的普通方法的锁对象是this,调用对象不唯一,这样就锁不住,而如果是synchronized修饰的静态方法就不一样了,因为这时这个方法不属于对象,而是属于类,所有对象共用该方法,**所以synchronized修饰的静态方法的锁对象是该类的字节码文件,**该文件唯一,所以不同对象调用也不会存在线程安全问题。

  1. Lock锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SellTicketExtendsThreadLock {
	public static void main(String[] args) {
		SellTicketThread2 t1 = new SellTicketThread2("窗口1");
		SellTicketThread2 t2 = new SellTicketThread2("窗口2");
		SellTicketThread2 t3 = new SellTicketThread2("窗口3");
		t1.start();
		t2.start();
		t3.start();
	}
}

class SellTicketThread2 extends Thread{
	
	private  static int ticket = 100;
	private static Lock lock =new ReentrantLock();
	//不使用static修饰则一个线程对象一个lock,锁不相同,
	//使用static关键字则是类的共用lock对象属性,是唯一的
	public SellTicketThread2(String name) {
		// TODO Auto-generated constructor stub
		super(name);
	}
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(0 < ticket) {
			lock.lock();
			if(0 < ticket) {
				System.out.println(Thread.currentThread().getName()+"正在售卖第"+ticket+"张票。");
				ticket--;	
			}else {
				System.out.println(Thread.currentThread().getName()+"票已售完");
			}
			lock.unlock();
		}
	}
}

注意事项:
使用Lock锁时,首先要先创建一个锁对象,至于为什么是静态的我就不重复上面的了,在需要加锁的地方加锁,使用锁对象调用加锁方法lock.lock();在加锁区域之后记得解锁:lock.unlock();

实现Runnable接口的线程

使用Runnable接口其实更贴近需求描述,有一个售卖100张的任务三个窗口一起卖。以下代码不正是一样么,创建一个任务类实现Runnable接口,然后主线程创建一个任务,然后创建三个线程来售卖。

  1. 使用同步代码块
public class SellTicketImplementsRinnableSynchronized {
public static void main(String[] args) {
		Task1 task1 = new Task1();//创建一个任务
		同时分配给三个窗口售卖
		Thread t1 =new Thread(task1,"窗口一");
		Thread t2 =new Thread(task1,"窗口二");
		Thread t3 =new Thread(task1,"窗口三");
		
		t1.start();
		t2.start();
		t3.start();
	}
	
}

class Task1 implements Runnable{
	private int ticket = 100;
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(0 < ticket) {
			synchronized (this) {
				if(0 < ticket) {
					System.out.println(Thread.currentThread().getName()+"正在售卖第"+ticket+"张票。");
					ticket--;	
				}else {
					System.out.println(Thread.currentThread().getName()+"票已售完");
				}
			}
		}
	}
}

注意事项:
细心的朋友可能发现了,这里我的共享资源ticket并没有添加static关键字,这里就是和上面继承Thread线程的区别,因为在这里,我们只创建了一个任务,也就是说从始至终我们就只有这一个100张ticket,然后分给开启三个线程去售卖,所以不存在多个ticket属性,还有这里我是用的锁对象是this,可能有的朋友受上面的影响认为这里不可以使用this,认为使用this运行时,三个线程使用的锁对象是不同的,这时你就得认真看看代码了,这里的锁对象并不是三个线程对象,而是Task1对象,因为只创建了一个Task1对象,所以锁对象是唯一的,而并非三个线程是这里的锁对象。
2. 使用同步方法

package com.thread;

public class SellTicketImplementsRinnableSynchronizedMethod {
	public static void main(String[] args) {
		Task2 task2 = new Task2();
		Thread t1 =new Thread(task2,"窗口一");
		Thread t2 =new Thread(task2,"窗口二");
		Thread t3 =new Thread(task2,"窗口三");
		t1.start();
		t2.start();
		t3.start();
	}
}

class Task2 implements Runnable{
	private int ticket = 100;
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(0 < ticket) {
			method();
		}
	}
	
	private synchronized void method() {
		if(0 < ticket) {
			System.out.println(Thread.currentThread().getName()+"正在售卖第"+ticket+"张票。");
			ticket--;	
		}else {
			System.out.println(Thread.currentThread().getName()+"票已售完");
		}
	}
}

注意事项:
前面我谈到,非静态上的锁对象就是调用对象本身,而这里的synchronized修饰的方法和上面没有使用static修饰的ticket一样,因为只创建了一个对象,所以这里的锁对象也是唯一的,也就是task2这个对象作为锁对象。

  1. 使用lock锁对象
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SellTicketImplementsRinnableLock {
	public static void main(String[] args) {
		Task3 task3 = new Task3();
		Thread t1 =new Thread(task3,"窗口一");
		Thread t2 =new Thread(task3,"窗口二");
		Thread t3 =new Thread(task3,"窗口三");
		t1.start();
		t2.start();
		t3.start();
	}
}

class Task3 implements Runnable{
	private int ticket = 100;
	private Lock lock = new ReentrantLock();
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(0 < ticket) {
			lock.lock();
				if(0 < ticket) {
					System.out.println(Thread.currentThread().getName()+"正在售卖第"+ticket+"张票。");
					ticket--;	
				}else {
					System.out.println(Thread.currentThread().getName()+"票已售完");
				}
			lock.unlock();
		}
	}
}

提示

以上只是我自己的看法,如有雷同纯属意外。如有错误,也请谅解,勿喷!如有收获或疑问,欢迎点赞评论!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值