线程安全(重难点)
1.1概念
线程安全就是说在多线程各种随机的调度顺序下,代码不存在bug,能够符合预期的方式进行执行。
1.2 线程不安全原因
修改共享数据;
多线程针对一个变量进行修改。
原子性;
一条Java语句不一定是原子的,也不一定只是一条指令。不保证原子性,线程可能发生抢占式执行,打断一个线程对变量的操作,可能导致结果错误。
可见性;
由于线程对变量的修改存在不能同时同步的情况,当两个线程对同一个变量进行操作时就会出现一定的问题。
代码顺序性;
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,指令重排序后可能会导致线程不安全问题。
1.3 解决方案
1.3.1 synchronized 加锁解决原子性问题
synchronized会起到互斥作用,当线程执行到某个对象的synchronized中,有其他线程如果也执行到同一个对象,synchronized就会阻塞等待。
synchronized在JVM中也叫“监视器锁”。
synchronized写法:
修饰普通方法,锁对象相当于this;
修饰代码块,锁对象在()中指定;
修饰静态方法,锁对象相当于类对象(不是锁整个类)。
synchronized的可重入性:
针对一个线程连续针对一把锁加锁两次,第二次加锁被占用,阻塞等待,等地一把锁被解锁,第二把所才能加锁成功,形成死锁问题,这样的锁称为不可重入锁。
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。
synchronized引入一个计数器,每次加锁计数器++,每次解锁计数器--,如果计数器为0,加锁操作才真加锁,同样计数器为0,解锁操作才真解锁。
无论是正常执行代码块还是异常执行代码块,都会触发解锁操作,不用担心发生死锁。
1.3.2 volatile关键字保证内存可见性
volatile强制读取内存,减慢速度但是保证数据准确性;
volatile修饰变量,不能保证内存原子性;
volatile与synchronized同时使用保证线程安全;
JMM:Java Memory Model(Java内存模型):工作内存和主内存(CPU寄存器和内存);
编译器优化可能会导致每次真的读取主内存,而是直接读取工作内存中的缓存,导致内存可见性问题,volatile禁止编译器优化,解决此问题,保证每次读取内存都是从主内存重新读取;
volatile还可以禁止指令重排序。
1.3.3 wait和notify解决抢占执行问题
wait和notify
wait是Object的方法,可以使用任意的类的实例,都能调用wait方法;
线程执行到wait就会发生阻塞,直到另一个线程调用notify才能将其唤醒;
释放当前锁->进行等待通知->满足条件时(调用notify)被唤醒,尝试重新获取锁;
写代码时需要保证加锁的对象和调用wait的对象是同一个对象,调用wait的对象和调用notify的对象也是同一个对象;
没有线程wait,notify不会有副作用。
notifyAll
多个线程都在wait,notify只唤醒一个,notifyAll全部唤醒。
wait、notify、notifyAll都是Object方法类。
wait 要搭配 synchronized 来使用,脱离 synchronized 使用 wait 会直接抛出异常。
1.3.4 wait和sleep对比
都是让线程进入阻塞等待状态;
sleep通过时间控制何时唤醒;
wait是由其他线程通过notify唤醒;
wait 需要搭配 synchronized 使用. sleep 不需要;
wait 是 Object 的方法 sleep 是 Thread 的静态方法。
单例模式
2.1 两种写法
饿汉模式:类加载阶段创建实例;
懒汉模式:需要用时创建实例。(效率高)
2.2 线程安全的单例模式
饿汉模式不存在线程安全问题;
懒汉模式需要判断实例是否为空:
加锁;
双重if(该加锁时再加,避免花费不必要的成本);
使用volatile,避免指令重排序,保证指令是有效的,避免得到不完全的对象,只有内存没有数据。
阻塞队列
3.1 非”先进先出“队列
优先级队列(PriorityQueue)
消息队列:出队列时会指定某个类型的元素先出
3.2 阻塞队列基本特点
线程安全;
带有阻塞功能:
如果队列满,继续入队列,入队列操作就会阻塞,直到队列不满,入队列才能完成;
如果队列空,继续出队列,出队列操作也会阻塞,直到队列不空,出队列才能完成。
3.3 阻塞队列应用场景
生产者消费模型:描述的是多线程协同工作的一种方式。
使用阻塞队列,有利于代码解耦合;
削峰填谷:当流量剧增时,没有承受压力的服务器按照原来的节奏消费数据,不受影响。
3.4 使用方法
标准库的阻塞队列(没有提供"带有阻塞"取队首元素功能);
自己实现阻塞队列:
先实现一个普通队列;(基于链表/数组)
加上线程安全;
加上阻塞的实现。
定时器
4.1 定义
在服务器开发中,客户端请求服务器需要等待服务器的响应,在这里,为了避免等待时间过长,就会设置一个定时器来设置一个时间,达到一个设定时间后就会执行某个指定好的代码。
4.2 使用方法
标准库中的定时器(可以安排多个任务);
自己实现定时器:
描述任务:任务信息、执行时间;
如果让定时器管理多个任务,一个timer可以安排多个任务;
任务安排到优先级阻塞队列中,从队列中取元素,创建一个单独扫描线程,让这个线程不停检查队首元素,如果时间到了则执行该任务
线程池
5.1使用方法
标准库中的线程池;
此处创建线程池,没有显示的new,而是通过另外Executors类的静态方法 newCachedThreadPool来完成,此方法叫做工厂方法;构造方法存在一定的局限性,为了避免局限引入工厂模式。
核心操作为submit将任务加入到线程池中。
自己实现线程池:
创建阻塞队列;
将任务放入阻塞队列;
创建N个线程取队列元素;
线程空,队列自然阻塞等待;线程非空,线程取任务执行任务直到任务执行完毕队列阻塞等待。