1 java 中常见的同步机制?
java 主要同步机制是 synchronized 关键字, 还有显式的 Lock,volatile,atomic, 还有一些同步集合, 阻塞队列等.
2 '++'操作是线程安全的吗?
不是, 它是由读取 - 修改 - 写入三个独立的操作组成.
3 共享变量在多线程下如何保证线程安全?
因为多线程是交替执行, 每个线程操作共享变量时可能会导致数据不一致, 要确保线程安全, 需要在访问共享变量时添加同步机制. 当然, 如果这个变量本身是线程安全的, 比如 AtomicLong, 那么多线程访问也是安全的.
4 是否共享变量都使用类似 AtomicLong 原子安全类, 多线程访问就是安全的?
这个不确定, 因为无法保证多个变量同时操作, 一个原子变量可以保证自己的安全性, 但是同时操作多个有逻辑依赖原子的变量, 仍可能带来线程安全问题. 单个安全不代表组合也安全.
5 synchronized 锁的使用?
synchronized 同步代码块, 修饰的方法是整个方法体, 同步代码块的锁就是方法调用所在的对象实例; 静态的 synchronized 方法以 Class 对象作为锁; 还可以修饰具体对象, 以具体对象为锁.
6 可重入锁, synchronized 是可重入的吗?
当一个线程拥有对象锁之后, 如果再次访问此对象的同步代码块或对象时, 不再需要获取锁, 这就是可重入锁; synchronized 是可重入的.
7 什么是重排序?
编译器, 处理器及运行时都有可能对操作的执行顺序进行一些意想不到的调整.
8 volatile 关键字的理解?
volatile 属于一个轻量的同步原语, 被它修饰的变量, 变量的更新操作可以通知到其他线程. 简单来说, 就是 volatile 修饰的变量不会被执行重排序, 所以保证了变量的可见性. 但 volatile 无法保证变量的原子性操作, 仍然是不安全的, 通常可以用作标志位, 如退出线程的循环变量.
9 final 修饰的不可变对象?
由关键字 final 修饰的对象是不可变的, 不能被重新赋值, 但是 final 仍可以修饰可变对象的引用, 例如集合: final 修饰的集合本身引用地址不能改变, 但是集合内的数据还是可以修改的. 不可变对象会减少加锁或保护性副本的需求, 可以带来一些性能上的优势.
10 常见的并发容器?
ConcurrentHashMap: 使用了分段锁, 锁的粒度变得更小, 多线程访问时, 可能都不存在锁的竞争, 所以大大提高了吞吐量. 简单对比来看, 就好比数据库上用行锁来取代表锁, 行锁无疑带来更大的并发.
CopyOnWriteArrayList: 写入时复制, 多线程访问时, 彼此不会互相干扰或被修改的线程所干扰, 当然 copy 时有开销的, 尤其时列表元素庞大, 且写入操作频繁时, 所以仅当迭代操作远远大于修改操作时, 才应该考虑使用.
BlockingQueue: 阻塞队列提供了可阻塞的 put 和 take 方法, 当队列已经满了, 那么 put 操作将阻塞到队列可用, 当队列为空时, take 操作会阻塞到队列里有数据. 有界的队列是一种强大的资源管理器, 可以在程序负荷过载时保护应用, 可作为一种服务降级的策略. 阻塞队列还提供 offer 操作, 当数据无法加入队列时, 返回失败状态, 给应用主动处理负荷过载带来更多灵活性.
11 常见的同步工具类?
CountDownLatch: 递减计数器闭锁, 直到达到某个条件时才放行, 多线程可以调用 await 方法一直阻塞, 直到计数器递减为零. 比如我们连接 zookeeper, 由于连接操作是异步的, 所以可以使用 countDownLatch 创建一个计数器为 1 的锁, 连接挂起, 当异步连接成功时, 调用 countDown 通知挂起线程; 再比如 5V5 游戏竞技, 只有房间人满了才可以开始游戏.
FutureTask: 带有计算结果的任务, 在计算完成时才能获取结果, 如果计算尚未完成, 则阻塞 get 方法. FutureTask 将计算结果从执行线程传递到获取这个结果的线程.
Semaphore: 信号量, 用来控制同时访问某个特定资源的数量, 只有获取到许可 acquire, 才能够正常执行, 并在完成后释放许可, acquire 会一致阻塞到有许可或中断超时. 使用信号量可以轻松实现一个阻塞队列.
CyclicBarrier: 类似于闭锁, 它可以阻塞一组线程, 只有所有线程全部到达以后, 才能够继续执行, so 线程必须相互等待. 这在并行计算中是很有用的, 将一个问题拆分为多个独立的子问题, 当线程到达栅栏时, 调用 await 等待, 一直阻塞到所有参与线程全部到达, 再执行下一步任务.
12 什么是 Executor?
Executor 执行已提交的 Runnable 任务, 把任务和执行机制分离开来, 可以看作是一个任务执行框架, 任务与执行的解耦, 可以更灵活的指定执行方式. 所以应该使用 Executor 代替 new Thread(Runnable).start().
13 什么是线程池, 如何创建线程池?
线程池就是由一组活跃的线程集合, 由于线程的创建与销毁开销都比较大, 所以利用线程池减少线程的性能开销, 提高响应性. 适当的调节线程池大小, 可以有效利用处理器, 同时防止过多线程相互竞争资源耗费内存. 使用 Executors 可以很方便的创建线程池.
14 Executors 可以创建哪些类型的线程池?
newFixedThreadPool: 创建一个固定大小的线程池, 每当提交一个任务就创建一个线程, 直到达到最大数量, 达到最大线程数后线程规模不再变化.
newCachedThreadPool: 创建一个可以根据需要创建新线程的线程池, 当线程规模大于处理请求时, 将回收空闲线程, 当需求增加时, 可以添加新的线程.
newSingleThreadExecutor: 创建一个单线程 Executor.
newScheduledThreadPool: 创建一个固定长度, 以延迟或定时的执行方式执行任务.
15 Executor 的生命周期?
ExcutorService 扩展了 Executor, 有三种状态: 运行, 关闭, 已终止. 可以调用 shutdown 方法, 优雅关闭任务, 这时将不再接受新的任务, 同时等待已提交的任务执行完成.
来源: http://www.jianshu.com/p/b00e205ea0ee