线程不安全
并发
并发:同一个对象被多个线程同时操作
并发问题:买火车的票的案例 多个线程同时操作同一个对象
package test;
import com.sun.org.apache.bcel.internal.generic.NEW;
//多个线程同时操作同一个对象
//买火车的票的案例
//发现问题 :多个线程操作同一个资源的情况下,线程不安全,数据紊乱
public class ThreadTest2 implements Runnable {
private int ticketNums = 10;
@Override
public void run() {
while (true) {
if (ticketNums <= 0) {
break;
}
//模拟延时 0.2秒
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums-- + "票");
}
}
public static void main(String[] args) {
ThreadTest2 ticketThread = new ThreadTest2();
new Thread(ticketThread, "小明").start();
new Thread(ticketThread, "老师").start();
new Thread(ticketThread, "黄牛党").start();
}
}
多个线程操作同一个资源的情况下,线程不安全,数据紊乱
线程同步
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。这个时候我们需要线程同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入到这个对象的等待池 形成队列,等待前面线程使用完毕,下一个线程再使用。类似生活中食堂排队打饭,排队,一个个来。
队列和锁(安全,损失性能)
由于同一个进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入 锁机制 synchronized ,当一个线程获得对象的排它锁,独占资源,其他线程必须等待使用后释放锁即可,存在以下问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起;
- 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换 和 调度延时,引发性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁 会导致优先级倒置,引起性能问题。
三大不安全的案例
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
- 不安全的买票
package test;
//不安全的买票
//线程不安全,有负数
public class UnsafeTicketBuying {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket,"抢票的我").start();
new Thread(buyTicket,"抢票的某人").start();
new Thread(buyTicket,"抢票黄牛党").start();
}
}
class BuyTicket implements Runnable{
//票
private int ticketNum = 10;
//外部停止方式
boolean flag = true;
@Override
public void run() {
//买票
while (flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//买票
private void buy() throws InterruptedException {
//判断是否有票
if(ticketNum<=0){
flag = false;
return;
}
//模拟延时
Thread.sleep(100);
//买到票
System.out.println(Thread.currentThread().getName()+"拿到第"+ticketNum-- +"张票");
}
}
- 不安全的取钱
package test;
//不安全的取钱
//两个人去银行取钱,账号
public class UnsafeBank {
public static void main(String[] args) {
//账户
Account account = new Account(100, "结婚基金");
Drawing you = new Drawing(account, 50, "你");
Drawing girlFriend = new Drawing(account, 100, "女朋友");
you.start();
girlFriend.start();
}
}
class Account {
//余额
int money;
//卡名
String name;
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
//银行:模拟取款
class Drawing extends Thread {
Account account;//账户
//取了多少钱
int drawingMoney;
//现在手里有多少钱
int nowMoney;
public Drawing(Account account, int drawingMoney, String name) {
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
//取钱
@Override
public void run() {
//判断有没有钱
if (account.money - drawingMoney < 0) {
System.out.println(Thread.currentThread().getName() + "钱不够了,取不了钱了");
return;
}
//sleep可以放大问题的发生性
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//卡内余额 = 余额 - 你取的钱
account.money = account.money - drawingMoney;
//你手里的钱
nowMoney = nowMoney + drawingMoney;
System.out.println(account.name + "余额为:" + account.money);
//Thread.currentThread().getName() = this.getName();
System.out.println(this.getName() + "手里的钱:" + nowMoney);
}
}
- 线程不安全的集合
比如一个 ArrayList 类,在添加一个元素的时候,它可能会有两步来完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。
在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;
而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。
那好,我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。
package test;
import java.util.ArrayList;
import java.util.List;
//线程不安全的集合
public class UnsafeList {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 5000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}