JUC并发编程
学习文档:https://www.cnblogs.com/th11/p/15330675.html
学习视频:https://www.bilibili.com/video/BV1B7411L7tE/
JUC
JUC就是java.util.concurrent下面的类包,专门用于多线程的开发。
进程
一个程序,QQ.EXE Music.EXE;数据+代码+pcb
一个进程可以包含多个线程,至少包含一个线程!
Java默认有几个线程?2个线程! main线程、GC线程(垃圾回收)
线程
开了一个进程Typora,写字,等待几分钟会进行自动保存(线程负责的)
对于Java而言:Thread、Runable、Callable进行开启线程的。
提问?JAVA真的可以开启线程吗? 开不了的!
Java是没有权限去开启线程、操作硬件的,这是一个native的一个本地方法,它调用的底层的C++代码。
并发
多线程操作同一个资源。
- CPU 只有一核,模拟出来多条线程。那么我们就可以使用CPU快速交替,来模拟多线程。
- 并发编程的本质:充分利用CPU的资源!
并行
并行: 多个人一起行走
- CPU多核,多个线程可以同时执行。 我们可以使用线程池!
线程的状态
public enum State { //运行 NEW, //运行 RUNNABLE, //阻塞 BLOCKED, //等待,死死等待 WAITING, //超时等待 TIMED_WAITING, //终止 TERMINATED; }
wait/sleep区别
1、来自不同的类
wait => Object
sleep => Thread
TimeUnit.DAYS.sleep(1); //休眠1天 TimeUnit.SECONDS.sleep(1); //休眠1s
2、关于锁的释放
wait 会释放锁;
sleep睡觉了,不会释放锁;
3、使用的范围是不同的
wait 必须在同步代码块中;
sleep 可以在任何地方睡;
4、是否需要捕获异常
wait是不需要捕获异常(11后也需要捕获异常了);
sleep必须要捕获异常;
传统的 synchronized
package com.marchsoft.juctest; import lombok.Synchronized; /** * Description:synchronized * * @author jiaoqianjin * Date: 2020/8/10 21:36 **/ public class Demo01 { public static void main(String[] args) { final Ticket ticket = new Ticket(); new Thread(()->{ for (int i = 0; i < 40; i++) { ticket.sale(); } },"A").start(); new Thread(()->{ for (int i = 0; i < 40; i++) { ticket.sale(); } },"B").start(); new Thread(()->{ for (int i = 0; i < 40; i++) { ticket.sale(); } },"C").start(); } } // 资源类 OOP 属性、方法 class Ticket { private int number = 30; //卖票的方式 public synchronized void sale() { if (number > 0) { System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票剩余" + number + "张票"); } } }
Lock锁
公平锁: 十分公平,必须先来后到~(不能插队);
非公平锁: 十分不公平,可以插队;(默认为非公平锁)
package com.marchsoft.juctest; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * Description: * * @author jiaoqianjin * Date: 2020/8/10 22:05 **/ public class LockDemo { public static void main(String[] args) { final Ticket2 ticket = new Ticket2(); new Thread(() -> { for (int i = 0; i < 40; i++) { ticket.sale(); } }, "A").start(); new Thread(() -> { for (int i = 0; i < 40; i++) { ticket.sale(); } }, "B").start(); new Thread(() -> { for (int i = 0; i < 40; i++) { ticket.sale(); } }, "C").start(); } } //lock三部曲 //1、 Lock lock=new ReentrantLock();//2、 lock.lock() 加锁 //3、 finally=> 解锁:lock.unlock(); class Ticket2 { private int number = 30; // 创建锁 Lock lock = new ReentrantLock(); //卖票的方式 public synchronized void sale() { lock.lock(); // 开启锁 try { if (number > 0) { System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票剩余" + number + "张票"); } }finally { lock.unlock(); // 关闭锁 } } }
Synchronized 与Lock 的区别
1、Synchronized 内置的Java关键字,Lock是一个Java类
2、Synchronized 无法判断获取锁的状态,Lock可以判断
3、Synchronized 会自动释放锁,lock必须要手动加锁和手动释放锁!可能会遇到死锁
4、Synchronized 线程1(获得锁->阻塞)、线程2(等待);lock就不一定会一直等待下去,lock会有一个trylock去尝试获取锁,不会造成长久的等待。
5、Synchronized 是可重入锁,不可以中断的,非公平的;Lock,可重入的,可以判断锁,可以自己设置公平锁和非公平锁;
6、Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码;
Synchronzied 版本
package com.xinyu; public class ConsumeAndProduct { public static void main(String[] args) { Data data = new Data(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "A").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "B").start(); } } class Data { private int num = 0; // +1 public synchronized void increment() throws InterruptedException { // 判断等待 if (num != 0) { this.wait(); } num++; System.out.println(Thread.currentThread().getName() + "=>" + num); // 通知其他线程 +1 执行完毕 this.notifyAll(); } // -1 public synchronized void decrement() throws InterruptedException { // 判断等待 if (num == 0) { this.wait(); } num--; System.out.println(Thread.currentThread().getName() + "=>" + num); // 通知其他线程 -1 执行完毕 this.notifyAll(); } }
存在问题(虚假唤醒)---当线程很多时
虚假唤醒就是当一个条件满足时,很多现场都被唤醒了,但是只有其中分不分是有用的唤醒,其他唤醒是失效的
解决方式 ,if 改为while即可,防止虚假唤醒
结论:就是用if判断的话,唤醒后线程会从wait之后的代码开始运行,但是不会重新判断if条件,直接继续运行if代码块之后的代码,而如果使用while的话,也会从wait之后的代码运行,但是唤醒后会重新判断循环条件,如果不成立再执行while代码块之后的代码块,成立的话继续wait。
这也就是为什么用while而不用if的原因了,因为线程被唤醒后,执行开始的地方是wait之后
package com.xinyu; public class ConsumeAndProduct { public static void main(String[] args) { Data data = new Data(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "A").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "B").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "c").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "d").start(); } } class Data { private int num = 0; // +1 public synchronized void increment() throws InterruptedException { // 判断等待 while(num != 0) { this.wait(); } num++; System.out.println(Thread.currentThread().getName() + "=>" + num); // 通知其他线程 +1 执行完毕 this.notifyAll(); } // -1 public synchronized void decrement() throws InterruptedException { // 判断等待 while (num == 0) { this.wait(); } num--; System.out.println(Thread.currentThread().getName() + "=>" + num); // 通知其他线程 -1 执行完毕 this.notifyAll(); } }
Lock版
public class LockCAP { public static void main(String[] args) { Data2 data = new Data2(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "A").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "B").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "C").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "D").start(); } } class Data2 { private int num = 0; Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); // +1 public void increment() throws InterruptedException { lock.lock(); try { // 判断等待 while (num != 0) { condition.await(); } num++; System.out.println(Thread.currentThread().getName() + "=>" + num); // 通知其他线程 +1 执行完毕 condition.signalAll(); }finally { lock.unlock(); } } // -1 public void decrement() throws InterruptedException { lock.lock(); try { // 判断等待 while (num == 0) { condition.await(); } num--; System.out.println(Thread.currentThread().getName() + "=>" + num); // 通知其他线程 +1 执行完毕 condition.signalAll(); }finally { lock.unlock(); } } }
Condition(通知指定线程)
/** * Description: * A 执行完 调用B * B 执行完 调用C * C 执行完 调用A **/ public class ConditionDemo { public static void main(String[] args) { Data3 data3 = new Data3(); new Thread(() -> { for (int i = 0; i < 10; i++) { data3.printA(); } },"A").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { data3.printB(); } },"B").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { data3.printC(); } },"C").start(); } } class Data3 { private Lock lock = new ReentrantLock(); private Condition condition1 = lock.newCondition(); //同步监视器 private Condition condition2 = lock.newCondition(); private Condition condition3 = lock.newCondition(); private int num = 1; // 1A 2B 3C public void printA() { lock.lock(); try { // 业务代码 判断 -> 执行 -> 通知 while (num != 1) { condition1.await(); } System.out.println(Thread.currentThread().getName() + "==> AAAA" ); num = 2; condition2.signal(); 唤醒同步线程2 }catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } public void printB() { lock.lock(); try { // 业务代码 判断 -> 执行 -> 通知 while (num != 2) { condition2.await(); } System.out.println(Thread.currentThread().getName() + "==> BBBB" ); num = 3; condition3.signal(); }catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } public void printC() { lock.lock(); try { // 业务代码 判断 -> 执行 -> 通知 while (num != 3) { condition3.await(); } System.out.println(Thread.currentThread().getName() + "==> CCCC" ); num = 1; condition1.signal(); }catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } } /* A==> AAAA B==> BBBB C==> CCCC A==> AAAA B==> BBBB C==> CCCC ... */
关键字synchronized与wait()/notify()这两个方法一起使用可以实现等待/通知模式, Lock锁的newContition()方法返回Condition对象,Condition类也可以实现等待/通知模式。
用notify()通知时,JVM会随机唤醒某个等待的线程, 使用Condition类可以进行选择性通知,Condition比较常用的两个方法:
● await()会使当前线程等待,同时会释放锁,当其他线程调用signal()时,线程会重新获得锁并继续执行。
● signal()用于唤醒一个等待的线程。
注意:在调用Condition的await()/signal()方法前,也需要线程持有相关的Lock锁,调用await()后线程会释放这个锁,在singal()调用后会从当前Condition对象的等待队列中,唤醒 一个线程,唤醒的线程尝试获得锁, 一旦获得锁成功就继续执行。
package com.wkcto.lock.condition;
锁
锁会锁住:对象、Class
锁的八个问题
问题1 ---两个同步方法,先执行发短信还是打电话
public class dome01 { public static void main(String[] args) { Phone phone = new Phone(); new Thread(() -> { phone.sendMs(); }).start(); TimeUnit.SECONDS.sleep(1); //主线程睡眠1秒 new Thread(() -> { phone.call(); }).start(); } } class Phone { public synchronized void sendMs() { System.out.println("发短信"); } public synchronized void call() { System.out.println("打电话"); } }
先法短信,再打电话
锁是建在对象身上的,同一时刻对于每一个类实例,其所有声明为synohronized的成员方法中至多只有一个处于可执行状态
一个对象一把锁,谁先拿到谁执行
问题2: 我们让发短信 延迟4s
public class dome01 { public static void main(String[] args) throws InterruptedException { Phone phone = new Phone(); new Thread(() -> { try { phone.sendMs(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { phone.call(); }).start(); } } class Phone { public synchronized void sendMs() throws InterruptedException { TimeUnit.SECONDS.sleep(4); System.out.println("发短信"); } public synchronized void call() { System.out.println("打电话"); } }
结果:还是先发短信,然后再打电话!
原因:并不是顺序执行,而是synchronized 锁住的对象是方法的调用!对于两个方法用的是同一个锁,谁先拿到谁先执行,另外一个等待
sleep并不会释放锁
问题三:加一个普通方法
public class dome01 { public static void main(String[] args) throws InterruptedException { Phone phone = new Phone(); new Thread(() -> { try { phone.sendMs(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { phone.hello(); }).start(); } } class Phone { public synchronized void sendMs() throws InterruptedException { TimeUnit.SECONDS.sleep(4); System.out.println("发短信"); } public synchronized void call() { System.out.println("打电话"); } public void hello() { System.out.println("hello"); } }
输出结果为
hello
发短信
原因:hello是一个普通方法,不受synchronized锁的影响,不用等待锁的释放
问题四: 如果我们使用的是两个对象,一个调用发短信,一个调用打电话,那么整个顺序是怎么样的呢?
public class dome01 { public static void main(String[] args) throws InterruptedException { Phone phone1 = new Phone(); Phone phone2 = new Phone(); new Thread(() -> { try { phone1.sendMs(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { phone2.call(); }).start(); } } class Phone { public synchronized void sendMs() throws InterruptedException { TimeUnit.SECONDS.sleep(4); System.out.println("发短信"); } public synchronized void call() { System.out.println("打电话"); } public void hello() { System.out.println("hello"); } }
输出结果
打电话
发短信
原因:两个对象两把锁,不会出现等待的情况,发短信睡了4s,所以先执行打电话
理解:一个对象只有一把钥匙,加了synchronized就是给方法上了锁,调用时需要钥匙打开,先跑起来的线程先拿到钥匙
package com.xinyu; import java.util.concurrent.TimeUnit; public class dome01 { public static void main(String[] args) throws InterruptedException { Phone phone1 = new Phone(); Phone phone2 = new Phone(); new Thread(() -> { try { phone1.sendMs(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); //TimeUnit.SECONDS.sleep(1); new Thread(() -> { phone2.call(); }).start(); hello(); } public synchronized static void hello() { System.out.println("hello"); } } class Phone { public synchronized void sendMs() throws InterruptedException { //TimeUnit.SECONDS.sleep(4); System.out.println("发短信"); } public synchronized void call() { System.out.println("打电话"); } }
结果将变得随机
问题五、六 如果我们把synchronized的方法加上static变成静态方法!那么顺序又是怎么样的呢?
(1)我们先来使用一个对象调用两个方法!
答案是:先发短信,后打电话
(2)如果我们使用两个对象调用两个方法!
答案是:还是先发短信,后打电话
原因是什么呢? 为什么加了static就始终前面一个对象先执行呢!为什么后面会等待呢?
原因是:对于static静态方法来说,对于整个类Class来说只有一份,对于不同的对象使用的是同一份方法,相当于这个方法是属于这个类的,如果静态static方法使用synchronized锁定,那么这个synchronized锁会锁住整个对象!不管多少个对象,对于静态的锁都只有一把锁,谁先拿到这个锁就先执行,其他的进程都需要等待!
package com.xinyu; import java.util.concurrent.TimeUnit; public class dome01 { public static void main(String[] args) throws InterruptedException { Phone phone1 = new Phone(); new Thread(() -> { try { phone1.sendMs(); } catch (InterruptedException e) { e.printStackTrace(); } },"a").start(); new Thread(() -> { phone1.call(); }).start(); new Thread(() -> { try { phone1.sendMs(); } catch (InterruptedException e) { e.printStackTrace(); } },"b").start(); } } class Phone { public static synchronized void sendMs() throws InterruptedException { TimeUnit.SECONDS.sleep(1); System.out.println(Thread.currentThread().getName() + "发短信"); } public synchronized void call() { System.out.println("打电话"); } }
静态锁和普通锁不同,对应的钥匙也不同
问题七 ----如果我们使用一个静态同步方法、一个同步方法、一个对象调用顺序是什么?
public class dome01 { public static void main(String[] args) throws InterruptedException { Phone phone1 = new Phone(); // Phone phone2 = new Phone(); new Thread(() -> { try { phone1.sendMs(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { phone1.call(); }).start(); } } class Phone { public static synchronized void sendMs() throws InterruptedException { TimeUnit.SECONDS.sleep(4); System.out.println("发短信"); } public synchronized void call() { System.out.println("打电话"); } public void hello() { System.out.println("hello"); } }
输出结果
打电话
发短信
原因:因为一个锁的是Class类的模板,一个锁的是对象的调用者。所以不存在等待,直接运行。---同上理解
问题八 ---如果我们使用一个静态同步方法、一个同步方法、两个对象调用顺序是什么?
public class dome01 { public static void main(String[] args) throws InterruptedException { Phone phone1 = new Phone(); Phone phone2 = new Phone(); new Thread(() -> { try { phone1.sendMs(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { phone2.call(); }).start(); } } class Phone { public static synchronized void sendMs() throws InterruptedException { TimeUnit.SECONDS.sleep(4); System.out.println("发短信"); } public synchronized void call() { System.out.println("打电话"); } public void hello() { System.out.println("hello"); } }
输出结果
打电话
发短信
原因:两把锁锁的不是同一个东西
小解
new 出来的 this 是具体的一个对象
static Class 是唯一的一个模板
集合不安全
List 不安全
//java.util.ConcurrentModificationException 并发修改异常! public class ListTest { public static void main(String[] args) { List<Object> arrayList = new ArrayList<>(); for(int i=1;i<=10;i++){ new Thread(()->{ arrayList.add(UUID.randomUUID().toString().substring(0,5)); System.out.println(arrayList); },String.valueOf(i)).start(); } } }
/*
UUID.randomUUID().toString()是javaJDK提供的一个自动生成主键的方法。UUID(Universally Unique Identifier)全局唯一标识符,是指在一台机器上生成的数字,它保证对在同一时空中的所有机器都是唯一的,是由一个十六位的数字组成,表现出来的形式。由以下几部分的组合:当前日期和时间(UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同),时钟序列,全局唯一的IEEE机器识别号(如果有网卡,从网卡获得,没有网卡以其他方式获得),UUID的唯一缺陷在于生成的结果串会比较长。
*/
会导致 java.util.ConcurrentModificationException 并发修改异常!
ArrayList 在并发情况下是不安全的
解决方案:
public class ListTest { public static void main(String[] args) { /** * 解决方案 * 1. List<String> list = new Vector<>(); * 2. List<String> list = Collections.synchronizedList(new ArrayList<>()); * 3. List<String> list = new CopyOnWriteArrayList<>(); */ List<String> list = new CopyOnWriteArrayList<>(); // for (int i = 1; i <=10; i++) { new Thread(() -> { list.add(UUID.randomUUID().toString().substring(0,5)); System.out.println(list); },String.valueOf(i)).start(); } } }
CopyOnWriteArrayList:写入时复制! COW 计算机程序设计领域的一种优化策略
核心思想是,如果有多个调用者(Callers)同时要求相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者视图修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。
多个线程调用的时候,list,读取的时候,固定的,写入(存在覆盖操作);在写入的时候避免覆盖,造成数据错乱的问题;
读写分离,写入时复制一份来修改,写完后将写好的数据插入进去
CopyOnWriteArrayList比Vector厉害在哪里?
Vector底层是使用synchronized关键字来实现的:效率特别低下。
CopyOnWriteArrayList使用的是Lock锁,效率会更加高效!
set 不安全
Set和List同理可得: 多线程情况下,普通的Set集合是线程不安全的;
解决方案还是两种:
- 使用Collections工具类的synchronized包装的Set类
- 使用CopyOnWriteArraySet 写入复制的JUC解决方案
public class SetTest { public static void main(String[] args) { /** * 1. Set<String> set = Collections.synchronizedSet(new HashSet<>()); * 2. Set<String> set = new CopyOnWriteArraySet<>(); */ // Set<String> set = new HashSet<>(); Set<String> set = new CopyOnWriteArraySet<>(); for (int i = 1; i <= 30; i++) { new Thread(() -> { set.add(UUID.randomUUID().toString().substring(0,5)); System.out.println(set); },String.valueOf(i)).start(); } } }
hashSet底层就是一个HashMap;
Map不安全
public class MapTest { public static void main(String[] args) { //map 是这样用的吗? 不是,工作中不使用这个 //默认等价什么? new HashMap<>(16,0.75); //默认加载因子是0.75,默认的初始容量是16 /** * 解决方案 * 1. Map<String, String> map = Collections.synchronizedMap(new HashMap<>()); * Map<String, String> map = new ConcurrentHashMap<>(); */ Map<String, String> map = new ConcurrentHashMap<>(); //加载因子、初始化容量 for (int i = 1; i < 100; i++) { new Thread(()->{ map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5)); System.out.println(map); },String.valueOf(i)).start(); } } }
hashmap
Hashmap的数据结构
数组,单线链表,红黑树(1.8后链表长度大于8的二叉树)
为什么HashMap的默认负载因子是0.75,而不是0.5或者是整数1呢?
- 阈值(threshold) = 负载因子(loadFactor) x 容量(capacity) 根据HashMap的扩容机制,他会保证容量(capacity)的值永远都是2的幂 为了保证负载因子x容量的结果是一个整数,这个值是0.75(4/3)比较合理,因为这个数和任何2的次幂乘积结果都是整数。
- 理论上来讲,负载因子越大,导致哈希冲突的概率也就越大,负载因子越小,费的空间也就越大,这是一个无法避免的利弊关系,所以通过一个简单的数学推理,可以测算出这个数值在0.75左右是比较合理的
HashMap的扩容机制
阈值(threshold) = 负载因子(loadFactor) x 容量(capacity)
当HashMap中table数组(也称为桶)长度 >= 阈值(threshold) 就会自动进行扩容。
扩容的规则是这样的,因为table数组长度必须是2的次方数,扩容其实每次都是按照上一次tableSize位运算得到的就是做一次左移1位运算,
假设当前tableSize是16的话 16转为二进制再向左移一位就得到了32 即 16 << 1 == 32 即扩容后的容量,也就是说扩容后的容量是当前
容量的两倍,但记住HashMap的扩容是采用当前容量向左位移一位(newtableSize = tableSize << 1),得到的扩容后容量,而不是当前容量x2
Callable
1、可以有返回值;
2、可以抛出异常;
3、方法不同,run()/call()
public class CallableTest { public static void main(String[] args) throws ExecutionException, InterruptedException { //new Thread(new Runnable()).start(); //new Thread(new FutureTask<V>()).start(); //new Thread(new FutureTask<V>( Callable )).start(); //new Thread().start();// 怎么启动Callable MyThread thread = new MyThread(); FutureTask futureTask = new FutureTask(thread);//适配类 new Thread(futureTask,"A").start(); new Thread(futureTask,"B").start();// 结果会被缓存,效率高,结果只打印一次 //获取Callable的返回结果 String o = (String) futureTask.get();//这个get 方法可能会产生阻塞(等待返回结果)!把他放到最后 或者使用异步通信来处理! System.out.println(o); } } class MyThread implements Callable<String> { @Override public String call() throws Exception { System.out.println("jjjj"); // 耗时的操作 return "hello"; } }
原理:futureTask传入callable并重写了run方法,里面调用了callable的call方法
常用的辅助类
CountDownLatch ----减法计数器
CyclickBarrier----加法计数器
public class CyclicBarrierDemo { public static void main(String[] args) { /** * 集齐7颗龙珠召唤神龙 */ // 召唤龙珠的线程 CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> { System.out.println("召唤神龙。。。"); }); for (int i = 1; i <= 7; i++) { // lambda不能操作到 i int temp = i; new Thread(() -> { System.out.println(Thread.currentThread().getName()+"收集第"+temp+"颗龙珠"); try { cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } }, String.valueOf(i)).start(); } } }
CyclicBarrier 与 CountDownLatch 区别
- CountDownLatch 是一次性的,CyclicBarrier 是可循环利用的
- CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。CyclicBarrier 参与的线程职责是一样的。
Semaphore(信号量)
public class SemaphoreDemo { public static void main(String[] args) { // 线程数量:停车位! 用来限流! Semaphore semaphore = new Semaphore(3); for (int i = 1; i <= 6; i++) { new Thread(() -> { //acquire() 得到 //release() 释放 try { semaphore.acquire(); System.out.println(Thread.currentThread().getName()+"抢到车位!"); TimeUnit.SECONDS.sleep(2); System.out.println(Thread.currentThread().getName()+"离开车位!"); } catch (InterruptedException e) { e.printStackTrace(); }finally { semaphore.release(); } }, String.valueOf(i)).start(); } } }
原理:
semaphore.acquire();
获得,假设如果已经满了,等待,等待被释放为止!semaphore.release();
释放,会将当前的信号量释放 + 1,然后唤醒等待的线程!
作用:
- 多个共享资源互斥的使用!并发限流,控制最大的线程数!
读写锁
读可以被多个线程读,写的时候只能又一个线程去写
独占锁(写锁)一次只能被一个线程占有
共享锁(读锁)
public class ReadWriteLockDemo { public static void main(String[] args) { MyCache2 myCache = new MyCache2(); int num = 6; for (int i = 1; i <= num; i++) { int finalI = i; new Thread(() -> { myCache.write(String.valueOf(finalI), String.valueOf(finalI)); },String.valueOf(i)).start(); } for (int i = 1; i <= num; i++) { int finalI = i; new Thread(() -> { myCache.read(String.valueOf(finalI)); },String.valueOf(i)).start(); } } } class MyCache2 { private volatile Map<String, String> map = new HashMap<>(); private ReadWriteLock lock = new ReentrantReadWriteLock(); public void write(String key, String value) { lock.writeLock().lock(); // 写锁 try { System.out.println(Thread.currentThread().getName() + "线程开始写入"); map.put(key, value); System.out.println(Thread.currentThread().getName() + "线程写入ok"); }finally { lock.writeLock().unlock(); // 释放写锁 } } public void read(String key) { lock.readLock().lock(); // 读锁 try { System.out.println(Thread.currentThread().getName() + "线程开始读取"); map.get(key); System.out.println(Thread.currentThread().getName() + "线程写读取ok"); }finally { lock.readLock().unlock(); // 释放读锁 } } }
阻塞队列
写入:如果满了,就必须阻塞等待
读取:如果队列是空的,就必须阻塞等待生产
BlockQueue
BlockingQueue 有四组api
方式 | 抛出异常 | 不会抛出异常,有返回值 | 阻塞,等待 | 超时等待 |
---|---|---|---|---|
添加 | add | offer | put | offer(timenum.timeUnit) |
移出 | remove | poll | take | poll(timenum,timeUnit) |
判断队首元素 | element | peek | - | - |
阻塞:
阻塞队列(ArrayBlockingQueue)
当我们的生产者向队列中生产数据时,若队列已满,那么生产线程会暂停下来,直到队列中有可以存放数据的地方,才会继续工作;而当我们的消费者向队列中获取数据时,若队列为空,则消费者线程会暂停下来,直到容器中有元素出现,才能进行获取操作。
ConcurrentLinkedQueue(非阻塞队列)
采用的是先进先出(FIFO)入队规则,对元素进行排序。当我们向队列中添加元素时,新插入的元素会插入到队列的尾部;而当我们获取一个元素时,它会从队列的头部中取出。因为ConcurrentLinkedQueue是链表结构,所以当入队时,插入的元素依次向后延伸,形成链表;而出队时,则从链表的第一个元素开始获取,依次递增;
- 抛出异常
public static void test1() { // 队列的大小 ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3); System.out.println(blockingQueue.add("a")); System.out.println(blockingQueue.add("b")); System.out.println(blockingQueue.add("c")); System.out.println(blockingQueue.element());//查看队首元素 System.out.println("---------------------"); //IllegalStateException: Queue full //System.out.println(blockingQueue.add("d")); System.out.println(blockingQueue.remove()); System.out.println(blockingQueue.remove()); System.out.println(blockingQueue.remove()); //java.util.NoSuchElementException 抛出异常! //System.out.println(blockingQueue.remove()); }
- 有返回值,不抛出异常
/** * 有返回值,没有异常 */ public static void test2() { // 队列的大小 ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3); System.out.println(blockingQueue.offer("a")); System.out.println(blockingQueue.offer("b")); System.out.println(blockingQueue.offer("c")); System.out.println("---------------------"); System.out.println(blockingQueue.peek());//查看队首元素 //System.out.println(blockingQueue.offer("d"));// false 不抛出异常! System.out.println(blockingQueue.poll()); System.out.println(blockingQueue.poll()); System.out.println(blockingQueue.poll()); System.out.println(blockingQueue.poll()); // null 不抛出异常! }
- 等待,阻塞(一直阻塞)(用的较少)
/** * 等待,阻塞(一直阻塞) */ public static void test3() throws InterruptedException { // 队列的大小 ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3); // 一直阻塞 blockingQueue.put("a"); blockingQueue.put("b"); blockingQueue.put("c"); // blockingQueue.put("d"); // 队列没有位置了,会一直阻塞 System.out.println(blockingQueue.take()); System.out.println(blockingQueue.take()); System.out.println(blockingQueue.take()); System.out.println(blockingQueue.take()); // 没有这个元素,一直阻塞 }
- 等待,阻塞(等待超时)(用的较多)
/** * 等待 超时阻塞 * 这种情况也会等待队列有位置 或者有产品 但是会超时结束 */ public static void test4() throws InterruptedException { ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3); blockingQueue.offer("a"); blockingQueue.offer("b"); blockingQueue.offer("c"); System.out.println("开始等待"); blockingQueue.offer("d",2, TimeUnit.SECONDS); //超时时间2s 等待如果超过2s就结束等待 System.out.println("结束等待"); System.out.println("===========取值=================="); System.out.println(blockingQueue.poll()); System.out.println(blockingQueue.poll()); System.out.println(blockingQueue.poll()); System.out.println("开始等待"); blockingQueue.poll(2,TimeUnit.SECONDS); //超过两秒 我们就不要等待了 System.out.println("结束等待"); }
同步队列
- 同步队列 没有容量,也可以视为容量为1的队列;
- 进去一个元素,必须等待取出来之后,才能再往里面放入一个元素;
- put方法 和 take方法; //put调用时就进行了阻塞,只有调用了take方法才能释放
- Synchronized 和 其他的BlockingQueue 不一样 它不存储元素;
- put了一个元素,就必须从里面先take出来,否则不能再put进去值!
- 并且SynchronousQueue 的take是使用了lock锁保证线程安全的
package com.marchsoft.queue; import java.util.concurrent.BlockingDeque; import java.util.concurrent.BlockingQueue; /** * Description: * * @author jiaoqianjin * Date: 2020/8/12 10:02 **/ public class SynchronousQueue { public static void main(String[] args) { BlockingQueue<String> synchronousQueue = new java.util.concurrent.SynchronousQueue<>(); // 网queue中添加元素 new Thread(() -> { try { System.out.println(Thread.currentThread().getName() + "put 01"); synchronousQueue.put("1"); System.out.println(Thread.currentThread().getName() + "put 02"); synchronousQueue.put("2"); System.out.println(Thread.currentThread().getName() + "put 03"); synchronousQueue.put("3"); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); // 取出元素 new Thread(()-> { try { System.out.println(Thread.currentThread().getName() + "take" + synchronousQueue.take()); System.out.println(Thread.currentThread().getName() + "take" + synchronousQueue.take()); System.out.println(Thread.currentThread().getName() + "take" + synchronousQueue.take()); }catch (InterruptedException e) { e.printStackTrace(); } }).start(); } }
线程池
线程池:三大方式、七大参数、四种拒绝策略
池化技术:事先准备好一些资源,如果有人要用,就来我这里拿,用完之后还给我,以此来提高效率。
线程池的好处:
1、降低资源的消耗;
2、提高响应的速度;
3、方便管理;
线程复用、可以控制最大并发数、管理线程;
线程池:三大方法
-
ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
-
ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //创建一个固定的线程池的大小
-
ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可伸缩的
//Executors 工具类, 3大方法 public class Demo01 { public static void main(String[] args) { ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程 // ExecutorService threadPool = Executors.newFixedThreadPool(5);//创建一个固定的线程池大小 // ExecutorService threadPool = Executors.newCachedThreadPool();//颗伸缩的,遇强则强,遇弱则弱 try { for (int i = 0; i < 10; i++) { // new Thread().start(); // 使用了线程池之后,使用线程池来创建线程 threadPool.execute(() -> { System.out.println(Thread.currentThread().getName() + " OK"); }); } } catch (Exception e) { e.printStackTrace(); } finally { // 线程池用完,程序结束,关闭线程池 threadPool.shutdown(); } } }
newSingleThreadExecutor()---单一线程只有一个线程在执行
newFixedThreadPool(5) ---限定最多五个线程同时执行
newCachedThreadPool() ---没有限定,遇强则强,遇弱则弱
七大参数
三者本质上都是调用ThreadPoolExecutor
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); } public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); } // 本质ThreadPoolExecutor() public ThreadPoolExecutor(int corePoolSize, // 核心线程池大小 int maximumPoolSize, // 最大核心线程池大小 long keepAliveTime, // 超时了没有人调用就会释放 TimeUnit unit, // 超时单位 BlockingQueue<Runnable> workQueue,// 阻塞队列 ThreadFactory threadFactory,// 线程工厂:创建线程的,一般不用动 RejectedExecutionHandler handler// 拒绝策略 ) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
手动创建一个线程池
package com.zzy.pool; import java.util.concurrent.*; //Executors 工具类, 3大方法 // Executors 工具类、3大方法 /** * new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异常 * new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里! * new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常! * new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和最早的竞争,也不会抛出异常! */ public class Demo01 { public static void main(String[] args) { // 自定义线程池!工作 ThreadPoolExecutor ExecutorService threadPool = new ThreadPoolExecutor(2, 5, 3,
TimeUnit.SECONDS, new LinkedBlockingDeque<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()//银行人满了,还有人进来,不处理这个人,抛出异常 ); try { // 最大承载:Deque + max // 超过 抛出异常: RejectedExecutionException for (int i = 1; i <= 8; i++) { // 使用了线程池之后,使用线程池来创建线程 threadPool.execute(() -> { System.out.println(Thread.currentThread().getName() + " OK"); }); } } catch (Exception e) { e.printStackTrace(); } finally { // 线程池用完,程序结束,关闭线程池 threadPool.shutdown(); } } }
* new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异常 * new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里! * new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常! * new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和最早的竞争,也不会抛出异常!
如何设置线程池的大小(调优)
1、CPU密集型:电脑的核数是几核就选择几;选择maximunPoolSize的大小---保持CPU效率最高
// 获取cpu 的核数 int max = Runtime.getRuntime().availableProcessors(); //获取CPU核数 ExecutorService service =new ThreadPoolExecutor( 2, max, 3, TimeUnit.SECONDS, new LinkedBlockingDeque<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() );
2、I/O密集型:判断程序中十分耗IO的线程
在程序中有15个大型任务,io十分占用资源;I/O密集型就是判断我们程序中十分耗I/O的线程数量,大约是最大I/O数的一倍到两倍之间。
四大函数式接口
新时代的程序员:lambda表达式、链式编程、函数式接口、Stream流式计算
函数式接口:只有一个方法的接口
@FunctionalInterface public interface Runnable { public abstract void run(); } // 传统技术必会:泛型、枚举、反射 // 新技术必会:lambda表达式、链式编程、函数式接口、Stream流式计算 // 超级多FunctionalInterface // 简化编程模型,在新版本的框架底层大量应用! // foreach(消费者类的函数式接口)
1)Function 函数型接口
/** * Function 函数型接口, 有一个输入参数,有一个输出(返回传入的参数) */ public class Demo01 { public static void main(String[] args) { //工具类:输出输入的值 Function function = new Function<String, String>() { @Override public String apply(String str) { return str; } }; //只要是函数型接口,就可以用 lambda表达式简化 Function function1 = (str) -> { return str; }; System.out.println(function1.apply("123")); } }
2)Predicate 断定型接口
/** * 断定型接口:有一个输入参数,返回值只能是 布尔值! */ public class Demo02 { public static void main(String[] args) { //判断字符串是否为空 Predicate<String> predicate = new Predicate<String>() { @Override public boolean test(String str) { return str.isEmpty(); } }; //只要是函数型接口,就可以用 lambda表达式简化 Predicate<String> predicate2 = (str) -> { return str.isEmpty(); }; System.out.println(predicate.test("asddd")); } }
3)Suppier 供给型接口
/** * Supplier 供给型接口 没有参数,只有返回值 */ public class Demo04 { public static void main(String[] args) { Supplier supplier = new Supplier<Integer>() { @Override public Integer get() { return 1024; } }; //只要是函数型接口,就可以用 lambda表达式简化 Supplier supplier2 = () -> { return 1024; }; System.out.println(supplier2.get()); } }
4)Consummer 消费型接口
/** * Consumer 消费型接口: 只有输入,没有返回值 */ public class Demo03 { public static void main(String[] args) { Consumer<String> consumer = new Consumer<String>() { @Override public void accept(String str) { System.out.println(str); } }; //只要是函数型接口,就可以用 lambda表达式简化 Consumer<String> consumer1 = (str)->{ System.out.println(str); }; consumer.accept("qwre"); } }
Stream流式计算
大数据:存储 + 计算
集合、MySQL 本质就是存储东西的;
计算都应该交给流来操作!
后续大数据scala也用得到。
/** * 题目要求:一分钟内完成此题,只能用一行代码实现! * 现在有5个用户!筛选: * 1、ID 必须是偶数 * 2、年龄必须大于23岁 * 3、用户名转为大写字母 * 4、用户名字母倒着排序 * 5、只输出一个用户! */ public class Test { public static void main(String[] args) { User u1 = new User(1, "a", 21); //new User类对象 User u2 = new User(2, "b", 22); User u3 = new User(3, "c", 23); User u4 = new User(4, "d", 24); User u5 = new User(6, "e", 25); // 集合是存储 List<User> list = Arrays.asList(u1, u2, u3, u4, u5); // 计算交给Stream流 // lambda表达式、链式编程、函数式接口、Stream流式计算 list.stream() .filter(u -> { return u.getId() % 2 == 0; }) .filter(u -> {return u.getAge() > 23;}) .map(u -> { return u.getName().toUpperCase(); }) .sorted((o1, o2) -> { return o2.compareTo(o1);}) .limit(1) .forEach(System.out::println); } }
ForkJoin
ForkJoin 在JDK1.7,并行执行任务!提高效率~。在大数据量速率会更快!
大数据中:MapReduce 核心思想->把大任务拆分为小任务!
ForkJoin 特点: 工作窃取
实现原理是:双端队列!从上面和下面都可以去拿到任务进行执行!
大数据中:MapReduce 核心思想->把大任务拆分为小任务
public class ForkJoinTest { private static final long SUM = 20_0000_0000; public static void main(String[] args) throws ExecutionException, InterruptedException { test1(); test2(); test3(); } /** * 使用普通方法 */ public static void test1() { long star = System.currentTimeMillis(); long sum = 0L; for (long i = 1; i < SUM ; i++) { sum += i; } long end = System.currentTimeMillis(); System.out.println(sum); System.out.println("时间:" + (end - star)); System.out.println("----------------------"); } /** * 使用ForkJoin 方法 */ public static void test2() throws ExecutionException, InterruptedException { long star = System.currentTimeMillis(); ForkJoinPool forkJoinPool = new ForkJoinPool(); ForkJoinTask<Long> task = new ForkJoinDemo(0L, 20_0000_0000L); ForkJoinTask<Long> submit = forkJoinPool.submit(task); //提交任务 Long along = submit.get(); System.out.println(along); long end = System.currentTimeMillis(); System.out.println("时间:" + (end - star)); System.out.println("-----------"); } /** * 使用 Stream 并行流计算 */ public static void test3() { long star = System.currentTimeMillis(); long sum = LongStream.range(0L, 20_0000_0000L).parallel().reduce(0, Long::sum);//parallel并行 System.out.println(sum); long end = System.currentTimeMillis(); System.out.println("时间:" + (end - star)); System.out.println("-----------"); } }
异步回调
没有返回值的runAsync异步回调
/** * 异步调用: CompletableFuture * // 异步执行 * // 成功回调 * // 失败回调 */ public class Demo01 { public static void main(String[] args) throws ExecutionException, InterruptedException { //发起一个请求 void // 没有返回值的 runAsync 异步回调 CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "runAsync=>void"); }); System.out.println("11111111"); completableFuture.get();//获取执行结果 } }
有返回值的异步回调supplyAsync
/** * 异步调用: CompletableFuture * // 异步执行 * // 成功回调 * // 失败回调 */ public class Demo01 { public static void main(String[] args) throws ExecutionException, InterruptedException { // completableFuture.get(); // 获取阻塞执行结果 // 有返回值的 supplyAsync 异步回调 // ajax,成功和失败的回调 // 失败返回的是错误信息; CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> { System.out.println(Thread.currentThread().getName() + "supplyAsync=>Integer"); // int i = 10 / 0; return 1024; }); System.out.println(completableFuture.whenComplete((t, u) -> { System.out.println("t=>" + t);// 正常的返回结果 System.out.println("u=>" + u);// 错误信息: }).exceptionally((e) -> { System.out.println(e.getMessage()); return 233;// 可以获取到错误的返回结果 }).get()); } /** * 工作中: * succee Code 200 * error Code 404 500 */ }
whenComplete: 有两个参数,一个是t 一个是u
T:是代表的 正常返回的结果;
U:是代表的 抛出异常的错误信息;
如果发生了异常,get可以获取到exceptionally返回的值;
总结:
1.我需要一个计算时间为5秒的方法的返回值
2.我不想等这5秒,我想要继续执行下面的代码,就异步执行这个方法
3.当我通过get去获取这个返回值时,如果已经过了5秒,也就是这个方法执行完了,那我就可以直接得到返回值
4.如果方法还没有执行完,这个时候就需要等待执行完才能拿到返回值
JMM
1.Volatile
Volatile 是 Java 虚拟机提供 轻量级的同步机制
1、保证可见性
2、不保证原子性
3、禁止指令重排
如何实现可见性
volatile变量修饰的共享变量在进行写操作的时候回多出一行汇编:
0x01a3de1d:movb $0×0,0×1104800(%esi);0x01a3de24:lock addl $0×0,(%esp);
Lock前缀的指令在多核处理器下会引发两件事情。
1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使其他cpu里缓存了该内存地址的数据无效。
多处理器总线嗅探:
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存后再进行操作,但操作不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据库读到处理器缓存中。
2)什么是JMM?
JMM:JAVA内存模型,不存在的东西,是一个概念,也是一个约定!
关于JMM的一些同步的约定:
1、线程解锁前,必须把共享变量立刻刷回主存;
2、线程加锁前,必须读取主存中的最新值到工作内存中;
3、加锁和解锁是同一把锁;
线程中分为 工作内存、主内存
- 主内存 :所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量)
- 本地内存 :每个线程都有一个私有的本地内存来存储共享变量的副本,并且,每个线程只能访问自己的本地内存,无法访问其他线程的本地内存。本地内存是 JMM 抽象出来的一个概念,存储了主内存中的共享变量副本。
8种操作:
读->加载->使用->赋值(assign)->写->存储
加锁、解锁
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
-
Read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;
-
load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中;
-
Use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令;
-
assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中;
-
store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用;
-
write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中;
-
lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态;
-
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
JMM对这八种指令的使用,制定了如下规则:
- 不允许read和load、store和write操作之一单独出现,必须成对使用。即使用了read必须load,使用了store必须write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作 - 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量对一个变量进行unlock操作之前,必须把此变量同步回主内存
问题: 程序不知道主内存的值已经被修改过了
volatile ---保证可见性
public class JMMDemo01 { // 如果不加volatile 程序会死循环 // 加了volatile是可以保证可见性的 private volatile static Integer number = 0; public static void main(String[] args) { //main线程 //子线程1 new Thread(()->{ while (number==0){ } }).start(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } //子线程2 new Thread(()->{ while (number==0){ } }).start(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } number=1; System.out.println(number); } }
不保证原子性
原子性:不可分割;
线程A在执行任务的时候,不能被打扰的,也不能被分割的,要么同时成功,要么同时失败。
/** * 不保证原子性 * number <=2w */ public class VDemo02 { private static volatile int number = 0; public static void add(){ //对此方法添加lock锁或syn可以保证原子性 number++; //++ 不是一个原子性操作,是两个~3个操作 } public static void main(String[] args) { //理论上number === 20000 for (int i = 1; i <= 20; i++) { new Thread(()->{ for (int j = 1; j <= 1000 ; j++) { add(); } }).start(); } while (Thread.activeCount()>2){ //main gc Thread.yield(); } System.out.println(Thread.currentThread().getName()+",num="+number); } }
如果不加lock和synchronized ,怎么样保证原子性?
使用原子类,解决原子性问题,比锁高效很多倍!