不安全案例与线程同步(二十三)
三大线程不安全案例与线程同步(二十三)
不安全的案例
不安全的集合
在下面的例子中,我们创建了1000个线程往List中添加数据,最后输出这个List的长度。运行一下我们发现,List的长度很少能有1000,经常在九百多。这是因为多个线程同时操作了一个数据,有的List元素被更新了,所以长度达不到期望的1000,这是一个不安全的集合操作。
package com.syc;
import java.util.ArrayList;
import java.util.List;
public class UnsafeList {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
Thread.sleep(1000);
System.out.println(list.size());
}
}
不安全的抢票系统
这是一个简易的抢票程序,有三个人同时不停的去抢10张票。
package com.syc;
public class UnsafeTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket, "小明").start();
new Thread(ticket, "小红").start();
new Thread(ticket, "黄牛").start();
}
}
class Ticket implements Runnable{
//票
private int tickets = 10;
//标识位
private boolean flag = true;
//买票
public void buyTicket() throws InterruptedException {
if (tickets<=0) {
flag = false;
return;
}
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"买到了第"+tickets--+"张票");
}
//重写run
@Override
public void run() {
while (flag) {
try {
buyTicket();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
我们来看结果,在结果中出现了-1张票的情况,很显然在最后剩下一张票的时候,几个人同时去操作这个数据,大致了这样的不安全情况。
小明买到了第8张票
黄牛买到了第9张票
小红买到了第10张票
小明买到了第7张票
黄牛买到了第6张票
小红买到了第5张票
黄牛买到了第2张票
小明买到了第4张票
小红买到了第3张票
小红买到了第1张票
黄牛买到了第-1张票
小明买到了第0张票
不安全的银行取钱
和上面的例子相同,银行账户中有100元钱,两个人同时取钱,一个人取50另一个人取100,最终取出来150元,账户剩余-50元,这样的银行岂不是要倒闭。
package com.syc;
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account("投资基金", 100);
GetMoney you = new GetMoney(account, 50, "You");
GetMoney me = new GetMoney(account, 100, "Me");
you.start();
me.start();
}
}
class Account {
String name;
int money;
public Account (String name, int money) {
this.name = name;
this.money = money;
}
}
class GetMoney extends Thread{
private Account account; //账户
private int moneyNum; //取钱数
public GetMoney(Account account, int moneyNum, String name) {
super(name);
this.account = account;
this.moneyNum = moneyNum;
}
@Override
public void run() {
if (moneyNum>account.money) {
System.out.println("账户余额不足,取钱失败。");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money-=moneyNum;
System.out.println(Thread.currentThread().getName()+"取了"+moneyNum+"元");
System.out.println(account.name+"剩余"+account.money);
}
}
You取了50元
Me取了100元
投资基金剩余-50
投资基金剩余-50
线程同步
为了处理上面的问题,我们有个线程同步synchronized
关键字来处理,线程同步实现了排队+锁的功能。就好比排队上厕所的场景,一个人进了厕所会锁上门,上完厕所打开门出来,第二个人再进去,依次处理。
synchronized默认锁的是this,也就是自身对象,如果锁的对象不是自身,可以用synchronized(对象){代码块}的方式来实现。
我们来看看,将上面的不安全例子使用synchronized“上锁”后的情况。
安全的集合
package com.syc;
import java.util.ArrayList;
import java.util.List;
public class UnsafeList {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(()->{
synchronized (list) {
// 使用synchronized包裹更新数据的代码,上面的list就是要操作的对象
list.add(Thread.currentThread().getName());
}
}).start();
}
Thread.sleep(1000);
System.out.println(list.size());
}
}
1000
Process finished with exit code 0
安全的抢票
package com.syc;
public class UnsafeTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket, "小明").start();
new Thread(ticket, "小红").start();
new Thread(ticket, "黄牛").start();
}
}
class Ticket implements Runnable{
//票
private int tickets = 10;
//标识位
private boolean flag = true;
//买票 --> 用synchronized关键字修饰方法
public synchronized void buyTicket() throws InterruptedException {
if (tickets<0) {
flag = false;
return;
}
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"买到了第"+tickets--+"张票");
}
//重写run
@Override
public void run() {
while (flag) {
try {
buyTicket();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
小明买到了第10张票
小明买到了第9张票
黄牛买到了第8张票
小红买到了第7张票
小红买到了第6张票
小红买到了第5张票
小红买到了第4张票
小红买到了第3张票
黄牛买到了第2张票
黄牛买到了第1张票
黄牛买到了第0张票
Process finished with exit code 0
安全的银行
package com.syc;
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account("投资基金", 100);
GetMoney you = new GetMoney(account, 50, "You");
GetMoney me = new GetMoney(account, 100, "Me");
you.start();
me.start();
}
}
class Account {
String name;
int money;
public Account (String name, int money) {
this.name = name;
this.money = money;
}
}
class GetMoney extends Thread{
private Account account; //账户
private int moneyNum; //取钱数
public GetMoney(Account account, int moneyNum, String name) {
super(name);
this.account = account;
this.moneyNum = moneyNum;
}
@Override
public void run() {
// 与集合一样,使用synchronized将代码包裹,这里操作的对象是account
synchronized (account) {
if (moneyNum>account.money) {
System.out.println("账户余额不足,取钱失败。");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money-=moneyNum;
System.out.println(Thread.currentThread().getName()+"取了"+moneyNum+"元");
System.out.println(account.name+"剩余"+account.money);
}
}
}
You取了50元
投资基金剩余50
账户余额不足,取钱失败。
Process finished with exit code 0
补充
CopyOnWriteArrayList
在第一个不安全集合的例子中,除了使用线程同步的方法,我们还可以使用CopyOnWriteArrayList来实现。CopyOnWriteArrayList是java.util.concurrent中提供的一个安全的集合类,看名字可以理解,他的操作方式是将原来的List复制一份,在复制的新List上进行操作,然后将原来的引用指向这个List。
package com.unsafe;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class SafeList {
public static void main(String[] args) {
// List<String> list = new ArrayList<String>();
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(100); // 这里加上等待,避免最后一个线程没跑完就计算长度
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
看这里的例子,和上面的不安全集合的代码相同,只是将ArrayList改成了CopyOnWriteArrayList,并没有加上线程同步的方式,他的结果也是安全的,如下:
10000
Process finished with exit code 0
Lock
Lock也是java.util.concurrent中提供的一种让线程安全的方式,我们可以自行创建一把锁,在需要保证安全的时候给资源上锁,在用完之后再打开锁。这样的方式相较于synchronized更加的自由,我们还是拿集合的例子来看:
package com.unsafe;
import java.util.ArrayList;
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
try {
lock.lock();
list.add(Thread.currentThread().getName());
} finally {
lock.unlock();
}
}).start();
}
Thread.sleep(100);
System.out.println(list.size());
}
}
结果当然也是安全的,10000,符合我们的预期。