5 线程同步(多个线程操作同一个资源)
5.1 基础概念
当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态。
处理多线程问题时,多个线程访问同一个对象,而且某些线程还想修改这个对象。这时我们就需要线程同步。线程同步是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下个线程再次使用。
并发:同一个对象被多个线程同时操作(抢票/取钱)
5.2 队列和锁
类似模型(排队上厕所,正在上厕所的人需要加锁避免被人抢占厕所资源)
同一进程的多个线程共享同一块存储空间,带来方便的同时也带来了访问冲突问题。为保证数据在方法中被访问时的正确性,在访问时加入锁机制(synchronized),当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。
可能存在的问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起。
- 多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级导致,引起性能问题。
5.3 三大线程不安全案例
5.3.1 不安全的买票
示例:三个人竞争买十张票
点击查看代码
public class UnsafeTicket {public static void main(String[] args) {BuyTicket buyTicket = new BuyTicket();new Thread(buyTicket, "Mike").start();new Thread(buyTicket, "John").start();new Thread(buyTicket, "Kiki").start();}}class BuyTicket implements Runnable{private int ticketNums = 10;private boolean flag = true; //外部停止方式@Overridepublic void run() {while(flag) buy();}private void buy(){if(ticketNums < 1){flag = false;return;}else{//模拟延时try {Thread.sleep(100);} catch (InterruptedException e){}System.out.println(Thread.currentThread().getName() + "买到" + ticketNums-- + "张票");}}}
运行结果:
点击查看运行结果
Kiki买到10张票John买到8张票Mike买到9张票Mike买到7张票John买到7张票Kiki买到6张票Mike买到5张票John买到4张票Kiki买到3张票Kiki买到2张票Mike买到0张票John买到1张票
Q1. 有人买到了同一张票(7Q2. 买票顺序不定(10, 8, 9)Q3. 出现买到第0张票
5.3.2 银行取钱
示例:两个人要取得钱超出了银行中得余额
点击查看代码
public class UnsafeBank {public static void main(String[] args) {Account account = new Account(10000, "Marry Money");Bank me = new Bank(account, 5000, 2000, "你");Bank girl = new Bank(account, 7000, 3000, "Girl");me.start();girl.start();}}//账户class Account{private int money; //余额private String name; //卡名public Account(int money, String name) {this.money = money;this.name = name;}public int getMoney() {return money;}public String getName() {return name;}public void setMoney(int money) {this.money = money;}public void setName(String name) {this.name = name;}}//银行(模拟取钱class Bank extends Thread{Account account;private int drawMoney;private int nowMoney;private String name;public Bank(Account account, int drawMoney, int nowMoney, String name) {super();this.account = account;this.drawMoney = drawMoney;this.nowMoney = nowMoney;this.name = name;}@Overridepublic void run() {if(account.getMoney() - drawMoney < 0){System.out.println(Thread.currentThread().getName() + "钱不够,无法取钱");return;}account.setMoney(account.getMoney() - drawMoney);nowMoney += drawMoney;System.out.println(account.getName() + "余额为:"+ account.getMoney());//Thread.currentThread.getName() = this.getName();System.out.println(this.name + "手中的钱" + this.nowMoney);}
运行结果:
点击查看运行结果
Marry Money余额为:3000Marry Money余额为:3000Girl手中的钱10000你手中的钱7000
Q.取得钱超过了银行余额仍然取出来了
5.3.3 线程不安全的集合
示例:线程向ArrayList中添加5000个数据
public class UnsafeList {public static void main(String[] args) {List<String> list = new ArrayList<>();for(int i = 0; i < 5000; i++){new Thread(() -> list.add(Thread.currentThread().getName())).start();}System.out.println(list.size());}}
运行结果:
4998
6 同步方法与同步块(synchronized)
从1.0 版开始,Java中的每一个对象都有一个内部锁。如果一个方法用synchronized 关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁 – 《Java核心技术卷1》即每一个对象有一个内部锁,并且该锁有一个内部条件。由锁来管理那些试图进入synchronized 方法的线程,由条件来管理那些调用wait 的线程。
我们只需针对方法提出一套机制(synchronized关键字),它包括两种用法:synchronized方法
public synchronized void method(int args){}
和synchronized块。
6.1 同步方法
synchronized方法控制对对象的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得锁并继续执行。这导致大的synchronized方法会影响效率。
6.2 同步块
使用方法:
(synchronized(Obj){})
6.2.1 Obj-同步监视器
- Obj可以是任何对象,推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class
6.2.2 Obj执行过程
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解除同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问。
6.2.3 同步块示例
注:所得对象就是变化的量(即需要增删改的对象)
6.2.3.1 (基于银行案例)
@Overridepublic synchronized void run() {synchronized (account) {if (account.getMoney() - drawMoney < 0) {System.out.println(this.name + ": 银行钱不够,无法取钱");return;}account.setMoney(account.getMoney() - drawMoney);nowMoney += drawMoney;System.out.println(account.getName() + "余额为:" + account.getMoney());//Thread.currentThread.getName() = this.getName();System.out.println(this.name + "手中的钱" + this.nowMoney);}}
6.2.3.2 (基于ArrayList)
for(int i = 0; i < 5000; i++){new Thread(() -> {synchronized (list){list.add(Thread.currentThread().getName());}}).start();}try{Thread.sleep(100);} catch (InterruptedException e){}
补充:JUC安全类型的集合(CopyOnWriteArrayList)
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();for (int i = 0; i < 10000; i++) {new Thread(() -> list.add(22)).start();}try{Thread.sleep(100);} catch (InterruptedException e){}System.out.println(list.size());
需要加sleep的原因为可能list在添加最后几个数据时main已经输出size了,需要将主线程Thread延时一下
7 锁(Lock)
7.1 死锁
7.1.1 死锁概述
- 在一组进程发生死锁的情况下,这组死锁进程中的每一个进程都在等待另一个死锁进程所占有的资源。
7.1.2 产生死锁的必要条件(缺一不可)
- 互斥条件(某些资源在一段时间内只能被一个进程/线程占用)
- 请求和保持条件(已保持了至少一个资源,但又提出了新的资源请求且该资源已被其他线程占有)
- 不可抢占条件(进程已获得的资源在未使用完之前不能被抢占,只能在进程使用完后自己释放)
- 循环等待条件(存在一个进程-资源的循环连,循环等待已被占用资源)
7.2 Java-Lock概述
- 从JDK5.0开始,Java提供了一种更强大的线程同步机制-显式定义同步锁对象来实现同步(使用Lock对象)
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源前应先获得Lock对象
- **ReentrantLock(可重入锁)**实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中比较常用的是ReentrantLock,可以显式加锁、释放锁。
7.3 实现案例(基于抢票系统)
点击查看代码
class TestLock2 implements Runnable{private int ticktNums = 10;private ReentrantLock lock = new ReentrantLock();@Overridepublic void run() {while(true){try{//开启锁,并将线程不安全代码置于try块lock.lock();if(ticktNums > 0){try{Thread.sleep(1000);} catch (InterruptedException e){e.printStackTrace();}System.out.println(ticktNums--);}else{break;}}finally {//解锁lock.unlock();}}}}
这一结构确保任何时刻只有一个线程进人临界区。一旦一个线程封锁了锁对象,其他任何线程都无法通过lock 语句。当其他线程调用lock 时,它们被阻塞,直到第一个线程释放锁对象。警告:把解锁操作括在finally 子句之内是至关重要的。如果在临界区的代码抛出异常,锁必须被释放。否则,其他线程将永远阻塞。
7.4 synchronized与lock
7.4.1 简单对比
- Lock为显式锁,synchronized是隐式锁(出了作用域自动释放)
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性
- 有限使用顺序:Lock>同步代码块>同步方法
7.4.2 使用场景
- 最好既不使用Lock/Condition 也不使用synchronized 关键字。在许多情况下你可以使用java.util.concurrent 包中的一种机制,它会为你处理所有的加锁。
- 如果synchronized 关键字适合你的程序,那么请尽量使用它,这样可以减少编写的代码数量,减少出错的几率。
- 如果特别需要Lock/Condition 结构提供的独有特性时,才使用Lock/Condition。