-
Synchronized 内置锁(悲观锁,非公平锁)
-
对象锁
- this
- 具体对象
-
类锁
- .class
private static Object obj = new Object();只有一份
-
可重入锁
int count; //如果是非重入锁,就会自己把自己锁死 public synchronized void test(){ count++; test(); } -
缺点
- 无法中断
- 无法尝试拿锁,无法尝试10s(一定时间)后拿锁
-
原理解析
获取monitor对象的锁
-
synchronized代码块
moniterenter moniterexit
-
synchronized方法
flags:ACC_SYNCHRONIZED表明当前方法是个同步方法 底层还是monitorenter和monitorexit
锁的存放位置:对象头
-

四种状态:
无锁状态(CAS)
偏向锁
轻量级锁
是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
重量级锁
锁可以升级,也可以降级(和虚拟机有关)
偏向锁是判断对象头的线程id,如果id还是原来线程的,那就直接执行,这个时候偏向锁会比自旋锁更快。
撤销偏向锁的时候需要stop the world,在有锁的竞争时,偏向锁会多做很多额外操作(比如修改栈帧中的东西),尤其是撤销偏向所的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用。
不同锁的比较
-

-
注意点
只要锁的是不同的对象,就可以同时执行
synchroinzed(null)不行,必须是对象,要存放在对象头中
多线程能不能共享一把锁:共享锁(可以),排它锁;共享锁又称读锁,排他锁又称写锁
Synchronized加锁是和具体对象关联的,普通的对象,类对象(.class对象,在虚拟机中一个类只会一个.class对象)
-
在Java中,我们为什么必须在synchronized方法或同步块中调用wait(),notify()或notifyAll方法
-
避免IllegalMonitorStateException
-
避免任何在wait和notify之间的潜在竞态条件(也就是外面加了个synchronized锁,避免因为程序的执行顺序影响执行结果)
-
wait()会释放锁,notify()或者notifyAll()方法并不是真正释放锁,必须等到synchronized方法或者语法块执行完才真正释放锁
-
参考 zhuanlan.zhihu.com/p/76625784 leokongwq.github.io/2017/02/24/…
-
-
显示锁
- 使用规范
private Lock lock = new ReentrantLock(); //默认是非公平锁,因为非公平锁效率高 private int count; public void incr(){ lock.lock(); //使用显示锁的规范 try { count++; }finally { lock.unlock(); } }-
非公平锁和公平锁
-
若在释放锁的时候总是没有新来的线程来打扰,则非公平锁等于公平锁
-
若释放锁的时候,正好有个新线程,而此时位于队列头的线程还没有被唤醒(因为上下文切换时需要 不少开销的),此后后来的线程则优先获得锁,成功打破公平,称为非公平锁
-
线程切换耗费一定的时间周期,其实就是非公平锁效率高于公平锁的原因,因为非公平锁减少了线程挂起的几率,后来的线程有一定的几率逃离被挂起的开销。
-

-
-
Thread创建有几种方式?
有且只有两种,可以查看Thread源码
//Thread There are two ways to create a new thread of execution.通过线程池,或者Callable(最终是包装成futuretask,最后的实现还是Runnable)都不是。
-
活锁(避免,需要sleep一小段时间)
-
饥饿
-
ThreadLocal
如果我们自己实现一个ThreadLocal,内部持有一个Map,key是线程,value是具体的值
class MyThreadLocal<T> { Map<Thread, T> locals = new ConcurrentHashMap<>(); //获取线程变量 T get() { return locals.get( Thread.currentThread()); } //设置线程变量 void set(T t) { locals.put( Thread.currentThread(), t); } }但是这样会带来一个问题,多个线程使用的情况下,会对这个Map的竞争比较激烈。
Java的实现方式

可以看到,Thread有ThreadLocalMap(该类是静态内部类,其实可以认为和外部类没什么关系),然后ThreadLocalMap里面有一个Entry[] table数组,Entry的构造器
Entry(ThreadLocal<?> key, Object value) { //key是threadlocal,value是具体的值
super(var1);
this.value = var2;
}
这样其实就是每个线程里面都有一个threadLocalMap,里面有一个数组,存放不同的threadlocal和对应的值。这样也不会造成对map的竞争激烈问题。而且效率还很高,如下图

还有一个原因是不容易产生内存泄露。在我们的设计方案中,ThreadLocal 持有的 Map 会持有 Thread 对象的引用,这就意味着,只要 ThreadLocal 对象存在,那么 Map 中的 Thread 对象就永远不会被回收。ThreadLocal 的生命周期往往都比线程要长,所以这种设计方案很容易导致内存泄露。而 Java 的实现中 Thread 持有 ThreadLocalMap,而且 ThreadLocalMap 里对 ThreadLocal 的引用还是弱引用(WeakReference),所以只要 Thread 对象可以被回收,那么 ThreadLocalMap 就能被回收。
ThreadLocal导致的内存泄漏:
在线程池中使用 ThreadLocal 为什么可能导致内存泄露呢?原因就出在线程池中线程的存活时间太长,往往都是和程序同生共死的,这就意味着 Thread 持有的 ThreadLocalMap 一直都不会被回收,再加上 ThreadLocalMap 中的 Entry 对 ThreadLocal 是弱引用(WeakReference),所以只要 ThreadLocal 结束了自己的生命周期是可以被回收掉的。但是 Entry 中的 Value 却是被 Entry 强引用的,所以即便 Value 的生命周期结束了,Value 也是无法被回收的,从而导致内存泄露。
手动释放:
ExecutorService es;
ThreadLocal tl;
es.execute(()->{
//ThreadLocal增加变量
tl.set(obj);
try {
// 省略业务逻辑代码
}finally {
//手动清理ThreadLocal
tl.remove();
}
});
-
CAS(Compare And Swap)自旋锁(乐观锁),现代CPU提供了一个专门的CAS指令:是一条原子指令,比较并执行
-
用其他锁也可以实现原子操作,但是过于笨重,所有人都可以同时操作就是乐观锁,悲观是必须排队一个一个来。
-
普通线程,一旦阻塞或者等待,就会发生上下文切换,时间周期5000~20000 3~5ms CAS指令 普通指令0.6ns / CAS指令几百纳秒 不会进入阻塞状态
-
CAS存在的问题(也是为什么锁还存在)
- ABA问题(必问的问题) 添加一个比较版本
解决:ABA问题用AtomicStampedReference,内部加了时间戳,比较的时候不仅比较值,还看时间戳是不是一样,类似版本号
- 开销问题 如果长期执行不成功会耗费CPU
解决:改用加锁
- 只能保证一个共享变量的原子操作
解决:打包到一个对象里面,可以用AtomicReference
-
什么时候使用CAS呢?
普通的累加
比较简单的操作
-
适应性自旋锁,次数控制在10次左右,一个线程上下文切换的时间,超过这个时间不再执行(避免引入重量级锁造成上下文切换)
-

markable改没改过
stamped改过几次
-
-
线程池
阻塞队列
-
ArrayBlockingQueue:一个由数组构成的有界阻塞队列。(常用)
-
LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。(常用)
-
PriorityBlickingQueue:一个支持优先级排序的无界阻塞队列。
-
DelayQueue:一个使用优先级队列实现的无阻塞队列。
Delay时间到了才能取出,可以做一个单机的缓存系统。
-
SynchronousQueue:一个不存储元素的阻塞队列。
不存储元素的阻塞队列(解耦)Okhttp 存取的时候必须有一个立马拿走,不然就不能继续存了。
-
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。 transfer方法 没有消费者消费,反复阻塞 tryTransfer() 看看有没有消费者消费,尝试,马上返回
-
LinkedBlockingDeque:一个由链接结构组成的双向阻塞队列。
阻塞队列不完全是阻塞方法,take()和put()等才是真正的阻塞方法。
线程池
核心线程池 -> 阻塞队列 -> 非核心线程池
拒绝策略
-
直接丢弃最老的那个
-
抛出异常(默认策略)
-
让调用者线程自己复制
-
把最新提交的任务扔掉
自定义拒绝策略:
比如,阻塞等待策略,利用take等方法
执行
execute(Runnable runnable)
submit(Runnable runnable): Future 需要拿到返回结果的时候
关闭
shutDown()中断没有执行任务的线程
shutDownNow()尝试中断正在执行的任务,线程的中断,协作机制所以不一定能中断(读取字节流的操作 就不能中断)
合理配置线程池中线程大小
任务特性:
CPU密集型 计算 机器的CPU核心数+1 一般磁盘一部分当做虚拟内存,但是磁盘的读写速度很慢,可能会产生页缺失,页缺失情况下,为了让CPU充分利用起来,所以要+1
IO密集型 网络,读写磁盘 (磁盘和网络的读取速度远远低于CPU的读取速度) CPU核心数*2
混合型
如果相差不大,考虑拆分成两个线程池
如果相差很大,自己判断,性能测试
其余概念
DMA机制(直接内存访问)
OS 内核空间,用户空间
零拷贝概念 mmap
允许你向我申请一段单独的空间,允许你直接访问这一段空间,不再有拷贝过程
CPU去操作磁盘的时候,通知磁盘操作,磁盘操作后发送一个中断消息(硬件方面)通知CPU,所以
IO操作基本不怎么耗费CPU
sleep(0)可以看成一个运行态的进程产生一个中断,由运行态直接转入就绪态。这样做是给其他就绪态进程使用时间片的机会。
-
-
AQS原理
AbstractQueuedSynchronizer 用来构建一个同步组件
同步工具类的内部,内部类 继承AQS 功能
- AQS的设计模式:模板方法
固定步骤,具体的步骤实现在子类中。
Android中模板方法:onDraw,dispatchDraw这两个方法
因为使用了模板方法模式,所以我们自己实现一个锁很简单,到架构师级别,可能需要自己写个锁,实现一个同步机制。
- 了解其中的方法,state
- 实现一个类型的ReentranLock的锁
AQS的基本思想CLH队列锁
-
自己实现锁的可重入

-
Java内存模型(Java Memory Model,也称JMM)



工作内存和主内存只是一个抽象的概念,不是一个真实的群体。
count = count+1 不是原子性操作 操作过程可能被打断
流水线和重排序 intel 十级流水线 提供执行效率
多条指令
-
volatile的实现原理
有volatile变量修饰的共享变量进行写操作的时候会使用CPU提供的Lock前缀指令
-
将当前处理器缓存行的数据写回到系统内存
-
这个写回内存的操作会使其他CPU里缓存了该内存地址的数据无效
volitale+CAS 替换synchroinzed 比如concurrentHashMap 为什么一个线程写,多个线程读,也要加volitale关键字呢? (可见性很重要)
-
-
问题:Java锁

更详细的并发编程面试题参考并发编程笔记。
参考文章: