JVM、JUC、网络、线程

1. JVM

面试常见:

  1. 请你谈谈你对 JVM 的理解?
  2. java 8 虚拟机和之前的变化更新?
  3. 什么是 OOM,什么是栈溢出 StackOverFlowError? 怎么分析?
  4. JVM的常用调优参数有哪些?
  5. 内存快照如何抓取,怎么分析 Dump 文件?
  6. 谈谈JVM中,类加载器你的认识

1. JVM的位置

JRE:java 开发环境,包含了 JVM( C++ 语言编写的)

一个个(.class)类文件
JRE–JVM
操作系统(Windows,Linux,Mac)
硬件体系(Intel,Spac…)

2. JVM的体系结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-56dSQAjb-1648561802064)(link-picture\image-20220107161935856.png)]

Java栈、本地方法栈、程序计数器不会有垃圾回收,否则程序会死掉

99% JVM 调优都是在方法区和堆中调优,Java 栈、本地方法栈、程序计数器是不会有垃圾存在的

2.1 类加载器(Class Loader)

类是模板,是抽象的,类实例化得到的对象是具体的。所有的对象反射回去得到的是同一个类模板

作用:加载 .class 文件,得到 Class

类加载器的种类

  1. 虚拟机自带的加载器

  2. 启动类(根)加载器:BootstrapClassLoader

  3. 扩展类加载器:ExtClassLoader

  4. 应用程序类加载器:AppClassLoader

  5. 双亲委派机制:保证安全,逐级查找:AppCL–>ExtCL–>BootstrapCL

双亲委派机制

类加载器收到类加载的请求,将这个请求向上委托父类加载器去完成,一 直向上委托,直到启动类加载器,启动加载器检查是否能够加载当前这个类,有就加载就结束, 使用当前的加载器;没有抛出异常,通知子加载器进行加载

java通过 native 调用操作系统的方法

native

  1. 凡是带了 native 关键字的,说明 java 的作用范围达不到了,会去调用底层c语言的库
  2. 会进入本地方法栈,调用本地方法

本地接口 JNI ( Java Native Interface )

JNI作用:拓展 Java 的使用,融合不同的编程语言为 Java 所用(最初: C、C++),它在内存区域中专门开辟了一块标记区域本地方法栈(Native Method Stack),登记 native 方法,在最终执行的时候,加载本地方法库中的方法通过 JNI,如Java程序驱动打印机或者Java 系统管理设备

本地方法栈(Native Method Stack)

它的具体做法是:本地方法栈中标记为native方法,在执行引擎 ( Execution Engine ) 执行的时候加载本地库(Native Libraies)

沙箱安全机制

Java 安全模型的核心就是 Java 沙箱。沙箱是一个限制程序运行的环境

沙箱机制就是将 Java 代码限定在虚拟机 ( JVM ) 特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问系统资源包括:CPU内存文件系统网络。不同级别的沙箱对这些资源访问的限制也可以不一样。

在 Java 中将执行程序分成:本地代码远程代码。本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱机制。

在Java1.2版本中,改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制

组成沙箱的基本组件

  1. 字节码校验器( bytecode verifier ):确保 Java 类文件遵循 Java 语言规范。这样可以帮助 Java 程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类
  2. 类装载器( class loader ) :其中类装载器在3个方面对 Java 沙箱起作用
    1. 它防止恶意代码去干涉善意的代码(双亲委派机制
    2. 它守护了被信任的类库边界
    3. 它将代码归入保护域,确定了代码可以进行哪些操作

2.2 方法区(Method Area)

方法区是被所有线程共享,所有字段方法字节码,以及一些特殊方法(如:构造函数,接口代码也在此定义)。简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间

静态变量(static)、常量(final)、类信息(构造方法、接口定义)、运行时的常量池 都存在方法区中,但是实例变量存在堆内存中,和方法区无关

方法区储存的是staticfinalClass常量池

2.3 PC寄存器

每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向像一条指令的地址, 也即是将要执行的指令代码),在执行引擎读取下一条指令, 是一个非常小的内存空间,几乎可以忽略不计

2.4 栈(Stack)

栈(数据结构):先进后出、后进先出

队列:先进先出( FIFO : First Input First Output )

栈内存

  1. 主管程序的运行,生命周期线程同步
  2. 线程结束,栈内存也就释放
  3. 对栈来说,不存在垃圾回收的问题

栈存储的是8大基本类型对象引用实例方法

栈帧:局部变量表 + 操作数栈

每执行一个方法,就会产生一个栈帧。程序正在运行的方法永远都会在栈顶

栈满了,就会报错 StackOverflowError

栈、堆、方法区的交互关系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i3d1K6dq-1648561802067)(link-picture\image-20220107165620842.png)]

2.5 堆(Heap)

一个 JVM 只有一个堆内存,堆内存的大小是可调节的

类加载器读取了类文件(.class)后,一般会把类、方法、常量、变量放到中,保存所有引用类型的真实对象

堆内存

堆内存分为三个区域:

  1. 新生区 Young:诞生成长或死亡的地方
    1. 伊甸园区(Eden Space):所有的对象都是在这里 new 出来的
    2. 幸存0区
    3. 幸存1区
  2. 养老区 old
  3. 永久区 Perm(在 JDK8 以后,永久存储区改名为元空间
    1. 永久区常驻内存的,用来存放 JDK 自身携带的Class对象Interface接口元数据,存储的是 Java 运行时的一些环境或类信息
    2. 永久区不存在垃圾回收,关闭 JVM 虚拟机就会释放这个区域的内存

注意

  • jdk1.6之前︰有永久代,常量池是在方法区;
  • jdk1.7:有永久代,但是慢慢的退化了,去永久代,常量池在
  • jdk1.8之后∶无永久代,常量池在元空间

GC 垃圾回收:主要在新生区和养老区

轻GC:轻量级垃圾回收,主要是在新生区
重GC:重量级垃圾回收,主要是在养老区,重 GC 就说明内存都要爆了(如:OOM(Out Of memory))

堆内存调优

Java 虚拟机默认情况下:分配的总内存是电脑内存的 1/4,而初始化的内存是电脑内存的 1/64

可以通过调整(Edit Configuration—>VM options)这个参数控制 Java 虚拟机初始内存和分配的总内存的大小

# 设置虚拟机的总内存和初始占用内存为:1G,并打印日志
-Xms1024m -Xmx1024m -XX:+PrintGCDetails  
# 设置虚拟机的总内存和初始占用内存为:1G,并假如堆内存heap出现了OOM则dump出这个异常
-Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryErro  

当新生代、老年代、元空间内存都满了之后才会报 OOM

内存快照分析工具

内存快照分析工具有:MATJprofiler

  1. MAT 最早集成于 Eclipse
  2. IDEA 中可以使用 Jprofiles 插件,在 Settings—>Plugins 中搜索 Jprofiles,安装改插件即可使用

工具作用

  • 分析 Dump 内存文件,快速定位内存泄露;(dump出的文件应该在src目录下)
  • 获得堆中的数据
  • 获得大的对象

2.6 垃圾回收(GC)

JVM 在进行 GC 时,并不是对这三个区域统一回收。大部分时候,回收都是新生代

  • 新生代
  • 幸存区(form , to):幸存0区 和 幸存1区 两者是会交替的,from和to的关系会交替变化
  • 老年区

GC 两种类:轻 GC (普通的 GC ),重 GC (全局 GC )

GC常用算法

  1. 标记清除法

    扫描对象,对对象进行标记;清除:对没有标记的对象进行清除

    优缺点

    1. 优点:不需要额外的空间!
    2. 缺点:两次扫描,严重浪费时间,会产生内存碎片
  2. 标记压缩

    改良:标记清除再压缩(压缩:防止内存碎片产生,再扫描,向一端移动存活的对象)

    再改进:先标记清除几次之后,再压缩1次

  3. 复制算法

    新生区主要是用复制算法,to 永远是干净的,空的

    每次 GC 都会将 Eden 区 活的对象移到幸存区中,一旦 Eden 区被 GC 后,就会是空的

    当一个对象经历了15次 GC 后,都还没死,可通过 -XX:MaxTenuringThreshold=9999 这个参数设定进入老年代的时间

    复制算法最佳使用场景:1. 对象存活度较低的时候 2. 新生区

    优缺点

    1. 优点:没有内存碎片
    2. 缺点:浪费内存空间(一个幸存区的空间永远是空:to)
  4. 引用计数器法(不常用)

GC算法总结

  • 内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)
  • 内存整齐度:复制算法=标记压缩算法>标记清除算法
  • 内存利用率:标记压缩算法=标记清除算法>复制算法

年轻代:存活率低,复制算法

老年代:区域大,存活率高,标记清除 (内存碎片不是太多) + 标记压缩混合实现

2. JUC

JUCjava.util.concurrent下面的类包),专门用于多线程开发

1. 线程和进程

线程和进程

进程:是操作系统中的应用程序、是资源分配的基本单位

线程:是用来执行具体的任务和功能,是CPU调度和分派的最小单位

  • 一个进程可以包含多个线程,至少包含一个线程;
  • Java 默认2个线程 :main 线程、GC 线程

对于Java而言:ThreadRunableCallable 进行开启线程的

Java是没有权限去开启线程、操作硬件的,这是一个 native 本地方法,它底层调用的 C++ 代码

并行和并发

并发:多线程操作同一个资源

  • CPU 只有一核,使用CPU快速交替,来模拟多线程。
  • 并发编程的本质:充分利用CPU的资源

并行: 多个人一起行走

  • CPU多核,多个线程可以同时执行。 我们可以使用**线程池**!
Runtime.getRuntime().availableProcessors();  // 获取 cpu 的核数

线程的状态

  1. NEW:新建
  2. RUNNABLE:运行
  3. BLOCKED:阻塞
  4. WAITING:等待
  5. TIMED_WAITING:超时等待
  6. 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:是不需捕获异常

    sleep:必须要捕获异常

2. 线程并发

synchronized 与 Lock

  1. 传统的 synchronized

    线程就是一份单独的资源类(包含属性方法),没有任何的附属操作

    public class Demo {
        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();
        }
    }
    
    // 线程就是一份单独的资源类,没有任何的附属操作,因此一般用实现 Runnable 的方式
    class Ticket {
        private int number = 30;
        public synchronized void sale() {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() 
                         + "卖出了第" + (number--) + "张票剩余" + number + "张票");
            }
        }
    }
    
  2. Lock

    ReentrantLock:可重入锁(非公平锁

    ReentrantReadWriteLock.ReadLock:可重入读锁

    ReentrantReadWriteLock.WriteLock:可重入写锁

    公平锁: 十分公平,必须先来后到

    非公平锁:十分不公平,可以插队

    // 主方法与上面相同
    class Ticket2 {
        private int number = 30;
        Lock lock = new ReentrantLock();  // 1.创建锁
        public synchronized void sale() {
            lock.lock(); // 2.加锁
            try {
                if (number > 0) {
                  	System.out.println(Thread.currentThread().getName() 
                         + "卖出了第" + (number--) + "张票剩余" + number + "张票");
                }
            }finally {
                lock.unlock(); // 3.解锁
            }
        }
    }
    
  3. synchronizedLock 的区别

    1. Synchronized 内置的 Java 关键字,Lock 是一个 Java 类
    2. Synchronized 无法获取锁的状态,Lock 可以判断锁的状态
    3. Synchronized 会自动释放锁,lock 必须要手动加锁手动释放锁,如果不释放可能会死锁
    4. Synchronized 线程1(获得锁->阻塞)、线程2(等待);lock 就不一定会一直等待下去,lock 会有一个 trylock尝试获取锁,不会造成长久的等待
    5. Synchronized 是可重入锁不中断的非公平的(可插队);Lock 是可重入锁可以判断锁可以设置公平锁和非公平锁
    6. Synchronized 适合锁少量代码同步问题,Lock 适合锁大量代码同步问题

3. 线程通信

线程之间的通信问题:生产者和消费者问题(等待唤醒、通知唤醒)

线程交替执行

  1. synchronized 实现

    if 判断会出现虚假唤醒(解决:等待应该总是放在循环中(不能使用 if 判断))

    // 
    
    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(); // 加1
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "A").start();
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    try {
                        data.decrement(); // 减1
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "B").start();
        }
    }
    
    // 等待 业务 通知
    class Data {
        private int num = 0;
        public synchronized void increment() throws InterruptedException { // +1
            while (num != 0) { // 判断等待:if 判断会出现虚假唤醒
                this.wait();
            }
            num++;
            System.out.println(Thread.currentThread().getName() + "=>" + num);
            this.notifyAll(); // 通知其他线程 +1 执行完毕
        }
        public synchronized void decrement() throws InterruptedException { // -1
            while (num == 0) { // 判断等待:if 判断会出现虚假唤醒
                this.wait();
            }
            num--;
            System.out.println(Thread.currentThread().getName() + "=>" + num);
            this.notifyAll(); // 通知其他线程 -1 执行完毕
        }
    }
    
  2. Lock 实现

    // 主方法与上面的相同
    // 等待 业务 通知
    class Data {
        private int num = 0;
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        public void increment() throws InterruptedException { // +1
            lock.lock(); // 加锁
            try{
                while (num != 0) { // 判断等待:if 判断会出现虚假唤醒
                    condition.wait();
                }
                num++;
                System.out.println(Thread.currentThread().getName() + "=>" + num);
                condition.signalAll(); // 通知其他线程 +1 执行完毕
            }finally{
                lock.unlock(); //解锁
            }
        }
        public void decrement() throws InterruptedException { // -1
            lock.lock(); // 加锁
            try{
                while (num == 0) { // 判断等待:if 判断会出现虚假唤醒
                    condition.wait();
                }
                num--;
                System.out.println(Thread.currentThread().getName() + "=>" + num);
                condition.signalAll(); // 通知其他线程 +1 执行完毕
            }finally{
                lock.unlock(); //解锁
            }
        }
    }
    
  3. Condition 的优势:精准的通知唤醒的线程

    用 Condition 来指定通知下一个进行顺序(按顺序执行)

    public class ConditionDemo {
        public static void main(String[] args) {
            Data3 data = new Data();
            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 Data {
        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(); // 唤醒 3
            }catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
        public void printC() {
            lock.lock();
            try {
                while (num != 3) {
                    condition1.await(); // 唤醒 1
                }
                System.out.println(Thread.currentThread().getName() + "==> CCCC" );
                num = 1;
                condition1.signal();
            }catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    }
    

4. 锁现象

锁是谁锁的是谁?(对象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("打电话");
        }
    }
    
  2. 普通方法不受锁影响

  3. synchronized 的方法加上 static 变成静态方法锁的是Class类的模板

5. 集合不安全

单线程操作集合是安全的,多线程操作集合就是不安全的

  1. List 不安全

    ArrayList 在并发情况下是不安全的

    解决方案

    1. List<String> list = new Vector<>();  //Vector是线程安全的
    2. List<String> list = Collections.synchronizedList(new ArrayList<>());
    3. List<String> list = new CopyOnWriteArrayList<>();
    

    CopyOnWriteArrayList:写入时复制! COW 计算机程序设计领域的一种优化策略

    CopyOnWriteArrayList

    核心思想是:如果有多个调用者同时调用相同的资源(如内存或者是磁盘上的数据存储),会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源内容时,系统才会真正复制一份专用副本该调用者,而其他调用者仍然保持不变。这过程对其他的调用者都是透明的。此做法主要的优点是如果调用者没有修改资源,就不会有副本被创建,因此多个调用者只是读取操作时可以共享同一份资源。读的时候不需要加锁,如果读的时候有多个线程正在向 CopyOnWriteArrayList 添加数据读还是会读到旧的数据,因为写的时候不会锁住旧的 CopyOnWriteArrayList 。

    多个线程调用的时候,list,读取的时候,固定的,写入(存在覆盖操作);在写入的时候避免覆盖,造成数据错乱的问题

    **CopyOnWriteArrayList 与 Vector **的区别

    1. Vector底层是使用 synchronized 关键字来实现的,效率特别低下
    2. **CopyOnWriteArrayList **使用的是 Lock 锁,效率会更加高效
  2. Set 不安全

    普通的 Set 集合在并发情况下是不安全的

    解决方案

    1. 使用 Collections 工具类的 synchronized 包装的 Set 类

    2. 使用 CopyOnWriteArraySet 写入时复制JUC 解决方案

      1. Set<String> set = Collections.synchronizedSet(new HashSet<>());
      2. Set<String> set = new CopyOnWriteArraySet<>();
      

    hashSet底层:就是一个HashMap ,所以,HashMap 基础类也存在并发修改异常

  3. Map 不安全

    new HashMap<>; 默认等价 new HashMap<>(16,0.75); // 初始化容量16,加载因子0.75
    

    解决方案

    1. 使用 Collections 工具类的 synchronized 包装的 Map 类

    2. 使用 ConcurrentHashMapJUC 解决方案

      1. Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
      2. Map<String, String> map = new ConcurrentHashMap<>();
      

6. Callable

Callable 与 Runnable 的区别

  1. 可以有返回值
  2. 可以抛出异常
  3. 方法不同,run()/call()
public class CallableTest {
    public static void main(String[] args) 
        throws ExecutionException, InterruptedException {
        for (int i = 1; i < 10; i++) {
            MyThread myThread = new MyThread();
            // FutureTask 是 Runnable 实现类,可以接收 Callable 
            FutureTask<Integer> futureTask = new FutureTask<>(myThread);
            // 放入Thread中使用,结果会被缓存
            new Thread(futureTask,String.valueOf(i)).start();
            // get方法可能会被阻塞,如果在call方法中是一个耗时的方法,
            // 所以一般情况会把这个放在最后或者使用异步通信
            int a = futureTask.get();
            System.out.println("返回值:" + s);
        }
    }
}
class MyThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("call()");
        return 1024;
    }
}

7. 常用的辅助类

  1. CountDownLatch减法计数器

    主要方法

    1. countDown 减一操作;
    2. await 等待计数器归零 ,归零就唤醒,再继续向下运行
    public class CountDownLatchDemo {
        public static void main(String[] args) throws InterruptedException {
            CountDownLatch countDownLatch = new CountDownLatch(6); // 总数是6
            for (int i = 1; i <= 6; i++) {
                new Thread(() -> {
                    System.out.println(
                        Thread.currentThread().getName() + "==> Go Out");
                    countDownLatch.countDown(); // 每个线程都数量 -1
                },String.valueOf(i)).start();
            }
            countDownLatch.await(); // 等待计数器归零 然后向下执行
            System.out.println("close door");
        }
    }
    
  2. CyclicBarrier加法计数器

    public class CyclicBarrierDemo {
        public static void main(String[] args) {
            // 主线程
            CyclicBarrier cyclicBarrier = new CyclicBarrier(7,() -> {
                System.out.println("召唤神龙");
            });
            for (int i = 1; i <= 7; i++) {// 子线程
                int finalI = i;
                new Thread(() -> {
                    System.out.println(Thread.currentThread().getName() 
                                       + "收集了第" + finalI + "颗龙珠");
                    try {
                        cyclicBarrier.await(); // 加法计数 等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }).start();
            }
        }
    }
    
  3. Semaphore:并发限流

    作用: 多个共享资源互斥使用并发限流,控制最大的线程数

    原理:

    semaphore.acquire():获得资源,如果资源使用完,就等待资源释放后再进行使用!

    semaphore.release():释放资源,会将当前的信号量释放,然后唤醒等待的线程!

    public class SemaphoreDemo {
        public static void main(String[] args) {
            Semaphore semaphore = new Semaphore(3); // 线程数量,停车位,限流
            for (int i = 0; i <= 6; i++) {
                new Thread(() -> {
                    try {
                        semaphore.acquire(); // 获得资源 阻塞式等待
                        System.out.println(
                            Thread.currentThread().getName() + "抢到车位");
                        TimeUnit.SECONDS.sleep(2);
                        System.out.println(
                            Thread.currentThread().getName() + "离开车位");
                    }catch (Exception e) {
                        e.printStackTrace();
                    }finally {
                        semaphore.release(); // 释放资源
                    }
                }).start();
            }
        }
    }
    

8. 读写锁

ReentrantLock:可重入锁(默认是非公平锁

ReentrantReadWriteLock.ReadLock:可重入读锁

ReentrantReadWriteLock.WriteLock:可重入写锁

如果不加锁的情况,多线程的读写会造成数据不可靠的问题。

  1. 可采用 synchronized 这种重量锁和轻量锁 lock 去保证数据可靠
  2. 还可采用更细粒度的 ReadWriteLock 读写锁来保证数据可靠

独占锁(写锁):一次只能被一个线程占有

共享锁(读锁):多个线程可以同时占有

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        int num = 6;
        for (int i = 1; i <= num; i++) {
            final 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(); // 释放读锁
        }
    }
}
// 方法未加锁,导致写的时候被插队
class MyCache {
    // volatile 保证共享资源的可见性
    private volatile Map<String, String> map = new HashMap<>();
    public void write(String key, String value) {
        System.out.println(Thread.currentThread().getName() + "线程开始写入");
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "线程写入ok");
    }
    public void read(String key) {
        System.out.println(Thread.currentThread().getName() + "线程开始读取");
        map.get(key);
        System.out.println(Thread.currentThread().getName() + "线程写读取ok");
    }
}

9. 阻塞队列

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0iI3Gd29-1648561802070)(link-picture\image-20220108131237100.png)]

  1. BlockingQueue:阻塞队列

    BlockingQueue:是 Collection 的一个子类

    使用阻塞队列的情况:多线程并发处理、线程池

    BlockingQueue 有四组 api :

    方式 抛出异常 不会抛出异常,有返回值 阻塞等待 超时等待
    添加 add offer put offer(timenum.timeUnit)
    移除 remove poll take poll(timenum,timeUnit)
    判断队首元素 element peek - -
    // 抛出异常
    public static void test1(){
        //需要初始化队列的大小
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(2);
        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        //如果多添加一个 抛出异常:java.lang.IllegalStateException: Queue full
        System.out.println(blockingQueue.add("c"));
        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<>(2);
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        //如果多添加一个 只会返回 false 不会抛出异常
        System.out.println(blockingQueue.offer("c"));
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        //如果多移除一个 只会返回 null 不会抛出异常
        System.out.println(blockingQueue.poll());
    }
    // 等待 一直阻塞
    public static void test3() throws InterruptedException {
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(2);
        //一直阻塞 不会返回
        blockingQueue.put("a");
        blockingQueue.put("b");
        //如果多添加一个  会一直等待这个队列 什么时候有了位置再进去,程序不会停止
        //blockingQueue.put("c");
        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<>(2);
        blockingQueue.offer("a");
        blockingQueue.offer("b");
        System.out.println("开始等待");
        //超时时间2s 等待如果超过2s就结束等待
        blockingQueue.offer("c",2, TimeUnit.SECONDS);  
        System.out.println("结束等待");
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println("开始等待");
        //超过两秒 我们就不要等待了
        blockingQueue.poll(2,TimeUnit.SECONDS); 
        System.out.println("结束等待");
    }
    
  2. SynchronousQueue:同步队列

    SynchronousQueue:是 BlockingQueue 阻塞队列的一个实现类

    同步队列:没有容量,也可视为容量为1的队列

    特点一进一出(进去一个元素,必须等待取出来后,才能再往里面放入一个元素)

    ​ put 进去一个元素,就必须从里面先 take 出来,否则不能再 put 进去值

    SynchronousQueue 和 其他的 BlockingQueue 区别

    1. SynchronousQueue 不存储元素
    2. SynchronousQueue 的 take 是使用了 lock锁保证线程安全
    public class SynchronousQueue {
        public static void main(String[] args) {
            // SynchronousQueue 不能初始化队列的大小
            BlockingQueue<String> synchronousQueue = new SynchronousQueue<>();
            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();
        }
    }
    

10. 线程池(阻塞队列的应用)

池化技术

程序的运行的本质占用系统资源!我们需要去优化资源的使用 ===> 池化技术

池化技术:资源的创建、销毁十分消耗资源,可以事先准备好一些资源,如果要用,就从这里来拿,用完之后还回来来,以此来提高效率

池的种类线程池JDBC的连接池内存池对象池等等

线程池

线程池的好处线程复用、可以控制最大并发数、管理线程

  1. 降低资源的消耗
  2. 提高响应的速度
  3. 方便管理

线程池:三大方式、七大参数、四种拒绝策略

10.1 线程池的三大方式

  1. ExecutorService threadPool = Executors.newSingleThreadExecutor(); // 单个线程
  2. ExecutorService threadPool = Executors.newFixedThreadPool(5); // 固定大小的线程池
  3. ExecutorService threadPool = Executors.newCachedThreadPool(); // 大小可伸缩的线程池
    Executors: 是一个工具类
public class Demo01 {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
        ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //固定的线程池
        ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可伸缩的
        try {
            for (int i = 1; i <=100 ; i++) {
                // 通过线程池创建线程
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+ " ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown(); // 线程池用完必须要关闭线程池
        }
    }
}

10.2 线程池的七大参数

Executors 创建线程池的本质是:使用 ThreadPoolExecutor 来创建线程池的

public ThreadPoolExecutor(int corePoolSize,    // 核心线程池大小
                          int maximumPoolSize, // 最大的线程池大小
                          long keepAliveTime,  // 超时了没有调用就会释放
                          TimeUnit unit, 	   // 超时单位
                          BlockingQueue<Runnable> workQueue, // 阻塞队列
                          ThreadFactory threadFactory, // 线程工厂 创建线程的 一般不用动
                          RejectedExecutionHandler handler   // 拒绝策略
                         ) {
   
}

一般情况我们要使用底层ThreadPoolExecutor自定义创建线程池

public class PollDemo {
    public static void main(String[] args) {
        int max = Runtime.getRuntime().availableProcessors(); // 获取cpu 的核数
        // 使用底层的 ThreadPoolExecutor 来自定义创建线程池
        ExecutorService service =new ThreadPoolExecutor(
                2,										// 核心线程池大小
                max,									// 最大的线程池大小
                3,										// 超时了,没有调用就会释放
                TimeUnit.SECONDS,						// 超时单位
                new LinkedBlockingDeque<>(3),			// 阻塞队列
                Executors.defaultThreadFactory(),		// 线程工厂 创建线程的
                new ThreadPoolExecutor.AbortPolicy()    // 拒绝策略
        );
        try {
            for (int i = 1; i <= 10; i++) {
                // 通过线程池创建线程
                service.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "ok");
                });
            }
        }catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            service.shutdown(); // 线程池用完必须要关闭线程池
        }
    }
}

10.3 线程池的四种拒绝策略

  1. new ThreadPoolExecutor.AbortPolicy()队列满了,不处理这个,并抛出异常
  2. new ThreadPoolExecutor.CallerRunsPolicy()哪来的,去哪里 ,由 main 线程进行处理
  3. new ThreadPoolExecutor.DiscardPolicy()队列满了,丢掉异常不会抛出异常
  4. new ThreadPoolExecutor.DiscardOldestPolicy() :队列满了,尝试去和最早的进程竞争不会抛出异常

10.4 如何设置线程池的大小

  1. CPU密集型电脑的核数是几核就选择几,来确定线程池 maximunPoolSize 的大小

    Runtime.getRuntime().availableProcessors(); // 获取cpu 的核数
    
  2. I/O密集型

    I/O 十分占用资源

    I/O 密集型就是判断程序中十分耗 I/O 的线程数量,大约是最大 I/O 数的一倍到两倍之间

11. 四大函数式接口

lambda表达式、链式编程、函数式接口、Stream流式计算

函数式接口:就是只有一个方法接口

四大原生的函数式接口:ConsumerFunctionPredicateSupplier

  1. Consumer:消费型接口(只有参数,没有返回值)

    // 源码
    @FunctionInterface
    public interface Consumer<T>{
        void accept(T t);
    }
    // 简单应用
    public class ConsumerDemo {
        public static void main(String[] args) {
            Consumer<String> consumer = (str)->{ System.out.println(str);};
            consumer.accept("abc");
        }
    }
    
  2. Function:函数型接口(传入参数 T,返回参数 R)

    // 源码
    @FunctionInterface
    public interface Function<T,R>{
        R apply(T t);
    }
    // 简单应用
    public class FunctionDemo {
        public static void main(String[] args) {
            /* 不使用 Lambda 表达式的写法
            Function function = new Function(){
                public String apply(String str){
                    return str;
                }
            }*/
            
            // 函数式接口 可以使用 Lambda 表达式简化
            Function<String, String> function = (str) -> {return str;};
            System.out.println(function.apply("aaaaaaaaaa"));
        }
    }
    
  3. Predicate:断定型接口(只能传入一个参数 T,返回一个 Boolean的值)

    // 源码
    @FunctionInterface
    public interface Predicate<T>{
        Boolean test(T t);
    }
    // 简单应用
    public class PredicateDemo {
        public static void main(String[] args) {
            Predicate<String> predicate = (str) -> {return str.isEmpty();};
            System.out.println(predicate.test("aaa"));// false
            System.out.println(predicate.test("")); // true
        }
    }
    
  4. Supplier:供给型接口(没有参数,只有返回值)

    // 源码
    @FunctionInterface
    public interface Supplier<T>{
        T get();
    }
    // 简单应用
    public class SupplierDemo {
        public static void main(String[] args) {
            Supplier<String> supplier = ()->{return "1024";};
            System.out.println(supplier.get());
        }
    }
    

12. Stream流式计算

大数据:储存 + 计算

  1. 集合、MySQL 本质就是储存数据的
  2. 计算就是用流 Stream 来计算的
/**
 * 题目要求: 用一行代码实现
 * 1. Id 必须是偶数
 * 2.年龄必须大于23
 * 3. 用户名转为大写
 * 4. 用户名倒序
 * 5. 只能输出一个用户
 **/
public class StreamDemo {
    public static void main(String[] args) {
        User u1 = new User(1, "a", 23);
        User u2 = new User(2, "b", 23);
        User u3 = new User(3, "c", 23);
        User u4 = new User(6, "d", 24);
        User u5 = new User(4, "e", 25);
        List<User> list = Arrays.asList(u1, u2, u3, u4, u5); // 储存交给集合或MySQL
        // lambda、链式编程、函数式接口、流式计算
        list.stream()										 // 计算交给流 Stream
            .filter(user -> {return user.getId()%2 == 0;}) // 断定型接口
            .filter(user -> {return user.getAge() > 23;})  // 断定型接口
            .map(user -> {return user.getName().toUpperCase();})       //函数型接口
            .sorted((user1, user2) -> {return user2.compareTo(user1);})//函数型接口
            .limit(1)
            .forEach(System.out::println);
    }
}

13. ForkJoin 分支合并

ForkJoin 在 JDK1.7,并行执行任务,提高效率(在大数据量速率会更快,在小数据量就不用了

大数据:将大任务拆分为小任务

ForkJoin 特点: 工作窃取(提高效率,可以调优)

实现原理是:双端队列 先执行完毕的进程会把没执行完的进程任务窃取过来执行

ForkJoin 的使用

  1. 通过ForkJoinPool异步执行给定任务

  2. 执行计算任务 ForkJoinPool.execute(ForkJoinTask task)

  3. 计算任务类要去继承 ForkJoinTask

    ForkJoinTask的子类有:

    1. RecursiveTask(递归任务):有返回值
    2. RecursiveAction(递归事件):没有返回值
    3. CountedCompleter(数字比较器)

ForkJoin 的计算类:继承ForkJoinTask

import java.util.concurrent.RecursiveTask; // 递归任务 ForkJoinTask的子类

public class ForkJoinDemo extends RecursiveTask<Long> {//任务类 继承ForkJoinTask
    private long star;
    private long end;
    private long temp = 1000000L; /** 临界值 100万 */

    public ForkJoinDemo(long star, long end) {
        this.star = star;
        this.end = end;
    }
    
    @Override
    protected Long compute() { // 计算方法
        if ((end - star) < temp) {  // 用一般方法来计算
            Long sum = 0L;
            for (Long i = star; i < end; i++) {
                sum += i;
            }
            return sum;
        }else {		// 使用ForkJoin 分而治之 的方法 计算
            long middle = (star + end) / 2;  //计算平均值
            ForkJoinDemo forkJoinDemo1 = new ForkJoinDemo(star, middle);//拆分任务1
            forkJoinDemo1.fork(); // 拆分任务1,把线程压入线程队列
            ForkJoinDemo forkJoinDemo2 = new ForkJoinDemo(middle, end);//拆分任务2
            forkJoinDemo2.fork(); // 拆分任务2,把线程压入线程队列
            long taskSum = forkJoinDemo1.join() + forkJoinDemo2.join();
            return taskSum;
        }
    }
}

ForkJoin 的测试类

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;

public class ForkJoinTest {
    private static final long SUM = 20_0000_0000; // 20亿
    public static void main(String[] args) 
        throws ExecutionException, InterruptedException {
        test1(); // 使用普通方法  		最慢
        test2(); // 使用 ForkJoin 方法	 还行,可以调优
        test3(); // 使用 Stream 流计算    最快
    }
    //使用普通方法,不能调优
    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));
    }
    // 使用ForkJoin 方法计算,可以调优
    public static void test2() throws ExecutionException, InterruptedException {
        long star = System.currentTimeMillis();	// 开始时间
        ForkJoinPool forkJoinPool = new ForkJoinPool();      //1.创建 ForkJoin池
        ForkJoinTask<Long> task = new ForkJoinDemo(0L, SUM); //2.创建计算任务,并初始化
        // forkJoinPool.execute(task); // 执行任务,返回void 不同
        // task.fork(); //也可以使用任务 task 来执行
        ForkJoinTask<Long> submit = forkJoinPool.submit(task); //3.提交任务
        Long along = submit.get(); //4.得到任务结果
        System.out.println(along);
        long end = System.currentTimeMillis();	// 结束时间
        System.out.println("时间:" + (end - star));
    }
    //使用 Stream 流计算,更加简洁,效率更高
    public static void test3() {
        long star = System.currentTimeMillis();	// 开始时间
        long sum = LongStream.rangeClosed(0L, 20_0000_0000L) //计算范围,包含首尾
            				 .parallel()   			 // 并行计算
            				 .reduce(0, Long::sum);  // 求和
        System.out.println(sum);
        long end = System.currentTimeMillis();	// 结束时间
        System.out.println("时间:" + (end - star));
    }
}

14. 异步回调

Future 设计的初衷:对将来的某个事件结果进行建模

异步回调 类似于 前端发送ajax异步请求给后端

Future 是一个接口,平时使用他的实现类 CompletableFuture ,使用他的一些方法来实现

**CompletableFuture 方法的应用 **

  1. 没有返回值runAsync 异步回调

    public static void main(String[] args) 
        throws ExecutionException, InterruptedException {
            System.out.println(System.currentTimeMillis());
        	// 创建 发起一个异步请求任务
            CompletableFuture<Void> future = CompletableFuture.runAsync(()->{
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+".....");
            });
            System.out.println(System.currentTimeMillis());
            System.out.println(future.get());  //阻塞式,获取异步任务的执行结果
     }
    
  2. 有返回值supplyAsync 异步回调

    public static void main(String[] args) 
        throws ExecutionException, InterruptedException {
        // 创建 发起一个异步请求任务
        CompletableFuture<Integer> future=CompletableFuture.supplyAsync(()->{
            System.out.println(Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(2);
                int i=1/0;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 1024;
        });
        System.out.println(future.whenComplete((t, u) -> { // success 回调
                System.out.println("t=>" + t); // 正常的返回结果
                System.out.println("u=>" + u); // 抛出异常的 错误信息
            }).exceptionally((e) -> {  // error回调
                System.out.println(e.getMessage());
                return 404;
            }).get()); // get 得到异常的返回值
    }
    

15. Java内存模型(JMM)

JMM(Java Memory Model):抽象的概念、理论,不存在的东西,是一个概念,也是一个约定

作用:缓存一致性协议,用于定义数据读写的规则

JMM 定义了线程主内存(只有一个)之间的抽象关系

线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),是从主内存拷贝过来的

15.1 JMM

线程中分为: 工作内存主内存

关于JMM的一些同步的约定:

  1. 线程解锁 unlock 前,必须把共享变量立刻刷回主存
  2. 线程加锁 lock 前,必须读取主存中的最新值到工作内存中;
  3. 加锁和解锁是同一把锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ObP619rA-1648561802073)(link-picture\image-20220108172131208.png)]

1. JMM 八种指令

JMM 的八种指令:read 和 load、store 和 write、lock 和 unlock、use 和 assign

  1. Read(读取):作用于主内存变量,把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用;
  2. load(载入):作用于工作内存变量,把 read 操作从主内存变量放入工作内存中;
  3. Use(使用):作用于工作内存变量,把工作内存变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令;
  4. assign(赋值):作用于工作内存变量,把一个从执行引擎中接受到的值放入工作内存的变量副本中;
  5. store(存储):作用于主内存变量,把一个从工作内存中一个变量的值传送到主内存中,以便后续的 write 使用;
  6. write(写入):作用于主内存变量,把 store 操作从工作内存中得到的变量的值放入主内存的变量中;
  7. lock(锁定):作用于主内存的变量,把一个变量标识线程独占状态
  8. unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

2.JMM 指令使用规则

JMM 对八种指令的使用,制定了如下规则:

  1. 不允许 read 和loadstore和write 操作单独出现。即使用了 read 必须 load,使用了 store 必须 write

  2. 不允许线程丢弃他最近的 assign 操作,即:工作变量数据改变了之后,必须告知主内存

  3. 不允许一个线程将没有 assign 的数据从工作内存同步回主内存

  4. 一个新变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量 use 、store 操作之前,必须经过assign 和 load 操作

  5. 一个变量同一时间只有一个线程能对其进行 lock 。多次lock后,必须执行相同次数的 unlock 才能解锁

  6. 如果对一个变量进行 lock 操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新 load 或 assign 操作初始化变量的值

  7. 如果一个变量没有被 lock ,就不能对其进行 unlock 操作。也不能 unlock 一个被其他线程锁住的变量

  8. 一个变量进行 unlock 操作之前,必须把此变量同步回主内存

JMM 对这八种操作规则volatile 的一些特殊规则就能确定哪些操作是线程安全,哪些操作是线程不安全的。但是这些规则实在复杂,很难在实践中直接分析。所以一般也不会通过上述规则进行分析。更多的时候,使用 java 的 happen-before 规则来进行分析

15.2 Volatile 关键字

volilate:解决共享对象 可见性,一旦刷新了就会很快的同步主内存

Volatile 是 Java 虚拟机提供 轻量级的同步机制

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

volatile 修饰的共享变量,在进行写操作的时候会多出一行汇编

Lock 前缀的指令在多核CPU下会引发两件事情:

  1. 当前 CPU 缓存行数据写回到系统内存
  2. 这个写回内存的操作会使其他 CPU 里缓存了该内存地址的数据无效

多处理器总线嗅探

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存后再进行操作,但操作不知道何时会写到内存。
如果对声明了 volatile 的变量进行写操作,JVM就会向处理器发送一条 lock 前缀的指令,将这个变量所在缓存行的数据写回到系统内存。

但是在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据库读到处理器缓存中

15.3 volatile 特性

public class JMMDemo {
    private static Integer number = 0;
    public static void main(String[] args) { //main线程
        new Thread(()->{ // 子线程
            while (number==0){
            }
        }).start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        number=1;
        System.out.println(number);
    }
}

遇到问题:子线程不知道主存中的值已经被修改过了,而导致死循环,这是因为子线程的 number 对主内存的 number 不可见导致的,加上 volatile (可以保证子线程主内存可见性)就可以了。

volatile 的特性

  1. volatile 保证可见性

    private volatile  static Integer number = 0; // volatile的可见性可解决上面死循环问题
    
  2. 不保证原子性

    原子性:不可分割,要么同时成功,要么同时失败

    // 不保证原子性
    public class VolatileDemo {
     // private static synchronized  int number = 0; // synchronized 保证原子性
        private static volatile int number = 0; // volatile 保证可见性
        public static void add(){
            number++;  //++ 不是一个原子性操作,是2个~3个操作
        }
        public static void main(String[] args) {
            for (int i = 1; i <= 20; i++) { //理论上number最后 == 20000
                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);
        }
    }
    

    如何来保证原子性?

    1. 使用synchronized 锁Lock 锁 来保证原子性synchronized可以解决,但较耗资源 )
    2. 使用原子类来保证原子性

    原子类 Automicjava.util.concurrent 包下的类

    原子类的底层都直接和操作系统挂钩!是在内存中修改值

    原子类底层使用了一个 Unsafe 类,Unsafe类是一个很特殊的存在,可以直接和内存进行交互

    // 使用原子类保证原子性
    public class VolatileDemo {
        // volatile 保证可见性
        private static volatile AtomicInteger number = new AtomicInteger(); 
        public static void add(){
            number.incrementAndGet();  // 底层是通过 CAS 保证的原子性
        }
        public static void main(String[] args) {
            for (int i = 1; i <= 20; i++) { //理论上number最后 == 20000
                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);
        }
    }
    
  3. 禁止指令重排

    指令重排:我们写的程序,计算机并不是按照我们自己写的那样去执行的

    程序执行过程:源代码–>编译器优化重排–>指令并行也可能会重排–>内存系统也会重排–>执行

    系统重排的前提:处理器在进行指令重排的时候,会考虑数据之间的依赖性

    volatile 可以避免指令重排,在运行的程序上会加一道内存屏障,这个内存屏障可以保证在这个屏障中指令顺序

    内存屏障:就是一些 CPU 指令,volatile 就是使用内存屏障来保证可见性

    内存屏障的作用

    1. 保证特定操作执行顺序
    2. 保证某些变量内存可见性(利用这些特性,就可以保证 volatile 实现的可见性)

    内存屏障使用最多的地方单例模式饿汉式DCL懒汉式(就使用了内存屏障))

    volatile 总结

    1. volatile 可以保证可见性不能保证原子性
    2. 由于内存屏障,可以保证避免指令重排

16. 单例模式

单例模式:饿汉式、DCL懒汉式

单例模式的特点

  1. 私有化构造器(private
  2. 提供公共的获得方法(public static
  3. 创建唯一的对象(private final static

注意:单例不安全, 因为反射

16.1 单例—饿汉式

可能会浪费空间

public class Hungry {
    // 可能会浪费空间
    private byte[] data1=new byte[1024*1024]; // 1M
    private byte[] data2=new byte[1024*1024];
    private byte[] data3=new byte[1024*1024];
    private byte[] data4=new byte[1024*1024];
    private Hungry(){} // 私有化构造器
    
    private final static Hungry hungry = new Hungry(); // 创建唯一的对象
    
    public static Hungry getInstance(){ // 提供公共的获得方法
        return hungry;
    }
}

16.2 单例—DCL懒汉式(双重检测锁模式)

  1. 简单的懒汉式

    这样的懒汉式单例模式,在单线程是没问题的,在多线程下是有问题的

    public class LazyMan {
        private static LazyMan lazyMan;
        
        private LazyMan(){ } // 私有化构造器
        
        public static LazyMan getInstance(){
           if(lazyMan == null){
               lazyMan = new LazyMan();
           }
           return lazyMan;
        }
    }
    
  2. 完善后的懒汉式(适合多线程)

    class LazyMan {
        private static boolean key = false; // 红绿灯
        private volatile static LazyMan lazyMan; // volatile 保证可见性和避免指令重排
        
        private LazyMan(){ // 私有化构造器
            synchronized (LazyMan.class){ // 保证安全性,避免反射破坏
                if (key==false){
                    key=true;
                }else{
                    throw new RuntimeException("不要试图使用反射破坏异常");
                }
            }
            System.out.println(Thread.currentThread().getName()+" ok");
        }
    
        public static LazyMan getInstance(){ //双重检测锁模式 简称 DCL 懒汉式
            if(lazyMan==null){ //需要加锁
                synchronized (LazyMan.class){
                    if(lazyMan==null){
                        lazyMan=new LazyMan(); // 不是一个原子性操作
                    }
                }
            }
            return lazyMan;
        }    
    }
    public class LazyManDemo{
        public static void main(String[] args) throws Exception {
    //        LazyMan instance = LazyMan.getInstance(); // 正常的获得单例对象
            
             //Java中有反射,利用反射破坏单例模式
            Field key = LazyMan.class.getDeclaredField("key");  // 获得 key 这个属性
            key.setAccessible(true);							//无视了私有性
            Constructor<LazyMan> declaredConstructor = 
                LazyMan.class.getDeclaredConstructor(null);     //获得空参构造器 
            declaredConstructor.setAccessible(true); 			//无视构造器的私有性
            LazyMan lazyMan1 = declaredConstructor.newInstance(); //创建实例
            key.set(lazyMan1,false);
            LazyMan instance = declaredConstructor.newInstance();
            System.out.println(instance);
            System.out.println(lazyMan1);
            System.out.println(instance == lazyMan1);
        }
    }
    

16.3 单例—静态内部类实现

public class Holder {
    private Holder(){} // 私有化构造器
    public static Holder getInstance(){ // 提供公共的获得方法
        return InnerClass.holder;
    }
    public static class InnerClass{ // 静态内部类
        private static final Holder holder = new Holder(); // // 创建唯一的对象
    }
}

16.4 枚举

Enum 本身就是一个 Class 类

枚举 Enum 可解决单例模式的不安全问题

反射不能破坏枚举,使用枚举,我们就可以防止反射破坏了

反编译方法javap -p .class文件

public enum EnumSingle {
    INSTANCE;
    public EnumSingle getInstance(){
        return INSTANCE;
    }
}
class Test{
    public static void main(String[] args) throws Exception {
        EnumSingle instance1 = EnumSingle.INSTANCE;  // 正常得到
        Constructor<EnumSingle> declaredConstructor = 
            EnumSingle.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance(); //反射得到 会报错
        //java.lang.NoSuchMethodException: com.ogj.single.EnumSingle.()
        
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

17. CAS

CAS(compareAndSet): 比较并交换 (如果期望的值达到了,就更新,否则就不更新)

CAS 是 CPU 的并发原语

Java 层面的 CAS

public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020); // 默认值是 2020

        //boolean compareAndSet(int expect, int update) // 期望值、更新值
        //如果实际值 和 我的期望值相同,那么就更新;不同,那么就不更新
        System.out.println(atomicInteger.compareAndSet(2020, 2021));//达到期望值
        System.out.println(atomicInteger.get()); // 得到更新后的值 2021

        //因为期望值是2020  实际值却变成了2021  所以会修改失败
        atomicInteger.getAndIncrement(); //++操作 底层通过 Unsafe 类来实现++操作的
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());
    }
}

CAS 的原理

CAS:比较当前工作内存中的值 和 主内存中的值,如果这个值是期望的(即是相等的),那么则执行更新交换操作,如果不是就一直循环,使用的是自旋锁

//atomicInteger.getAndIncrement() 的底层就是使用的 while 自旋锁
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));//while 自旋锁
    return var5;
}

CAS缺点:

  • 循环会耗时

  • 一次性只能保证一个共享变量的原子性

  • 它会存在ABA问题(狸猫换太子)(使用 乐观锁 解决 ABA 问题

原子类 Automic 的底层是通过 CAS 保证的原子性

原子类底层使用了一个 Unsafe 类和内存进行交互的,所以效率很高

Unsafe 类

java 无法操作内存,但可以通过 native 方法调用 C++ ,C++可以调用内存

Unsafe 类相当于 java 的后门,可以通过 Unsafe 类来操作内存

CAS 的 ABA问题

public class ABADemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020); //比较值2020
		// 线程1 拿到值2020并修改值,后又将值改为2020
        System.out.println(atomicInteger.compareAndSet(2020, 2021));//true
        System.out.println(atomicInteger.get());// 2021
        System.out.println(atomicInteger.compareAndSet(2021, 2020));//true
        System.out.println(atomicInteger.get());//2020

        //线程2 还是拿到2020,以为没有被修改,其实是已修改过后的值 这就是ABA狸猫换太子的问题
        System.out.println(atomicInteger.compareAndSet(2020, 6666));//true
        System.out.println(atomicInteger.get());//6666
    }
}

平时写的 SQL:使用 乐观锁 来解决 ABA 问题

18. 原子引用

使用原子引用类AtomicReference 类)来解决 ABA 问题

原子引用带版本号原子操作

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

public class CASDemo {
    // AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题
    // 正常在业务操作,这里面比较的都是一个个对象
    static AtomicStampedReference<Integer> atomicStampedReference = 
        new AtomicStampedReference<>(1, 1); //参数为:初始值(比较值),初始戳(版本号)

    public static void main(String[] args) {
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp(); // 获得版本号
            System.out.println("a1=>" + stamp);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 修改操作时,版本号更新 + 1
            atomicStampedReference.compareAndSet(1, 2,      // 期望值,更新值
                    atomicStampedReference.getStamp(),      // 获得版本号
                    atomicStampedReference.getStamp() + 1); // 版本号更新 + 1
            System.out.println("a2=>" + atomicStampedReference.getStamp());
            // 重新把值改回去, 版本号更新 + 1
            System.out.println(atomicStampedReference.compareAndSet(2, 1,
                    atomicStampedReference.getStamp(),
                    atomicStampedReference.getStamp() + 1));
            System.out.println("a3=>" + atomicStampedReference.getStamp());
        }, "a").start();
        
        // 乐观锁的原理相同!
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp(); // 获得版本号
            System.out.println("b1=>" + stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicStampedReference.compareAndSet(1, 3,
                    stamp, stamp + 1));
            System.out.println("b2=>" + atomicStampedReference.getStamp());
        }, "b").start();
    }
}

Integer 使用了对象缓存机制,默认范围是-128~127,推荐使用静态工厂方法 valueOf 获取对象实例,而不是 new,因为 valueOf 使用缓存,而 new 一定会创建新的对象分配新的内存空间

19. 各种锁

19.1 公平锁,非公平锁

  1. 公平锁:非常公平,不能插队,必须先来后到

  2. 非公平锁:非常不公平,允许插队,可以改变顺序

    Synchonized 锁:默认非公平锁

    ReentrantLock默认就是非公平锁,可以使用他的重载构造方法修改为公平锁

    // ReentrantLock 的默认构造方法
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    
    // 使用他的重载构造方法更改锁的公平性
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    

19.2 可重入锁(递归锁)

拿到外面的锁之后,就可以拿到里面的锁,自动获得

  1. Synchonized 锁(默认非公平锁

    public class Demo01 {
        public static void main(String[] args) {
            Phone phone = new Phone();
            new Thread(()->{ phone.sms(); },"A").start();
            new Thread(()->{ phone.sms(); },"B").start();
        }
    }
    class Phone{
        public synchronized void sms(){  // synchronized 就一把锁
            System.out.println(Thread.currentThread().getName()+"=> sms");
            call();//这里也有一把锁
        }
        public synchronized void call(){
            System.out.println(Thread.currentThread().getName()+"=> call");
        }
    }
    
  2. Lock

    public class Demo01 {
        public static void main(String[] args) {
            Phone phone = new Phone();
            new Thread(()->{ phone.sms(); },"A").start();
            new Thread(()->{ phone.sms(); },"B").start();
        }
    }
    class Phone{
        Lock lock=new ReentrantLock();
        public void sms(){
            lock.lock();//细节:这个是两把锁,两个钥匙,lock锁必须配对,否则就会死锁在里面
             try {
                System.out.println(Thread.currentThread().getName()+"=> sms");
                call();//这里也有一把锁
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
        public void call(){
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "=> call");
            }catch (Exception e){
                e.printStackTrace();
            }
            finally {
                lock.unlock();
            }
        }
    }
    

19.3 自旋锁

  1. 自旋锁SpinLock

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        return var5;
    }
    
  2. 自定义自旋锁( CAS 原理,使用原子类原子引用

    自旋锁

    public class SpinlockDemo {
        AtomicReference<Thread> atomicReference=new AtomicReference<>(); //原子引用
        //加锁
        public void myLock(){ 
            Thread thread = Thread.currentThread();
            System.out.println(thread.getName()+"===> mylock");
            //自旋锁
            while (!atomicReference.compareAndSet(null,thread)){ // CAS比较
                System.out.println(Thread.currentThread().getName()+" ==> 自旋中~");
            }
        }
        //解锁
        public void myUnlock(){
            Thread thread=Thread.currentThread();
            System.out.println(thread.getName()+"===> myUnlock");
            atomicReference.compareAndSet(thread,null); // CAS比较
        }
    }
    

    测试自旋锁

    public class TestSpinLock {
        public static void main(String[] args) throws InterruptedException {
            ReentrantLock reentrantLock = new ReentrantLock();
            reentrantLock.lock();
            reentrantLock.unlock();
    
            SpinlockDemo spinlockDemo=new SpinlockDemo(); //使用CAS实现自旋锁
            new Thread(()->{
                spinlockDemo.myLock(); // 加锁
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    spinlockDemo.myUnlock(); // 3秒后 解锁
                }
            },"t1").start();
            TimeUnit.SECONDS.sleep(1);
            new Thread(()->{
                spinlockDemo.myLock(); // 加锁
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    spinlockDemo.myUnlock(); // 3秒后 解锁
                }
            },"t2").start();
        }
    }
    

    运行结果:t2进程必须等待t1进程Unlock后,才能Unlock,在这之前进行自旋等待

19.4 死锁

死锁:各自拥有对方的锁,相互抢夺对方的锁

import java.util.concurrent.TimeUnit;
public class DeadLock {
    public static void main(String[] args) {
        String lockA= "lockA";
        String lockB= "lockB";
        new Thread(new MyThread(lockA,lockB),"t1").start();
        new Thread(new MyThread(lockB,lockA),"t2").start();
    }
}

class MyThread implements Runnable{
    private String lockA;
    private String lockB;
    public MyThread(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }
    @Override
    public void run() {
        synchronized (lockA){ // lockA 锁
            System.out.println(
                Thread.currentThread().getName()+" lock"+lockA+"===>get"+lockB);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB){ // lockB 锁
                System.out.println(
                  Thread.currentThread().getName()+" lock"+lockB+"===>get"+lockA);
            }
        }
    }
}

如何解开死锁

  1. 使用 jps 定位进程号,jdk的bin目录下: 有一个 jps

    命令:jps -l:定位进程号

  2. 使用 jstack 进程进程号 找到死锁信息

    命令:jstack 进程进程号:找到死锁信息

排查问题的方法

  1. 查看日志信息
  2. 查看堆栈信息

3. 网络编程( socket )

网络编程时常用类和方法

InetAddress  	 // 获取地址
InetAddress.getCanonicalHostName		// 规范的名字
InetAddress.getHostAddress		// IP
InetAddress.getHostName		// 域名或自己电脑的名字
InetSocketAddress		// 实现 IP 地址及端口
InetAddress				// 实现 IP 地址
ServerSocket				// 建立服务的端口
.accept						// 阻塞监听等待连接
Socket						// 创建连接
.getInputStream				// 获取IO输入流
.getOutputStream			// 获取IO输出流
ByteArrayOutputStream		// byte类型数组管道输出流
FileOutputStream		// 文件字符输出流
FileInputStream			// 文件字符输入流
shutdownOutput			// 停止输出
DatagramSocket			// 数据包端口
DatagramPacket			// 数据包
.send			// 发送
.receive		// 阻塞接收
BufferedReader			// 缓存区读取
InputStreamReader		// 输入流读取
.readLine				// 读取的一行内容
URL			// 统一资源定位符
.openConnection				// 打开连接
HttpURLConnection			// 指定协议HTTP

1 网络编程概述

计算机网络:是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统

网络编程的目的:数据交换,通信

网络通信要思考的问题:

  1. 如何准确的定位网络上的一台主机? IP :192.168.1.11 命令:ping url
  2. 如何定位到一个计算机上的某个资源?端口:8080(tomcat)
  3. 找到了这个主机,如何传输数据? 通信协议:UDP、TCP、http、ftp、smtp

网络通信的组成要素IP端口号通信协议

TCP/IP参考模型

OSI七层网格模型 TCP/IP四层概念模型 对应网络协议
应用层(Application) 应用层 HTTP, TFTP, FTP, NFS, WAIS, SMTP
表示层(Presentation) Telnet, Rlogin, Snmp, Gopher
会话层(Session) SMTP, DNS
传输层(Transport) 传输层 TCP, UDP
网络层(Network) 网络层 IP, ICMP, ARP, RARP, AKP, UUCP
数据链路层(Data link) 数据链路层 FDDI, Ethernet, Arpanet, PDN, SLIP, PPP
物理层(Physical) IEEE 802.1A , IEEE 802.2 到 IEEE 802.11

2. IP

java.net 包下的类

InetAddress:IP 地址类,操作 IP 的类

IP 地址分类

  1. IPV4、IPV6
    1. IPV4:127.0.0.1 ,4个字节组成,0~255,总共42亿;30亿都在北美,亚洲4亿,2011年就已经用尽
    2. IPV6:128位。8个无符号整数。2001:acca:0ac1:0002:0ab7:1153:2210:ccc1
  2. 公网(互联网)、私网(局域网)
    1. ABCD 类地址
    2. 192.168.xxx.xx 专门给组织内部使用

域名:来标识 IP,方便 IP 的记忆,如:www.baidu.com

IP 常用操作

  1. ping url:连接网络

  2. ipconfig:查看 ip

IP 常用类和方法

InetAddress  	 // 获取地址 由于没有构造器,不能new出来。只能利用静态方法
// 查看本机地址
InetAddress.getLocalHost()			// 本机地址
InetAddress.getByName("localhost")	// 本机地址
InetAddress.getByName("127.0.0.1")	// 本机地址
InetAddress.getByName("www.baidu.com")	// 查看网站ip地址  相当于 ping www.baidu.com
// 常用方法
InetAddress.getAddress()			// 返回一个 IP 数组
InetAddress.getCanonicalHostName	// 规范的名字
InetAddress.getHostAddress			// IP
InetAddress.getHostName				// 域名或自己电脑的名字
public class InetAddressDemo {
    public static void main(String[] args) throws UnknownHostException {
        //查询本机地址
        InetAddress inetAddress1 = InetAddress.getByName("127.0.0.1");
        InetAddress inetAddress3 = InetAddress.getByName("localhost");
        InetAddress inetAddress4 = InetAddress.getLocalHost();
        System.out.println(inetAddress1+":"+inetAddress3+":"+inetAddress4);
        //查询网站ip地址
        InetAddress inetAddress2 = InetAddress.getByName("www.baidu.com");
        System.out.println(inetAddress2);
        //常用方法
        System.out.println(Arrays.toString(inetAddress2.getAddress()));//返回数组
        System.out.println(inetAddress2.getCanonicalHostName());      //规范的名字
        System.out.println(inetAddress2.getHostAddress());      //ip
        System.out.println(inetAddress2.getHostName());   //域名,或者自己电脑的名字
    }
}

3. 端口

InetSocketAddress: IP 地址及端口类

端口:表示计算机上的一个程序的进程,用来区分软件 IP 用来定位计算机

  • 不同的进程有不同的端口

  • 一个协议下被规定 0~65535 个端口,不能使用相同的端口

    单个协议下,端口号不能冲突;协议不同,使用相同的端口号不冲突:tcp : 80, udp : 80 这样不影响

  • TCP:065535,UDP:065535 所以端口号总共可有:65535*2 个

端口分类

  1. 公有端口 0~1023 (尽量不用

    • HTTP:80
    • HTTPS:443
    • FTP:21
    • Telent:23
  2. 程序注册端口:1024~49151,分配给用户或者程序

    • Tomcat:8080
    • MySQL:3306
    • Oracle:1521
  3. 动态、私有:49152~65535(尽量不用

常用 dos 命令

netstat -ano 				#查看所有的端口
netstat -ano|findstr "5900" #查看指定的端口
tasklist|findstr "8696" 	#查看指定端口的进程

InetSocketAddress 类的简单应用

//端口
public class InetSocketAddressDemo {
    public static void main(String[] args) {
        InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1",8080);
        InetSocketAddress socketAddress2 = new InetSocketAddress("localhost",8080);
        
        System.out.println(socketAddress);
        System.out.println(socketAddress2);
        
        System.out.println(socketAddress.getAddress());		// IP地址 /127.0.0.1
        System.out.println(socketAddress.getHostName());    // 主机名  127.0.0.1
        System.out.println(socketAddress.getPort());        // 端口   8080
    }
}

4. 通信协议

通信协议(或约定):双方使用相同可识别的语言

网络通信协议包含的内容:速率、传输码率、代码结构、传输控制… …

主要通信协议

  • TCP:用户传输协议 { 类似于打电话,需要两边进行连接 }
  • UDP:用户数据报协议 { 类似于发短信,不需要两边连接也可以发出,但不一定能送到 }
  • IP:网络互连协议

4.1 TCP

TCP 特点

  1. 连接、稳定
  2. 三次握手、四次挥手
  3. 客户端、服务端:主动 和 被动的过程
  4. 传输完成,释放连接,效率低

TCP 应用

  1. TCP 简单应用

    1. 服务端

      1. 建立服务的端口 ServerSocket
      2. 等待用户的连接 accept
      3. 接收用户的信息
      public class TcpServerDemo {
          public static void main(String[] args) throws IOException {
              ServerSocket serverSocket = new ServerSocket(9999);//1.建立一个地址
              Socket socket = null;
              InputStream is = null;
              ByteArrayOutputStream baos = null; // 字节数组管道流 避免中文乱码
              int i = 0;
              while (true){
                  //2.等待客户端连接过来
                  System.out.println("等待客户端连接");
                  socket = serverSocket.accept();
                  //3.读取客户端的信息
                  is =socket.getInputStream();
                  System.out.println("读取信息成功"+i);
                  baos = new ByteArrayOutputStream();
                  //创建一个接收数据的byte[]数组,及数组的有效长度len
                  byte[] buffer = new byte[1024];
                  int len;
                  while ((len=is.read(buffer))!=-1){
                      baos.write(buffer,0,len);
                  }
                  System.out.println(baos.toString());
                  
                  /*//一种方法 会出现中文乱码的情况
                 //创建一个接收数据的byte[]数组,及数组的有效长度len
                  byte[] buffer = new byte[1024];
                  int len;
                  while ((len = is.read(buffer))!=-1){
                      String msg = new String(buffer,0,len);
                      System.out.println(msg);
                  }*/
              }
              
              //关闭资源,先开后关,后开先关。
              baos.close();
              is.close();
              socket.close();
              serverSocket.close();
          }
      }
      
    2. 客户端

      1. 连接服务器 Socket
      2. 发送消息
      public class TcpClientDemo {
          public static void main(String[] args) throws IOException {
              //1.要知道服务器的地址,和端口号
              InetAddress serverIP = InetAddress.getByName("127.0.0.1");
              int port = 9999;
              System.out.println("客户端连接成功");
              Socket socket = new Socket(serverIP,port);  //2.创建一个 socket 连接
              OutputStream os = socket.getOutputStream(); //3.发送消息 IO 流
              os.write("你好,欢迎学习狂神说Java".getBytes());
              System.out.println("已发送");
              socket.close();//4.关流
              os.close();
          }
      }
      
  2. TCP 文件上传

    1. 服务器端

      public class TcpServiceDemo {
          public static void main(String[] args) throws IOException {
              ServerSocket serverSocket = new ServerSocket(9000);//1.创建服务
              System.out.println("等待连接");
              Socket socket = serverSocket.accept(); //2.阻塞式监听,会一直等待客户端连接
              InputStream is = socket.getInputStream();//3.获取输入流
              //文件输出
              FileOutputStream fos = new FileOutputStream(new File("666.jpg"));
              byte[] buffer = new byte[1024];
              int len;
              while ((len=is.read(buffer))!=-1){
                  fos.write(buffer,0,len);
              }
              //客户端传输完了,服务器接收完,通知客户端我接收完毕了
              //服务器端接收完毕,并回复信息
              OutputStream os = socket.getOutputStream();
              os.write("我接收完毕,你可以断开".getBytes());
      
              //关闭资源
              os.close();
              fos.close();
              is.close();
              socket.close();
              serverSocket.close();
          }
      }
      
    2. 客户端

      public class TcpClientDemo {
          public static void main(String[] args) throws Exception {
              //1.创建一个Socket连接
              Socket socket = new Socket(InetAddress.getByName("127.0.0.1"), 9000);
              OutputStream os = socket.getOutputStream();//2.创建一个输出流
              //3.读取文件
              FileInputStream fis = new FileInputStream(new File("321.jpg"));
              //4.写出文件
              byte[] buffer = new byte[1024];
              int len;
              while((len = fis.read(buffer))!=-1){
                  os.write(buffer,0,len);
              }
              //传输完后,通知服务器,确定服务器接收完毕,才能断开连接
              socket.shutdownOutput(); //客户端已经传输完毕,关闭输出流,停止输出
              
              InputStream inputStream = socket.getInputStream(); //接收服务端完毕信息
              //由于收到的是 String byte[]数组,使用byte输出管道流
              ByteArrayOutputStream baos = new ByteArrayOutputStream();
              byte[] buffer2 = new byte[1024];
              int len2;
              while((len2 = inputStream.read(buffer))!=-1){
                  baos.write(buffer,0,len2);
              }
              System.out.println(baos.toString());
      
              //关闭资源
              baos.close();
              inputStream.close();
              fis.close();
              os.close();
              socket.close();
          }
      }
      

4.2 UDP

UDP 需要使用的类:DatagramSocketDatagramPacket

UDP 特点

  1. 不连接( 只需知道对方的地址 )、不稳定

  2. 客户端、服务端:没有明确的界限

  3. 不管有没有准备好,都可以发送

UDP 简单应用

  1. 发生消息

    1. 发送端

      public class UdpClientDemo {
          public static void main(String[] args) throws Exception {
              DatagramSocket socket = new DatagramSocket(8080); // 1.建立一个socket
              String msg = "你好啊,服务器!"; //发送的数据
              InetAddress localhost = InetAddress.getByName("localhost"); //
              int port = 9090;
              // 2.建立一个能发送的包,包的内容:数据,数据长度,要发送给谁
              DatagramPacket packet = new DatagramPacket(msg.getBytes(),  // 数据
                                                   0,						
                                                   msg.getBytes().length, // 数据长度
                                                   localhost,port); // 要发送给谁
              socket.send(packet); // 3.发送包
              socket.close(); // 4.关闭流
          }
      }
      
    2. 接收端

      public class UdpServerDemo {
          public static void main(String[] args) throws Exception {
              // 1.开放端口,等待客户端的连接
              DatagramSocket socket = new DatagramSocket(9090);
              byte[] buffer = new byte[1024]; //接收临时区域
              // 2.接收数据包
              DatagramPacket packet = new DatagramPacket(buffer,0,buffer.length);
              socket.receive(packet); // 3.阻塞接收
              
              System.out.println(packet.getAddress().getHostAddress());
              System.out.println(
                  new String(packet.getData(),0,packet.getLength()));
      
              socket.close(); // 4.关闭流
          }
      }
      
  2. 持续发送

    1. 发送端

      public class UdpSenderDemo01 {
          public static void main(String[] args) throws Exception {
              DatagramSocket socket = new DatagramSocket(8888);
              while(true){
                  //准备数据:控制台读取 System.in
                  BufferedReader reader = 
                      new BufferedReader(new InputStreamReader(System.in));
                  String data = reader.readLine();    //包内数据 
                  byte[] datas = data.getBytes();     
                  DatagramPacket packet = new DatagramPacket(datas,0,
                            datas.length, new InetSocketAddress("localhost",6666));
                  socket.send(packet); //发送包
                  if (data.equals("bye")){
                      break;
                  }
              }
              socket.close();  //关闭流
          }
      }
      
    2. 接收端

      public class UdpReceiveDemo01 {
          public static void main(String[] args) throws Exception {
              DatagramSocket socket = new DatagramSocket(6666);
              while(true){
                  byte[] container = new byte[1024]; //准备接收包裹
                  DatagramPacket packet = 
                      new DatagramPacket(container,0,container.length);
                  socket.receive(packet); //阻塞式接收
                  byte[] data = packet.getData(); //断开连接,将接收包转换为 String 格式
                  String receiveData = new String(data,0,data.length);
                  System.out.println(receiveData);
                  if (receiveData.equals("bye")){
                      break;
                  }
              }
              socket.close(); //关闭流
          }
      }
      
  3. 多线程发送

    1. 发送线程

      public class TalkSend implements Runnable{
          DatagramSocket socket = null;
          BufferedReader reader = null;
          private int fromPort;
          private String toIP;
          private int toPort;
          
          public TalkSend(int fromPort, String toIP, int toPort) {
              this.fromPort = fromPort;
              this.toIP = toIP;
              this.toPort = toPort;
              try {
                  socket = new DatagramSocket(fromPort);
              } catch (SocketException e) {
                  e.printStackTrace();
              }
          }
      
          @Override
          public void run() {
              try {
                  while(true){
                      //准备数据:控制台读取 System.in
                      BufferedReader reader = 
                          new BufferedReader(new InputStreamReader(System.in));
                      String data = reader.readLine(); //发包内数据
                      byte[] datas = data.getBytes();
                      DatagramPacket packet = new DatagramPacket(datas,0,
                        datas.length,new InetSocketAddress(this.toIP,this.toPort));
                      socket.send(packet); //发送包
                      if (data.equals("bye")){
                          break;
                      }
                  }
              } catch (Exception e) {
                  e.printStackTrace();
              }
              socket.close(); //关闭流
          }
      }
      
    2. 接收线程

      public class TalkReceive implements Runnable{
          DatagramSocket socket =null;
          private int port;
          private String msgfrom;
          
          public TalkReceive(int port, String msgfrom) {
              this.port = port;
              this.msgfrom = msgfrom;
              try {
                  socket = new DatagramSocket(port);
              } catch (SocketException e) {
                  e.printStackTrace();
              }
          }
      
          @Override
          public void run() {
              try {
                  while(true){
                      //准备接收包裹
                      byte[] container = new byte[1024];
                      DatagramPacket packet = 
                          new DatagramPacket(container,0,container.length);
                      socket.receive(packet); //阻塞式接收
                      byte[] data = packet.getData();//断开连接,将接收包转换为 String
                      String receiveData = new String(data,0,data.length);
                      System.out.println(msgfrom+":"+receiveData);
                      if (receiveData.equals("bye")){
                          break;
                      }
                  }
              } catch (Exception e) {
                  e.printStackTrace();
              }
              socket.close();
          }
      }
      
    3. 学生端

      public class StudentTalk {
          public static void main(String[] args) { // 开启两个线程
              //学生自己的发送端口、接收IP、接收端口
              new Thread(new TalkSend(6666,"localhost",9999)).start();
              //学生自己接收端口、发送过来者
              new Thread(new TalkReceive(8888,"老师")).start();
          }
      }
      
    4. 老师端

      public class TeacherTalk {
          public static void main(String[] args) {
              //老师自己的发送端口、接收IP、接收端口
              new Thread(new TalkSend(7777,"localhost",8888)).start();
              //老师自己接收端口、发送过来者
              new Thread(new TalkReceive(9999,"学生")).start();
          }
      }
      

5. URL

URL:是统一资源定位符的简称,用来定位资源的,定位互联网上的某一个资源

     https://www.baidu.com/
协议(https)://ip地址:端口/项目名/资源

DNS:表示域名解析的过程就叫 DNS(将 www.baidu.com 的域名解析为:xxx.x…x…x 的 IP 号)

URL常用方法

url.getProtocol()	// 协议 http
url.getHost()		// 主机ip localhost
url.getPort()		// 端口 8080
url.getPath()		// 文件的地址
url.getFile()		// 文件全路径
url.getQuery()		// 参数
url.openConnection	// 打开连接,连接到这个资源
HttpURLConnection
public class DRLDemo {
    public static void main(String[] args) throws MalformedURLException {
        URL url = new URL("http://localhost:8080/helloworld/index.jsp?username=kuangshen&password==123");
        System.out.println(url.getProtocol()); 	// 协议http
        System.out.println(url.getHost());  	// 主机ip,localhost
        System.out.println(url.getPort());		// 端口,8080
        System.out.println(url.getPath());		//文件,/helloworld/index.jsp
        //全路径,/helloworld/index.jsp?username=kuangshen&password==123
        System.out.println(url.getFile());
        System.out.println(url.getQuery());	//参数,username=kuangshen&password==123
    }
}

简单应用:URL网络下载

public class UrlDown {
    public static void main(String[] args) throws Exception {
        URL url = new URL("//www.baidu.com/img/PCtm_d.png"); // 1.下载地址
        // 2.连接到这个资源 HTTP
        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
        InputStream inputStream = urlConnection.getInputStream(); // 3.输入流
        FileOutputStream fos = new FileOutputStream("123.jpg");   // 4.下载到存放地址
        byte[] buffer = new byte[1024];  // 5.写出数据
        int len ;
        while((len = inputStream.read(buffer))!=-1){
            fos.write(buffer,0,len);
        }
        fos.close();				// 6.断开连接
        inputStream.close();
        urlConnection.disconnect(); 

    }
}

4. 多线程

1. 线程简介

多任务:同时做多件事(单 CPU 实际上是分时进行的,分时处理,时间交替很快)

多线程:多条线路同时执行任务

进程:在操作系统中运行的程序(qq,播放器,游戏,IDE)

进程:是系统分配资源的单位(程序跑起来才叫一个进程)

线程:是CPU调度和执行的单位

进程和线程的关系:一个进程可以包含若干个线程,一个进程至少包含一个线程

真正的多线程是指有多个 CPU (即多核)

单 CPU 的多线程是模拟出来的,即在一个CPU的情况下,在同一个时间点,CPU只能执行一个代码,因为切换得很快,所以就有同时执行的错局

总结

  1. 线程就是独立的执行路径

  2. 在程序运行时,即使没有自己创建线程,后台也会有多个线程【如:main,GC】

  3. 在一个进程中,如果开辟了多个线程,线程的运行由调度器调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的

  4. 同一资源操作时,会存在资源抢夺的问题,需要加入并发控制

  5. 线程会带来额外的开销【如:CPU调度时间、并发控制开销(让线程排队执行)】

  6. 每个线程在自己的工作内存,内存控制不当会造成数据不一致

2. 线程创建方式

注意

  1. 线程开启不一定立即执行,由CPU调度执行
  2. 直接调用 run 方法相当于调用普通方法,不会创建新的线程

线程创建方式

  1. 继承 Thread 类(不建议使用:为了避免 OOP 单继承局限性

    Thread 类本身实现了 Runnable 接口 (使用了静态代理模式

  2. 实现 Runnable 接口(推荐使用:方便同一资源被多个线程使用)

  3. 实现 Callable 接口(有返回值

    1. 实现 Callable 接口,需要返回值类型,重写 call 方法,需要抛出异常
    2. 创建目标对象
    3. 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1); 线程池
    4. 提交执行:Future result1 = ser.submit(t1);
    5. 获取结果:boolean r1 = result1.get()
    6. 关闭服务:ser.shutdownNow();

简单应用

  1. 买票问题(实现 Runnable 接口)

    public class ThereadDemo implements Runnable{
        private int ticketNums=100;//票数
        
        public static void main(String[] args) {
            ThereadDemo test = new ThereadDemo();
            new Thread(test,"1").start();
            new Thread(test,"2").start();
            new Thread(test,"3").start();
        }
        @Override
        public void run() {
            while (true){
               if(ticketNums<=0){
                   break;
               }
                try {
                    Thread.sleep(200);//模拟延时
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(
                   Thread.currentThread().getName()+"-->拿到了第"+ticketNums--+"张票");
            }
       }
    }
    

    结果:出现数据紊乱,线程不安全

  2. 龟兔赛跑(实现 Runnable 接口)

    public class Race implements Runnable{
        private static String winner; //用static,保证只有一个胜利者
    
        public static void main(String[] args) {
            new Thread(new Race();,"兔子").start();
            new Thread(new Race();,"乌龟").start();
        }
        @Override
        public void run() {
            for (int i = 0; i <= 1000; i++) {
                String threadName = Thread.currentThread().getName();
                //模拟兔子休息,每10步休息一下
                if(threadName.equals("兔子")&&i%10==0){ //注意不要用==
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if(gameOver(i)){ //判断比赛是否结束,如果比赛结束,停止程序
                    break;
                }
                System.out.println(Thread.currentThread().getName()+"-->跑了"+i+"步");
            }
        }
        private boolean gameOver(int steps){ //判断是否完成比赛
            if(winner!=null){
                return true;
            }
            if(steps>=1000) {
                winner= Thread.currentThread().getName();
                System.out.println("winneer is"+winner);
                return true;
            }
            return false;
        }
    }
    
  3. Callable 的简单应用

    public class CallableDemo implements Callable<Boolean> {
        private String url;	// 网络图片地址
        private String name;// 保存的文件名
        
        public CallableDemo(String url, String name) {
            this.url = url;
            this.name = name;
        }
        @Override
        public Boolean call(){
            WebDownLoader webDownLoader=new WebDownLoader();
            webDownLoader.downloader(url,name);
            System.out.println("下载了文件名为"+name);
            return true;
        }
    
        public static void main(String[] args) throws Exception {
            CallableDemo t1=new CallableDemo("https://image.baidu.com/dog.jpeg","dog1");
            CallableDemo t2=new CallableDemo("https://image.baidu.com/dog2.jpeg","dog2");
            CallableDemo t3=new CallableDemo("https://image.baidu.com/dog3.jpeg","dog3");
           // 创建执行服务
            ExecutorService ser= Executors.newFixedThreadPool(3);//线程池,放3个线程
            Future<Boolean> r1=ser.submit(t1); // 提交执行
            Future<Boolean> r2=ser.submit(t2);
            Future<Boolean> r3=ser.submit(t3);
            boolean rs1=r1.get(); // 获取结果
            boolean rs2=r2.get();
            boolean rs3=r3.get();
            ser.shutdownNow(); //关闭服务
        }
    }
    class WebDownLoader{
        public void downloader(String url,String name)  {
            try {  // 使用 FileUtils 类需要导入 commons-io 的jar 包
                FileUtils.copyURLToFile(new URL(url),new File(name));
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("IO异常,downloader方法出现问题");
            }
        }
    }
    

3. Lamda表达式

函数式接口:只有一个抽象方法的接口

对于函数式接口,可通过 Lamda 表达式来创建该接口的对象,Lamda 可简化程序

简化过程:函数式接口(前提) --> 实现类 --> 静态内部类 --> 局部内部类 --> 匿名内部类 --> Lamda 表达式

注意

  1. Lamda 表达式只有一行代码的情况下,可简化成为一行
  2. 如果有多行,那么就用代码块包裹
  3. 多个参数也可以去掉参数类型,要去掉就都去掉,必须加上括号

4. 线程状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ysPxnyic-1648561802075)(link-picture\20210123215142964.png)]

1. 线程方法

方法 说明
setPriority(int newPriority) 更改线程的优先级
sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠
join() 等待该线程终止
yield() 暂停当前正在执行的线程对象,并执行其他线程
interrupt() 中断线程,别用这个方式
isAlive() 测试线程是否处于活动状态
stop()/destory()—过时,不使用 停止线程(不推荐使用 JDK 提供的这两个方法)
setDaemon(boolean flag) 设置是否为守护线程
  1. 停止线程(使用一个标志位进行终止线程)

    不推荐使用 JDK 提供的 stop(),destory() 方法,建议使用一个标志位进行终止线程,当 flag=false ,则线程终止运行

    public class Teststop implements Runnable {
        private boolean flag = true; //1.定义一个标识位
        @Override
        public void run (){
            while (flag) { //2.判断标识位
            	systepaoit.println ( "run. . . Thread" );
            }
        }
        public void stop(){ //3.对外提供方法改变标识
            this.flag = false;
        }
        
        public static void main(String[] args) {
            TestStop testStop = new TestStop();
            new Thread(testStop).start();
            for (int i = 0; i <1000 ; i++) {
                System.out.println("main"+i);
                if (i==900){
                    testStop.stop();  // 停止线程
                    System.out.println("run线程停止了!");
                }
            }
        }
    }
    
  2. 线程休眠(sleep)

    注意

    1. sleep 时间达到后,线程进入就绪状态
    2. sleep 不会释放锁

    sleep 可以模拟网络延时(放大问题的发生性,如多线程卖票,一票多卖),倒计时

    public class TestSleep implements Runnable {
        public static void main(String[] args) throws InterruptedException {
            tenDown();
            Date startTime=new Date(System.currentTimeMillis()); //打印当前系统时间
            while(true){
                Thread.sleep(1000);
                System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
                startTime=new Date(System.currentTimeMillis());
            }
        }
        public static void tenDown() throws InterruptedException {//模拟 10s 倒计时
            int num=10;
            while(true){
              Thread.sleep(1000);
                System.out.println(num--);
                if(num<=0) {
                    break;
                }
            }
        }
        @Override
        public void run() {}
    }
    
  3. 线程礼让(yield)-- 礼让不一定成功

    礼让线程:让当前正在执行的线程暂停,但不阻塞,将线程从运行状态转为就绪状态

    public class MyYield implements Runnable{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"线程开始执行");
            Thread.yield();
            System.out.println(Thread.currentThread().getName()+"线程结束执行");
        }
        public static void main(String[] args) {
            MyYield myYield=new MyYield();
            new Thread(myYield,"a").start();
            new Thread(myYield,"b").start();
        }
    }
    
  4. 线程强制执行(join)

    join:加入线程,待此线程执行完毕之后,在执行其他线程,其他线程阻塞,可以想象成插队

    public class TestJoin implements Runnable {
        public static void main(String[] args) throws InterruptedException {
            Thread thread=  new Thread(new TestJoin());
            thread.start();
            for (int i = 0; i < 50; i++) {
              if(i==25){
                  thread.join();
              }
                System.out.println("主线程"+i);
            }
        }
        @Override
        public void run() {
            for (int i = 0; i < 200; i++) {
                System.out.println("join线程"+i);
            }
        }
    }
    
  5. 线程的优先级

    线程优先级高不一定优先执行,但是优先执行的权重就大

    先设置优先级,再start线程

    thread.getPriority()		// 得到优先级
    thread.setPriority(int xxx)	// 设置优先级
    
  6. 守护线程(daemon)

    线程分为:用户线程守护线程 ( 后台记录操作日志,监控内存,垃圾回收等 )

    守护线程在用户线程执行完毕之后也会执行完毕,不过 JVM 需要一点时间

    虚拟机必须确保用户线程执行完毕

    虚拟机不用等待守护线程执行完毕

2. 观测线程状态(Thread.State)

线程状态 说明
NEW 尚未启动的线程处于此状态
RUNNABLE 在Java虚拟机中正在执行的线程处于此状态
BLOCKED 被阻塞等待监视器锁定的线程处于此状态
WA工TING 正在等待另一个线程执行特定动作的线程处于此状态
TIMED_ WA工TING 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
TERMINATED 已退出的线程处于此状态
public class TestState {
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            for (int i = 0; i <5 ; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread.State state = thread.getState();
        System.out.println(state);	//new
        thread.start();				//启动线程
        state=thread.getState();	//runnable
        System.out.println(state);
        while (state!= Thread.State.TERMINATED){	//只要线程不终止就输出线程状态
            Thread.sleep(100);
            state=thread.getState();	//更新线程状态
            System.out.println(state);	//TIME_WAITING
            
        }
    }
}

5. 线程并发

线程并发多个线程 同时操作同一资源的情况下,出现不同的线程抢夺同一资源,线程不安全,数据紊乱

线程同步

处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时就需要线程同步。线程同步其实就是一种等待机制。多个需要同时访问此对象的线程进入这个对象的等待池形成**队列**,等待前面线程使用完毕,下一个线程再使用

由于同一进程的多个线程共享同一块存储空间,就带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制 synchronized ,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,但也存在以下问题:

  1. 一个线程持有锁会导致其它所有需要此锁的线程挂起
  2. 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换调度延时,引起性能问题
  3. 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题

线程同步形成条件:队列 + 锁synchronized

1. 三大不安全案例

  1. 买票

    1. 线程不安全

      public class UnsafeBuyTicket {
          public static void main(String[] args) {
              BuyTicket buyTicket = new BuyTicket();
              new Thread(buyTicket,"1").start();
              new Thread(buyTicket,"2").start();
              new Thread(buyTicket,"3").start();
          }
      }
      class BuyTicket implements  Runnable{
          private int tickNums=10; 	// private 表示操作的是同一对象的
          boolean flag=true;			// 标志位,用于线程的外部停止方式
          @Override
          public void run() {
              while(flag){
                  buy();
              }
          }
          private void buy() { //买票
              if (tickNums <= 0){   //判断是否有票
                  flag=false;
                  return;
              }
              try {
                  Thread.sleep(100); //模拟延时
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println(Thread.currentThread().getName()+"拿到"+tickNums--);
          }
      }
      

      结果出现了负数,分析:只剩一张票的时候,3个线程都可以抢票,记住每个线程都有自己的工作内存,内存控制不当会造成数据不一致,每个线程会把这个剩下的1放到自己的内存,于是都买了

    2. 线程安全(synchronized

      //synchronized同步方法,锁的是this
      private synchronized void buy() throws InterruptedException {
          if (tickNums <= 0){ //判断是否有票
              flag=false;
              return;
          }
          // Thread.sleep(10);  //模拟延时
          System.out.println(Thread.currentThread().getName()+"拿到"+tickNums--);
      }
      
  2. 银行取钱

    1. 不安全取钱

      public class UnsafeBank {
          public static void main(String[] args) {
              Account account=new Account(100,"结婚基金");
              Drawing you=new Drawing(account,50,"你");
              Drawing girl=new Drawing(account,100,"girlFriend");
              you.start();
              girl.start();
          }
      }
      class Account{	// 账户
          int money;	// 余额
          String name;// 卡名
          public Account(int money, String name) {
              this.money = money;
              this.name = name;
          }
      }
      // 银行
      class Drawing extends Thread{ //使用继承Thread的原因是:不涉及多个多个线程操作同一对象???
          Account account;	// 账户		account 不能加 private,不然就是同一对象的
          int drawingMoney;	// 取多少钱
          int nMoney;			// 现在有多少钱
          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;
              }
              try {
                  Thread.sleep(100);  // 模拟延时
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              account.money = account.money-drawingMoney;  	// 卡内余额
              nMoney = nMoney+drawingMoney;  					// 现在手里的钱
              System.out.println(account.name+"余额为:"+account.money);
             //Thread.currentThread().getName()=this.getName()
              System.out.println(this.getName()+"手里的钱"+nMoney);
          }
      }
      

      结果出现负数,不安全

    2. 安全取钱( synchronized 同步块

      public synchronized  void run() {...}  // 锁的银行
      
      @Override
      public  void run() {
          synchronized(account){  // 锁 account:共享资源
              if(account.money-drawingMoney<0){ //判断有没有钱
                  System.out.println(Thread.currentThread().getName()+"钱不够,取不了");
                  return;
              }
              try {
                  Thread.sleep(100);  //模拟延时
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              account.money = account.money-drawingMoney;  	// 卡内余额
              nMoney = nMoney+drawingMoney;  					// 现在手里的钱
              System.out.println(account.name+"余额为:"+account.money);
              //Thread.currentThread().getName()=this.getName()
              System.out.println(this.getName()+"手里的钱"+nMoney);
          }
      }
      

      因为 synchronized 默认所得独享是 this ,那么这里就是锁的银行,但是我们操作是对 account 进行操作的,银行是没有变的,所以我们需要 synchronized 同步块,锁应该是 account

2. 解决线程同步的方法

由于可以通过 private 关键字来保证数据对象只能被方法访问,所以只需要针对方法提出一套机制。这套机制就是 synchronized 关键字,它包括两种用法: synchronized 方法synchronized 代码块

2.1 同步方法

synchronized 方法控制对“对象”的访问,每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行

同步方法弊端

  1. 若将一个大的方法申明为 synchronized 将会影响效率
  2. 只读代码是不需要加锁的,方法里面需要修改的内容才需要锁,锁的太多,浪费资源

2.2 同步代码块

格式:synchronized(Obj){}

Obj 称之为同步监视器,Obj 可以是任何对象,但是推荐使用共享资源作为同步监视器

同步方法中无需指定同步监视器,因为同步方法的同步监视器就是 this ,就是这个对象本身,或者是 class

在集合上的应用

  1. 多线程不安全的集合,使用同步方法处理

    public class UnsafeList{
        public static void main(String[] args) {
            List<String> list = new ArrayList<String>();
            for(int i = 0; i < 10000; i++){
                new Thread(()->{
                    synchronized(list){ // 同步代码块 同步监视器为list
                        list.add(Thread.currentThread().getName());
                    }
                }).start();
            }
            try {
                Thread.sleep(3000);  //延时 3s
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(list.size()); // 10000
        }
    }
    
  2. 多线程安全的集合:CopyOnWriteArrayList(写入时复制)

    import java.util.concurrent.CopyOnWriteArrayList;
    
    public class testJuuc implements Runnable {
        static CopyOnWriteArrayList<String>list=new CopyOnWriteArrayList<String>();
        
        public static void main(String[] args) throws InterruptedException {
            testJuuc thread=new testJuuc();
            for (int i = 0; i < 10; i++) {
                new Thread( thread).start();
            }
            Thread.sleep(3000);
            System.out.println(list.size());
            System.out.println(list.toString());
        }
        @Override
        public void run() {
            list.add(Thread.currentThread().getName());
        }
    }
    

2.3 Lock 锁

JDK 5.0 开始,Java 提供了更强大的线程同步机制——通过显式定义 同步锁对象来实现同步。同步锁使用 Lock 对象充当

java.util.concurrent.locks.Lock 接口是控制多个线程共享资源进行访问的工具。锁提供了对共享资源独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前先获得 Lock 对象

ReentrantLock (可重入锁) 类实现了 Lock ,它拥有与 synchronized 相同的并发性内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock ,可以显式加锁、释放锁

class A{
    private final ReentrantLock lock = new Reen TrantLock();
    public void m(){
        try{
            lock.lock(); // 加锁 保证线程安全的代码;
        }
        finally{
            lock.unlock(); // 解锁 如果同步代码有异常,要将unlock()写入finally语句块
        }
    }
}

Synchronized 与 Lock 对比

  1. Lock 是显式锁(手动开启和关闭,别忘记关闭锁)

    Synchronized 是隐式锁,出了作用域自动释放

  2. Lock 只有代码块锁

    Synchronized 有代码块锁方法锁

  3. 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且有更好的扩展性(提供更多的子类

优先使用顺序

Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)

3. 死锁

多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形,某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生死锁的问题

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 Lipstick lipstick=new Lipstick(); //需要的资源只有一份,用static修饰来保证只有一份
    static 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(1000);
                synchronized (mirror){//一秒中后获得镜子的锁
                    System.out.println(this.girlName+"获得镜子的锁");
                }
            }
        }else{
            synchronized (mirror){//获得镜子的锁
                System.out.println(this.girlName+"获得镜子的锁");
                Thread.sleep(2000);
                synchronized (lipstick){//一秒中后获得口红的锁
                    System.out.println(this.girlName+"获得口红的锁");
                }
            }
        }

    }
}

解决方法

把同步块拿出来,不同时获得两把锁

 private void makeup() throws InterruptedException {
     if(choice==0){
         synchronized (lipstick){//获得口红的锁
             System.out.println(this.girlName+"获得口红的锁");
             Thread.sleep(1000);
         }
         synchronized (mirror){//一秒中后获得镜子的锁
             System.out.println(this.girlName+"获得镜子的锁");
         }
     }else{
         synchronized (mirror){//获得镜子的锁
             System.out.println(this.girlName+"获得镜子的锁");
             Thread.sleep(2000);

         }
         synchronized (lipstick){//一秒中后获得口红的锁
             System.out.println(this.girlName+"获得口红的锁");
         }
     }
 }

产生死锁的四个必要条件

  1. 互斥条件:一个资源每次只能被一个进程使用
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  3. 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

6. 线程通信

线程通信:就是线程之间的交流

1. 并发协作模型

并发协作模型:生产者/消费者模式

生产者生产,消费者消费,两个线程之间可以通信

生产者和消费者共享同一资源,并且生产者和消费者之间相互依赖,互为条件

对于生产者,没有生产产品之前,要通知消费者等待,而生产了产品之后,又需要马上通知消费者消费
对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费
在生产者消费者问题中,仅有 synchronized 是不够的

synchronized 可阻止并发更新同一个共享资源实现同步不能用来实现不同线程之间的消息传递(通信)

解决线程通信的方法 (等待唤醒)

Java提供了几个方法解决线程之间的通信问题
注意:均是 Object 类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常

方法 描述
wait() 表示线程一直等待,直到其他线程通知,会释放锁,sleep 不会释放锁
wait(long timeout) 指定等待的毫秒数
notify() 唤醒一个处于等待状态的线程
notifyall() 唤醒同一个对象上所有调用 wait 方法的线程,优先级别高的线程优先调度

生产者消费者问题解决方式

管程法、信号灯法

2.管程法

生产者:负责生产数据的模块(可能是方法、对象、线程、进程)

消费者:负责处理数据的模块(可能是方法、对象、线程、进程)

缓冲区∶消费者不能直接使用生产者的数据,他们之间有个缓冲区

生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据

public class TestPC {
    public static void main(String[] args) {
        SynContainer container=new   SynContainer();
        new  Productor( container).start();
        new  Productor( container).start();
    }
}
//生产者
class Productor extends Thread{
    SynContainer container;
    public Productor(SynContainer container) {
        this.container = container;
    }
    @Override
    public void run() { //生产
        for (int i = 0; i < 100; i++) {
            container.push(new Chicken(i));
            System.out.println("生产了"+i+"只鸡");
        }
    }
}
//消费者
class Consumer extends Thread{
    SynContainer container;
    public SynContainer getContainer() {
        return container;
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("消费了"+ container.pop().id+"只鸡");
        }
    }
}
//产品
class Chicken{
    int id;//产品编号
    public Chicken(int id) {
        this.id = id;
    }
}
//缓冲区
class SynContainer{
    Chicken[] chickens=new Chicken[10]; // 需要一个容器大小 能放10只鸡
    int count=0;  //容器计数器

    //生产者放入产品    涉及到并发,需要同步
    public synchronized void push(Chicken chicken){  
        if(count==chickens.length){ //如果容器满了,就需要等待消费者消费
            try {
                this.wait();  //通知消费者消费,生产者等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        chickens[count]=chicken; //如果没有满,就需要加入产品
        count++;
        this.notifyAll();  //可以通知消费者消费了
    }

    //消费者消费产品
    public synchronized Chicken pop(){
        if(count==0){   //判断能否消费
            try {
                this.wait();  //等待生产者生产,消费者等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        count--;  //如果可以消费
        Chicken chicken=chickens[count];
        this.notifyAll();  //通知生产者生产
        return chicken;
    }
}

3. 信号灯法

信号灯法:即设置标志位

public class TestPC2 {
    public static void main(String[] args) {
        TV tv = new TV();
        new Player(tv).start();
        new Watcher(tv).start();
    }
}
//生产者:演员
class Player extends Thread{
    TV tv;
    public Player(TV tv) {
        this.tv = tv;
    }
    @Override
    public void run() {;
        for (int i = 0; i < 20; i++) {
            if(i%2==0){
               this.tv.play("快乐大本营"); // 节目
            }else{
                this.tv.play("抖音:记录美好生活"); // 广告
            }
        }
    }
}
//消费者:观众
class Watcher extends Thread{
    TV tv;
    public Watcher(TV tv) {
        this.tv = tv;
    }
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            tv.watch();
        }
    }
}
//产品:节目
class TV{
    String voice;      //表演的节目
    boolean flag=true; //true:演员表演。false:演员等待

    public synchronized void play(String voice){ //表演
        if(!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("演员表演了"+voice);
        this.notifyAll();//通知唤醒 通知观众观看
        this.voice=voice;
        this.flag=!this.flag;
    }
   
    public synchronized  void watch(){ //观看
        if(flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("观看了:"+voice);
        this.notifyAll();  //通知演员表演
        this.flag=!flag;
    }
}

7. 线程池

背景:线程经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大

思路提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用

好处:

  1. 提高响应速度(减少了创建新线程的时间)
  2. 降低资源消耗(重复利用线程池中线程,不需要每次都创建,便于线程管理

线程池

JDK 5.0 起提供了线程池相关API:ExecutorServiceExecutors(工具类、线程池的工厂类)
ExecutorService:真正的线程池接口。常见子类 ThreadPoolExecutor

  1. 线程池核心参数

    1. corePoolSize:核心池的大小
    2. maximumPoolSize:最大线程数
    3. keepAliveTime:线程没有任务时最多保持多长时间后会终止
  2. 线程池常用方法

    方法 说明
    execute(Runnable command) 执行任务/命令,没有返回值,一般用来执行 Runnable
    submit(Callables task) 执行任务,有返回值,一般用来执行 Callable
    shutdown() 关闭连接池
  3. Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

线程池的简单使用

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestPool {
    public static void main(String[] args) {
        // 1.创建服务,创建线程池
        ExecutorService service= Executors.newFixedThreadPool(10); // 线程池大小
        service.execute(new MyThread());  // 2.执行
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.shutdown(); // 3.关闭连接
    }
}
class MyThread implements  Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"此生辽阔");
    }
}

5. Stream流式编程

1. 流式编程概述

Stream 它并不是一个容器,它只是对容器的功能进行了增强,添加了很多便利的操作,如查找、过滤、分组、排序等一系列的操作。并且有串行并行两种执行模式,并行模式充分的利用了多核处理器的优势,使用 ForkJoin 框架进行了任务拆分,同时提高了执行速度

简而言之,Stream 就是提供了一种高效易于使用处理数据方式

Stream 特点

  1. Stream 自己不存储元素
  2. Stream 操作不会改变源对象。相反,会返回一个持有结果的新 Stream
  3. Stream 操作是延迟执行的。它会等到需要结果的时候才执行,也就是执行终端操作的时候

Stream 执行流程

  1. 创建 Stream,从集合、数组获取一个流
  2. 中间操作链,对数据进行处理
  3. 终端操作,用来执行中间操作链,返回结果

为什么需要流式操作

集合 API 是 Java API 中最重要的部分。基本上每一个 java 程序都离不开集合,但现有的集合处理在很多方面都无法满足需要

  1. 现有集合处理数据,代码繁琐,不够简洁,需要用控制流程自己实现所有数据查询的底层细节

    许多其他的语言或者类库声明的方式来处理特定的数据模型,比如 SQL 语言(可从表中查询,按条件过滤数据,并且以某种形式将数据分组,而不需了解查询是如何实现的——数据库帮你做所有的脏活。这样做的好处是代码很简洁)。而 Java 需要用控制流程自己实现所有数据查询的底层细节

  2. 现有集合无法处理大量数据

    传统情况下,为了加快处理过程,你会利用多核架构。但是并发程序不太好写,而且很容易出错

Stream API 很好的解决了这两个问题。它抽象出一种叫做的东西以声明的方式处理数据,更重要的是,它还实现了多线程,帮你处理底层诸如线程、锁、条件变量、易变变量等

2. Stream 的创建

Stream 的创建方式:

  1. 集合 Collection.stream()
  2. 数组工具类 Arrays.stream
  3. 静态方法 Stream.of

2.1 由集合创建

Java 8 中的 Collection 接口被扩展,提供了两个获取流的方法,这两个方法是 default 方法,也就是说所有实现 Collection 的接口都不需要实现就可以直接使用

Collection 接口获取流的方法:

  1. default Stream stream() : 返回一个串行流
  2. default Stream parallelStream() :返回一个并行流
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
Stream<Integer> stream = list.stream();				// 获得串行流
Stream<Integer> stream1 = list.parallelStream();	// 获得并行流

2.2 由数组工具类创建

Java 8 中的 Arrays 的静态方法 stream() 可以获取数组流

int[] array = {1,2,3};
Stream<Integer> stream = Arrays.stream(array);

2.3 由静态方法 Stream.of 创建

使用静态方法 Stream.of(),通过显示值创建一个流。它可以接收任意数量的参数

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8);

3. Stream 中间操作

如果 Stream 只有中间操作是不会执行的,当执行终端操作的时候才会执行中间操作,这种方式称为延迟加载惰性求值。多个中间操作组成一个中间操作链,只有当执行终端操作的时候才会执行一遍中间操作链

准备数据

//计算机俱乐部
private static List<Student> computerClub = Arrays.asList(
        new Student("2015134001", "小明", 15, "1501"),
        new Student("2015134003", "小王", 14, "1503"),
        new Student("2015134006", "小张", 15, "1501"),
        new Student("2015134008", "小梁", 17, "1505")
);
//篮球俱乐部
private static List<Student> basketballClub = Arrays.asList(
        new Student("2015134012", "小c", 13, "1503"),
        new Student("2015134013", "小s", 14, "1503"),
        new Student("2015134015", "小d", 15, "1504"),
        new Student("2015134018", "小y", 16, "1505")
);
//乒乓球俱乐部
private static List<Student> pingpongClub = Arrays.asList(
        new Student("2015134022", "小u", 16, "1502"),
        new Student("2015134021", "小i", 14, "1502"),
        new Student("2015134026", "小m", 17, "1504"),
        new Student("2015134027", "小n", 16, "1504")
);
private static List<List<Student>> allClubStu = new ArrayList<>();
allClubStu.add(computerClub);
allClubStu.add(basketballClub);
allClubStu.add(pingpongClub);

3.1 distinct 去重操作

distinct 可以对 Stream 中包含的元素进行去重操作(去重逻辑依赖元素的 equals 方法),新生成的 Stream 中没有重复的元素

List<String> list = Arrays.asList("b","b","c","a");
list.forEach(System.out::print); 					//bbca
list.stream().distinct().forEach(System.out::print);//bca

3.2 filter 过滤操作

filter 可以对 Stream 中包含的元素使用给定的过滤函数进行过滤操作新生成的 Stream 只包含符合条件的元素

private static List<List<Student>> allClubStu = new ArrayList<>(); // 创建集合
allClubStu.add(computerClub);

computerClub.stream().filter(  //筛选1501班的学生
    e -> e.getClassNum().equals("1501")).forEach(System.out::println);
List<Student> collect = computerClub.stream().filter( //筛选年龄大于15的学生
    e -> e.getAge() > 15).collect(Collectors.toList());

3.3 map 转换操作

map 可以对 Stream 中包含的元素使用给定的转换函数进行转换操作新生成的 Stream 只包含转换生成的元素。

  1. mapToInt:把原始 Stream 转换成一个新的 Stream,这个新生成的 Stream 中的元素都是 int 类型
  2. mapToLong
  3. mapToDouble

之所以会有这样三个变种方法,可以免除自动装箱/拆箱的额外消耗

//篮球俱乐部所有成员名 + 暂时住上商标^_^,并且获取所有队员名
List<String> collect1 = computerClub.stream()
                                    .map(e -> e.getName() + "^_^")
                                    .collect(Collectors.toList());
collect1.forEach(System.out::println);

3.4 flatMap 转换操作

flatMapmap 类似,不同的是其每个元素转换得到的是 Stream对象,会把子 Stream 中的元素压缩到父集合中

//获取年龄大于15的所有成员
List<Student> collect2 = Stream.of(basketballClub, computerClub, pingpongClub)
                            .flatMap(e -> e.stream().filter(s -> s.getAge() > 15))
                            .collect(Collectors.toList());
collect2.forEach(System.out::println);

//用双层list获取所有年龄大于15的俱乐部成员
List<Student> collect3 = allClubStu.stream()
                            .flatMap(e -> e.stream().filter(s -> s.getAge() > 15))
                            .collect(Collectors.toList());
collect3.forEach(System.out::println);

3.5 peek 消费函数

peek 生成一个包含原 Stream 的所有元素的新 Stream,同时会提供一个消费函数(Consumer实例),新 Stream 每个元素被消费的时候都会执行给定的消费函数

//篮球俱乐部所有成员名 + 赞助商商标^_^,并且获取所有队员详细内容
List<Student> collect = basketballClub.stream()
                                    .peek(e -> e.setName(e.getName() + "^_^"))
                                    .collect(Collectors.toList());
collect.forEach(System.out::println);
//Student{idNum='2015134012', name='小c^_^', age=13, classNum='1503'}

3.6 limit 截断操作

limit 可对 Stream 进行截断操作,获取其前 N 个元素,如果原 Stream 中包含的元素个数小于 N,那就获取其所有的元素

List<String> list = Arrays.asList("a","b","c");
// 获取 list 中 top2 即截断取前两个
List<String> collect1 = list.stream().limit(2).collect(Collectors.toList());
collect1.forEach(System.out::print); // ab

3.7 skip 截断操作

skip 返回一个丢弃原 Stream 的前 N 个元素后剩下元素组成的新 Stream,如果原Stream中包含的元素个数小于 N,那么返回空 Stream

List<String> list = Arrays.asList("a","b","c");
// 获取 list 中 top2 即截断取前两个
List<String> collect1 = list.stream().skip(2).collect(Collectors.toList());
collect1.forEach(System.out::print); // c

3.8 sorted 排序操作

sorted有两种形式存在:

  1. sorted(Comparator):指定比较规则进行排序。
  2. sorted(): 产生一个新流,按照自然顺序排序
List<String> list = Arrays.asList("b","c","a");
// 获取 list 中 top2 即截断取前两个
List<String> collect1 = list.stream().sorted().collect(Collectors.toList());
collect1.forEach(System.out::print);//abc

4. Stream 的终端操作

Stream 中间操作返回的是 Stream终端操作返回的就是最终转换的结果

4.1 汇聚操作

  1. foreach(Consumer c) :遍历操作

  2. collect(Collectors): 将流转化为其他形式

    其中 Collectors 具体方法有:

    1. toList: List 把流中元素收集到 List
    2. toSet :Set 把流中元素收集到 Set
    3. toCollection:Coolection 把流中元素收集到 Collection 中
    4. groupingBy:Map 根据 K 属性对流进行分组
    5. partitioningBy:Map 根据 boolean 值进行分组
    6. groupingByConcurrent:Map 根据给定条件进行分组
    7. joining:字符串拼接
    List<Student> collect = computerClub.stream().collect(Collectors.toList());
    Set<Student> collect1 = pingpongClub.stream().collect(Collectors.toSet());
    
    //注意 key 必须是唯一的 如果不是唯一的会报错而不是像普通map那样覆盖  把流中元素收集到 Map 中
    Map<String, String> collect2 = 
        pingpongClub.stream()
        			.collect(Collectors.toMap(Student::getIdNum, Student::getName));
    
    //分组 类似于数据库中的 group by 分组
    Map<String, List<Student>> collect3 = 
        pingpongClub.stream()
            		.collect(Collectors.groupingBy(Student::getClassNum));
    
    //字符串拼接 参数1:分隔符 参:2:前缀 参:3:后缀
    String collect4 = pingpongClub.stream()
        .map(Student::getName)
        .collect(Collectors.joining(",", "【", "】")); //【小u,小i,小m,小n】
    
    //三个俱乐部符合年龄要求的按照班级分组
    Map<String, List<Student>> collect5 = 
        Stream.of(basketballClub, pingpongClub, computerClub)
            .flatMap(e -> e.stream().filter(s -> s.getAge() < 17)) // 选出所有小于17岁的
            .collect(Collectors.groupingBy(Student::getClassNum)); // 按班级分组
    
    //按照是否年龄>16进行分组 key为true和false
    ConcurrentMap<Boolean, List<Student>> collect6 = 
        Stream.of(basketballClub, pingpongClub, computerClub)
            .flatMap(Collection::stream)
            .collect(Collectors.groupingByConcurrent(s -> s.getAge() > 16));
    

4.2 匹配操作

匹配操作的方法有:

  1. booelan allMatch(Predicate) :都符合
  2. boolean anyMatch(Predicate): 任一元素符合
  3. boolean noneMatch(Predicate) :都不符合
boolean b = basketballClub.stream().allMatch(e -> e.getAge() < 20);
boolean b1 = basketballClub.stream().anyMatch(e -> e.getAge() < 20);
boolean b2 = basketballClub.stream().noneMatch(e -> e.getAge() < 20);

4.3 寻找操作

寻找操作的方法有:

  1. findFirst:返回当前流中的第一个元素
  2. findAny:返回当前流中的任意元素
Optional<Student> first = basketballClub.stream().findFirst(); // 返回当前流中的第一个元素
if (first.isPresent()) { // 判断有没有元素
    Student student = first.get();
    System.out.println(student);
}

Optional<Student> any = basketballClub.stream().findAny(); // 返回当前流中的任意元素
if (any.isPresent()) { // 判断有没有元素
    Student student2 = any.get();
    System.out.println(student2);
}
Optional<Student> any1 = basketballClub.stream().parallel().findAny(); // 开启并行流,获得
System.out.println(any1);

4.4 计数和极值

计数和极值的方法有:

  1. count :返回流中元素的总个数
  2. max(Comparator) :返回流中最大值
  3. min(Comparator) :返回流中最小值
long count = basketballClub.stream().count(); // 返回流中元素的总个数
Optional<Student> max = basketballClub.stream()
    								  .max(Comparator.comparing(Student::getAge)); // max
if (max.isPresent()) { 
    Student student = max.get();
}
Optional<Student> min = basketballClub.stream()
    								  .min(Comparator.comparingInt(Student::getAge));
if (min.isPresent()) {
    Student student = min.get();
}

5. Fork/Join框架

Stream 的并行模式使用了 Fork/Join 框架

Fork/Join框架是 java 7 中加入的一个并行任务框架,可以将任务拆分为多个小任务,每个小任务执行完的结果再合并成为一个结果。在任务的执行过程中使用工作窃取(work-stealing)算法,减少线程之间的竞争

工作窃取算法:就是多线程同步执行,当一个线程把自己队列任务完成后去“窃取其他线程队列任务继续干。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行

6. Stream 的使用

Stream 没用之前,针对集合的遍历筛选等操作更多的是for-loop/while-loop,用了 Stream 后发现原来代码可以更加简洁,并且类似 SQL 语句,甚至可以做很多复杂的动作

6.1 Stream 的操作分类

Stream 使用方式类似 SQL 语句,提供对集合运算的高阶抽象,可以将其处理的元素集合看做一种数据流,流在管道中传输,数据在管道节点上进行处理,比如筛选、排序、聚合等。

数据流在管道中经过中间操作处理,由终止操作得到前面处理的结果

Stream操作分为两类:

  1. 中间操作:将流一层层的进行处理,并向下一层进行传递,如 filter map sorted等

    1. 有状态:必须等上一步操作完拿到全部元素后才可操作,如 sorted

    2. 无状态:该操作的数据不受上一步操作的影响,如 filter map

  2. 终止操作:触发数据的流动,并收集结果,如 collect findFirst forEach等。

    1. 短路操作:会在适当的时刻终止遍历,类似于 break,如 anyMatch findFirst等
    2. 非短路操作:会遍历所有元素,如 collect max等

6.2 Stream的实现过程

Stream 的实现使用流水线的方式巧妙的避免了多次迭代,基本思想是一次迭代中尽可能多的执行用户指定的操作

Stream 采用某种方式记录用户每一步的操作中间操作会返回流对象多个操作最终串联成一个管道,管道并不直接操作数据,当用户调用终止操作时将之前记录的操作叠加到一起,尽可能地在一次迭代中全部执行掉

操作如何记录

Stream 中使用 Stage 的概念来描述一个完整的操作,并用某种实例化后的 PipelineHelper 来代表 Stage,将各 Pipeline 按照先后顺序连接到一起,就构成了整个流水线

与 Stream 相关类和接口的继承关系如下图,其中蓝色表示继承关系,绿色表示接口实现:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DiZN0dFl-1648561802077)(link-picture\20210221182246981.png)]

使用 Collection.streamArrays.streamStream.of 等接口会生成 Head

Head 用于表示第一个 Stage,该 Stage 不包含任何操作StatelessOpStatefulOp 分别表示无状态有状态的Stage

操作如何叠加

叠加后的操作如何执行

执行结果在哪儿

坑点,这些有点复杂,就不深究了

6.3 Stream 并行执行原理

使用 Collection.parallelStream 或 Stream.parallel 等方法可以将当前的 Stream 流标记为并行执行

坑点中提到,在调用 Stream 的终止操作时,会执行 AbstractPipeline.evaluate 方法,根据 paraller 标识是执行并行操作还是串行操作

return isParallel()
?terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags())) //并发执行
:terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags()));//串行执行
  1. 如果被标记为 sequential ,则会调用 TerminalOp.evaluateSequential 串行执行

    (evaluateSequential的调用过程在坑点中提到)

  2. 如果被标记为 parallel ,则会调用 TerminalOp.evaluateParallel 并行执行

对于AbstractPipeline.evaluate 方法,不同的 TerminalOp 会有不同的实现,但都使用了 ForkJoin 框架,将原始数据不断拆分为更小的单元,对每一个单元做 evaluateSequential 类似的动作,最后将每一个单元计算的结果依次整合,得到最终结果

默认情况下,ForkJoin 的线程数即为机器的 CPU 核数

   Stream.of(basketballClub, pingpongClub, computerClub)
       .flatMap(Collection::stream)
       .collect(Collectors.groupingByConcurrent(s -> s.getAge() > 16));



### 4.2 匹配操作

匹配操作的方法有:

1. booelan `allMatch`(Predicate) :都符合
2. boolean `anyMatch`(Predicate): 任一元素符合
3. boolean `noneMatch`(Predicate) :都不符合

~~~java
boolean b = basketballClub.stream().allMatch(e -> e.getAge() < 20);
boolean b1 = basketballClub.stream().anyMatch(e -> e.getAge() < 20);
boolean b2 = basketballClub.stream().noneMatch(e -> e.getAge() < 20);

4.3 寻找操作

寻找操作的方法有:

  1. findFirst:返回当前流中的第一个元素
  2. findAny:返回当前流中的任意元素
Optional<Student> first = basketballClub.stream().findFirst(); // 返回当前流中的第一个元素
if (first.isPresent()) { // 判断有没有元素
    Student student = first.get();
    System.out.println(student);
}

Optional<Student> any = basketballClub.stream().findAny(); // 返回当前流中的任意元素
if (any.isPresent()) { // 判断有没有元素
    Student student2 = any.get();
    System.out.println(student2);
}
Optional<Student> any1 = basketballClub.stream().parallel().findAny(); // 开启并行流,获得
System.out.println(any1);

4.4 计数和极值

计数和极值的方法有:

  1. count :返回流中元素的总个数
  2. max(Comparator) :返回流中最大值
  3. min(Comparator) :返回流中最小值
long count = basketballClub.stream().count(); // 返回流中元素的总个数
Optional<Student> max = basketballClub.stream()
    								  .max(Comparator.comparing(Student::getAge)); // max
if (max.isPresent()) { 
    Student student = max.get();
}
Optional<Student> min = basketballClub.stream()
    								  .min(Comparator.comparingInt(Student::getAge));
if (min.isPresent()) {
    Student student = min.get();
}

5. Fork/Join框架

Stream 的并行模式使用了 Fork/Join 框架

Fork/Join框架是 java 7 中加入的一个并行任务框架,可以将任务拆分为多个小任务,每个小任务执行完的结果再合并成为一个结果。在任务的执行过程中使用工作窃取(work-stealing)算法,减少线程之间的竞争

工作窃取算法:就是多线程同步执行,当一个线程把自己队列任务完成后去“窃取其他线程队列任务继续干。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行

6. Stream 的使用

Stream 没用之前,针对集合的遍历筛选等操作更多的是for-loop/while-loop,用了 Stream 后发现原来代码可以更加简洁,并且类似 SQL 语句,甚至可以做很多复杂的动作

6.1 Stream 的操作分类

Stream 使用方式类似 SQL 语句,提供对集合运算的高阶抽象,可以将其处理的元素集合看做一种数据流,流在管道中传输,数据在管道节点上进行处理,比如筛选、排序、聚合等。

数据流在管道中经过中间操作处理,由终止操作得到前面处理的结果

Stream操作分为两类:

  1. 中间操作:将流一层层的进行处理,并向下一层进行传递,如 filter map sorted等

    1. 有状态:必须等上一步操作完拿到全部元素后才可操作,如 sorted

    2. 无状态:该操作的数据不受上一步操作的影响,如 filter map

  2. 终止操作:触发数据的流动,并收集结果,如 collect findFirst forEach等。

    1. 短路操作:会在适当的时刻终止遍历,类似于 break,如 anyMatch findFirst等
    2. 非短路操作:会遍历所有元素,如 collect max等

6.2 Stream的实现过程

Stream 的实现使用流水线的方式巧妙的避免了多次迭代,基本思想是一次迭代中尽可能多的执行用户指定的操作

Stream 采用某种方式记录用户每一步的操作中间操作会返回流对象多个操作最终串联成一个管道,管道并不直接操作数据,当用户调用终止操作时将之前记录的操作叠加到一起,尽可能地在一次迭代中全部执行掉

操作如何记录

Stream 中使用 Stage 的概念来描述一个完整的操作,并用某种实例化后的 PipelineHelper 来代表 Stage,将各 Pipeline 按照先后顺序连接到一起,就构成了整个流水线

与 Stream 相关类和接口的继承关系如下图,其中蓝色表示继承关系,绿色表示接口实现:

[外链图片转存中…(img-DiZN0dFl-1648561802077)]

使用 Collection.streamArrays.streamStream.of 等接口会生成 Head

Head 用于表示第一个 Stage,该 Stage 不包含任何操作StatelessOpStatefulOp 分别表示无状态有状态的Stage

操作如何叠加

叠加后的操作如何执行

执行结果在哪儿

坑点,这些有点复杂,就不深究了

6.3 Stream 并行执行原理

使用 Collection.parallelStream 或 Stream.parallel 等方法可以将当前的 Stream 流标记为并行执行

坑点中提到,在调用 Stream 的终止操作时,会执行 AbstractPipeline.evaluate 方法,根据 paraller 标识是执行并行操作还是串行操作

return isParallel()
?terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags())) //并发执行
:terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags()));//串行执行
  1. 如果被标记为 sequential ,则会调用 TerminalOp.evaluateSequential 串行执行

    (evaluateSequential的调用过程在坑点中提到)

  2. 如果被标记为 parallel ,则会调用 TerminalOp.evaluateParallel 并行执行

对于AbstractPipeline.evaluate 方法,不同的 TerminalOp 会有不同的实现,但都使用了 ForkJoin 框架,将原始数据不断拆分为更小的单元,对每一个单元做 evaluateSequential 类似的动作,最后将每一个单元计算的结果依次整合,得到最终结果

默认情况下,ForkJoin 的线程数即为机器的 CPU 核数

你可能感兴趣的:(java)