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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值