1.1多线程(解决可见性、原子性、有序性)
1.1.1创建线程方式
- 继承Thread
- 实现Runable接口:Runnable接口只定义了一种方法:run()方法。这是每个线程的主方法。当执行start()方法启动新线程时,它将调用run()方法
- 实现Callable
- 匿名内部类
1.1.2线程的生命周期
出生(实例化)—就绪(start)—执行—阻塞—计时等待—等待—死亡
1.1.3中断线程的方式
InterruptedException与interrupt()方法:
① interrupt
请求中断,而不是强制停止,因为这样可以避免数据错乱
②volatile
线程阻塞场景不适用
三种线程优先级:高级获得更多的cpu执行时间
1.1.4只有一种实现线程的方法的说法:
①实现Runnable接口:
构造一个Thread类,传入Runable对象,Thread调用start()启动线程,调用runable中的run方法
②继承Thread类:
需要重写run方法,调用start()方法启动线程,执行run方法
③实现 Runnable 接口比继承 Thread 类实现线程要好
Thread类的话执行需要进行线程的创建销毁等步骤
Runable的话可以使用线程池来避免性能浪费,(把任务丢到线程池)
1.1.5为什么 wait 必须在 synchronized 保护的同步代码中使用?
一般把wait()方法放synchronized保护的下while循环中,
因为要保证在while判断条件与wait是原子性的,如果没有synchronized保护,在执行while判断条件后会因线程调度而执行其他内容,出现混乱
1.1.6线程之间的协助方式
join():线程加入,加入的线程执行完才能执行其他的
wait() notify() notifyAll():调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。
1.2.6并发关键字
- Synchronized
是JVM实现的
并发(上).docx第一期图解(后续)
- volatile
可见性: 可见性问题主要指一个线程修改了共享变量值,而另一个线程却看不到。引起可见性问题的主要原因是每个线程拥有自己的一个高速缓存区——线程工作内存
禁止重排序:操作系统可以对指令进行重排序;最经典是防止对象暴漏
不保证原子性:只保证单次读/写操作是原子性的
可见性解释:线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,只要有一个线程修改完自己的工作空间的值并写回到主内存中,要及时通知其他线程,这样及时通知的情况就俗称JMM内存模型中的第一个重要特性俗称可见性
- final
1.2JUC大局观
Lock框架 + tools工具 + collections并发集合 + Atomic原子类 + exector线程池
1.2.1并发集合
容器(Vector,ConcurrentHashMap,HashTable)
不同版本ConcurrentHashMap数据结构实现:
· HashTable : 使用了synchronized关键字对put等操作进行加锁;
· ConcurrentHashMap JDK1.7: 使用分段锁机制实现;无法扩容
· ConcurrentHashMap JDK1.8: 则使用数组+链表+红黑树数据结构和CAS原子操作实现
同一时刻,只能有一个线程能写入
并发容器,队列,集合
Vector:add方法使用了Synchronized
HashTable:使用了Synchronized
ConcurrentHashMap:分段使用Synchronized
- Queue: ArrayBlockingQueue 一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。队列的头部 是在队列中存在时间最长的元素。队列的尾部 是在队列中存在时间最短的元素。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。
- Queue: LinkedBlockingQueue 一个基于已链接节点的、范围任意的 blocking queue。此队列按 FIFO(先进先出)排序元素。队列的头部 是在队列中时间最长的元素。队列的尾部 是在队列中时间最短的元素。新元素插入到队列的尾部,并且队列获取操作会获得位于队列头部的元素。链接队列的吞吐量通常要高于基于数组的队列,但是在大多数并发应用程序中,其可预知的性能要低。
- Queue: LinkedBlockingDeque 一个基于已链接节点的、任选范围的阻塞双端队列。
- Queue: ConcurrentLinkedQueue 一个基于链接节点的无界线程安全队列。此队列按照 FIFO(先进先出)原则对元素进行排序。队列的头部 是队列中时间最长的元素。队列的尾部 是队列中时间最短的元素。新的元素插入到队列的尾部,队列获取操作从队列头部获得元素。当多个线程共享访问一个公共 collection 时,ConcurrentLinkedQueue 是一个恰当的选择。此队列不允许使用 null 元素。
- Queue: ConcurrentLinkedDeque 是双向链表实现的无界队列,该队列同时支持FIFO和FILO两种操作方式。
- Queue: DelayQueue 延时无界阻塞队列,使用Lock机制实现并发访问。队列里只允许放可以“延期”的元素,队列中的head是最先“到期”的元素。如果队里中没有元素到“到期”,那么就算队列中有元素也不能获取到。
- Queue: PriorityBlockingQueue 无界优先级阻塞队列,使用Lock机制实现并发访问。priorityQueue的线程安全版,不允许存放null值,依赖于comparable的排序,不允许存放不可比较的对象类型。
- Queue: SynchronousQueue 没有容量的同步队列,通过CAS实现并发访问,支持FIFO和FILO。
- Queue: LinkedTransferQueue JDK 7新增,单向链表实现的无界阻塞队列,通过CAS实现并发访问,队列元素使用 FIFO(先进先出)方式。LinkedTransferQueue可以说是ConcurrentLinkedQueue、SynchronousQueue(公平模式)和LinkedBlockingQueue的超集, 它不仅仅综合了这几个类的功能,同时也提供了更高效的实现。
- List: CopyOnWriteArrayList ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。这一般需要很大的开销,但是当遍历操作的数量大大超过可变操作的数量时,这种方法可能比其他替代方法更 有效。在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时,它也很有用。
- Set: CopyOnWriteArraySet 对其所有操作使用内部CopyOnWriteArrayList的Set。即将所有操作转发至CopyOnWriteArayList来进行操作,能够保证线程安全。在add时,会调用addIfAbsent,由于每次add时都要进行数组遍历,因此性能会略低于CopyOnWriteArrayList。
- Set: ConcurrentSkipListSet 一个基于ConcurrentSkipListMap 的可缩放并发 NavigableSet 实现。set 的元素可以根据它们的自然顺序进行排序,也可以根据创建 set 时所提供的 Comparator 进行排序,具体取决于使用的构造方法。
- Map: ConcurrentHashMap 是线程安全HashMap的。ConcurrentHashMap在JDK 7之前是通过Lock和segment(分段锁)实现,JDK 8 之后改为CAS+synchronized来保证并发安全。
- Map: ConcurrentSkipListMap 线程安全的有序的哈希表(相当于线程安全的TreeMap);映射可以根据键的自然顺序进行排序,也可以根据创建映射时所提供的 Comparator 进行排序,具体取决于使用的构造方法。
1.2.2同步类工具
① Semaphore
Semaphore底层是基于AbstractQueuedSynchronizer来实现的。Semaphore称为计数信号量,它允许n个任务同时访问某个资源,可以将信号量看做是在向外分发使用资源的许可证,只有成功获取许可证,才能使用资源
② CountDownLatch
并发(下).docxCountDownLatch详解(后续)
CountDownLatch典型的用法是将一个程序分为n个互相独立的可解决任务,并创建值为n的CountDownLatch。当每一个任务完成时,都会在这个锁存器上调用countDown,等待问题被解决的任务调用这个锁存器的await,将他们自己拦住,直至锁存器计数结束
主线程要等待5个 Worker 线程执行完才能退出
eg: 所有的玩家都准备好之前,主线程是处于等待状态的,也就是游戏不能开始。当所有的玩家准备好之后,下一步的动作实施者为主线程,即开始游戏
③ CyclicBarrier
来了10个线程,这10个线程互相等待,到齐后一起被唤醒,各自执行接下来的逻辑;然后,这10个线程继续互相等待,到齐后再一起被唤醒。每一轮被称为一个Generation,就是一次同步点
eg: 假设有一家公司要全体员工进行团建活动,活动内容为翻越三个障碍物,每一个人翻越障碍物所用的时间是不一样的。但是公司要求所有人在翻越当前障碍物之后再开始翻越下一个障碍物,也就是所有人翻越第一个障碍物之后,才开始翻越第二个
- Exchanger
Exchanger用于进行两个线程之间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange()方法交换数据,当一个线程先执行exchange()方法后,它会一直等待第二个线程也执行exchange()方法,当这两个线程到达同步点时,这两个线程就可以交换数据了
⑤ Phaser(平替)
1.用Phaser替代CountDownLatch
2.用Phaser替代CyclicBarrier
新特性:动态调整线程个数;层次Phaser
1.2.3并发原子类
AtomicInteger底层实现?
CAS+volatile
volatile保证线程的可见性,多线程并发时,一个线程修改数据,可以保证其它线程立马看到修改后的值CAS 保证数据更新的原子性。
在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入
- 基本类型原子类:AtomicBoolean,AtomicInteger,AtomicLong
- 数组原子类:AtomicIntegerArray,AtomicLongArray
- 引用类型原子类:略后面再学
- 字段原子类:……
1.2.3JUC锁
- AQS思想:
AQS(AbstractQueuedSynchronizer)
AQS核心思想:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。使用volatile int类型的变量,表示当前同步状态
AQS独占模式:即只允许一个线程获取同步状态,当这个线程还没有释放同步状态时,其他线程是获取不了的,只能加入到同步队列,进行等待
AQS共享模式:允许多个线程同时获取到同步状态,共享模式下的AQS也是不响应中断的
- 锁
synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的
①synchronized:在类,方法,代码块中加(属于关键字)
锁的获取和释放分别是monitorenter 和 monitorexit 指令,其他任何线程都无法再获得同一个monitor对象
锁升级:无锁-偏向锁-轻量级锁-重量级锁
可重入:计数器加1,当为0,没有锁
非公平锁
②ReentrantLock(属于java类):可重入锁,⾼并发量情况下使⽤,只能使用与代码块s
需要手动获取并释放锁
公平锁:先对锁提出获取请求的线程会先被分配到锁
非公平锁:JVM按随机、就近原则分配锁的机制(默认采用)
③Semaphore
Semaphore基本能完成ReentrantLock的所有⼯作,使⽤⽅法也与之类似,通过acquire()与release()⽅法来获得和释放临界资源
④AtomicInteger/AtomicLong
对于一个整数的加减操作,要保证线程安全,基于CAS实现,用的是“自旋”策略,如果拿不到锁,就会一直重试
AtomicBoolean和AtomicReference:compare和set两个操作合在一起的原子性
Atomic的性能会优于ReentrantLock⼀倍左右。但是其有⼀个缺点,就是只能同步⼀个值,⼀段代码中只能出现⼀个Atomic的变量,多于⼀个同步⽆效
- Lock(synchronized扩展版)
属于java类,只能给代码块加锁,需要手动获取锁并释放锁
对比:更加公平,可轮询等待一段时间
- CAS乐观锁 内存中的值(V)、预期原始值(A)、修改后的新值
解释一下乐观锁:一个a线程持有预期初始值V,线程b拿到锁,将内存中的值修改为自身携带的值B,其他线程来拿锁时候对比目前值与原始值不同,则自旋等待,线程a执行完毕将初始值修改为原内存值,其他线程就可以拿锁了。
1.2.5线程池
FixedThreadPool:固定线程数量的线程池,的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务
SingleThreadExecutor:只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务
CachedThreadPool:线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
ScheduledThreadPoolExecutor:主要用来在给定的延迟后运行任务,或者定期执行任务
为什么要有线程池?
线程池能够对线程进行统一分配,调优和监控:
降低资源消耗(线程无限制地创建,然后使用完毕后销毁)
提高响应速度(无须创建线程)
提高线程的可管理性
1.3构建一个高并发系统
1.3.1分布式系统
1.3.2MQ
交由异步处理
1.3.3数据库存储
Mysql:
- 分库将请求负载均衡到其他数据库中
- 分表将数据按一定分片规律分表
- 采用读写分离
- 采用索引提高查询速率
- 利用锁机制保护数据安全
Mongdb:
1.3.2数据库缓存(保护数据源并提高请求响应速率)
Redis
- 缓存与数据源一致性
- 主从模式
- 数据集群分片分布到其他服务器
- 哨兵机制
1.4面试题
1.线程和进程的区别
①进程是一个运行的程序,是系统资源分配的基本单位,每个进程独有独立的代码与数据空间
②线程是一个进程中一个任务执行的控制单元,一个进程内的所有线程共享本进程的资源空间
2.什么是上下文切换:
一般线程数会大于cpu核数,但是同一时间内,一个cpu核心只能执行一个线程,则cpu采用时间分片轮转的策略,当一个线程的时间片用完了,线程会重新处于就绪状态并让给其他线程使用。
3.查询cpu使用率(wndows和linux)
4.线程死锁:
①相互等待释放锁②无期限阻塞
规避:①定时锁
5.创建线程的方式有哪些
①继承Tread类
- 实现runable接口(无返回值)
③实现callable接口(有返回值)
④使用匿名内部类
6.线程中的run()和start()区别
run()是线程体,用于执行线程代码,start()是通过调用Tread类方法来启动线程
7.java中线程调度算法
①分时调度模型:平均分配cpu时间片
②抢占式调度模型:根据线程的优先级
8.线程调度的相关方法(结合synchronized同步关键字来使用)
Wait()等待;sleep()睡眠;notify()唤醒;notityAll
- wait: 释放当前锁,阻塞直到被notify或notifyAll唤醒,或者超时,或者线程被中断(InterruptedException) 是Object中的方法
②notify: 任意选择一个(无法控制选哪个)正在这个对象上等待的线程把它唤醒,其它线程依然在等待被唤醒
- notifyAll: 唤醒所有线程,让它们去竞争,不过也只有一个能抢到锁
- sleep: 是Thread类的静态方法,让当前线程持有锁阻塞指定时间
interrupted为中断线程
9.sleep与yield的区别
①sleep让出的线程运行机会不考虑线程的优先级,yield只给相同或高级线程
②sleep将现在转入阻塞状态,yeild将线程转入就绪状态
③yeild不用声明异常
10.同步代码块好还是同步方法好
同步代码块更好,不会锁住整个对象
11.提交任务时,队列满了,怎么办?
无界:没关系,相当于无穷大队列
有界:会增加线程数
12.servlet和springMVC都是线程不安全的
13.如何保证线程安全
①使用原子类Atomiclnteger
- 使用自动锁synchronized
- 使用手动锁lock
14. 什么是ThreadLocal? 用来解决什么问题的
ThreadLocal是通过线程隔离的方式防止任务在共享资源上产生冲突, 线程本地存储是一种自动化机制,可以为使用相同变量的每个不同线程都创建不同的存储
将在多线程中为每一个线程创建单独的变量副本的类
15.ThreadLocal存在内存泄漏问题
用线程池来操作ThreadLocal 对象确实会造成内存泄露, 因为对于线程池里面不会销毁的线程, 里面总会存在着<ThreadLocal, LocalVariable>的强引用,不会被释放
16.后续补充