黑马程序员技术交流社区

标题: Java集合线程笔记 [打印本页]

作者: i欸你看    时间: 2018-11-20 16:53
标题: Java集合线程笔记
Map体系
Map集合概述
java.util.Map<K, V>接口
Map集合特点:
        1. 是双列集合, 一个元素包含两个值 (键key, 值value)
        2. key和value的类型可以相同, 也可以不同
        3. key不允许重复, value可以重复
        4. key和value是一一对应的, 一个键只能对应一个值
Map集合适合存储一对一关系的数据
Map常用子类
java.util.Map<K, V>接口: 双列集合的根接口, 规定了共性的方法
        |
        |_ HashMap<K, V>类: 底层哈希表. key存取无序不可重复
                |_ LinkedHashMap类: 底层哈希表+链表. key存取有序不可重复     
映射: 键和值的对应关系 mapping
HashSet底层使用的就是HashMap
LinkedHashSet底层使用的就是LinkedHashMap
Map中常用方法
Map<String, Phone> map = new HashMap<>();
map.put("张三", p1);
map.put("张三", p2);
java.util.Map接口: 双列集合的顶层
        // 成员方法
        V put(K key, V value): 添加/修改键值对. 如果键存在, 则用新值替换已有值
        V remove(Object key): 根据键删除键值对, 返回被删除元素的值
        V get(Object key): 根据键获取值. 如果键不存在, 则返回null
        boolean containsKey(Object key): 判断是否包含指定的键  
        Set<K> keySet(): 获取Map集合中所有的键, 存储到Set集合中

        Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的Entry对象的集合(Set集合
Map遍历方式一: keySet()方法实现通过键找值
Set<K> keySet(): 获取Map集合中所有的键, 存储到Set集合中  
keySet()遍历步骤:
        1. Map对象调用 keySet() 方法, 获取包含所有key的Set集合
        2. 遍历Set集合, 获取每个key
        3. 通过Map对象调用 get(Object key) 方法根据key获取到value

Entry键值对对象介绍
java.util.Map.Entry接口:
        // 常用成员方法
        K getKey(): 获取Entry对象中的键
        V getValue(): 获取Entry对象中的值      
Entry对象就是一个节点, 节点中存储了key和value
拿到一个Entry对象就可以从中获取key和value

Map遍历方式2: 通过entrySet()获取Entry对象形式遍历
java.util.Map接口
        Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)
entrySet()方法遍历Map步骤:
        1. Map对象调用 entrySet() 获取包含所有Entry对象的Set集合
        2. 遍历Set集合, 获取每个Entry对象
        3. 调用Entry对象的 getKey() 和 getValue() 方法获取键和值

HashMap存储自定义数据类型作为键
HashMap存储自定义JavaBean对象作为key保证key唯一不重复, 需要让JavaBean重写 hashCode() 和 equals() 方法         

LinkedHashMap类
LinkedHashMap底层: 哈希表 + 链表
        key不允许重复, 但key存取有序
LinkedHashSet的底层就是使用LinkedHashMap实现的
Hashtable类
Hashtable和HashMap:
        相同点:
                1. 底层都是哈希表
        不同点:
                1. Hashtable不允许存储null值和null键; HashMap允许存储null值和null键
                2. Hashtable线程安全效率低; HashMap线程不安全效率高

JDK9对集合添加的优化
使用集合添加大量元素时, 反复add(...)比较麻烦. JDK 9 为集合提供了一些静态方法, 可以方便的对集合进行初始化
java.util.List
        // 静态方法
        static List<E> of(E... e): 返回包含指定元素的 不可变List 集合        
java.util.Set
        // 静态方法
        static Set<E> of(E... e): 返回包含指定元素的 不可变Set 集合      
java.util.Map
        // 静态方法
        static Map<K, V> of(K k1, V v1, ...): 返回包含指定键值对的 不可变Map 集合        
注意:
        1. of() 方法只适用于List接口, Set接口, Map接口, 不适用于接接口的实现类
        2. of() 方法的返回值是一个不能改变的集合, 集合不能再使用 add(), put() 方法添加元素, 会抛出异常
        3. Set接口和Map接口在调用 of() 方法的时候, 不能有重复的元素, 否则会抛出异常
将 不可变集合 的元素转移到常见集合实现类中:

day05  异常 多线程
异常的分类
// 异常的体系结构
java.lang.Throwable  // 体系最顶层
        |_ Error                  // 错误
        |_ Exception              // 编译时异常
                |_ RuntimeException   // 运行时异常

错误(Error): 不能捕获处理的严重问题. 绝症
        必须将程序停下来, 修改代码才能解决
        错误的类名都是 "XxxError" 方式
异常(Exception): 可以捕获处理的问题. 发烧感冒吃个药就好了
        程序执行起来后, 如果有合适的处理方式, 即使发生异常, 程序也能处理该异常并继续运行
        异常的类名都是 "XxxException" 方式
        1. 编译时异常:
                编译时期就会发生的异常, 必须在编译时期处理  Unhandled exception XxxException
        2. 运行时异常:
                编译时正常, 运行时才会发生的异常
异常的产生过程解析
异常产生的过程:
        1. 当执行的代码发生异常操作时, JVM会创建一个对应的异常类对象, 包含异常的内容, 原因, 位置
                如 new ArrayIndexOutOfBoundsException(), new NullPointerException()
        2. 如果执行代码的方法没有对异常进行 try...catch 处理, 则会向该方法调用处的方法抛(向上层抛). 如果所有方法(包括main()方法)都没有 try...catch 处理异常, 则该异常会被JVM按照默认的处理方式处理
        3. JVM对于异常的默认处理方式是:
                1. 将异常信息(内容, 原因, 位置)打印到控制台
                2. 终止当前的程序

异常关键字: throw制造异常
throw关键字作用:
        在方法中抛出指定的异常对象      
格式:
        throw new 异常类名("异常原因字符串");

注意:
        1. throw 必须写在方法的内部
        2. throw 后面new的异常对象, 必须是 "Exception" 或 "Excetion的子类" 的对象
        3. 一个方法内部 throw 了一个异常对象, 则该方法可以分为2种情况来处理该异常:
                如果 throw 的是"运行时异常"(RuntimeException及其子类)对象, 那么可以不处理
                        该异常最终会交给JVM处理, 结果就是: 打印异常信息到控制台, 并立刻结束程序
                如果 throw 的是"编译时异常"(Exception及其子类), 则必须处理:
                        处理方式1: throws 抛出
                        处理方式2: try...catch 捕获

Objects工具类方法: requireNonNull()非空判断
java.util.Objects: 操作各种对象相关的工具类
        // 静态方法
        static <T> T requireNonNull(T obj): 检查传入参数是不是null. 是null则抛异常; 非null则返回该对象
        static <T> T requireNonNull(T obj, String message): 检查传入参数是不是null. 是null则抛异常, 同时带有原因; 非null则返回该对象      
可以用来简化代码的编写

异常的处理方式1: throws声明抛出异常
异常处理的第一种方式:
        throws, 声明抛出 (方法自己不处理, 交给方法调用者处理, "甩锅给别人")
        作用: 告诉方法的调用者, 调用此方法有可能发生异常, 需要在编写代码时处理
格式:
        修饰符 返回值类型 方法名() throws 异常类名1, 异常类名2, ... {         
        }

注意:
        1. throws 必须写在方法声明上
        2. throws 后面的异常类名, 一般是 Exception 或 Exception的子类
                (RuntimeException及其子类也行, 但是没有什么意义)
        3. 方法内部如果抛出了多个异常对象, throws 后也必须声明多个异常
                如果抛出的异常对象有子父类关系, 那么直接声明父类异常即可
        4. 调用了一个带有 throws 声明抛出异常的方法, 就必须处理该异常:
                要么继续声明 throws 抛出
                要么 try...catch 捕获处理异常

异常注意事项1: 捕获多个异常的3种方式
捕获多个异常:
        1. 多个异常分别 try...catch 处理
        2. 一个 try 多个 catch
        如果异常存在继承关系, 子类异常在上, 父类异常在下
        3. 多个异常, 一次捕获一次处理
                用Exception多态捕获

多线程计算机基本概念: 并发与并行
并发: (交替执行) 指两个或多个事件在"同一时间段内"发生
并行: (同时执行) 指两个或多个事件在"同一时刻"发生 (同时发生)
我们研究的是"并发"
并发的实现, 是依靠电脑CPU快速地在多个任务之间切换执行实现的
计算机基本概念: 进程
进程: 一个应用程序在内存中的一次执行过程
        每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程
        进程也是程序的一次执行过程,是系统运行程序的基本单位
        系统运行一个程序即是一个进程从创建、运行到消亡的过程

计算机基本概念: 线程
线程: 是进程内的一个独立执行单元 (一条代码执行路径)
        一个程序运行后至少有一个进程, 一个进程中可以包含多个线程      
        公司 > 部门 > 员工
        程序 > 进程 > 线程
多线程的好处:
        效率高
        多个线程之间互不影响
线程的调度
线程的调度方式:
        1. 分时调度: 所有线程轮流使用CPU, 平分占用CPU的时间
        2. 抢占式调度: 优先让优先级高的线程使用CPU; 如果优先级相同, 则随机选择一个线程执行        
        1-10
        线程1 10 可能性更大
        线程2 9
        线程3      
Java使用的是"抢占式"调度
主线程
Exception in thread "main"
主线程:
        我们以前编写的代码, 也在一条线程中执行, 该线程叫作"main线程", 也称为"主线程"
如果我们没有额外创建线程, 那么我们的程序就只有一个线程, 即主线程, 此时程序是"单线程"的
单线程的执行特点:
        同一个线程内的代码, 从上往下依次执行
创建多线程程序的第一种方式: 继承Thread类
实现多线程的第一种方式:
        1. 定义类, 继承 Thread 类
        2. 重写 run() 方法, run方法内部是线程要执行的任务
        3. 创建Thread子类的对象, 调用 start() 方法启动线程
java.lang.Thread类: 表示线程. 实现了Runnable接口
    void start(): 启动线程, 即让线程开始执行run()方法中的代码
注意:
        必须调用 start() 方法来开启线程, 不能直接调用 run() 方法, 调用 run() 会变成单线程
        同一个线程对象, 不能多次调用 start() 方法
        Java是抢占式调度, 不同线程的代码, 执行顺序是随机的


day06  线程 同步
线程多线程原理1: 线程执行的随机性
CPU执行哪个线程是随机的, 不能人为干预
Java线程调度是抢占式的, 多个线程互相抢夺CPU的执行权
补充:
第一种方式: 继承Thread类
        定义类继承Thread
        重写run()方法, 要执行的任务
        创建子类的对象, 调用start()方法启动线程
多线程原理2: 多线程的内存
多线程情况下, 每个线程都有各自的栈内存
每个线程各自的方法调用, 进的是各自线程的栈
栈是每个线程各自的, 堆是所有线程共用的
Thread常用方法: getName(), currentThread()
java.lang.Thread类: 表示线程. 实现了Runnable接口
        // 构造方法
    Thread(): 创建Thead对象
    Thread(String threadName): 创建Thead对象并指定线程名
    Thread(Runnable target): 通过Runnable对象创建Thread对象
    Thread(Runnable target, String threadName): 通过Runnable对象创建对象并指定线程名
        // 成员方法
    void run(): 用于让子类重写, 表示该线程要执行的任务.不能直接调用
    void start(): 启动线程, 即让线程开始执行run()方法中的代码
    String getName(): 获取线程的名称
    void setName(String name): 设置线程名称
        // 静态方法
    static Thread currentThread(): 返回对当前正在执行的线程对象的引用
    static void sleep(long millis): 让所在线程睡眠指定的毫秒

Thread常用方法: setName(), Thread(String name)
java.lang.Thread类: 表示线程. 实现了Runnable接口
        // 构造方法
    Thread(String threadName): 创建Thead对象并指定线程名
    // 成员方法
    void setName(String name): 设置线程名称
Thread常用方法: sleep()
java.lang.Thread类: 表示线程. 实现了Runnable接口
        // 静态方法
    static void sleep(long millis): 让所在线程睡眠指定的"毫秒"

创建多线程程序的方式2: 实现Runnable接口
创建线程的第2种方式:
        1. 定义类, 实现Runnable接口
        2. 重写 run() 方法
        3. 创建Runnable实现类对象
        4. 创建Thread类对象, 在构造方法中传入Runnable实现类对象
        5. 通过Thread对象调用start()方法启动线程
java.lang.Thread类: 表示线程. 实现了Runnable接口
        // 构造方法
    Thread Thread(Runnable target): 通过Runnable对象创建Thread对象
    Thread Thread(Runnable target, String threadName): 通过Runnable对象创建对象并指定线程名
解决线程安全问题方式1: 同步代码块
解决多线程操作共享数据的安全问题的3种方式:
        1. 同步代码块
        2. 同步方法
        3. 锁机制        
同步代码块: 使用 synchronized 关键字修饰的代码块, 并传入一个当作锁的对象
格式:
    synchronized (锁对象) {
                // 操作共享数据的代码
    }

同步技术解决线程安全问题的原理
锁对象, 也称为"同步锁", "对象监视器"
同步的原理:
    线程进入同步代码块前, 会争夺锁对象, 只有一个线程会抢到锁对象
    进入同步代码块的线程, 会持有锁对象, 并执行同步代码块中的代码
    此时同步代码块外的线程, 处于阻塞状态, 只能等待
    当同步代码块内的线程执行完代码块, 会离开同步代码块, 并归还锁对象给同步代码块
    等在同步代码块外的其他线程就可以继续争夺锁对象
解决线程安全问题方式2: 同步方法
同步方法: 使用 synchronized 关键字修饰的方法, 具有默认的锁对象
非静态同步方法的锁对象: this
        // 非静态同步方法
        public synchronized void method(){
                // 可能会产生线程安全问题的代码
        }

静态同步方法:
        public static synchronized void method(){
                // 可能会产生线程安全问题的代码
        }
静态同步方法的锁对象: 当前类的字节码对象
获取一个类的字节码对象的3种方式:
        1. 对象.getClass()
        2. 类名.class
        3. Class.forName("类的全路径");
解决线程安全问题方式3: Lock锁
java.util.concurrent.locks.Lock接口: JDK 5 新增的Lock接口
        // 成员方法
        void lock(): 获取锁
        void unlock(): 释放锁      
java.util.concurrent.locks.ReentrantLock类: Lock的实现类

线程的状态线程状态概述
线程的生命周期中, 有哪6种状态
提前了解:
        锁对象, 也称为"同步锁", "对象监视器"
        Object类中关于线程的方法:
java.lang.Object类:
        // 成员方法 (只能通过"锁对象"调用)
        void notify(): 随机唤醒在同一个锁对象上的某一个处于等待状态的线程
        void notifyAll(): 唤醒所有在同一个锁对象上处于等待状态的线程
        void wait(): 让当前线程处于无限等待状态
        void wait(long timeout): 让当前线程处于计时等待状态, 时间到或被唤醒后结束此状态
        void wait(long timeout, int nanos): 让当前线程处于计时等待状态, 时间到或被唤醒后结束此状态
总结:
线程的生命周期中, 可以出现有6种状态:
    1. NEW 新建
       线程被创建, 但没有调用 start() 启动
    2. RUNNABLE 可运行
       调用 start()方法后已启动, 但可能正在执行 run() 方法的代码, 也可能正在等待CPU的调度
    3. BLOCKED 阻塞
       线程试图获取锁, 但此时锁被其他线程持有
    4. WAITING 无限等待
       通过锁对象调用无参的 wait() 进入此状态.
       等待其他线程通过锁对象执行 notify() 或 notifyAll() 才能结束这个状态
    5. TIMED_WAITING 计时等待
       如通过锁对象调用有参的 wait(long millis) 或 sleep(long millis), 则进入此状态.
       直到时间结束之前被其他线程通过锁对象执行notify()或notifyAll()唤醒, 或时间结束自动唤醒
    6. TERMINATED 终止
       run()方法结束(执行结束, 或内部出现异常), 则进入此状态










欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2