多线程知识点
并发编程中的三个概念
- 原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
- 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- 有序性:即程序执行的顺序按照代码的先后顺序执行。
happens-before 原则。
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作(单线程)
- 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
实现线程安全的三种方式
互斥同步
互斥同步锁都是可重入锁,好处是可以保证不会死锁。但是因为涉及到核心态和用户态的切换,因此比较消耗性能。
- 临界区:syncronized、ReentrantLock
- 信号量 semaphore
- 互斥量 mutex
非阻塞同步
是不可重入的,否则会造成死锁。
- CAS(Compare And Swap)就是只能同步一个值,一段代码中只能出现一个Atomic的变量,多于一个同步无效。因为他不能在多个Atomic之间同步。
无同步方案
- 可重入代码:不会依赖堆上的共享资源
- 使用Threadlocal 类来包装共享变量,做到每个线程有自己的copy
- 线程本地存储
volatile
- 保证可见性
- 禁止指令重排序(对象的创建包含分配空间,初始化,对象引用(不为null)无序时第二步和第三步可能混乱,导致为初始化化完成的对象被赋值出去)。
- 使用条件:
- 对变量的写操作不依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中。
ThreadLocal
每个线程都有一个 ThreadLocalMap,统一归ThreadLocal管理,访问前,先根据确定访问的线程,在调到对应的Map中修改变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal在没有线程池使用的情况下,正常情况下不会存在内存泄露,但是如果使用了线程 池的
话,就依赖于线程池的实现,如果线程池不销毁线程的话,那么就会存在内存泄露。(核心线程不消亡)
高并发解决方案
提升高并发量服务器性能解决思路
- HTML静态化:效率最高、消耗最小的就是纯静态化的html页面
- 图片服务器分离:图片是最消耗资源
- 数据库集群、库表散列:
- 缓存
- 镜像
6、负载均衡 - CDN加速技术
分布式和集群
- 分布式是以缩短单个任务的执行时间来提升效率的,而集群则是通过提高单位时间内执行的任务数来提
升效率。 - 分布式是指将不同的业务分布在不同的地方;而集群指的是将几台服务器集中在一起,实现同一业务。
- 分布式中的每一个节点,都可以做集群。 而集群并不一定就是分布式的。
BlockingQueue接口
提供了3个添加元素方法。
- add:添加元素到队列里,添加成功返回true,由于容量满了添加失败会抛出IllegalStateException异常
- offer:添加元素到队列里,添加成功返回true,添加失败返回false
- put:添加元素到队列里,如果容量满了会阻塞直到容量不满
3个删除方法。 - poll:删除队列头部元素,如果队列为空,返回null。否则返回元素。
- remove:基于对象找到对应的元素,并删除。删除成功返回true,否则返回false
- take:删除队列头部元素,如果队列为空,一直阻塞到队列有元素并删除
ArrayBlockingQueue的原理就是使用一个可重入锁
和这个锁生成的两个条件对象进行并发控制
LinkedBlockingQueue内部使用放锁和拿锁,这两个锁
实现阻塞
进程通讯的方式
信号,管道,消息队列,信号量,共享内存
- 程序执行中随时被各种信号中断,进程可以忽略该信号,也可以中断当前程序转而去处理信号.
- 管道的优点是不需要加锁,缺点是默认缓冲区太小,只有4K,同时只适合父子进程间通信,而且一个管道只适合单向通信,如果要双向通信需要建立两个。而且不适合多个子进程,因为消息会乱
- 消息队列也不要加锁,默认缓冲区和单消息上限都要大一些,并不局限于父子进程间通信,只要一个相同的key,就可以让不同的进程定位到同一个消息队列上,也可以用来给双向通信
- 信号量是一种用于提供不同进程间或一个进程间的不同线程间线程同步手段的原语
- 共享内存的几乎可以认为没有上限,它也是不局限与父子进程,采用跟消息队列类似的定位方式,因为内存是共享的,不存在任何单向的限制,最大的问题就是需要应用程序自己做互斥.
线程池优缺点
- 线程池主要用来解决线程生命周期开销问题和资源不足问题
- 多线程共有的并发风险,诸如同步错误和死锁,特定于线程池的少数其它风险,与池有关的死锁、资源不足和线程泄漏。
任务进入线程池的执行顺序
- 如果线程池中线程数量 < core(核心线程数),新建一个线程执行任务;
- 如果线程池中线程数量 >= core ,则将任务放入任务队列
- 如果线程池中线程数量 >= core 且 < maxPoolSize,则创建新的线程;
- 如果线程池中线程数量 > core ,当线程空闲时间超过了keepalive时,则会销毁线程;由此可见线程池的队列如果是无界队列,那么设置线程池最大数量是无效的;
解决高速缓存读取值不一致性问题,
1)通过在总线加LOCK#锁的方式
2)通过缓存一致性协议
主线程和子线程
通过子线程的join()
等待子线程完成并通知主线程工作
终止线程 4 种方式
正常运行结束
使用退出标志退出线程
Interrupt 方法结束线程
stop 方法终止线程(线程不安全)
java锁
- 乐观锁
- 悲观锁
- 自旋锁:自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!
- Synchronized 同步锁(任意一个非 NULL 的对象):独占式的悲观锁,同时属于可重入锁
- 偏向锁:它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。
synchronized的执行过程:
- 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁
- 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1
- 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
- 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁
- 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
- 如果自旋成功则依然处于轻量级状态。
- 如果自旋失败,则升级为重量级锁。
ReentrantLock 与synchronized
- ReentrantLock 通过方法 lock()与 unlock()来进行加锁与解锁操作,与 synchronized 会被 JVM 自动解锁机制不同,ReentrantLock 加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操作。
- ReentrantLock 相比 synchronized 的优势是可中断、公平锁、多个锁。这种情况下需要使用ReentrantLock。
锁优化
减少锁持有时间
减小锁粒度
锁分离
锁粗化
锁消除
CyclicBarrier、CountDownLatch、Semaphore 的用法
- CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。它要等待其他 若干个任务执行完毕之后才能执行
- CyclicBarrier 回环栅栏-等待至 barrier 状态再全部同时执行
- Semaphore 翻译成字面意思为 信号量,Semaphore 可以控制同时访问的线程个数
什么是 AQS
抽象的队列式的同步器,AQS 定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,CyclicBarrier、CountDownLatch、Semaphore