数据库
数据库三范式
- 第一范式:表中不要重复意义的列,每列的值应当有原子性,不可再拆分。
- 第二范式 : 数据库表中的每一列都要和主键相关,而不能只与主键的某一部分相关(主要针对联合主键而言)。也就是说在一个数据库表中,一个表中只能保存一种数据,不可以把多种数据保存在同一张数据库表中。
- 第三范式:数据库中的非键列必须相互之前没有关系,完全独立。如果改变一列中的值需要改变另外一列的值,那就是违反了第三范式。
Mysql
DECIMAL
精度高于FLOAT
和DOUBLE
FULLTEXT
查找通常比LIKE
查询要好一些。TEXT类型字段可以利用缓存,而LIKE不行。- 如果创建的主键没有任何其他意义和目的,就称其为
代理主键
。
Java基础
多态
多态主要是通过继承来实现的。参数不同,编译时多态,称为重载。子类覆盖父类方法,称为覆写。
ClassLoader类加载器
双亲委派机制
在
java.lang.ClassLoader
的loadClass()
方法中,先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()
方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载失败,则抛出ClassNotFoundException
异常后,再调用自己的findClass()
方法进行加载。
Java内存模型
Heap堆
对象,数组,及对象的实例变量。
堆的优点是动态分配内存大小,生存期也不必事先告诉编译器。缺点是速度较慢。
Stack栈
在函数中定义的基本类型变量和对象的引用变量都在函数的栈内存中分配,方法,局部变量等。
栈的优点是存取速度比堆快,仅次于CPU中的寄存器,另外栈数据可以共享。缺点是存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
Java正则
1 | String text = "gogo"; |
Java IO
字节流
- 字节输入流
InputStream
FileInputStream
/ObjectInputStream
/ByteArrayInputStream
/BufferedInputStream
/StringBufferStream
- 字节输出流
OutputStream
FileOutputStream
/ObjectOutputStream
/ByteArrayOutputStream
/BufferedOutputStream
1 | Writer w = new BufferedWriter(new FileWriter(filePath, false)); |
字符流
- 字符输入流
Reader
StringReader
/BufferedRead
/FileReader
- 字符输出流
Writer
StringWriter
/BufferedWriter
/FileWriter
Java异常处理
Throwable
Error
Exception
RuntimeException
IOException
等
Java集合
- 接口:
Collection
(包括List
,Set
)/Map
- 类:
ArrayList
,HashSet
,LinkedList
,HashMap
,LinkedHashMap
,WeakHashMap
,ConcurrentHashMap
ArrayList
,数组,允许Null
值LinkedList
,有序,双向链表,允许Null
值HashSet
,哈希,链表,允许Null
值HashMap
,哈希,数组,链表,允许Null
键和Null
值LinkedHashMap
,有序,双向链表,允许Null
键和Null
值,可实现LRUcache
,根据最后使用来排序WeakHashMap
ConcurrentHashMap
,同步,最多支持16并发写
进阶复习
计算机基础
OSI七层模型
开放式系统互联通信参考模型(Open System Interconnection Reference Model)
主要关注传输层和应用层,主要包括HTTP和TCP协议。
应用层 (
Application Layer
)为应用程序提供服务
提供为应用软件而设的界面,以设置与另一应用软件之间的通信。如
HTTP
,HTTPS
,FTP
,TELNET
,SSH
,SMTP
,POP3
等。表示层 (
Presentation Layer
)数据格式转化,数据加密
把数据转换为能与接收者的系统格式兼容并适合传输的格式。
会话层 (
Session Layer
)建立/管理和维护会话
负责在数据传输中设置和维护电脑网络中两台电脑之间的通信连接。
传输层 (
Transport Layer
)建立/管理和维护端到端的连接
把传输表头 ( TH ) 加至数据以形成数据包。传输表对包含了所使用的协议等发送信息。如TCP协议等。
网络层 (
Network Layer
)IP选址及路由选择
决定数据的路径选择和转寄,将网络表头 ( NH ) 加至数据包,以形成分组。网络表头包含了网络数据。如IP协议等。
数据链路层 (
Data Link Layer
)提供介质访问和链路管理
负责网络寻址/错误侦测和改错。当表头和表尾被回至数据包时,会形成帧。数据链表头 ( DLH ) 是包含了物理地址和错误侦测及改错的方法。数据链表尾(DLT)是一串指示数据包末端的字符串。例如以太网、无线局域网(Wi-Fi)和通用分组无线服务(GPRS)等。
物理层 (
Physical Layer
)物理层
在局部局域网上传送数据框 ( frame ) ,它负责管理电脑通信设备和网络媒体之间的互通。包括了针脚/电压/线缆规范/集线器/中继器/网卡/主机适配器等。
传输层的作用
为它上面的应用层提供通信服务。
在OSI七层参考模型中,传输层是面向通信的最高层,也是用户功能的最底层。
复用
在发送端,多个应用进程公用一个传输层。
分用
在接收端,传输层根据端口号将数据分派给不同的应用进程
和网络层的区别:
- 网络层为不同主机提供通信服务,而传输层为不同主机的不同应用提供通信服务。
- 网络层只对报文头部进行差错检测,而传输层对整个报文进行差错检测。
传输层复用和分用的含义
传输层复用和分用的含义
传输层和网络层的区别
UDP协议的特点
UDP协议的报文结构
TCP协议的特点
TCP协议的报文结构
TCP三次握手过程
TCP四次挥手过程
TCP可靠传输是如何实现的
停止等待协议
滑动窗口协议
TCP的流量控制
TCP拥塞控制
HTTP协议
HTTP协议(HyperText Transfer Protocol,超文本传输协议)是用于从WWW服务器传输超文本到本地浏览器的传送协议。它可以使浏览器更加高效,使网络传输减少。它不仅保证计算机正确快速地传输超文本文档,还确定传输文档中的哪一部分,以及哪部分内容首先显示(如文本先于图形)等。
HTTP是基于TCP协议之上的。在TCP/IP协议参考模型的各层对应的协议如下图,其中HTTP是应用层的协议。
- HTTP请求响应模型
HTTP由请求和响应构成,是一个标准的客户端服务器模型(B/S)。HTTP协议永远都是客户端发起请求,服务器回送响应。
HTTP是一个无状态的协议。无状态是指客户机(Web浏览器)和服务器之间不需要建立持久的连接,这意味着当一个客户端向服务器端发出请求,然后服务器返回响应(response),连接就被关闭了,在服务器端不保留连接的有关信息.HTTP遵循请求(Request)/应答(Response)模型。客户机(浏览器)向服务器发送请求,服务器处理请求并返回适当的应答。所有HTTP连接都被构造成一套请求和应答。
HTTP工作流程
地址解析
如请求这个地址:http://localhost.com:8080/index.htm
从中分解中协议名/主机名/端口/对象路径等部分。对于我们的这个地址,解析得到的结果如下:
协议名:http
主机名:localhost.com
端口:8080
对象路径:/index.htm在这一步,需要域名系统DNS解析域名localhost.com,得主机的IP地址。
封装HTTP请求数据包
把以上部分结合本机自己的信息,封装成一个HTTP请求数据包
封装成TCP包,建立TCP连接(TCP的三次握手)
客户机发送请求命令
建立连接后,客户机发送一个请求给服务器,请求方式的格式为:统一资源标识符(URL)、协议版本号,后边是MIME信息包括请求修饰符、客户机信息和可能的内容。
服务器响应
服务器接到请求后,给予相应的响应信息,其格式为一个状态行,包括信息的协议版本号、一个成功或错误的代码,后边是MIME信息包括服务器信息、实体信息和可能的内容。
实体消息是服务器向浏览器发送头信息后,它会发送一个空白行来表示头信息的发送到此为结束,接着,它就以Content-Type应答头信息所描述的格式发送用户所请求的实际数据。
服务器关闭TCP连接
一般情况下,一旦Web服务器向浏览器发送了请求数据,它就要关闭TCP连接,然后如果浏览器或者服务器在其头信息加入了这行代码
Connection:keep-alive
TCP连接在发送后将仍然保持打开状态,于是,浏览器可以继续通过相同的连接发送请求。保持连接节省了为每个请求建立新连接所需的时间,还节约了网络带宽。
HTTP请求格式
HTTP 1.1中的8种请求方式
HTTP响应格式
HTTP中重要的请求头和响应头字段
HTTP常用状态码及其含义
HTTPS协议
- HTTPS协议与HTTP协议的区别
- HTTPS协议的工作流程
数据库
索引
索引分类
B+ 树索引
传统意义上的索引,最常用/最有效的索引。
数据库以页为存储单元,一个页是8K(8192Byte),一页可以存放N条记录。
页在B+树中分为:数据页和索引页。
B+树的高一般为2-4层,因此查找某一键值的行记录只需2-4次IO,效率较高。哈希索引
哈希索引是一种自适应的索引,数据库会根据表的使用情况自动生成哈希索引,我们人为是没办法干预的。
全文索引
用于实现关键词查询
RTree索引
在mysql很少使用,仅支持geometry数据类型;相对于BTREE,RTREE的优势在于范围查找。
事务
事务就是一组具有原子性的操作,这一组操作要么全都正确执行,要么全都不执行。
事务能保证数据库从一种一致性状态转换为另一种一致性状态。
四大特性 ACID
原子性
原子性指事务不可分割,要么全部执行,要么全部不执行。
一致性
事务开始前和结束后, 数据库的完整性约束没有被破坏。
隔离性
事务的执行是相互独立的,不会相互干扰。一个事务不会看到另一个正在运行过程中的事务的数据。
持久性
事务结束后,其结果永久保存。
分类
扁平事务
实际生产环境中最常用/最简单的事务类型,事务从begin work开始,从commit work或rollback work结束。发生错误时回滚到事务的起始位置,无法回滚部分操作。而回滚所有的操作开销太大。
带有保存点的扁平事务
这种事务能在中间设置多个保存点,当发生错误时可以回滚到事务中指定的保存点,而不需要将整个事务回滚。
链事务
嵌套事务
分布式事务
并发可能存在的问题
更新丢失
当有两个并发执行的事务,更新同一行数据,那么有可能一个事务会把另一个事务的更新覆盖掉。
当数据库没有加任何锁操作的情况下会发生。脏读(Dirty Read)
一个事务读到另一个尚未提交的事务中的数据。
该数据可能会被回滚从而失效。
如果第一个事务拿着失效的数据去处理那就发生错误了。不可重复读(NoRepeatable Read)
主要是说多次读取一条记录, 发现该记录中某些列值被修改过
一个事务对同一行数据读了两次,却得到了不同的结果。原因:事务1在两次查询的过程中,事务2对该表进行了更新、删除操作,从而事务1第二次查询的结果发生了变化。
与『脏读』的区别?
脏读读到的是尚未提交的数据,而不可重复读读到的是已经提交的数据,只不过在两次读的过程中数据被另一个事务改过了。幻读(Phantom Read)
主要是说多次读取一个范围内的记录(包括直接查询所有记录结果或者做聚合统计), 发现结果不一致(标准档案一般指记录增多, 记录的减少应该也算是幻读)。
幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
不可重复度和幻读区别:
不可重复读的重点是修改,幻读的重点在于新增或者删除。
例1(同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 ):事务1中的A先生读取自己的工资为 1000的操作还没完成,事务2中的B先生就修改了A的工资为2000,导 致A再读自己的工资时工资变为 2000;这就是不可重复读。
例2(同样的条件, 第1次和第2次读出来的记录数不一样 ):假某工资单表中工资大于3000的有4人,事务1读取了所有工资大于3000的人,共查到4条记录,这时事务2 又插入了一条工资大于3000的记录,事务1再次读取时查到的记录就变为了5条,这样就导致了幻读。
事务的隔离级别
Read uncommitted 读未提交
最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
该级别下,一个事务对一行数据修改的过程中,不允许另一个事务对该数据进行修改,但允许另一个事务对该行数据读。因此本级别下,不会出现更新丢失,但会出现脏读、不可重复读。
Read commited 读提交
允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
在该级别下,未提交的写事务不允许其他事务访问该行,因此不会出现脏读;但是读取数据的事务允许其他事务的访问该行数据,因此会出现不可重复读的情况。
Repeatable read 重复读
对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
Serializable 序列化
最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
该级别要求所有事务都必须串行执行,因此能避免一切因并发引起的问题,但效率很低。
Mysql默认级别为Repeatable read 重复读
√:可能出现 X:不会出现
脏读 | 不可重复读 | 幻读 | |
---|---|---|---|
Read uncommitted | √ | √ | √ |
Read committed | x | √ | √ |
Repeatable read | x | x | √ |
Serializable | x | x | x |
数据库锁
乐观锁
假定不会发生并发冲突,只在提交数据时检查是否违反数据完整性。通过标记位(版本号/时间戳)来判断当前数据是否允许更新。
悲观锁
假定会发生并发冲突,先将数据锁定,操作完成后,再释放锁。
1
> select * from user where id = 1 for update; //悲观锁,mysql默认行级锁,但需要通过主键查询或者通过有索引的列查询,否则锁住整张表
Java并发
并发是为了提升程序的执行速度,但并不是多线程一定比单线程高效。而且并发编程容易出错。若要实现正确且高效的并发。在开发时需要注意三个问题:上下文切换、死锁、资源限制。
上下文切换
当一条线程的时间片用完后,操作系统会暂停该线程,并保存该线程相应的信息,然后再随机一条新线程去执行,这个过程称为上下文切换。
为减少线程的上下文切换:
- 减少线程数量
- 控制同一把锁上的线程数据
- 采用无锁并发编程
- HASH分段->ConcurrentHashMap,最多支持16线程并发写。适用于并发执行的任务没有共享变量。
- CAS算法->如果任务需要共享变量,使用CAS算法,仅在线程内部需要更新共享变量时使用CAS算法来更新。
死锁
当多个线程相互等待已经被对方占用的资源时,就会产生死锁。
- 不要在一条线程中嵌套使用多个锁
- 不要在一条线程中嵌套占用多个计算工机资源
- 给锁和资源加超时时间
资源限制
并发编程中,并不是线程越多越好,有时线程多了反而拉低执行效率。线程多了会导致上下文切换频繁,CPU处理任务时间减少。
共享数据
通信
共享内存 显示同步,隐式通信
共享内存指的是多条线程共享同一片内存,发送者将消息写入内存,接收者从内存中读取消息,从而实现了消息的传递。
但这种方式有个弊端,即需要程序员来控制线程的同步,即线程的执行次序。
消息传递 隐式同步,显式通信
消息传递指的是发送线程直接将消息传递给接收线程。
由于执行次序由并发机制完成,因此不需要程序员添加额外的同步机制,但需要声明消息发送和接收的代码。
同步
同步是指,控制多条线程之间的执行次序。
Java使用共享内存 的方式实现多线程的消息传递。因此,需要额外的代码用于线程之间的同步。
Java提供synchronized
/volatile
关键字实现同步。
volatile
重排序
volatile
会禁用重排序内存可见性
volatile
会在线程修改完共享变量后,立即将值写入主内存,所有线程都将读取到最新的值原子性
在Java中的所有类型中,有long、double类型比较特殊,他们占据8字节(64比特),其余类型都小于64比特。在32位操作系统中,CPU一次只能读取/写入32位的数据,因此对于64位的long、double变量的读写会进行两步。在多线程中,若一条线程只写入了long型变量的前32位,紧接着另一条线程读取了这个只有“一半”的变量,从而就读到了一个错误的数据。
为了避免这种情况,需要在用volatile修饰long、double型变量。在内存可见性与原子性上,volatile就相当于是同步的setter和getter函数。但并不具有volatile的重排序规则,同步块只确保同步块内部的指令不发生重排序,并不确保同步块以外的指令的重排序。
QA:在同步块中调用wait函数是否会破坏原子性?
会,调用wait函数后,会释放锁。
线程( Thread )的状态
6大状态
- New 初始态
- Runnable 运行态
- Blocked 阻塞态
- Waiting 等待态
- Timed_waiting 超时等待态
- Terminated 结束态
New 初始态
Thread.new()
Runnable 运行态
Runnable 就绪态
Thread.start()
、Object.notify()
、Object.notifyAll()
、Thread.yield()
Running 运行态
由系统调度,获取到CPU执行权
Blocked 阻塞态
获取锁失败
Waiting 等待态
Object.wait()
、Thread.join()
Timed_waiting 超时等待态
Object.wait(long)
、Thread.join(long)
Terminated 结束态
Thrad.interrupt()
线程间通信
volatile
、synchronized
等待/通知机制
Object.wait()
/notify()
,注意:必须放在同步块中,只能由所处同步块的锁对象调用。锁对象A.notify()/notifyAll()只能唤醒由锁对象A wait的线程。调用notify/notifyAll函数后仅仅是将线程从等待队列转移到阻塞队列,只有当该线程竞争到锁后,才能从wait方法中返回,继续执行接下来的代码。管道流
管道流用于在两个线程之间进行字节流或字符流的传递。
- 管道流的实现依靠
PipedOutputStream
、PipedInputStream
、PipedWriter
、PipedReader
。分别对应字节流和字符流。 - 他们与IO流的区别是:IO流是在硬盘、内存、Socket之间流动,而管道流仅在内存中的两条线程间流动。
- 管道流的实现依靠
join
Thread.join()
能将并发执行的多条线程串行执行。在ThreadB中调用ThreadA.join(),ThreadB将等待ThreadA执行完成后,再执行。
线程池Executors
Executor框架包括:线程池,Executor,Executors,ExecutorService,CompletionService,Future,Callable等。
线程池一般由两种角色构成:
工作线程
工作线程是一组已经运行中的线程,它们不断地向阻塞队列中领导任务执行。
阻塞队列
阻塞队列用于存储工作线程来不及处理的任务。当工作线程都在执行任务时,到来的新任务就只能暂时在阻塞队列中存储。
Executor
1 | public interface Executor { |
Executors
Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。
public static ExecutorService newFixedThreadPool(int nThreads)
- 创建固定数目线程的线程池
1
2
3
4
5public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}public static ExecutorService newCachedThreadPool()
- 创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有60秒钟未被使用的线程。
1
2
3
4
5public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}public static ExecutorService newSingleThreadExecutor()
- 创建一个单线程化的Executor。
1
2
3
4public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
1
2
3public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
ExecutorService
ExecutorService接口继承自Executor接口,它提供了更丰富的实现多线程的方法,比如,ExecutorService提供了关闭自己的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。
ExecutorService的生命周期包括三种状态:运行、关闭、终止。创建后便进入运行状态,当调用了shutdown()方法时,便进入关闭状态,此时意味着ExecutorService不再接受新的任务,但它还在执行已经提交了的任务,当所有已经提交了的任务执行完后,便到达终止状态。如果不调用shutdown()方法,ExecutorService会一直处在运行状态,不断接收新的任务,执行新的任务,服务器端一般不需要关闭它,保持一直运行即可。
1 | //运行callable |
ThreadPoolExecutor类
1 | public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue) { |
corePoolSize
:线程池中所保存的核心线程数,包括空闲线程。线程池认为这是一个最合理的值,它会尽量使得线程数量维持在这个值上下。maximumPoolSize
:池中允许的最大线程数。keepAliveTime
:线程池中的空闲线程所能持续的最长时间。unit
:持续时间的单位。workQueue
:任务执行前保存任务的队列,仅保存由execute方法提交的Runnable任务。ArrayBlockingQueue
数据实现的阻塞队列,FIFO。
LinkedBlockingQueue
链表实现的阻塞队列,FIFO。
吞吐量通常要高于ArrayBlockingQueue,fixedThreadPool使用的阻塞队列就是它,它是一个无界队列。
SynchronousQueue
没有存储空间的阻塞队列,任务提交之后必须要交给一条工作线程处理,如果没有空闲的工作线程,则立即创建一条新的工作线程。cachedThreadPool用的阻塞队列就是它。
PriorityBlockingQueue
优先权阻塞队列。
threadFactory
:线程池创建新线程所使用的工厂方法handler
:当线程数和任务队列满了的时候,所采取的处理方式AbortPolicy
默认方式,抛出异常DiscardPolicy
丢掉任务DiscardOldestPolicy
丢掉最久的任务,再将该任务加入workQueue队列CallerRunsPolicy
调用者所在线程直接运行该任务
当试图通过excute方法讲一个Runnable任务添加到线程池中时,按照如下顺序来处理:
- 如果线程池中的线程数量小于corePoolSize,即使线程池中有空闲线程,也会创建一个新的线程来执行新添加的任务;
- 如果线程池中的线程数量大于等于corePoolSize,小于maximumPoolSize,但缓冲队列workQueue未满,则不再创建新的线程,并将新任务放到workQueue中,按照FIFO的原则依次等待执行(线程池中有线程空闲出来后依次将缓冲队列中的任务交付给空闲的线程执行);
- 如果线程池中的线程数量大于等于corePoolSize,小于maximumPoolSize,且缓冲队列workQueue已满,则会创建新的线程来处理被添加的任务;
- 如果线程池中的线程数量等于maximumPoolSize,并且缓冲队列workQueue已满,则会通过RejectedExecutionHandler处理runnable任务。
- 另外,当线程池中的线程数量大于corePoolSize时,如果里面有线程的空闲时间超过了keepAliveTime,就将其移除线程池,这样,可以动态地调整线程池中线程的数量。
合理设置线程池的大小
CPU密集型任务
尽量使用较小的线程池,一般为CPU核心数+1。
因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,只能增加上下文切换的次数,因此会带来额外的开销。IO密集型任务
可以使用稍大的线程池,一般为2*CPU核心数。
IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间。混合型
可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。
只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
因为如果划分之后两个任务执行时间相差甚远,那么先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。
CountDownLatch
闭锁。
若有多条线程,其中一条线程需要等到其他所有线程准备完所需的资源后才能运行,可以使用闭锁。
1 | // 初始化闭锁,并设置资源个数 |
CyclicBarrier
同步屏障。
若有多条线程,任一线程到达屏障时会被阻塞,只有当所有线程都到屏障时才能打开屏障,继续执行。
1 | // 创建同步屏障对象,并制定需要等待的线程个数 和 打开屏障时需要执行的任务 |
Semaphore
信号量。
若有m个资源,但有n条线程 ( n > m ),因此同一时刻只能允许m条线程访问资源,此时可使用Semaphore控制访问资源的线程数量。
1 | // 创建信号量对象,并给予3个资源 |
线程安全
如果一个对象构造完成后,调用者无需额外的操作,就可以在多线程环境下随意地使用,并且不发生错误,那么这个对象就是线程安全的。
对『线程安全性』的讨论必须建立在对象内部存在共享变量这一前提,若对象在多条线程间没有共享数据,那这个对象一定是线程安全的!
实现线程安全的方法
互斥同步(阻塞同步)
同步指同一时刻,只有一条线程操作共享变量。
互斥会引起阻塞,当一条线程请求一个已经被另一线程使用的锁时,就会进入阻塞态;而进入阻塞态会涉及上下文切换。因此,使用互斥来实现同步的开销是很大的。
可重入锁:当前线程在已经获得锁的情况下,可以再次获取该锁,因此不会出现当前线程把自己锁死的情况。
实现同步的方式有:
悲观锁
synchronized
可重入锁。
非公平锁,对于被阻塞的线程竞争锁是随机的。
ReetrantLock
可重入锁。
等待可中断。
公平锁,对被阻塞线程按先来后到的顺序给予锁。
可锁定多个条件:synchronized可使用wait/notify来实现等待/通知机制,但一个synchronized同步块只能使用一次,若要使用多次,就需要嵌套同步块;但ReentrantLock可以通过newCondition创建多个条件。
CAS操作 (非阻塞同步)
乐观锁
JUC中各种整形原子类的自增、自减等操作就使用了CAS。
CAS操作过程:CAS操作存在3个值:共享变量V、预期的旧值A、新值B,若V与A相同,则将V更新成B,否则就不更新,继续循环比较,直到更新完成为止。
CAS操作可能引发的问题:ABA问题。
若V一开始的值为A,但在准备赋新值的过程中A变成了B,又变成了A,而CAS操作误认为V没有被改过。无同步
可重入代码
只要输入值一样,结果就一样的代码。
线程封闭
Web服务器采用线程封闭,将涉及共享变量操作的任务放在一个线程中运行。
不可变对象
final修饰变量。
锁优化
自旋锁
当一条线程需要请求一把已经被占用的锁时,并不会进入阻塞状态,而是继续持有CPU执行权等待一段时间,该过程称为『自旋』。
- 优点:由于自旋等待锁的过程线程并不会引起上下文切换,因此比较高效
- 缺点:自旋等待过程线程一直占用CPU执行权但不处理任何任务,因此若该过程过长,那就会造成CPU资源的浪费。
- 自适应自旋:自适应自旋可以根据以往自旋等待时间的经验,计算出一个较为合理的本次自旋等待时间。
锁清除
编译器会清除一些使用了同步,但同步块中没有涉及共享数据的锁,从而减少多余的同步。
锁粗化
若有一系列操作,反复地对同一把锁进行上锁和解锁操作,编译器会扩大这部分代码的同步块的边界,从而只使用一次上锁和解锁操作。
轻量级锁
使用CAS取代互斥同步。
轻量级锁与重量级锁的比较:
- 重量级锁是一种悲观锁,它认为总是有多条线程要竞争锁,所以它每次处理共享数据时,不管当前系统中是否真的有线程在竞争锁,它都会使用互斥同步来保证线程的安全;
- 而轻量级锁是一种乐观锁,它认为锁存在竞争的概率比较小,所以它不使用互斥同步,而是使用CAS操作来获得锁,这样能减少互斥同步所使用的『互斥量』带来的性能开销。
偏向锁
偏向锁是为了消除无竞争情况下的同步原语,进一步提升程序性能
- 与轻量级锁的区别:轻量级锁是在无竞争的情况下使用CAS操作来代替互斥量的使用,从而实现同步;而偏向锁是在无竞争的情况下完全取消同步。
- 与轻量级锁的相同点:它们都是乐观锁,都认为同步期间不会有其他线程竞争锁。
- 原理:当线程请求到锁对象后,将锁对象的状态标志位改为01,即偏向模式。然后使用CAS操作将线程的ID记录在锁对象的Mark Word中。以后该线程可以直接进入同步块,连CAS操作都不需要。但是,一旦有第二条线程需要竞争锁,那么偏向模式立即结束,进入轻量级锁的状态。
- 优点:偏向锁可以提高有同步但没有竞争的程序性能。但是如果锁对象时常被多条线程竞争,那偏向锁就是多余的。
- 偏向锁可以通过虚拟机的参数来控制它是否开启。
Java并发容器
List和Set
JUC包中List接口的实现类:
CopyOnWriteArrayList
,是线程安全的ArrayList
。JUC包中Set接口的实现类:
CopyOnWriteArraySet
、ConcurrentSkipListSet
。CopyOnWriteArraySet
是线程安全的Set,它内部包含了一个CopyOnWriteArrayList
,因此本质上是由CopyOnWriteArrayList
实现的。ConcurrentSkipListSet
相当于线程安全的TreeSet
。它是有序的Set。它由ConcurrentSkipListMap
实现。
Map
ConcurrentHashMap
:线程安全的HashMap。采用分段锁实现高效并发。ConcurrentSkipListMap
:线程安全的有序Map。使用跳表实现高效并发。
Queue
ConcurrentLinkedQueue
:线程安全的无界队列。底层采用单链表。支持FIFO。ConcurrentLinkedDeque
:线程安全的无界双端队列 。底层采用双向链表。支持FIFO和FILO。ArrayBlockingQueue
:数组实现的阻塞队列。LinkedBlockingQueue
:链表实现的阻塞队列。LinkedBlockingDeque
:双向链表实现的双端阻塞队列。
CopyOnWrite容器(写时复制容器)
CopyOnWrite容器包括:CopyOnWriteArrayList
和CopyOnWriteArraySet
。
特性:
- 适用于读操作远远多于写操作,并且数据量较小的情况。
- 修改容器的代价是昂贵的,因此建议批量增加
addAll
、批量删除removeAll
。
实现:
- 使用
volatile
修饰数组引用:确保数组引用的内存可见性。 - 对容器修改操作进行同步:从而确保同一时刻只能有一条线程修改容器(因为修改容器都会产生一个新的容器,增加同步可避免同一时刻复制生成多个容器,从而无法保证数组数据一致性)
- 修改时复制容器:确保所有修改操作都作用在新数组上,原本的数组在创建过后就不用变化,从而其他线程可以放心地读。
迭代:
CopyOnWriteArrayList
拥有内部类:COWIterator
,它是LitIterator
的子类- 当调用
iterator
函数返回的是COWIterator
COWIterator
不允许修改容器,调用会抛出UnsupportedOperationException
缺点:
数据一致性问题
由于迭代的是容器当前的快照,因此迭代过程中容器发生的修改不能实时被当前正在迭代的线程感知。
内存占用问题
由于修改容器都会复制数组,从而当数组超大时修改容器效率很低,因此写时复制容器适合存储小容量数据。
优点:
- 读操作无需加锁,从而高效。
ConcurrentHashMap
分段锁原理:
ConcurrentHashMap
由多个Segment
构成,每个Segment
都包含一张哈希表。每次操作只将操作数据所属的Segment
锁起来,从而避免将整个锁住。
ConcurrentHashMap
内容包含了Segment
数组,而每个Segment
又继承自ReetrantLock
,因此它是一把可重入的锁。Segment
内容拥有一个HashEntry
数组,它就是一张哈希表。HashEntry
是单链表的一个节点,HashEntry
数组存储链表的表头节点。ConcurrentHashMap 在并发访问的性能上要比HashTable和同步包装之后的HashMap的性能提高很多。在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设置为 16),及任意数量线程的读操作。
ConcurrentSkipListMap
- 它是一个有序的Map,相当于
TreeMap
。 TreeMap
采用红黑树来实现排序,而ConcurrentSkipListMap
采用跳表实现有序。
跳表:
作用:存储有序序列,并且实现高效的查找与插入删除。
存储有序序列最简单的办法就是使用数组,从而查找可以采用二分搜索,但插入删除需要移动元素较为低效。因此出现了二叉搜索树,用来解决插入删除移动元素的问题。但二叉搜索树在最坏情况下会退化成一条单链表,搜索的效率降为O(n)。为了避免二叉搜索树的退化,出现了二叉平衡树,它在每次插入删除节点后都会重新调整树形,使得它仍然保持平衡,从而保证了搜索效率,也保证了插入删除的效率。
此外,根据平衡算法的不同,二叉平衡树又分为:B+树、B-树、红黑树。但平衡算法过于复杂,因此出现跳表。
跳表是条有序的单链表,它的每个节点都有多个指向后继节点的引用。
它有多个层次,上层都是下层的子集,从而能跳过不必要的节点,提升搜索速度。它通过空间来换取时间。
如查找19的过程:
ConcurrentSkipListSet
- 是一个有序的、线程安全的Set,相当于线程安全的TreeSet。
- 内部拥有
ConcurrentSkipListMap
实例,本质上就是一个ConcurrentSkipListMap,只不过仅使用了Map中的key。
ArrayBlockingQueue
ArrayBlockingQueue
是一个数组实现的线程安全的有限阻塞队列。- 继承自
AbstractQueue
,并实现了BlockingQueue
接口。 - 内部由
Object[]
数组存储元素,构造时必须要指定队列容量。 - 由
ReentrantLock
实现队列的互斥访问,并由notEmpty
、notFull
这两个Condition
分别实现队空、队满的阻塞。 - 分为公平锁和非公平锁,可以在构造时指定。默认为非公平锁。
- 队满阻塞:当添加元素时,若队满,则调用
notFull.await()
阻塞当前线程;当移除一个元素时调用notFull.signal()
唤醒在notFull
上等待的线程。 - 队空阻塞:当删除元素时,若队为空,则调用
notEmpty.await()
阻塞当前线程;当队首添加元素时,调用notEmpty.signal()
唤醒在notEmpty
上等待的线程。
LinkedBlockingQueue
LinkedBlockingQueue
是一个 单链表实现的、线程安全的、无限阻塞队列。- 继承自
AbstractQueue
,实现了BlockingQueue
接口。 - 由单链表实现,因此是个无限队列。但为了方式无限膨胀,构造时可以加上容量加以限制。
- 分别采用读取锁和插入锁控制读取/删除 和 插入过程的并发访问,并采用notEmpty和notFull两个Condition实现队满队空的阻塞与唤醒。
LinkedBlockingDeque
- 由双向链表实现的、线程安全的、 双端无限阻塞队列。
ConcurrentLinkedQueue
- 是一个由单链表实现的、线程安全的、无限队列。
- 仅仅继承了AbstractQueue,并未实现BlockingQueue接口,因此它不是阻塞队列,仅仅是个线程安全的普通队列。
特性:
head
、tail
、next
、item
均使用volatile
修饰,保证其内存可见性,并未使用锁,从而提高并发效率。