JAVA基础01

## java多线程 ### synchronized 的底层实现 > 1. synchronized是在对象上加锁的,加锁机制是对象的头部有markword,前三位是锁的信息 > 2. 需要答出锁升级的概念,无锁,偏向锁,轻量级锁,重锁 > 1. 偏量锁->轻量级锁 只要有两个线程进行锁争抢的时候就会从偏量锁升级为轻量级锁 > 2. 轻量锁->重量级锁 1.6之前是有自旋10次就会升级,之后这个设置取消了.改为更复杂的判断 > 3. 偏向锁->重量级锁 调用了wait方法 > 3. 需要答出各个锁的优缺点 > 1. 偏量锁:优点是比较快,缺点是需要有个锁撤销的过程,如果一开始就知道一定会有争抢的话,还不如直接用轻量级锁好一点(单线程的时候效率高,JVM启动的时候会有很多线程竞争,所以jvm启动的时候默认不打开偏向锁,过一段时间会打开,默认的时候为4秒) > 2. 轻量级锁:优点在用户空间的锁,不需要用内核资源,因此速度重量级锁要快缺点,自旋和多线程争抢的时候线程仍然在运行会造成资源的浪费 > 3. 重量级锁:优点他会把没有获取到锁的放到等待队列里面,不会消耗cpu的资源,缺点是耗时需要调用cpu的方法才能实现 ``` 0.基础补充 1)CAS compare and set(swap) CAS的中的操作是cpu规定的原子的 ABA问题 期望的值是1,获取的时候确实是1,但是这个值是先变成了2又变回了1这就是ABA问题(在CAS的时候增加版本号(增加时间戳)) 例子AtomicInteger 的incrementAndGet 底层 lock cmpxchg 2)用户态和内核态 jdk早起synchronized是重量级的锁 操作系统给系统的操作进行了分级内核态是rang0级,用户态是rang3级(0x80 阿里p9级别的问题) 3)内存布局 markword-->8字节 所信息写在markword中 4)锁升级初步 普通对象未进行加锁,让它去获取锁的时候,先给他加偏向锁,如果发现了小的争抢,加一个轻量级的锁(自旋锁),如果争抢激烈或者wait过久就加重量级的锁,偏向锁和轻量级锁都是用户空间的锁 偏向锁:直接吧线程的id放到markword中,偏量锁不一定 轻量锁:争取吧自己线程中的lockRecord的指针放到锁中 重量级锁:操作系统的锁 5)锁升级 偏量锁->轻量级锁 只要有两个线程进行锁争抢的时候就会从偏量锁升级为轻量级锁 轻量锁->重量级锁 1.6之前是有自旋10次就会升级,之后这个设置取消了. 偏向锁->重量级锁 调用了wait方法 6)各个锁的优缺点 偏量锁:优点是比较快,缺点是需要有个锁撤销的过程,如果一开始就知道一定会有争抢的话,还不如直接用轻量级锁好一点(单线程的时候效率高,JVM启动的时候会有很多线程竞争,所以jvm启动的时候默认不打开偏向锁,过一段时间会打开,默认的时候为4秒) 轻量级锁:优点在用户空间的锁,不需要用内核资源,因此速度重量级锁要快缺点,自旋和多线程争抢的时候线程仍然在运行会造成资源的浪费 重量级锁:优点他会把没有获取到锁的放到等待队列里面,不会消耗cpu的资源,缺点是耗时需要调用cpu的方法才能实现 1.什么是synchronized java加锁的关键字 2.synchronized有几种写法 1)在方法上直接加上synchronized关键字 2)synchronized(o) 3.synchronized常见的问题 1)是可重入锁 2)程序出现异常锁默认会被释放 3)sychronized锁静态方法锁的是XX.class 4)sychronized方法中的对象不能是String(常量) Integer Long 这些对象 String常量是怕和其他人冲突,Integer是内部做了特殊处理,值的改变,表面上是一个对象实际可能是一个新的对象了 5)锁要进行细化,锁更少的代码,这样效率更高 6)锁的粗化,如果需要加好多个锁就把锁合并 7)sychronized对象不能进行改变,一旦改变锁就锁不住了,因此需要在前面增加final的关键字 4.synchronized底层对对象加锁的流程是什么样子的 就是锁升级的过程 ``` --- ### synchronized异常是如何处理的?是否需要解锁 > synchronized 异常后,跳出synchronized代码块,会释放锁。 > > 不需要进行解锁 --- ### volatile关键字java是怎么实现的 > 1. 保证线程可见性 > 1. MESI缓存一致性协议 > 2. 在对象上添加读写屏障 > 2. 禁止指令重排序 > 1. 当第一个操作为volatile变量的读时,在它之后的任何操作都不能被重排序到volatile读的前面 > 2. 当第二个操作为volatile变量的写时,在它之前的任何操作都不能被重排序到volatile写的后面 > 3. 当第一个操作为volatile写,第二个操作为volatile读时,不能重排序 --- ### 指令重排细节 > 1. 有两种重排:编译时指令重排,运行时指令重排 > 2. 原因 > 1. 编译重排因为java->class的时候会进行拆分 > 2. 运行时重排的原因是,内存速度远远小于cpu的缓存区,因此为了cpu利用率更高进行了指令重排 > 3. 禁止指令重排序 > 1. 当第一个操作为volatile变量的读时,在它之后的任何操作都不能被重排序到volatile读的前面 > 2. 当第二个操作为volatile变量的写时,在它之前的任何操作都不能被重排序到volatile写的后面 > 3. 当第一个操作为volatile写,第二个操作为volatile读时,不能重排序 ``` 1.什么时候会发生指令重排 1) 编译时期进行指令重排 2) 运行的时候进行指令重排 2.为什么会发生指令重排 1) 编译时期进行指令重排:因为java代码翻译成.class的之后会步骤会进行拆分 2) 运行的时候进行指令重排:因为内存的运行速度远远小于cpu的缓存区(寄存器4个) a. 合并读取,当cpu尝试获取内存中的一个值的时候,因为回来的比较慢,因此cpu会去看之后的指令是否与当前指令有关联性,如果没有就会继续往下面执行 b. 合并写,当cpu往内存中写数据的时候发现下面的指令也要写到内存中,因此会等待其他的几个一起进行写入(告诉缓冲区一般是4个,验证方法就是 for循环批量写1000次,每次写6条数据和每次8条数据) 3.常见的指令重排 1) 合并读取 2) 合并写 3) 乱序 4.如何禁止指令重排 1) 在字节码层面增加ACC_VOLATILE 2) jvm 层面在volatile内存区的读写都加屏障 3) 在硬件和os层面 5.什么是MESI(缓存一致性协议,因特尔cpu用的) 1) MESI是在内核级别对对象的标记 6.什么是内存屏障 1) 在读写等操作前后都增加屏障,不允许前后进行乱序 7.什么是缓存行 1) cpu在从内存中获取数据的时候是以缓存行为单位获取的,因此,在考虑MESI的时候就会出现伪共享问题 2) 同一个缓存行有x和y两个值,核1对x进行更新,核2对y进行更新,那么当多线程更新的时候不论核1还是核2对缓存行进行了修改另一个要修改的时候都要重新从内存中获取内容 3) 内核使用极致.高效使用的时候在频繁使用的内容前和后都增加64位的空白内容,保证频繁操作的内容独享内核 ``` --- ### 锁的内部实现 > synchronized的锁的内部是用过三种锁来实现的, > > 1. 偏向锁 > 2. 自旋锁 > 3. 重量级锁 --- ### 介绍一下常见的锁 > synchronized,reentrantLock,countDownLatch > > 1. synchronized是java的关键字锁,采用的是所升级的概念 > 2. reentrantLock等是CAS锁,各自实现了AQS底层用的是,原子操作status,queue和LockSupport来实现的 ```java 1.有几种锁 synchronized 锁 锁的机制是锁升级 reentrantLock CAS的锁 countDownLatch 写法: 2 reentrantLock: Lock lock = new ReentrantLock(); try{ lock.lock(); } finally{ lock.unlock(); } 3 countDownLatch CountDownLatch latch = new CountDownLatch(); latch.countDown(); latch.await(); 4. CyclicBarrier 参数n为等够多少的数量 CyclicBarrier barrier = new CyclicBarrier( n ,new Runnable(){ @Override public void run(){ } }); CyclicBarrier barrier = new CycliBarrier( n ) 5.Phaser 分段并行 继承phaser 比较复杂 6.ReadWrite 读写锁 读的锁是一个共享锁 ReadWriteLock readWriteLock = readWriteLock.readLock(); Lock readLock = readWriteLock.readLock(); Lock writeLock = readWriteLock.writeLock(); 7 Semaphore n是可以同时通过的数量 限流使用的 Semaphore semaphore = new Semaphore(n); semaphore.acquire(); semaphore.release(); 8 Exchanger 交换器 交换的时候是阻塞的 Exchanger<String> exchanger = new Exchanger<>(); s = exchanger.exchange(s); 9 LockSupport 线程锁 LockSuport.park(); LockSuport.unpark();一旦调用了那么线程之后再调用park也不会进行锁 区别: 1.synchronized 加锁方式是所升级 {}中的代码执行结束就是锁结束 2.reentrantLock 加锁方式是CAS 必须需要unlock解锁 有tryLock方法 可以去尝试获取锁 lockinterupptibly 可以去打断 支持公平和非公平的切换 ``` --- ### 介绍一下CAS和AQS > [介绍一下CAS和AQS](https://blog.csdn.net/fz13768884254/article/details/82862437) > > CAS compare and sweep > > 有一个想定直的概念,每一次对数据的变更之前,先去判断当前的值是否是想定的值,如果不是就跳过循环,运用了unSafe方法是原子操作的 > > AQS > > java的自旋锁的具体实现逻辑,有以下几个核心的地方 > > 1. status: int类型存储的是锁的状态,对status的更新操作用的CAS的操作. > 2. queue: 如果加锁失败就会判断是否有等待队列,如果有等待队列就把当前线程放到等待队列里,如果没有就创建队列把当前线程放到等待队列里 > 3. LockSupport: 放入等待队列的线程会调用LockSupport的park方法,而执行结束的方法需要调用LockSupport的unPark方法 --- ### Java读写锁如何实现 > [Java读写锁如何实现](https://blog.csdn.net/fygu18/article/details/81784574) > > 稍后补充 --- ### 介绍一下 ThreadLocal > 1. 他是线程私有的一个对象集合 > 2. ThreadLocalMap > 1. 他的key是弱引用,垃圾回收器扫描到就会清除掉,这个key在线程中用强引用进行关联,如果线程结束,强引用消失弱引用关联的key也会消失. > 2. 他的value是强引用因此用完之后必须要remove ```java ThreadLocal线程独有的 1.原理 ThreadLocal set方法源码理解: Thread t = Thread.currentThread(); //获取当前线程 ThreadLocalMap map = getMap(t); // 从当前线程对象中获取到ThreadLocalMap if (map != null) // map不为空就直接用,为空就创建 map.set(this, value);//如果用当前对象为key获取value有值就更新 else createMap(t, value); ThreadLocal get方法源码理解: Thread t = Thread.currentThread(); //获取当前线程 ThreadLocalMap map = getMap(t); // 从当前线程对象中获取到ThreadLocalMap if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this);// 获取当前对应的key和value if (e != null) { // 如果不为空就返回value @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); 如果map为空就返回默认值 --默认值为null 2.ThreadLocal用途 声明式事务,保证同一个connection 3.ThreadLocal的坑 当使用完之后一定要remove掉,虽然key是弱引用,但是value没有清理机制,因此value还是会存在内存泄露问题,因此使用完后一定要手动执行remove方法 4.ThreadLocal 的key为什么要用弱引用,因为当前线程中定义的ThreadLocal和ThreadLocalMap的key都指向同一个值,区别是线程中ThreadLocal是定义的强引用,而ThreadLocalMap是弱引用,因此当强引用消失的时候,弱引用会被gc进行回收. ``` --- ### 使用 ThreadLocal 存在什么问题,有什么风险 >当使用完之后一定要remove掉,虽然key是弱引用,但是value没有清理机制,因此value还是会存在内存泄露问题,因此使用完后一定要手动执行remove方法 --- ### ThreadPoolExecutor包含哪些参数,各个参数是什么含义 >ThreadPoolExecutor >1.corePoolSize 核心线程池数 >2.MaxPS 最大线程数 >3.keepAliveTime 存活时间 >4.TimeUnit 时间单位 >5.BlockingQueue 任务队列 >6.ThreadFactory 生成线程工厂 >7.RjectStrategy 拒绝策略 >1)Abort(抛异常) Discard(扔掉) DiscardOld(扔掉最老的) CallerRuns() 自定义 > >当核心线程数都在用,并且等待队列满了的时候,如果没有达到最大线程数,就会启动新的线程,如果线程达到了最大线程就会进行拒绝策略,默认的拒绝策略是抛异常 --- ### 线程间相互通信 > [线程间相互通信](https://www.cnblogs.com/bequt/p/5655043.html) > > 线程之前通过wait和notify方法进行线程通讯 --- ### 线程锁 syn 和 lock的区别 > [线程锁 syn 和 lock的区别](https://www.cnblogs.com/baizhanshi/p/6419268.html) > > synchronized,reentrantLock,countDownLatch > > 1. synchronized是java的关键字锁,采用的是所升级的概念 > 2. reentrantLock是CAS锁,实现了AQS底层用的是,原子操作status,queue和LockSupport来实现的 --- ### 多进程、多线程、多进程单线程、单进程多线程,场景举例,怎么选择? > [多进程单线程模型与单进程多线程模型之争](https://blog.csdn.net/hzrandd/article/details/51699067) > > 多进程单线程模型典型代表:**nginx** > 单进程多线程模型典型代表:**memcached** > > 1. 单进程多线程肯定比多进程单线程快一些。 > > 这是因为,**多进程单线程**的CPU切换,是从一个进程到另一个进程,而**单进程多线程**的CPU切换则只在一个进程内,每个进程|线程都有自己的上下文堆栈保存,进程间的切换消耗更大一些。 > > 2. 单进程的缺点 > > **只有一个进程,一旦其中出现一个错误,整个进程都有可能挂掉。你当然可以为ta编写一个“守护程序”来重启,但是重启期间,你的服务器是真的“挂掉了”。** > > --- ### 进程,线程,协程 > [进程,线程,协程](https://www.cnblogs.com/williamjie/p/11195069.html) > > [进程,线程,协程.简书](https://www.jianshu.com/p/6dde7f92951e) > > 进程、线程、协程的对比 > > - 协程既不是进程也不是线程,协程仅仅是一个特殊的函数,协程它进程和进程不是一个维度的。 > - 一个进程可以包含多个线程,一个线程可以包含多个协程。 > - 一个线程内的多个协程虽然可以切换,但是多个协程是串行执行的,只能在一个线程内运行,没法利用CPU多核能力。 > - 协程与进程一样,切换是存在上下文切换问题的。 --- ### 线程切换的上下文要装载什么 > 线程切换的上下文要装载什么 > > 可能还需要更详细一点进程切换,线程切换,携程切换 > > 稍后补充 SP:堆栈指针,指向当前栈的栈顶地址 PC:程序计数器,存储下一条将要执行的指令 EAX:累加寄存器,用于加法乘法的缺省寄存器