线程安全之售票案例
在本篇博客中我将讲述继承Thread的线程的三种上锁方式(同步代码块、同步方法、Lock锁),以及实现Runnable接口的三种上锁方式。
需求描述
电影院即将上线一部新电影,现先预售100张票,有三个窗口售卖这100张票。
继承Thread的线程
- 使用同步代码块
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等,因为类的字节码文件也是唯一的,所以也可以使用。
- 同步方法
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修饰的静态方法的锁对象是该类的字节码文件,**该文件唯一,所以不同对象调用也不会存在线程安全问题。
- 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接口,然后主线程创建一个任务,然后创建三个线程来售卖。
- 使用同步代码块
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这个对象作为锁对象。
- 使用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();
}
}
}
提示
以上只是我自己的看法,如有雷同纯属意外。如有错误,也请谅解,勿喷!如有收获或疑问,欢迎点赞评论!