Java进阶学习总结(IO流、多线程)

373 阅读21分钟

#博学谷IT学习技术支持#

08 IO流

8-1 File

01-File和IO的概述

  • 它是文件和目录路径名的抽象表示
  • 文件和目录是可以通过File封装成对象的
  • 对于File而言,其封装的并不是一个真正存在的文件,仅仅是一个路径名而已.它可以是存在的,也可以是不存在的.将来是要通过具体的操作把这个路径的内容转换为具体存在的

02-File的构造方法

方法名说明
File(String pathname)通过将给定的路径名字符串转换为抽象路径名来创建新的 File实例
File(String parent, String child)从父路径名字符串和子路径名字符串创建新的 File实例
File(File parent, String child)从父抽象路径名和子路径名字符串创建新的 File实例

04-File的创建功能

方法名说明
public boolean createNewFile()当具有该名称的文件不存在时,创建一个由该抽象路径名命名的新空 文件 ·
public boolean mkdir()创建由此抽象路径名命名的目录
public boolean mkdirs()创建由此抽象路径名命名的目录,包括任何必需但不存在的父目录

05-File的删除方法

方法名说明
public boolean delete()删除由此抽象路径名表示的文件或目录

06-File的获取和判断方法

判断功能

方法名说明
public boolean isDirectory()测试此抽象路径名表示的File是否为目录
public boolean isFile()测试此抽象路径名表示的File是否为文件
public boolean exists()测试此抽象路径名表示的File是否存在

获取功能

方法名说明
public String getAbsolutePath()返回此抽象路径名的绝对路径名字符串
public String getPath()将此抽象路径名转换为路径名字符串
public String getName()返回由此抽象路径名表示的文件或目录的名称
public File[] listFiles()返回此抽象路径名表示的目录中的文件和目录的File对象数组

07-File的listFile方法

 File f2 = new File("E:\test"); 
 File[] fileArray = f2.listFiles(); 
 for(File file : fileArray) { }

8-2 字节流

12-IO的概述

  • IO:输入/输出(Input/Output)
  • 流:是一种抽象概念,是对数据传输的总称.也就是说数据在设备间的传输称为流,流的本质是数据传输
  • IO流就是用来处理设备间数据传输问题的.常见的应用: 文件复制; 文件上传; 文件下载

13-IO的分类

  • 按照数据的流向
    • 输入流:读数据
    • 输出流:写数据
  • 按照数据类型来分
    • 字节流
      • 字节输入流
      • 字节输出流
    • 字符流
      • 字符输入流
      • 字符输出流

14-字节流-字节输出流入门

  • 字节流抽象基类
    • InputStream:这个抽象类是表示字节输入流的所有类的超类
    • OutputStream:这个抽象类是表示字节输出流的所有类的超类
    • 子类名特点:子类名称都是以其父类名作为子类名的后缀
  • 字节输出流
    • FileOutputStream(String name):创建文件输出流以指定的名称写入文件
  • 使用字节输出流写数据的步骤
    • 创建字节输出流对象(调用系统功能创建了文件,创建字节输出流对象,让字节输出流对象指向文件)
    • 调用字节输出流对象的写数据方法
    • 释放资源(关闭此文件输出流并释放与此流相关联的任何系统资源)
 public class FileOutputStreamDemo01 { 
     public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("myByteStream\fos.txt"); 
        fos.write(97);     
        fos.close(); 
}}

16-字节流-一次写多个数据

 byte[] bys = new byte[1024]; //1024及其整数倍 int len; 
 //循环读取 
 while ((len=fis.read(bys))!=‐1) {
  System.out.print(new String(bys,0,len)); 
   fos.write(bys,0,len); 
} 

17-字节流-两个问题

  • 字节流写数据如何实现追加写入
    • 创建文件输出流以指定的名称写入文件。如果第二个参数为true ,则字节将写入文件的末尾而不是开头
    • public FileOutputStream(String name,boolean append)

27-缓冲流-一次读写一个字节数组

lBufferOutputStream:该类实现缓冲输出流.通过设置这样的输出流,应用程序可以向底层输出流写入字 节,而不必为写入的每个字节导致底层系统的调用

lBufferedInputStream:创建BufferedInputStream将创建一个内部缓冲区数组.当从流中读取或跳过字 节时,内部缓冲区将根据需要从所包含的输入流中重新填充,一次很多字节

8-4 字符流&字符缓冲流

01-字节流操作文本文件出现乱码的问题

  • 字符流 = 字节流 + 编码表

02-字符流-编码表

编码规则:

  • 128个US-ASCII字符,只需一个字节编码 拉丁文等字符,需要二个字节编码
  • 大部分常用字(含中文),使用三个字节编码
  • 其他极少使用的Unicode辅助字符,使用四字节编码

03-字符流-编码和解码的方法

04-字节流读取中文出现乱码的原因

  • 用字节流如何识别是中文的?
    • 汉字在存储的时候,无论选择哪种编码存储,第一个字节都是负数

06-字符流-写出数据

  • 介绍
    • Writer: 用于写入字符流的抽象父类
    • FileWriter: 用于写入字符流的常用子类
  • 构造方法

  • 成员方法

08-字符流-flush和close方法

  • 刷新和关闭的方法

09-字符流-读取数据

11-字符缓冲输入流-读取数据

  • 介绍

    • BufferedWriter:将文本写入字符输出流,缓冲字符,以提供单个字符,数组和字符串的高效写入,可 以指定缓冲区大小,或者可以接受默认大小。默认值足够大,可用于大多数用途
    • BufferedReader:从字符输入流读取文本,缓冲字符,以提供字符,数组和行的高效读取,可以指定缓 冲区大小,或者可以使用默认大小。 默认值足够大,可用于大多数用途
  • 构造方法

13-缓冲流-特有方法

BufferedWriter:

BufferedReader:

15-IO流-小结

8-5 转换流&对象操作流

16-转换流-概念

  • InputStreamReader:是从字节流到字符流的桥梁,父类是Reader 它读取字节,并使用指定的编码将其解码为字符 它使用的字符集可以由名称指定,也可以被明确指定,或者可以接受平台的默认字符集
  • OutputStreamWriter:是从字符流到字节流的桥梁,父类是Writer 是从字符流到字节流的桥梁,使用指定的编码将写入的字符编码为字节 它使用的字符集可以由名称指定,也可以被明确指定,或者可以接受平台的默认字符集

17-转换流-指定编码读写

18-对象操作流-基本特点

  • 对象序列化介绍
    • 对象序列化:就是将对象保存到磁盘中,或者在网络中传输对象
    • 这种机制就是使用一个字节序列表示一个对象,该字节序列包含:对象的类型、对象的数据和对象中存储的属性等信息
    • 字节序列写到文件之后,相当于文件中持久保存了一个对象的信息
    • 反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化

19-对象操作流-序列化

  • 对象序列化流: ObjectOutputStream

    • 将Java对象的原始数据类型和图形写入OutputStream。 可以使用ObjectInputStream读取(重构)对 象。 可以通过使用流的文件来实现对象的持久存储。 如果流是网络套接字流,则可以在另一个主机上或 另一个进程中重构对象
  • 构造方法

  • 序列化对象的方法

  • 注意事项
    • 一个对象要想被序列化,该对象所属的类必须必须实现Serializable 接口
    • Serializable是一个标记接口,实现该接口,不需要重写任何方法

20-对象操作流-反序列化

  • 对象反序列化流: ObjectInputStream

    • ObjectInputStream反序列化先前使用ObjectOutputStream编写的原始数据和对象
  • 构造方法

  • 反序列化对象的方法

8-6 Properties

24-Properties-概述

  • 是一个Map体系的集合类
  • Properties可以保存到流中或从流中加载
  • 属性列表中的每个键及其对应的值都是一个字符串

26-Properties-特有方法

09 多线程

9-1 多线程

01-多线程概述-初步了解多线程

是指从软件或者硬件上实现多个线程并发执行的技术。 具有多线程能力的计算机因有硬件支持而能够在同一时间执 行多个线程,提升性能。

02-多线程概述-并发和并行

  • 并行:在同一时刻,有多个指令在多个CPU上同时执行
  • 并发:在同一时刻,有多个指令在单个CPU上交替执行。

03-多线程概述-进程和线程

  • 进程:是正在运行的程序
    • 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
    • 动态性:进程的实质 是程序的一次执行过程,进程是动态产生,动态消亡的
    • 并发性:任何进程都可以同其他进程一起并发执行
  • 线程:是进程中的单个顺序控制流,是一条执行路径
    • 单线程:一个进程如果只有一条执行路径,则称为单线程程序
    • 多线程:一个进程如果有多条执行路径,则称为多线程程序

04-多线程的实现方式-继承Thread

  • 实现步骤

    • 定义一个类MyThread继承Thread类
    • 在MyThread类中重写run()方法
    • 创建MyThread类的对象
    • 启动线程
  public class MyRunnable implements Runnable {
      @Override
      public void run() {
          for(int i=0; i<100; i++) {
              System.out.println(Thread.currentThread().getName()+":"+i);
          }
      }
  }

  public class MyRunnableDemo {
      public static void main(String[] args) {
          MyRunnable my = new MyRunnable();
          //创建Thread类的对象,把MyRunnable对象作为构造方法的参数
          Thread t1 = new Thread(my,"坦克");
          Thread t2 = new Thread(my,"飞机");
          t1.start(); //启动线程
          t2.start();
      }
  }

05-多线程的实现方式-两个小问题

  • 为什么要重写run()方法?
    • 因为run()是用来封装被线程执行的代码
  • run()方法和start()方法的区别?
    • run():封装线程执行的代码,直接调用,相当于普通方法的调用
    • start():启动线程;然后由JVM调用此线程的run()方法

06-多线的实现方式-实现Runnable接口

  • Thread构造方法

  • 实现步骤
    • 定义一个类MyRunnable实现Runnable接口
    • 在MyRunnable类中重写run()方法
    • 创建MyRunnable类的对象
    • 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
    • 启动线程

07-多线程的实现方式-实现callable接口

  • 方法介绍

  • 实现步骤
    • 定义一个类MyCallable实现Callable接口
    • 在MyCallable类中重写call()方法
    • 创建MyCallable类的对象
    • 创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数
    • 创建Thread类的对象,把FutureTask对象作为构造方法的参数
    • 启动线程
    • 再调用get方法,就可以获取线程结束之后的结果。
  public class MyCallable implements Callable<String> {
      @Override
      public String call() throws Exception {
          for (int i = 0; i < 100; i++) {
              System.out.println( + i);
          }
          return "ok";
      }
  }

  public class Demo {
      public static void main(String[] args) throws ExecutionException, InterruptedException {
          //线程开启之后需要执行里面的call方法
          MyCallable mc = new MyCallable();
          //Thread t1 = new Thread(mc);
          //可以获取线程执行完毕之后的结果.也可以作为参数传递给Thread对象
          FutureTask<String> ft = new FutureTask<>(mc);
          //创建线程对象
          Thread t1 = new Thread(ft);
          // String s = ft.get();
          //开启线程
          t1.start();
          String s = ft.get();
          System.out.println(s);
      }
  }

08-三种实现方式的对比

  • 三种实现方式的对比
    • 实现Runnable、Callable接口
      • 好处: 扩展性强,实现该接口的同时还可以继承其他的类
      • 缺点: 编程相对复杂,不能直接使用Thread类中的方法
    • 继承Thread类
      • 好处: 编程比较简单,可以直接使用Thread类中的方法
      • 缺点: 可以扩展性较差,不能再继承其他的类

09-Thread方法-设置获取名字 + 获得线程对象

11-Thread方法-sleep

12-Thread方法-线程的优先级

  • 线程调度
    • 两种调度方式
      • 分时调度模型:所有线程轮流使用
      • CPU 的使用权,平均分配每个线程占用 CPU 的时间片
    • 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一 个,优先级高的线程获取的 CPU 时间片相对多一些
    • Java使用的是抢占式调度模型
    • 随机性
      • 假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也 就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一 定的
  • 优先级相关方法

{
      //优先级: 1 - 10 默认值:5
      MyCallable mc = new MyCallable();
      FutureTask<String> ft = new FutureTask<>(mc);
      Thread t1 = new Thread(ft);
      t1.setName("飞机");
      t1.setPriority(10);
      //System.out.println(t1.getPriority());//5
      t1.start();

      MyCallable mc2 = new MyCallable();
      FutureTask<String> ft2 = new FutureTask<>(mc2);

      Thread t2 = new Thread(ft2);
      t2.setName("坦克");
      t2.setPriority(1);
      //System.out.println(t2.getPriority());//5
      t2.start();
  }

13-Thread方法-守护线程

 //当普通线程执行完之后,那么守护线程也没有继续运行下去的必要了. 
 t2.setDaemon(true); 

9-2 线程安全问题

15-线程安全问题-原因分析

  • 安全问题出现的条件
    • 是多线程环境
    • 有共享数据
    • 有多条语句操作共享数据

16-线程安全问题-同步代码块

  • 如何解决多线程安全问题呢
    • 基本思想:让程序没有安全问题的环境
  • 怎么实现呢
    • 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
    • Java提供了同步代码块的方式来解决
  • 同步代码块格式:
 synchronized(任意对象) { 
     多条语句操作共享数据的代码 
 } 

synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁

  • 同步的好处和弊端
    • 好处:解决了多线程的数据安全问题
    • 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的 运行效率

17-线程安全问题-锁对象唯一

 private Object obj = new Object(); 
synchronized (obj) {}

18-线程安全问题-同步方法

  • 同步方法的格式
    • 同步方法:就是把synchronized关键字加到方法上
 修饰符 synchronized 返回值类型 方法名(方法参数) {  方法体; } 
  • 同步方法的锁对象是: this
  • 静态同步方法
    • 同步静态方法:就是把synchronized关键字加到静态方法上
 修饰符 static synchronized 返回值类型 方法名(方法参数) { 方法体; } 
  • 同步静态方法的锁对象是: 类名.class

19-线程安全问题-lock

为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化

  • ReentrantLock构造方法

  • 加锁解锁方法

private ReentrantLock lock = new ReentrantLock(); 
lock.lock(); 
lock.unlock(); 

20-死锁

  • 线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
  • 什么情况下会产生死锁
    1. 资源有限
    2. 同步嵌套

9-3 生产者和消费者

所谓生产者消费者问题,实际上主要是包含了两类线程: 一类是生产者线程用于生产数据 一类是消费者线程用于消费数据 为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库 生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为 消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为

  • Object类的等待和唤醒方法

21-生产者和消费者思路分析

  • 桌子类(Desk):定义表示包子数量的变量,定义锁对象变量,定义标记桌子上有无包子的变量
  • 生产者类(Cooker):实现Runnable接口,重写run()方法,设置线程任务
    • 1.判断是否有包子,决定当前线程是否执行
    • 2.如果有包子,就进入等待状态,如果没有包子,继续执行,生产包子
    • 3.生产包子之后,更新桌子上包子状态,唤醒消费者消费包子
  • 消费者类(Foodie):实现Runnable接口,重写run()方法,设置线程任务
    • 1.判断是否有包子,决定当前线程是否执行
    • 2.如果没有包子,就进入等待状态,如果有包子,就消费包子
    • 3.消费包子后,更新桌子上包子状态,唤醒生产者生产包子
  • 测试类(Demo):里面有main方法,main方法中的代码步骤如下
    • 创建生产者线程和消费者线程对象
    • 分别开启两个线程

24-阻塞队列-基本使用

  • 阻塞队列继承结构

  • 常见BlockingQueue:
    • ArrayBlockingQueue: 底层是数组,有界
    • LinkedBlockingQueue: 底层是链表,无界.但不是真正的无界,最大为int的最大值
  • BlockingQueue的核心方法:
    • put(anObject): 将参数放入队列,如果放不进去会阻塞
    • take(): 取出第一个数据,取不到会阻塞
  public static void main(String[] args) throws Exception {
      // 创建阻塞队列的对象,容量为 1
      ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(1);
      // 存储元素
      arrayBlockingQueue.put("汉堡包");
      // 取元素
      System.out.println(arrayBlockingQueue.take());
      System.out.println(arrayBlockingQueue.take()); // 取不到会阻塞
      System.out.println("程序结束了");
  }

25-阻塞队列-实现等待唤醒机制

  • 生产者类(Cooker):实现Runnable接口,重写run()方法,设置线程任务
    • 1.构造方法中接收一个阻塞队列对象
    • 2.在run方法中循环向阻塞队列中添加包子
    • 3.打印添加结果
  • 消费者类(Foodie):实现Runnable接口,重写run()方法,设置线程任务
    • 1.构造方法中接收一个阻塞队列对象
    • 2.在run方法中循环获取阻塞队列中的包子
    • 3.打印获取结果
  • 测试类(Demo):里面有main方法,main方法中的代码步骤如下
    • 创建阻塞队列对象
    • 创建生产者线程和消费者线程对象,构造方法中传入阻塞队列对象
    • 分别开启两个线程

9-4 线程池&volatile

01-线程状态

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期 有不同的状态 。

状态被定义在了java.lang.Thread.State枚举类中,State枚举类的源码如下:

public class Thread {
    public enum State {
        /* 新建 */
        NEW , 
        /* 可运行状态 */
        RUNNABLE , 
        /* 阻塞状态 */
        BLOCKED , 
        /* 无限等待状态 */
        WAITING , 
        /* 计时等待 */
        TIMED_WAITING , 
        /* 终止 */
        TERMINATED;
        }
    // 获取当前线程的状态
    public State getState() {
        return jdk.internal.misc.VM.toThreadState(threadStatus);
    }
}

各个状态的转换,如下图所示:

02-线程池-基本原理

线程池在启动的时,会创建大量空闲线程,当我们向线程池提交任务的时,线程池就 会启动一个线程来执行该任务。等待任务执行完毕以后,线程并不会死亡,而是再次返回到线程池中 称为空闲状态。等待下一次任务的执行。

03-线程池-Executors默认线程池

概述 : JDK对线程池也进行了相关的实现,在真实企业开发中我们也很少去自定义线程池,而是使用JDK中自带的线 程池。

  • 我们可以使用Executors中所提供的静态方法来创建线程池
    • static ExecutorService newCachedThreadPool() 创建一个默认的线程池
    • static newFixedThreadPool(int nThreads) 创建一个指定最多线程数量的线程池
  • 代码实现
//static ExecutorService newCachedThreadPool()   创建一个默认的线程池
//static newFixedThreadPool(int nThreads)            创建一个指定最多线程数量的线程池

public class MyThreadPoolDemo {
    public static void main(String[] args) throws InterruptedException {
        //1,创建一个默认的线程池对象.池子中默认是空的.默认最多可以容纳int类型的最大值.
        ExecutorService executorService = Executors.newCachedThreadPool();
        //Executors --- 可以帮助我们创建线程池对象
        //ExecutorService --- 可以帮助我们控制线程池
        executorService.submit(()->{
            System.out.println(Thread.currentThread().getName() + "在执行了");
        });
        //Thread.sleep(2000);
        executorService.submit(()->{
            System.out.println(Thread.currentThread().getName() + "在执行了");
        });
        executorService.shutdown();
    }
}

04-线程池-Executors创建指定上限的线程池

  • 使用Executors中所提供的静态方法来创建线程池
    • static ExecutorService newFixedThreadPool(int nThreads) : 创建一个指定最多线程数量的线程池
  • 代码实现
//static ExecutorService newFixedThreadPool(int nThreads)
//创建一个指定最多线程数量的线程池

public class MyThreadPoolDemo2 {
    public static void main(String[] args) {
        //参数不是初始值而是最大值
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        ThreadPoolExecutor pool = (ThreadPoolExecutor) executorService;
        System.out.println(pool.getPoolSize());//0

        executorService.submit(()->{
            System.out.println(Thread.currentThread().getName() + "在执行了");
        });
        executorService.submit(()->{
            System.out.println(Thread.currentThread().getName() + "在执行了");
        });

        System.out.println(pool.getPoolSize());//2
//        executorService.shutdown();
    }

}

05-线程池-ThreadPoolExecutor

  • 创建线程池对象
    • ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(核心线程数量,最大线程数量,空闲线程最 大存活时间,任务队列,创建线程工厂,任务的拒绝策略);
public class MyThreadPoolDemo3 {

//    参数一:核心线程数量
//    参数二:最大线程数
//    参数三:空闲线程最大存活时间
//    参数四:时间单位
//    参数五:任务队列
//    参数六:创建线程工厂
//    参数七:任务的拒绝策略
    public static void main(String[] args) {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(2,5,2,TimeUnit.SECONDS,new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());
        pool.shutdown();
    }
}

06-线程池-参数详解

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

corePoolSize:   核心线程的最大值,不能小于0

maximumPoolSize:最大线程数,不能小于等于0,maximumPoolSize >= corePoolSize

keepAliveTime:  空闲线程最大存活时间,不能小于0

unit:           时间单位

workQueue:      任务队列,不能为null

threadFactory:  创建线程工厂,不能为null      

handler:        任务的拒绝策略,不能为null  

07-线程池-非默认任务拒绝策略

RejectedExecutionHandler是jdk提供的一个任务拒绝策略接口,它下面存在4个子类。

ThreadPoolExecutor.AbortPolicy:         丢弃任务并抛出RejectedExecutionException异常。是默认的策略。

ThreadPoolExecutor.DiscardPolicy:      丢弃任务,但是不抛出异常 这是不推荐的做法。

ThreadPoolExecutor.DiscardOldestPolicy:抛弃队列中等待最久的任务 然后把当前任务加入队列中。

ThreadPoolExecutor.CallerRunsPolicy:    调用任务的run()方法绕过线程池直接执行。

注:明确线程池对多可执行的任务数 = 队列容量 + 最大线程数

9-5 原子性

09-volatile

当A线程修改了共享数据时,B线程没有及时获取到最新的值,如果还在使用原先的值,就会出现问题

1,堆内存是唯一的,每一个线程都有自己的线程栈。

2 ,每一个线程在使用堆里面变量的时候,都会先拷贝一份到变量的副本中。

3 ,在线程中,每一次使用是从变量的副本中获取的。

Volatile关键字 : 强制线程每次在使用的时候,都会看一下共享区域最新的值 java

 public class Money { public static volatile int money = 100000; } 
while(Money.money == 100000){ }

10-synchronized

synchronized解决 :

  1. 线程获得锁
  2. 清空变量副本
  3. 拷贝共享变量最新的值到变量副本中
  4. 执行代码
  5. 将修改后变量副本中的值赋值给共享数据
  6. 释放锁

11-原子性

概述 : 所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的 干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体

12-volatile关键字不能保证原子性

volatile只有写操作是原子性的,也就是数据操作完成后会立刻刷新到主内存中。但是被volatile修饰的变量在读的时候可能会被多个线程读也就是说int i = 1;i++;

A线程读 i = 1同时B线程也读了i = 1,然后自增完成刷新入主内存。i的值是2。

所以如果该变量是volatile修饰的,那可以完全保证此时取到的是最新信息。但在入栈和自增计算执行过程中,该变量有可能正在被其他线程修改,最后计算出来的结果照样存在问题,因此volatile并不能保证非原子操作的原子性,仅在单次读或者单次写这样的原子操作中,volatile能够实现线程安全。

解决方案 : 给count++操作添加锁,那么count++操作就是临界区中的代码,临界区中的代码一次只能被 一个线程去执行,所以count++就变成了原子操作。

synchronized (lock) {
    count++;
    System.out.println("已经送了" + count + "个冰淇淋");
}

13-原子性_AtomicInteger

概述:java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种 用法简单,性能高效,线程安全地更新一个变量的方式。因为变量的类型有很多种,所以在Atomic包里一共提供了13个类,属于4种类型的原子更新方式,分别是原子更新基本类 型、原子更新数组、原子更新引用和原子更新属性(字段)

使用原子的方式更新基本类型Atomic包提供了以下3个类

  • AtomicBoolean: 原子更新布尔类型
  • AtomicInteger: 原子更新整型
  • AtomicLong: 原子更新长整型
public AtomicInteger():                  初始化一个默认值为0的原子型Integer
public AtomicInteger(int initialValue):  初始化一个指定值的原子型Integer


int get():                               获取值
int getAndIncrement():                   以原子方式将当前值加1,注意,这里返回的是自增前的值。
int incrementAndGet():                   以原子方式将当前值加1,注意,这里返回的是自增后的值。
int addAndGet(int data):                 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
int getAndSet(int value):                以原子方式设置为newValue的值,并返回旧值。

14-AtomicInteger-内存解析

AtomicInteger原理 : 自旋锁 + CAS

算法 CAS算法:

有3个操作数(内存值V, 旧的预期值A,要修改的值B)

当旧的预期值A == 内存值 此时修改成功,将V改为B

当旧的预期值A!=内存值 此时修改失败,不做任何操作

并重新获取现在的最新值(这个重新获取的动作就是自旋)

16-悲观锁和乐观锁

  • synchronized和CAS的区别 :
    • 相同点:在多线程情况下,都可以保证共享数据的安全性。
    • 不同点:
      • synchronized总是从最坏的角度出发,认为每次获取数据的时候,别人都有可能修改。所以在每次操作 共享数据之前,都会上锁。(悲观锁)
      • cas是从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁。只不过在修改共享数据的时候,会检查一下,别人有没有修改过这个数据。
        • 如果别人修改过,那么我再次获取现在最新的值。
        • 如果别人没有修改过,那么我现在直接修改共享数据的值.(乐观锁)

9-5 并发工具类

17-并发工具类-Hashtable

Hashtable出现的原因 : 在集合类中HashMap是比较常用的集合对象,但是HashMap是线程不安全的(多线程环 境下可能会存在问题)。为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下。

Hashtable<String, String> hm = new Hashtable<>();

18-并发工具类-ConcurrentHashMap基本使用

  • ConcurrentHashMap出现的原因 : 在集合类中HashMap是比较常用的集合对象,但是HashMap是线程不安全 的(多线程环境下可能会存在问题)。
  • 为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下。
  • 基于以上两个原因我们可以使用JDK1.5以后所提供的ConcurrentHashMap。

体系结构

总结

  1. HashMap是线程不安全的。多线程环境下会有数据安全问题
  2. Hashtable是线程安全的,但是会将整张表锁起来,效率低下
  3. ConcurrentHashMap也是线程安全的,效率较高。 在JDK7和JDK8中,底层原理不一样。

19-并发工具类-ConcurrentHashMap1.7原理

20-并发工具类-ConcurrentHashMap1.8原理

总结 :

1,如果使用空参构造创建ConcurrentHashMap对象,则什么事情都不做。 在第一次添加元素的时候创建哈希表

2,计算当前元素应存入的索引。

3,如果该索引位置为null,则利用cas算法,将本结点添加到数组中。

4,如果该索引位置不为null,则利用volatile关键字获得当前位置最新的结点地址,挂在他下面,变成链表。

5,当链表的长度大于等于8时,自动转换成红黑树6,以链表或者红黑树头结点为锁对象,配合悲观锁保证多线程 操作集合时数据的安全性

21-并发工具类-CountDownLatch

CountDownLatch类

使用场景: 让某一条线程等待其他线程执行完毕之后再执行

总结 :

  1. CountDownLatch(int count):参数写等待线程的数量。并定义了一个计数器。
  2. await():让线程等待,当计数器为0时,会唤醒等待的线程
  3. countDown(): 线程执行完毕时调用,会将计数器-1。

22-并发工具类-Semaphore

使用场景 : 可以控制访问特定资源的线程数量。

public class MyRunnable implements Runnable {
    //1.获得管理员对象,
    private Semaphore semaphore = new Semaphore(2);
    @Override
    public void run() {
        //2.获得通行证
        try {
            semaphore.acquire();
            //3.开始行驶
            System.out.println("获得了通行证开始行驶");
            Thread.sleep(2000);
            System.out.println("归还通行证");
            //4.归还通行证
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class MySemaphoreDemo {
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();
        for (int i = 0; i < 100; i++) {
            new Thread(mr).start();
        }
    }
}