线程安全问题-抢票 – 同步代码块解决
注:这里为了避免数据量大,只写了三张牌;如果需要自行演示查看,建议扩大到100张票 如下图所示,票池中有三张票(三个线程),期望大家能够正常凭借手速抢票:
public class MyTicket implements Runnable {
private int ticket = 3;
@Override
public void run() {
while (true) {
if (ticket < 0 || ticket == 0) {
return;
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket + "张票");
--ticket;
}
}
}
public class MyTicket_Main {
public static void main(String[] args) throws InterruptedException {
MyTicket myTicket = new MyTicket();
Thread person01 = new Thread(myTicket, "用户1");
Thread person02 = new Thread(myTicket, "用户2");
Thread person03 = new Thread(myTicket, "用户3");
person01.start();
person02.start();
person03.start();
}
}
// 执行结果:
/**
* 用户2抢到了第3张票
* 用户1抢到了第3张票
* 用户3抢到了第3张票
* 用户1抢到了第1张票
* 用户2抢到了第2张票
*/
执行代码发现,结果并不是我们期望的,用户2、用户1、用户3抢到了同一张票,这是不允许的。
可以通过现实中的例子解决该问题:
上厕所的时候,我们将厕门关闭上锁的方式来保持一个人独占厕所;当结束,打开锁,就算想继续占用,也需要重新排队
synchronized关键字就相当于一把锁,当然,必须保证每一次都是同一把锁
public class MyTicket implements Runnable {
private int ticket = 3;
private final Object object = new Object();
@Override
public void run() {
while (true) {
synchronized (object) {
// 每一个和公共变量相关的都应该被包裹在同步代码块中
if (ticket < 0 || ticket == 0) {
return;
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket + "张票");
--ticket;
}
}
}
}
public class MyTicket_Main {
public static void main(String[] args) throws InterruptedException {
MyTicket myTicket = new MyTicket();
Thread person01 = new Thread(myTicket, "用户1");
Thread person02 = new Thread(myTicket, "用户2");
Thread person03 = new Thread(myTicket, "用户3");
person01.start();
person02.start();
person03.start();
}
}
/**
* 用户1抢到了第3张票
* 用户1抢到了第2张票
* 用户1抢到了第1张票
*/
发现,已经实现了目标,只不过全是由用户1抢到了票,我们可以使用sleep来降低每一个用户获取到线程的频率
public class MyTicket implements Runnable {
private int ticket = 3;
private final Object object = new Object();
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (object) {
if (ticket < 0 || ticket == 0) {
return;
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket + "张票");
--ticket;
}
}
}
}
/**
* 用户3抢到了第3张票
* 用户2抢到了第2张票
* 用户1抢到了第1张票
*/
因为CPU执行的速度很快,在100ms的时间段中,三个用户都进入了run方法,其中某一个,假设为A用户,开始执行同步代码块中的内容;其余两个用户进入等待,直到A用户让出CPU资源,其余用户之一开始执行代码块.
问题:为什么不使用继承Thread的方式来写呢?
同一个Thread实例,只能start一次;如果要创建三个线程,那么只能创建三个Thread实例,就会出现一个问题,每一个实例对象,都有成员变量ticket=3;
也就是说每一个人都有三张票,和题目就无关了,所以使用Runnable接口。
public class MyTicket extends Thread {
private int ticket = 3;
@Override
public void run() {
while (true) {
if (ticket < 0 || ticket == 0) {
return;
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket + "张票");
--ticket;
}
}
}
public class MyTicket_Main {
public static void main(String[] args) throws InterruptedException {
// MyTicket myTicket = new MyTicket();
MyTicket person01 = new MyTicket();
MyTicket person02 = new MyTicket();
MyTicket person03 = new MyTicket();
person01.start();
person02.start();
person03.start();
}
}
// 运行结果:
/**
* Thread-0抢到了第3张票
* Thread-2抢到了第3张票
* Thread-1抢到了第3张票
* Thread-2抢到了第2张票
* Thread-2抢到了第1张票
* Thread-0抢到了第2张票
* Thread-1抢到了第2张票
* Thread-0抢到了第1张票
* Thread-1抢到了第1张票
*/
注意:这里强调的是同一把锁,如果使用的是多把锁,那么依然会出现线程安全问题
比如:通过继承Thread类来创建多线程。那么需要创建多个Thread实例,三个实例都各自由一个Object对象,是三把锁
public class MyTicket implements Runnable {
private int ticket = 3;
// 使用列表、数组仍然视为同一把锁,因为是相同的对象
private List<String> object = new ArrayList<>(100);
@Override
public void run() {
while (true) {
synchronized (object) {
if (ticket < 0 || ticket == 0) {
return;
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket + "张票");
--ticket;
}
}
}
}
线程安全问题-抢票 – 同步方法解决
两种同步方法
普通同步方法
正常的方法声明如下:
修饰符 返回类型 方法名(参数列表) { } public void function(String agr) { }
普通同步方法声明如下:
修饰符 synchronized 返回类型 方法名(参数列表) { } public synchronized void function() { }
public class MyTicket implements Runnable {
private int ticket = 3;
@Override
public synchronized void run() {
while (true) {
if (ticket < 0 || ticket == 0) {
return;
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket + "张票");
--ticket;
}
}
}
public class MyTicket_Main {
public static void main(String[] args) throws InterruptedException {
MyTicket myTicket = new MyTicket();
Thread person01 = new Thread(myTicket, "用户1");
Thread person02 = new Thread(myTicket, "用户2");
Thread person03 = new Thread(myTicket, "用户3");
person01.start();
person02.start();
person03.start();
}
}
// 也可以将MyTicket写作
@Override
public void run() {
while (true) {
synchronized (this) {
if (ticket < 0 || ticket == 0) {
return;
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket + "张票");
--ticket;
}
}
}
同步方法实际上,就是synchronized(this), 即当前对象为锁;
我们在main函数中只创建了一个MyTicket实例,所以三个线程共享的是同一把锁
静态同步方法
public class MyTicket implements Runnable {
private static int ticket = 3;
@Override
public void run() {
sub();
}
public static synchronized void sub() {
while (true) {
if (ticket < 0 || ticket == 0) {
return;
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket + "张票");
--ticket;
}
}
}
静态同步方法,实际上,就是synchronized(MyTicket.class)