#博学谷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类中的方法
- 缺点: 可以扩展性较差,不能再继承其他的类
- 实现Runnable、Callable接口
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-死锁
- 线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
- 什么情况下会产生死锁
- 资源有限
- 同步嵌套
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解决 :
- 线程获得锁
- 清空变量副本
- 拷贝共享变量最新的值到变量副本中
- 执行代码
- 将修改后变量副本中的值赋值给共享数据
- 释放锁
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。
体系结构
总结
- HashMap是线程不安全的。多线程环境下会有数据安全问题
- Hashtable是线程安全的,但是会将整张表锁起来,效率低下
- ConcurrentHashMap也是线程安全的,效率较高。 在JDK7和JDK8中,底层原理不一样。
19-并发工具类-ConcurrentHashMap1.7原理
20-并发工具类-ConcurrentHashMap1.8原理
总结 :
1,如果使用空参构造创建ConcurrentHashMap对象,则什么事情都不做。 在第一次添加元素的时候创建哈希表
2,计算当前元素应存入的索引。
3,如果该索引位置为null,则利用cas算法,将本结点添加到数组中。
4,如果该索引位置不为null,则利用volatile关键字获得当前位置最新的结点地址,挂在他下面,变成链表。
5,当链表的长度大于等于8时,自动转换成红黑树6,以链表或者红黑树头结点为锁对象,配合悲观锁保证多线程 操作集合时数据的安全性
21-并发工具类-CountDownLatch
CountDownLatch类
使用场景: 让某一条线程等待其他线程执行完毕之后再执行
总结 :
- CountDownLatch(int count):参数写等待线程的数量。并定义了一个计数器。
- await():让线程等待,当计数器为0时,会唤醒等待的线程
- 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();
}
}
}