我总结出了很多互联网公司的面试题及答案,并整理成了文档,以及各种学习的进阶学习资料,免费分享给大家。
扫描二维码或搜索下图红色VX号,加VX好友,拉你进【程序员面试学习交流群】免费领取。也欢迎各位一起在群里探讨技术。
本文主要讲并行优化的几种方式, 其结构如下:
减少锁的持有时间
例如避免给整个方法加锁
1 public synchronized void syncMethod(){
2 othercode1();
3 mutextMethod();
4 othercode2();
5 }
改进后
1 public void syncMethod2(){
2 othercode1();
3 synchronized(this){
4 mutextMethod();
5 }
6 othercode2();
7 }
减小锁的粒度
将大对象,拆成小对象,大大增加并行度,降低锁竞争. 如此一来偏向锁,轻量级锁成功率提高.
一个简单的例子就是jdk内置的ConcurrentHashMap与SynchronizedMap.
Collections.synchronizedMap
其本质是在读写map操作上都加了锁, 在高并发下性能一般.
ConcurrentHashMap
内部使用分区Segment来表示不同的部分, 每个分区其实就是一个小的hashtable. 各自有自己的锁.
只要多个修改发生在不同的分区, 他们就可以并发的进行. 把一个整体分成了16个Segment, 最高支持16个线程并发修改.
代码中运用了很多volatile声明共享变量, 第一时间获取修改的内容, 性能较好.
读写分离锁替代独占锁
顾名思义, 用ReadWriteLock将读写的锁分离开来, 尤其在读多写少的场合, 可以有效提升系统的并发能力.
锁分离
在读写锁的思想上做进一步的延伸, 根据不同的功能拆分不同的锁, 进行有效的锁分离.
一个典型的示例便是LinkedBlockingQueue,在它内部, take和put操作本身是隔离的,
有若干个元素的时候, 一个在queue的头部操作, 一个在queue的尾部操作, 因此分别持有一把独立的锁.
1 /** Lock held by take, poll, etc */
2 private final ReentrantLock takeLock = new ReentrantLock();
3
4 /** Wait queue for waiting takes */
5 private final Condition notEmpty = takeLock.newCondition();
6
7 /** Lock held by put, offer, etc */
8 private final ReentrantLock putLock = new ReentrantLock();
9
10 /** Wait queue for waiting puts */
11 private final Condition notFull = putLock.newCondition();
锁粗化
通常情况下, 为了保证多线程间的有效并发, 会要求每个线程持有锁的时间尽量短,
即在使用完公共资源后, 应该立即释放锁. 只有这样, 等待在这个锁上的其他线程才能尽早的获得资源执行任务.
而凡事都有一个度, 如果对同一个锁不停的进行请求 同步和释放, 其本身也会消耗系统宝贵的资源, 反而不利于性能的优化
一个极端的例子如下, 在一个循环中不停的请求同一个锁.
1 for(int i = 0; i < 1000; i++){
2 synchronized(lock){
3
4 }
5 }
6
7 // 优化后
8 synchronized(lock){
9 for(int i = 0;i < 1000; i++){
10
11 }
12 }
锁粗化与减少锁的持有时间, 两者是截然相反的, 需要在实际应用中根据不同的场合权衡使用.
JDK中各种涉及锁优化的并发类可以看之前的博文: 并发包总结
除了控制有限资源访问外, 我们还可以增加资源来保证对象线程安全.
对于一些线程不安全的对象, 例如SimpleDateFormat, 与其加锁让100个线程来竞争获取,
不如准备100个SimpleDateFormat, 每个线程各自为营, 很快的完成format工作.
示例
1 public class ThreadLocalDemo {
2
3 public static ThreadLocal threadLocal = new ThreadLocal();
4
5 public static void main(String[] args){
6 ExecutorService service = Executors.newFixedThreadPool(10);
7 for (int i = 0; i < 100; i++) {
8 service.submit(new Runnable() {
9 @Override
10 public void run() {
11 if (threadLocal.get() == null) {
12 threadLocal.set(new SimpleDateFormat("yyyy-MM-dd"));
13 }
14
15 System.out.println(threadLocal.get().format(new Date()));
16 }
17 });
18 }
19 }
20 }
原理
对于set方法, 先获取当前线程对象, 然后getMap()获取线程的ThreadLocalMap, 并将值放入map中.
该map是线程Thread的内部变量, 其key为threadlocal, vaule为我们set进去的值.
1 public void set(T value) {
2 Thread t = Thread.currentThread();
3 ThreadLocalMap map = getMap(t);
4 if (map != null)
5 map.set(this, value);
6 else
7 createMap(t, value);
8 }
对于get方法, 自然是先拿到map, 然后从map中获取数据.
1 public T get() {
2 Thread t = Thread.currentThread();
3 ThreadLocalMap map = getMap(t);
4 if (map != null) {
5 ThreadLocalMap.Entry e = map.getEntry(this);
6 if (e != null)
7 return (T)e.value;
8 }
9 return setInitialValue();
10 }
内存释放
1 public class StaticThreadLocalTest {
2
3 private static ThreadLocal tt = new ThreadLocal();
4 public static void main(String[] args) throws InterruptedException {
5 ExecutorService service = Executors.newFixedThreadPool(1);
6 for (int i = 0; i < 3; i++) {
7 service.submit(new Runnable() {
8 @Override
9 public void run() {
10 BigMemoryObject oo = new BigMemoryObject();
11 tt.set(oo);
12 // 做些其他事情
13 // 释放方式一: 手动置null
14 // tt.set(null);
15 // 释放方式二: 手动remove
16 // tt.remove();
17 }
18 });
19 }
24 // 释放方式三: 关闭线程或者线程池
25 // 直接new Thread().start()的场景, 会在run结束后自动销毁线程
26 // service.shutdown();
27
28 while (true) {
29 Thread.sleep(24 * 3600 * 1000);
30 }
31 }
32
33 }
34 // 构建一个大内存对象, 便于观察内存波动.
35 class BigMemoryObject{
36
37 List list = new ArrayList<>();
38
39 BigMemoryObject() {
40 for (int i = 0; i < 10000000; i++) {
41 list.add(i);
42 }
43 }
44 }
内存泄露
内存泄露主要出现在无法关闭的线程中, 例如web容器提供的并发线程池, 线程都是复用的.
由于ThreadLocalMap生命周期和线程生命周期一样长. 对于一些被强引用持有的ThreadLocal, 如定义为static.
如果在使用结束后, 没有手动释放ThreadLocal, 由于线程会被重复使用, 那么会出现之前的线程对象残留问题,
造成内存泄露, 甚至业务逻辑紊乱.
对于没有强引用持有的ThreadLocal, 如方法内变量, 是不是就万事大吉了呢? 答案是否定的.
虽然ThreadLocalMap会在get和set等操作里删除key 为 null的对象, 但是这个方法并不是100%会执行到.
看ThreadLocalMap源码即可发现, 只有调用了getEntryAfterMiss后才会执行清除操作,
如果后续线程没满足条件或者都没执行get set操作, 那么依然存在内存残留问题.
1 private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal key) {
2 int i = key.threadLocalHashCode & (table.length - 1);
3 ThreadLocal.ThreadLocalMap.Entry e = table[i];
4 if (e != null && e.get() == key)
5 return e;
6 else
7 // 并不是一定会执行
8 return getEntryAfterMiss(key, i, e);
9 }
10
11 private ThreadLocal.ThreadLocalMap.Entry getEntryAfterMiss(ThreadLocal key, int i, ThreadLocal.ThreadLocalMap.Entry e) {
12 ThreadLocal.ThreadLocalMap.Entry[] tab = table;
13 int len = tab.length;
14
15 while (e != null) {
16 ThreadLocal k = e.get();
17 if (k == key)
18 return e;
19 // 删除key为null的value
20 if (k == null)
21 expungeStaleEntry(i);
22 else
23 i = nextIndex(i, len);
24 e = tab[i];
25 }
26 return null;
27 }
最佳实践
不管threadlocal是static还是非static的, 都要像加锁解锁一样, 每次用完后, 手动清理, 释放对象.
与锁相比, 使用CAS操作, 由于其非阻塞性, 因此不存在死锁问题, 同时线程之间的相互影响,
也远小于锁的方式. 使用无锁的方案, 可以减少锁竞争以及线程频繁调度带来的系统开销.
例如生产消费者模型中, 可以使用BlockingQueue来作为内存缓冲区, 但他是基于锁和阻塞实现的线程同步.
如果想要在高并发场合下获取更好的性能, 则可以使用基于CAS的ConcurrentLinkedQueue.
同理, 如果可以使用CAS方式实现整个生产消费者模型, 那么也将获得可观的性能提升, 如Disruptor框架.
关于无锁, 这边不再赘述, 之前博文已经有所介绍, 具体见: Java高并发之无锁与Atomic源码分析
转载:https://www.cnblogs.com/xdecode/p/9137804.html
推荐内容:
两道面试题,带你解析Java类加载机制
Java的深拷贝和浅拷贝
面试题思考:什么是 Java 的反射机制
Java面试题——中级(下)
java开发常用技术
java 协程
Java精选面试题之Spring Boot 三十三问
推荐几个IDEA插件,Java开发者撸码利器。
Java面试题收集
Java集合必会14问(精选面试题整理)