1 并发和并行
-
并发指同一个对象被多个线程同时操作
-
并发是两个队列交替使用一台咖啡机,并行指两个队列同时使用两台咖啡机
-
并发和并行都可以有多个线程,不同之处在于这些线程是否同时被(多个)CPU执行,如果可以就是并行,并发时多个线程被(一个)CPU轮流切换着执行
2 线程同步
- 现实生活中会遇到”同一个资源,多个人都想使用“的问题,例如食堂排队打饭
- 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想要修改这个对象,此时需要线程同步
- 线程同步其实是一种等待机制,多个需要同时访问此对象的对象进入该对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用
- 解决线程同步的关键在于队列和锁
- 由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,需要加入锁机制
synchronized
- 当一个线程获得对象的排他锁和独占资源时,其他线程必须等待,使用后释放锁,存在以下问题
- 一个线程持有锁会导致其他所有需要此锁的线程挂起
- 在多线程竞争下,加锁和释放锁会导致较多的上下文切换和调度延时,引起性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级导致,引起性能问题
3 线程不安全案例
-
抢火车票
public class UnsafeBuyTicket { public static void main(String[] args) { BuyTicket station= new BuyTicket(); new Thread(station, "大黄").start(); new Thread(station, "小黑").start(); new Thread(station, "旺财").start(); } } class BuyTicket implements Runnable { private int ticketNums = 10; boolean flag = true; @Override public void run() { while (flag) { try { buy(); } catch (InterruptedException e) { e.printStackTrace(); } } } private void buy() throws InterruptedException { if (ticketNums <= 0) { flag = false; return; } // 模拟延时 Thread.sleep(100); System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNums-- + "张票"); } }
此时会出现两个线程买到同一张票,或者某个线程买到-1张票的情况。
-
取钱
// 两个人去银行取钱 public class UnsafeBank { public static void main(String[] args) { Account account = new Account(100, "存款"); Drawing you = new Drawing(account, 50, 0, "你"); Drawing yourWife = new Drawing(account, 100, 0, "你媳妇"); you.start(); yourWife.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, int nowMoney, String name) { super(name); this.account = account; this.drawingMoney = drawingMoney; this.nowMoney = nowMoney; } // 取钱 @Override public void run() { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } // 判断钱是否够取出 if (account.money - drawingMoney < 0) { System.out.println("账户余额不足"); return; } account.money -= drawingMoney; // 卡内余额 nowMoney += drawingMoney; // 手中钱数 System.out.println(account.name + "余额为:" + account.money); // Thread.currentThread().getName() = this.getName() System.out.println(this.getName() + "手中有:" + nowMoney); } }
此时可能会出现余额不足但是还是能取出钱,导致余额为负数的情况
-
线程不安全集合
public class UnsafeList{ public static void main(String[] args) throws InterruptedException { List<String> list = new ArrayList<>(); for (int i = 0; i < 10000; i++) { new Thread(() -> { list.add(Thread.currentThread().getName()); }).start(); } Thread.sleep(500); System.out.println(list.size()); // 期待值是10000 } }
此时最终输出的结果可能不足10000
4 同步方法
-
使用
synchronized
关键字修饰的方法为同步方法public synchronized void method() { // todo }
-
synchronized
方法控制对“对象”的访问,每个对象有一把锁,每个synchronized
方法都必须获得调用该对象的方法的锁才能执行,否则线程会阻塞。 -
同步方法一旦执行,就独占锁,直到方法返回时释放锁,后续被阻塞的线程才能获得这个锁继续执行
-
若将一个大的方法声明为
synchronized
将会影响效率 -
同步方法相当于锁定了
this
5 同步块
-
synchronized
可以修饰一个代码块synchronized (obj) { // todo }
-
obj
称为同步监视器-
obj
可以是任何对象,但推荐使用共享资源作为同步监视器 -
同步方法中无需指定同步监视器,因为同步方法的同步监视器就是
this
,即对象本身,或者是class
-
public void method() { synchronized(this) { // todo } }
此写法与同步方法等价,都是锁定了整个方法中的内容
-
-
同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
6 CopyOnWriteArrayList
import java.util.concurrent.CopyOnWriteArrayList;
public class TestJUC{
public static void main(String[] args) throws InterruptedException {
// 线程安全的ArrayList
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
list.add(Thread.currentThread().getName());
}).start();
}
Thread.sleep(500);
System.out.println(list.size());
}
}
CopyOnWriteArrayList
是线程安全的读操作无锁的ArrayList
,其中所有的可变操作都是对底层数组进行一次新的复制来实现CopyOnWrite
就是对一块内存进行修改时,不直接在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后,再将原来指向的内存指针指到新的内存,原来的内存就可以被回收。CopyOnWriteArrayList
适合读操作远大于写操作的场景中,例如缓存CopyOnWriteArrayList
不存在扩容的概念,每次写操作都要复制一个副本,性能很差CopyOnWriteArrayList
无法保证实时性要求,它拷贝数组、新增元素都需要时间,所以一个写操作后可能读取到的数据还是旧的,但能保证最终一致性
7 死锁
-
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,导致两个或多个线程都在等待对方释放资源,从而停止执行的场景。某一个同步块中同时拥有两个或多个对象的锁时,可能会发生死锁
-
死锁可以理解为多个线程互相持有对方需要的资源,然后形成僵持的局面
public class DeadLock { public static void main(String[] args) { Makeup girl1 = new Makeup(0, "小丽"); Makeup girl2 = new Makeup(1, "小红"); // 此时出现死锁 girl1.start(); girl2.start(); } } class Lipstick {} class Mirror {} class Makeup extends Thread { // 需要的资源只有一份,用static关键字修饰 static final Lipstick lipstick = new Lipstick(); static final Mirror mirror = new Mirror(); int choice; String girlName; // 使用化妆品的人 public Makeup(int choice, String girlName) { this.choice = choice; this.girlName = girlName; } @Override public void run() { try { makeup(); } catch (InterruptedException e) { e.printStackTrace(); } } // 互相持有对方的锁 private void makeup() throws InterruptedException { if (choice == 0) { synchronized (lipstick) { System.out.println(this.girlName + "获得口红"); Thread.sleep(500); synchronized (mirror) { System.out.println(this.girlName + "获得镜子"); } } } else { synchronized (mirror) { System.out.println(this.girlName + "获得镜子"); Thread.sleep(1000); synchronized (lipstick) { System.out.println(this.girlName + "获得口红"); } } } } }
-
产生死锁的必要条件:
- 互斥条件:进程要求对所分配的资源进行排他性控制,即在一段时间内某资源仅为一进程所用
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放
- 不可剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源条件
-
解决死锁的基本方法
- 预防死锁:资源一次性分配(破坏请求和保持条件);可剥夺资源,即当某进程新的资源未满足时,释放已占有的资源(破坏不可剥夺条件);资源有序分配法,即系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏循环等待条件)
- 避免死锁:预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得较满意的系统性能。最具代表性的避免死锁算法是银行家算法
- 检测死锁:允许死锁发生,但会通过一些手段检测出来,JVM自带的 jstack 堆栈跟踪工具和JDK自带的 JConsole 监控工具可用于检测死锁
- 解除死锁:发现有进程死锁后,便应立即把它从死锁状态中解脱出来
8 Lock
对象
-
从JDK5之后,Java提供了更强大的线程同步机制,通过显式定义同步锁实现同步,使用
Lock
对象充当同步锁 -
java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock
对象加锁,线程开始访问共享资源之前应先获得Lock
对象 -
ReentrantLock
类实现了Lock
接口,它拥有与synchronized
相同的并发性和内存语义。在实现线程安全控制时,较常用ReentrantLock
,可以显式加锁和释放锁import java.util.concurrent.locks.ReentrantLock; public class TestLock { public static void main(String[] args) { BuyTicket station= new BuyTicket(); new Thread(station).start(); new Thread(station).start(); new Thread(station).start(); } } class BuyTicket implements Runnable { private int ticketNums = 10; // 定义锁 private final ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (true) { lock.lock(); // 加锁 try { if (ticketNums > 0) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(ticketNums--); } else { break; } } finally { lock.unlock(); // 释放锁 } } } }
-
synchronized
与Lock
的对比Lock
是显式锁(手动开启和关闭锁),synchronized
是隐式锁,运行出作用域自动释放Lock
只有代码块锁,synchronized
有代码块锁和方法锁- 使用
Lock
锁,JVM将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类) - 优先级:
Lock
> 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)