jacky自问自答-java并发编程
1、java Web中线程不是由tomcat这类web容器负责的吗?为什么还要我控制多线程?
答:这个问题很多初学者都会有的疑惑,举一个我以前做的一个需求,java作为中间平台,是socket服务端,接受C语言端的请求,去财政局查“公积金基础信息”,“公积金交易明细信息”,“公积金贷 款基础和明细”等信息,然后再通过httpClient去请求财政局的webService,再把数据解密,封装成C端要求的格式返回给C端,没什么经验人的会直接写一个简单的socket服务端,那么问题就来了,bio是同步阻塞的通信方式,如果一个线程还在处理中,又来一个请求不能马上处理;有经验会在socket服务端接受到请求之后,开启一个线程去请求财局,处理返回数据,然后socket服务端继续阻塞?线程频繁创建,销毁,造成较大的开销,于是写的一个线程池。
2、java调用方式时,是值传递还是引用传递?
答:java只有值传递,没有引用传递
参考:http://www.cnblogs.com/coderising/p/5697986.html
3、Lock和synchronized的区别?
答:总结来说,Lock和synchronized有以下几点不同:
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以选择一段时间获取不到锁之后,选择不等待
6)lock和synchronized都是可重入锁,但synchronized是非公平锁,不可中断锁,而lock属于可中断锁,可限时(等待一段时间获取不到锁,就不等),可以设置公平锁
参考:http://www.cnblogs.com/dolphin0520/p/3920373.html
4、volatile有什么特性?
答:并发编程中要保证可见性,原子性,有序性,而volatile就是保证可见性和有序性的
1)可见性: java内存模型中,java内存模型规定所有的变量存在主存中,每个线程都要自己工作内存(高速缓存,与主存是两个东西),对变量的所有操作都要在线程自己的工作内存中,例如线程A执行i++操作,首先要把i从主存中读到工作内存,然后读取工作内存的i,进行加1操作,把结果写到工作内存,再写到主存中,所以线程A执行了i+1操作之后,线程B不一定马上看的到,因为可能线程A并没有没有把值立刻写到主存中,而volatile就是保证线程A对i进行加1操作后,线程B立刻可以看的到。
2)有序性:
jvm遇到volatile关键字时,会多出一个lock前缀指令”,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会主要功能是它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
注意:volatile不能保证原子性,不能代替synchronized
5、ThreadLocal是什么?
答:是threadLocal是线程安全同步类,从共享变量中复制一份副本,存储到当前线程ThreadLocalMap类型的ThreadLocal中,这样这个每个线程都会使用自己的数据,多个线程不操作共享数据,也就不会出现线程安全问题了,是属于一种空间换时间做法。
6、关于ThreadLocal问题中,每个线程的副本存储在哪里?
Thread类中有一个应用变量ThreadLocals,是属于ThreadLocalMap类型,key是ThreadLocal对象,value就是线程要独享的值,每个线程的副本就存储在这里。
7、ThreadLocal是怎么获得副本的?
可以从ThreadLocal的get函数中看出来,首先是调用getMap(Thread T)方法,从当前线程中取出类型为ThreadLocalMap的threadLocals变量,然后再根据当前threadLocal对象取出value(副本)
8、ThreadLocal是怎么存储一份副本到当前线程中的?
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
根据ThreadLocal的set方法可以看出,首先获得当前线程,由当前线程获得ThreadLocalMap,然后不为null,就可以设置了,键是当前ThreadLocal对象,value就是副本,如果ThreadLocalMap为空,则说明还没初始化,调用createMap,创建一个ThreadLocalMap对象,并且赋值。
参考:http://blog.csdn.net/imzoer/article/details/8262101
参考:http://www.cnblogs.com/dolphin0520/p/3920407.html
9、jdk1.7中hashMap是存储key,value的原理?
hashMap通过数组+单向链表的方式实现的,hashMap中有个Entry内部类,有成员变量key value Entry<key ,value>,hash(key的hashcode进行hash运算的值),如下:
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; //存储指向下一个Entry的引用,单链表结构 int hash; //对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算 /** * Entry的构造方法 */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; }
在put的时候通过hash(key)%len获得,也就是元素的key的哈希值对数组长度(Entry[]数组)取模得到数组的下标,如果数组对应的下标元素不为null,说明数组中原来就有存放Entry元素, 这时会遍历数组下标所对应的单向链表,取出链表上的每个Entry元素,调用equal判断两个对象是否相同,如果相同,则用新value替代旧value,(如下图)
public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { //这时会遍历数组下标所对应的单向链表,取出链表上的每个Entry元素,调用equal判断两个对象是否相同 Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }
如果数组下标所对应的元素为null,或者遍历完了数组下标所对应的单向链表,也没有相同的,就要自己new一个Entry元素,替代数组下标所对应的元素,
如下
void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
这里要注意,如果是数组下标所对应的元素为null,这样第一个entry元素的next值为null;
10、如果两个key通过hash%Entry[].length得到的index相同,会不会有覆盖的危险?
这里HashMap里面用到链式数据结构的一个概念。上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。所以疑问不用担心。也就是说数组中存储的是最后插入的元素。
11、jdk1.7中hashMap根据key获取value原理?
答:先根据hash(key)%len定位到数组元素,再遍历该元素处的链表,判断链表上每个entry元素的key值,是否跟自己传进来的key值相同,如果相同就取出其value值
参考:http://blog.csdn.net/vking_wang/article/details/14166593
12、ConcurentHashMap能够实现线程安全的原理?
答:采用锁分段技术,一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构,同时segment继承了可重入锁ReenTranLock; 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护着一个HashEntry数组元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁,这样多个线程就不用使用同一把锁了,默认支持16个线程并发操作ConcurentHashMap。
13、ConcurentHashMap的put操作?
答:Put方法首先根据key的hashCode,再使用Wang/Jenkins hash的变种算法对元素的hashCode进行一次再哈希定位到Segment,然后在Segment里进行插入操作。插入操作需要经历两个步骤,第一步判断是否需要对Segment里的HashEntry数组进行扩容,第二步定位添加元素的位置然后放在HashEntry数组里。
是否需要扩容。在插入元素前会先判断Segment里的HashEntry数组是否超过容量(threshold),如果超过阀值,数组进行扩容。
如何扩容。扩容的时候首先会创建一个两倍于原容量的数组,然后将原数组里的元素进行再hash后插入到新的数组里。为了高效ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容。
14、我们知道 HashTable的get的操作时要加锁的,那么concurentHashMap的get操作加锁吗?是怎么实现的?
答:get的整个过程是不加锁的。
1)jdk1.6:实现原理:
首先hashEntry的value字段被volatile修饰的,如果当A线程get的同时,如果B线程修改了value值,A线程也会拿到最新值,同时ConcurrentHashMap是弱一致性,比如:可能你期望往ConcurrentHashMap底层数据结构中删除一个元素后,立马能对get可见,但ConcurrentHashMap并不能如你所愿。换句话说,remove操作将一个元素加入到底层数据结构后,get可能在某段时间内还看不到这个元素,在1.6中remove操作会将被删 key之前的结点全部复制一份,并将被删结点前一个结点的next指针指向其下一个结点。导致存在两个链表(新链表,和原链表),如果get操作的 对象是原链表(概率不高),虽然不会带来异常,但是会导致看不到删除结点后的影响 。同时复制链表这样的操作也会带来性能上的损耗,jdk1.6的put弱一致性的原因还没想清楚,个人认为put的时候是加在hashEntry链表的头部,而hashEntry数组是被volatile修改的,应该是立刻可见的。
参考:http://blog.csdn.net/jianghuxiaojin/article/details/52006110
参考:http://ifeve.com/concurrenthashmap-weakly-consistent/
2)jdk1.7实现原理:
在1.7中HashEntry 源码中,增加了setNext方法,next指针可变是volatile,而setNext使用了UNSAFE 的本地方法putOrderedObject来更新其值。注释已经说明了 这个操作是lazySet 延时写,而这里之所以使用延时写是为了提升性能,在锁退出时修改自然会更新到内存中,如果采用直接赋值给next字段,由于next时volatile字段,会引起更新直接写入内存而增加开销,但是这样的话就会导致get等操作的弱一致性依然存在,也就是说A线程put或remove,不是对b线程get立刻可见。
参考:http://www.th7.cn/Program/cp/201610/996145.shtml
15、AtomicInteger的是干嘛的?实现原理是什么?
AtomicInteger是做实现原子性递增,递减操作的,利用CAS无锁的方式来实现原子性的,里面有三个值,内存中的值A、预期值B、修改值C,当A=B时,才设置新值C,否则设置值失败,轮训再次设置
值。比如getAndIncrement方法,首先获得内存值,再获得修改值,然后调用native方法compareAndSet进行递增,jdk还提供了AtomicBoolean,AtomicLong基本类型原子操作。
/** * Atomically increments by one the current value. * * @return the previous value */ public final int getAndIncrement() { for (;;) { int current = get(); //获得内存值 int next = current + 1; //获得修改值 if (compareAndSet(current, next)) 然后调用native方法compareAndSet进行递增
return current; } }
16、ArrayBlockingQueue实现原理?
ArrayBlockingQueue是数组实现的线程安全的有界的阻塞队列,
线程安全:是添加或者删除的时候,采用互斥锁ReentrantLock ;
队列有界:则是指ArrayBlockingQueue对应的数组是有界限的,只是数组的长度,在new数组阻塞队列的时候指定。
怎么做到阻塞:ArrayBlockingQueue有个两个Condition类型的变量notEmpty和not Full,如果对列为空的话 ,notEmpty调用wait()进行阻塞,同样道理,如果对列已经满的话,notFull调用wait方法进行阻塞。
参考:http://www.cnblogs.com/200911/p/5994044.html
17、object的wait和notify方法与condition的wait和signal有什么区别?
condition的wait和signal比较灵活,signal可做指定唤醒某一类线程(被同一个condition对象wait的线程),而notify只能随机唤醒某一个线程。
参考:https://my.oschina.net/itblog/blog/515687
18、bio和nio有什么区别?
bio是同步阻塞,nio是同步非阻塞
同步调用,比如client调用server,在server返回结果之前,client的代码不会往下执行,异步就反过来,server没有返回结果,client可以继续往下执行;而非阻塞体现通道对象调用读方法和写方法时,没有如果没有可读和可写的数据时,立刻返回,我们可以把这件事记下来,记录的方式通常是在Selector(通道管理器 )上注册标记位,然后切换到其它就绪的连接(channel)继续进行读写,阻塞就反过来,如果数据不可读,不可写,不能立刻返回,线程只能干等。
19、nio是怎么体现非阻塞的?
体现通道对象调用读方法和写方法时,没有如果没有可读和可写的数据时,立刻返回,我们可以把这件事记下来,记录的方式通常是在Selector(通道管理器 )上注册标记位,然后切换到其它就绪的连接(channel)继续进行读写
20、nio是怎么做到不用线程池处理客户端多个请求的?
nio有三件套,selector(通道管理器),channel通道,缓冲区,客户端同时有多个请求过来的时候,注册到通道管理器上,然后另一个线程遍历通道管理器同的通道,根据连接事件,来处理读写 请求,所以服务端能够处理多个请求
21、代理模式的特征?
代理模式是常用的java设计模式,他的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理数据、过滤数据、把数据转发给委托类,以及事后处理数据等
22、java静态代理和动态代理的区别?
静态代理:由程序员创建或特定工具自动生成源代码,再对其编译。在程序运行前,代理类文件就已经存在了。
动态代理:在程序运行时,运用反射机制动态创建而成。
23、java动态代理和cglib的区别?
JDK的动态代理机制只能代理实现了接口的类,而没有实现接口的类是不能实现JDK的动态代理的;cglib是针对类来实现代理的,他的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。
24、 LockSupport.park()和unpark(),与object.wait()和notify()的区别?
LockSuport比较灵活,可以指定阻塞队列的目标对象,每次可以指定具体的线程唤醒。Object.wait()是以对象为纬度,阻塞当前的线程和唤醒单个(随机)或者所有线程。
25、CountDownLatch是干嘛的?
CountDownLatch是倒数计时器,用来在某个线程执行之前,其他线程先做好准备工作,比如火箭发射之前,要 先检查10项工作,每检查一项就减1,当减到0的时候,就发射火箭
26、CyclicBarrier是干嘛的?
一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点,比如,赛跑的时候,当所有选手做好准备之后,才起跑。
27、Semaphore的作用?
Semaphore是用来保护一个或者多个共享资源的访问,Semaphore内部维护了一个计数器,其值为可以访问的共享资源的个数。一个线程要访问共享资源,先获得信号量,如果信号量的计数器值大于1,意味着有共享资源可以访问,则使其计数器值减去1,再访问共享资源。
如果计数器值为0,线程进入休眠。当某个线程使用完共享资源后,释放信号量,并将信号量内部的计数器加1,之前进入休眠的线程将被唤醒并再次试图获得信号量。
就好比一个厕所管理员,站在门口,只有厕所有空位,就开门允许与空厕数量等量的人进入厕所。多个人进入厕所后,相当于N个人来分配使用N个空位。为避免多个人来同时竞争同一个侧卫,在内部仍然使用锁来控制资源的同步访问
Semaphore semaphore = new Semaphore(10,true); semaphore.acquire(); //获得信号量 //do something here semaphore.release(); //释放信号量
28、线程池ThreadPoolExecutor有什么参数,以及参数所代表的的含义?
- 核心线程数
- 最大线程数
- 线程空闲时间,超时时间:当线程空闲时间达到keepAliveTime,该线程会退出,直到线程数量等于corePoolSize。如果allowCoreThreadTimeout设置为true,则所有线程均会退出直到线程数量为0。
- 线程空闲时间对应的单位
- 线程工厂 :ThreadFactory 在创建线程的同时可以设置线程的一些属性,比如是否为守护线程,设置线程名称
- 拒绝执行处理器:RejectedExecutionHandler
- allowCoreThreadTimeout 是否允许核心线程空闲退出(若线程数等于最大线程数,并且任务队列已满)
- queueCapacity 任务线程队列
-
线程池按以下行为执行任务
- 当线程数小于核心线程数时,创建线程。
- 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
- 当线程数大于等于核心线程数,且任务队列已满
3.1、若线程数小于最大线程数,创建线程
3.2若线程数等于最大线程数,按照拒绝执行处理器来执行(记录日志,或者抛出异常)
29、Java的Executors提供了什么线程池?
- newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
- newScheduledThreadPool 创建一个定长计划线程池,支持定时及周期性任务执行。
- newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
30、Synchronized(对象锁)和Static Synchronized(类锁)区别?
- synchronized是对类的当前实例(当前对象)进行加锁,防止其他线程同时访问该类的该实例的所有synchronized块,如果当前对象是单例对象,也可作为 全局锁用
- static synchronized针对的是类,无论实例多少个对象,那么线程都共享该锁,是属于类锁
参考:http://blog.csdn.net/cs408/article/details/48930803
31、负载均衡算法有哪些?
轮询
每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除
权重(weight)
指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况
ip_hash
每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题
fair(第三方)
按后端服务器的响应时间来分配请求,响应时间短的优先分配
url_hash(第三方)
按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。
32、什么是AQS?
AQS的全称是抽象队列同步器,用来构建锁或者其他同步组件(信号量、倒数计时器)的基础框架类。JDK中许多并发工具类的内部实现都依赖于AQS,如ReentrantLock, Semaphore, CountDownLatch等等,从功能的角度去分类包含独占锁和共享锁,结构是在AQS内部会保存一个状态变量state,通过CAS修改该变量的值,修改成功的线程表示获取到该锁,没有修改成功,则把线程封装成Node,然后放入到FIFO双向链表队列的尾部,并把线程挂起,等待被唤醒。
AQS是个抽象类,子类通过实现tryAcquire和tryRelease可以实现独占锁,实现tryAcquireShared和tryReleaseShared可以实现共享锁。比如ReentrantLock的实现原理
33、说说Runnable与Callable的区别?
相同点:
- 两者都是接口;(废话)
- 两者都可用来编写多线程程序;
- 两者都需要调用Thread.start()启动线程;
不同点:
- 两者最大的不同点是:实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果;
- Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;
参考:https://www.cnblogs.com/frinder6/p/5507082.html
java.util.concurrent.BlockingQueue的特性是:当队列是空的时,从队列中获取或删除元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞。
阻塞队列不接受空值,当你尝试向队列中添加空值的时候,它会抛出NullPointerException。
阻塞队列的实现都是线程安全的,所有的查询方法都是原子的并且使用了内部锁或者其他形式的并发控制。
BlockingQueue 接口是java collections框架的一部分,它主要用于实现生产者-消费者问题。
36、Callable和Future的区别?
是Callable和Future,它俩很有意思的,一个产生结果,一个拿到结果。
Callable接口类似于Runnable,从名字就可以看出来了,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值
37、FutureTask是什么?
可用于异步获取执行结果。通过传入Callable的任务给FutureTask,直接调用其run方法或者放入线程池执行,之后可以在外部通过FutureTask的get方法异步获取执行结果
38、CountDownLatch和CyclicBarrier的区别?
在网上看到很多人对于CountDownLatch和CyclicBarrier的区别简单理解为CountDownLatch是一次性的,而CyclicBarrier在调用reset之后还可以继续使用,我觉得不应该这么简单,从源码来看CountDownLatch是利用AQS实现的,一个线程等待N个线程完成之后才能执行,CyclicBarrier是利用condition.await方法,N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待
39、object的线程协作和condition线程协作?
Condition它更强大的地方在于:能够更加精细的控制多线程的等待与唤醒。
对于同一个锁,我们可以创建多个Condition,就是多个监视器的意思。在不同的情况下使用不同的Condition。
例如,假如多线程读/写同一个缓冲区:当向缓冲区中写入数据之后,唤醒"读线程";当从缓冲区读出数据之后,唤醒"写线程";
参考:http://huangyunbin.iteye.com/blog/2181493
40、解决cas造成的aba问题?
各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题,在Java中,AtomicStampedReference<E>也实现了这个作用,它通过包装[E,Integer]的元组来对对象标记版本戳stamp
41、java读写锁存在的意义是什么?
一个线程拥有了对象A的写锁,在释放写锁前其他线程无法获得A的读锁、写锁,因此其他线程此时无法读写;
一个线程拥有了对象A的读锁,在释放前其他线程可以获得A的读锁但无法获得A的写锁,因此其他线程此时可以读不可以写。
不加读锁的话其他线程是可以读,但也可以写,这时就可能导致数据不一致了
https://www.nowcoder.com/discuss/37157?type=0&order=0&pos=15&page=1