一、JUC入门介绍 JUC即 java.util.concurrent
涉及三个包:
java.util.concurrent java.util.concurrent.atomic java.util.concurrent.locks 1.1、进程/线程 1、进程 一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。
2、线程 进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
与进程不同的是同类的多个线程共享进程的堆 和方法区 资源,但每个线程有自己的程序计数器 、虚拟机栈 和本地方法栈 ,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
3、举例 使用QQ,查看进程一定有一个QQ.exe的进程,我可以用qq和A文字聊天,和B视频聊天,给C传文件,给D发一段语言,QQ支持录入信息的搜索。
大四的时候写论文,用word写论文,同时用QQ音乐放音乐,同时用QQ聊天,多个进程。
word如没有保存,停电关机,再通电后打开word可以恢复之前未保存的文档,word也会检查你的拼写,两个线程:容灾备份,语法检查
4、wait/sleep 功能都是当前线程暂停,有什么区别?
wait放开手去睡,放开手里的锁 sleep握紧手去睡,醒了手里还有锁 1.2、并发/并行 1、并发 并发:多个线程任务通过一个cpu执行,所以这些线程任务只能通过切换执行来实现并发,只不过这些线程切换的速度很快,宏观上看似乎就是同时执行的,其实还是一个一个执行的,只不过可以切换执行。
2、并行 并行:多个线程任务通过多个cpu执行,真正意义上的同时执行。因为有多个cpu,一个cpu执行一个任务就好了。
1.3、SaleTicket 三个售票员卖出三十张票
多线程编程固定套路:线程 + 操作 + 资源类
1、编写一个资源类Ticket 资源类 = 实例变量 + 实例方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Ticket { private int number = 30 ; Lock lock = new ReentrantLock(); public void sale () { lock.lock(); try { if (number > 0 ) { System.out.println(Thread.currentThread().getName() + "\t" + "卖出第" + (number--) + "\t 还剩下" + number + "张票" ); } } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
2、创建一个SaleTicket方法,创建三个线程后开始买票 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class SaleTicket { public static void main (String[] args) { Ticket ticket = new Ticket(); new Thread(() -> { for (int i = 0 ; i < 40 ; i++) { ticket.sale(); } },"A" ); new Thread(() -> { for (int i = 0 ; i < 40 ; i++) { ticket.sale(); } },"B" ); new Thread(() -> { for (int i = 0 ; i < 40 ; i++) { ticket.sale(); } },"C" ); } }
测试
3、优化上述代码 使用λ表达式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class SaleTicket { public static void main (String[] args) { Ticket ticket = new Ticket(); new Thread(() -> { for (int i = 0 ; i < 40 ; i++) { ticket.sale(); } },"A" ); new Thread(() -> { for (int i = 0 ; i < 40 ; i++) { ticket.sale(); } },"B" ); new Thread(() -> { for (int i = 0 ; i < 40 ; i++) { ticket.sale(); } },"C" ); } }
1.4、锁 1、锁概述 线程安全问题的产生前提:多个线程并发访问共享数据。
将多个线程对共享数据的并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问.
锁可以理解为对共享数据进行保护的一个许可证,对于同一个许可证保护的共享数据来说,任何线程想要访问这些共享数据必须先持有该许可证。一个线程只有在持有许可证的情况下才能对这些共享数据进行访问。并且一个许可证一次只能被一个线程持有。
锁具有排他性 ,即一个锁一次只能被一个线程持有
2、锁的作用 锁可以实现对共享数据的安全访问
保障线程的原子性、可见性和有序性
3、注意 使用锁保证线程的安全性,必须满足以下条件:
这些线程在访问共享数据时必须使用同一个锁 即使是读取共享数据的线程也需要使用同步锁 1.5、锁的相关概念 1、可重入性(Reentrancy) 可重入性(Reentrancy)描述这样一个问题:一个线程持有该锁的时候能否再次(多次)申请该锁
如果一个线程持有一个锁的时候还能成功申请该所,称该锁为可重入锁,否则称该锁为不可重入锁
2、锁的争用和调度 Java平台中内部锁(synchronized) 属于非公平锁 ,显式 Lock
锁既支持公平锁,也支持非公平锁
3、锁的粒度 一个锁可以保护的共享数据的数量大小称为锁的粒度,锁的粒度是相对的。
锁保护的共享数据量大,称该锁的粒度粗,否则称锁的粒度细。
如果锁的粒度过粗,会导致线程在申请锁时进行不必要的等待。
如果锁的粒度过细,会增加锁调度的开销。
1.6、可见性、有序性和原子性 1、原子性 原子性 :原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉 。及时在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。
判断以下指令是否满足原子性
满足,将10直接赋值给线程工作内存中的变量a
不满足,语句a++实际上包括了三个操作:
读取变量a的值 将读取出来的a值 + 1 将+1后的值赋给变量a 2、有序性 有序性 : 编译器和处理器为了优化程序性能而对指令序列进行重排序,也就是你编写的代码顺序和最终执行的指令顺序是不一致的,重排序可能会导致多线程程序出现内存可见性问题
3、可见性 可见性 : 多个线程访问同一个共享变量时,其中一个线程对这个共享变量值的修改,其他线程能够立刻获得修改以后的值
二、Lock接口 2.1、是什么?
锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。
2.2、Lock接口实现–ReentrantLock可重入锁
1、如何使用? 1 2 3 4 5 6 7 8 9 10 11 12 class X { private final ReentrantLock lock = new ReentrantLock(); public void m () { lock.lock(); try { } finally { lock.unlock(); } } }
2、Lock和synchronized的区别 二者区别
首先synchronized是Java内置关键字,在jvm层面,而Lock是Java中的一个接口 synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁; synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁; 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了; synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可) Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。 三、线程间通信 引入题目:现有两个线程,可以操作一个初始值为0的变量
实现一个线程令该变量 + 1,一个线程令该变量 - 1
实现交替.
3.1、入门案例 1、编写资源类 资源类中包括变量和操控变量的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Counter { private int number = 0 ; public synchronized void add () throws InterruptedException { if (number != 0 ) { this .wait(); } number++; System.out.println(Thread.currentThread().getName() + "\t" + number); this .notifyAll(); } public synchronized void sub () throws InterruptedException { if (number == 0 ) { this .wait(); } number--; System.out.println(Thread.currentThread().getName() + "\t" + number); this .notifyAll(); } }
2、在main方法中创建线程进行测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Counter counter = new Counter(); new Thread(() -> { for (int i = 1 ;i <= 10 ;i++) { try { counter.add(); } catch (InterruptedException interruptedException) { interruptedException.printStackTrace(); } } },"A" ).start(); new Thread(() -> { for (int i = 1 ;i <= 10 ;i++) { try { counter.sub(); } catch (InterruptedException interruptedException) { interruptedException.printStackTrace(); } } },"B" ).start();
3、结果
3.2、将线程数从两个增加到4个,两个加两个减 1、增加线程数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 Counter counter = new Counter(); new Thread(() -> { for (int i = 1 ;i <= 10 ;i++) { try { counter.add(); } catch (InterruptedException interruptedException) { interruptedException.printStackTrace(); } } },"A" ).start(); new Thread(() -> { for (int i = 1 ;i <= 10 ;i++) { try { counter.sub(); } catch (InterruptedException interruptedException) { interruptedException.printStackTrace(); } } },"B" ).start(); new Thread(() -> { for (int i = 1 ;i <= 10 ;i++) { try { counter.add(); } catch (InterruptedException interruptedException) { interruptedException.printStackTrace(); } } },"C" ).start(); new Thread(() -> { for (int i = 1 ;i <= 10 ;i++) { try { counter.sub(); } catch (InterruptedException interruptedException) { interruptedException.printStackTrace(); } } },"D" ).start();
2、测试查看结果 此时发现结果不符合预期
3、出现以上问题的原因是多线程的虚假唤醒 如何解决?
修改资源类,在add方法和sub方法中使用while 代替if ,查看Java 8 Api文档
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Counter { private int number = 0 ; public synchronized void add () throws InterruptedException { while (number != 0 ) { this .wait(); } number++; System.out.println(Thread.currentThread().getName() + "\t" + number); this .notifyAll(); } public synchronized void sub () throws InterruptedException { while (number == 0 ) { this .wait(); } number--; System.out.println(Thread.currentThread().getName() + "\t" + number); this .notifyAll(); } }
此时重启测试,查看结果,发现问题已经解决。
4、总结 高内聚低耦合前提下,线程操作资源类 判断/工作/通知 多线程交互 中,必须要防止多线程的虚假唤醒 ,即判断只用while 而不用if 注意标志位的修改和定位 5、虚假唤醒出现原因 现有4个线程,两个生产者,两个消费者
在第一个生产者生产完后,会调用notifyAll唤醒其余线程,此时被唤醒的线程可能是消费者,也可能是生产者,如果是另一个生产者唤醒,那么由于number已经不为0,所以进来的生产者会执行 this.wait() 。
由于wait方法会交出锁的持有权,所以此时第一个生产者、其他两个消费者会重新抢夺锁,如果此时抢到锁的是第一个生产者,那么由于number已经不为0,所以进来的第一个生产者也会执行this.wait() 。
此时由于wait方法,第一个生产者交出了手中的锁,此时两个消费者线程重新抢夺时间片,(注意此时资源类对象的number依然为1 ),在消费者线程将number–后会调用notify唤醒其他所有线程,此时由于两个生产者线程已经wait很久,所以会优先给这两个线程分配时间片,所以此时两个消费者线程都会执行number++,number从0变为2。
如果使用while代替if,那么在生产者被消费者唤醒后会进行一次判断,此时就不会出现两个生产者线程都让number+1的情况。
3.3、使用Lock代替synchronized 1、关系图
使用await代替wait
使用signal代替notify
使用signalAll代替notifyAll
2、修改资源类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class LockCounter { private int number = 0 ; private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void add () { lock.lock(); try { while (number != 0 ) { condition.await(); } number++; System.out.println(Thread.currentThread().getName() + "\t" + number); condition.signalAll(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void sub () { lock.lock(); try { while (number == 0 ) { condition.await(); } number--; System.out.println(Thread.currentThread().getName() + "\t" + number); condition.signalAll(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
3、运行,查看结果
3.4、使用Lock的原因 1、引入新问题 多线程之间实现顺序调用,即A->B->C
A打印5次,B打印10次,C打印15次
接着
A打印5次,B打印10次,C打印15次
重复十轮
打印顺序要求如下A->B->C->A
使用一个标志位,如果标志位为1,那么A输出,为2则B输出,为3则C输出
2、编写资源类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 class ShareResource { private int number = 1 ; private Lock lock = new ReentrantLock(); private Condition condition1 = lock.newCondition(); private Condition condition2 = lock.newCondition(); private Condition condition3 = lock.newCondition(); public void print5 () { lock.lock(); try { while (number != 1 ) { condition1.await(); } for (int i = 0 ; i < 5 ; i++) { System.out.print("A" + "\t" ); } number = 2 ; condition2.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void print10 () { lock.lock(); try { while (number != 2 ) { condition2.await(); } for (int i = 0 ; i < 10 ; i++) { System.out.print("B" + "\t" ); } number = 3 ; condition3.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void print15 () { lock.lock(); try { while (number != 3 ) { condition3.await(); } for (int i = 0 ; i < 15 ; i++) { System.out.print("C" + "\t" ); } number = 1 ; condition1.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
3、创建线程进行调用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ShareResource resource = new ShareResource(); new Thread(() -> { for (int i = 0 ; i < 10 ; i++) { resource.print5(); } },"A" ).start(); new Thread(() -> { for (int i = 0 ; i < 10 ; i++) { resource.print10(); } },"B" ).start(); new Thread(() -> { for (int i = 0 ; i < 10 ; i++) { resource.print15(); } },"C" ).start();
4、查看结果 测试成功
5、结论 Lock配合Condition使用可以达到精确唤醒的效果
3.5、面试题解答 两个线程,一个线程打印1-52,另一个打印字母A-Z打印顺序为12A34B…5152Z
要求用线程间通信
1、编写一个资源类 由于打印的顺序为两个数字一个字母,那么判断条件就是:当number为3的倍数时,打印字母,其余时间打印数字。
多线程问题一般的解决套路
资源类 判断/工作/唤醒 注意标志位的修改和定位 防止虚假唤醒 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 class Printer { private static int number = 1 ; private final Lock lock = new ReentrantLock(); private final Condition engCondition = lock.newCondition(); private final Condition numCondition = lock.newCondition(); public void printEng () { lock.lock(); try { for (char i = 'A' ; i <= 'Z' ; i++) { while (number % 3 != 0 ) { engCondition.await(); } System.out.println(Thread.currentThread().getName() + "-->" + i); number++; numCondition.signal(); } } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void printNum () { lock.lock(); try { for (int i = 1 ; i <= 54 ; i++) { while (number % 3 == 0 ) { numCondition.await(); } System.out.println(Thread.currentThread().getName() + "-->" + i); number++; engCondition.signal(); } } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
2、创建两个线程,调用资源类中的方法 1 2 3 Printer printer = new Printer(); new Thread(() -> printer.printEng(),"打印字母的线程" ).start();new Thread(() -> printer.printNum(),"打印数字的线程" ).start();
3、运行,查看结果
3.6、多线程8锁 1、8锁现象 创建一个资源类Phone
1 2 3 4 5 6 7 8 9 class Phone { public synchronized void sendEmail () throws Exception { System.out.println("---------sendEmail" ); } public synchronized void sendSMS () throws Exception { System.out.println("---------sendSMS" ); } }
在main方法中编写以下代码,查看输出结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Phone phone = new Phone(); new Thread(() -> { try { phone.sendEmail(); } catch (Exception exception) { exception.printStackTrace(); } },"A" ).start(); new Thread(() -> { try { phone.sendSMS(); } catch (Exception exception) { exception.printStackTrace(); } },"B" ).start();
此时启动程序,sendSMS和sendEmail方法执行的前后顺序无法确定。
AB两个线程访问同一个资源类对象,此时由于锁的是一个资源类对象,所以两个线程谁先拿到CPU执行权,谁就先执行。
在两个线程的启动中添加Thread.sleep()方法,此时main函数中代码为 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Phone phone = new Phone(); new Thread(() -> { try { phone.sendEmail(); } catch (Exception exception) { exception.printStackTrace(); } },"A" ).start(); Thread.sleep(2000 ); new Thread(() -> { try { phone.sendSMS(); } catch (Exception exception) { exception.printStackTrace(); } },"B" ).start();
此时启动程序,先调用sendEmail方法,这是因为AB线程的start方法间有sleep方法,导致A先抢到了锁
在邮件方法中使用sleep暂停4秒钟,且两个线程中间任何休眠两秒 1 2 3 4 public synchronized void sendEmail () throws Exception { Thread.sleep(4000 ); System.out.println("---------sendEmail" ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Phone phone = new Phone(); new Thread(() -> { try { phone.sendEmail(); } catch (Exception exception) { exception.printStackTrace(); } },"A" ).start(); Thread.sleep(2000 ); new Thread(() -> { try { phone.sendSMS(); } catch (Exception exception) { exception.printStackTrace(); } },"B" ).start();
此时启动程序,发现还是先调用sendEmail方法,这是因为Thread.sleep()方法不会释放对象锁,所以A抢到锁后先抱着锁睡了4秒,然后执行sendEmail方法。
1 2 3 public void hello () { System.out.println("---------Hello" ); }
修改main方法中的代码,令B线程调用hello方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Phone phone = new Phone(); new Thread(() -> { try { phone.sendEmail(); } catch (Exception exception) { exception.printStackTrace(); } },"A" ).start(); Thread.sleep(2000 ); new Thread(() -> { try { phone.hello(); } catch (Exception exception) { exception.printStackTrace(); } },"B" ).start();
此时启动程序,先执行hello()方法,再执行sendEmail方法
引入另一个资源类对象,并令线程B调用另外一个对象的sendSMS方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Phone phone = new Phone(); Phone phone1 = new Phone(); new Thread(() -> { try { phone.sendEmail(); } catch (Exception exception) { exception.printStackTrace(); } },"A" ).start(); Thread.sleep(100 ); new Thread(() -> { try { phone1.sendSMS(); } catch (Exception exception) { exception.printStackTrace(); } },"B" ).start();
启动代码,发现先调用sendSms方法,这是由于锁的不是同一个对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Phone { public static synchronized void sendEmail () throws Exception { Thread.sleep(4000 ); System.out.println("---------sendEmail" ); } public static synchronized void sendSMS () throws Exception { System.out.println("---------sendSMS" ); } public void hello () { System.out.println("---------Hello" ); } }
main方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 new Thread(() -> { try { Phone.sendEmail(); } catch (Exception exception) { exception.printStackTrace(); } },"A" ).start(); Thread.sleep(100 ); new Thread(() -> { try { Phone.sendSMS(); } catch (Exception exception) { exception.printStackTrace(); } },"B" ).start();
由于synchronized修饰的是静态方法,则等同于synchronized(this.getClass),两个方法锁的是同一个类对象,所以还是sendEmail方法先被调用
由于静态同步方法锁的是资源类,所以结果同上
一个普通同步方法,一个静态同步方法,一个资源类对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Phone { public static synchronized void sendEmail () throws Exception { Thread.sleep(4000 ); System.out.println("---------sendEmail" ); } public synchronized void sendSMS () throws Exception { System.out.println("---------sendSMS" ); } public void hello () { System.out.println("---------Hello" ); } }
main方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Phone phone = new Phone(); new Thread(() -> { try { Phone.sendEmail(); } catch (Exception exception) { exception.printStackTrace(); } },"A" ).start(); Thread.sleep(100 ); new Thread(() -> { try { phone.sendSMS(); } catch (Exception exception) { exception.printStackTrace(); } },"B" ).start();
此时先调用sendSMS方法,然后调用sendEmail方法,这是因为锁的对象不是同一个,所以先调用没有sleep的sendSMS方法
一个普通同步方法,一个静态同步方法,两个资源类对象 同上
2、8锁的解释 一个对象里面如果有多个synchronized方法,某一时刻内,只要一个线程去调用其中的一个synchronized方法了,那么其他的线程只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法,锁的是当前对象this,被锁定后,其他的线程都不能进行到当前对象的其他synchronized方法。
如果添加一个普通方法,那么与同步锁无关。
如果操控两个资源类对象,那么锁的就不是同一个对象了。
可以把操作两个资源类对象的情况想象成线程A在用自己的苹果手机发短信,而线程B在用自己的手机发邮件,此时两线程操作两台手机,不用争抢,井水不犯河水。
如果使用synchronized修饰静态方法,那么此时锁的对象是资源类对象的Class ,也即synchronized(this.getClass)
和资源类对象的个数没关系
3、synchronized总结 synchronized实现同步的基础:Java中的每一个对象都可以作为锁。
具体表现为以下3种形式。
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
也就是说如果一个实例对象的非静态同步方法 获取锁后,该实例对象的其他非静态同步方法 必须等待获取锁的方法释放锁后才能获取锁,
可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。
所有的静态同步方法用的也是同一把锁——类对象本身 ,这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。
但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象!