java 基础复习 为了面试
- 新建状态(New):线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
- 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
- 运行状态(Running): 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
- 阻塞状态(Blocked): 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
- 等待阻塞 – 通过调用线程的wait()方法,让线程等待某工作的完成。
- 同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
- 其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
- 死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
- 正常停止->利用次数,不建议死循环
- 使用标志位->设置一个标志位
- 不要用stop或者destory 过时方法
乐观锁不是数据库自带的,需要我们自己去实现。乐观锁是指操作数据库时(更新操作),想法很乐观,认为这次的操作不会导致冲突,在操作数据时,并不进行任何其他的特殊处理(也就是不加锁),而在进行更新后,再去判断是否有冲突了。
通常实现是这样的:在表中的数据进行操作时(更新),先给数据表加一个版本(version)字段,每操作一次,将那条记录的版本号加1。
不足:两个事务都读取了数据库的某一行,经过修改以后写回数据库,这时就遇到了数据竞争。
java的乐观锁收拾基于CAS(Compare And Swap)实现的
-
CAS(V,A,B),内存值V,期待值A,修改值B(V是否等于A,等于执行,不等于将B赋给V)
-
ABA问题
- 读取原值
- 通过原子操作比较和替换。
- 虽然比较和替换是原子性,但是读取原值和比较替换这两部不是原子性的,期间原值可能被其他线程修改;
对该变量增加一个版本号,每次修改更新其版本号。JUC包提供了AtomicStampedRefenrence
(2)自旋次数过多
CAS操作在不成功是会重新读取内存值并自旋尝试,当系统并发量非常高每次读取新值又被改动,导致CAS操作失败不断的自旋重试,此时使用CAS并不能提高效率,反而因为自旋多次不如加锁进行操作的效率高。
(3)只能保证一个变量的原子性
当一个变量操作是,CAS可以保证其原子性,但是通多操作多个变量CAS无能为力
可以封装成对象,再对对象进行CAS操作,或者直接加锁。
悲观锁就是在操作数据时,认为此操作会出现数据冲突,所以在进行每次操作时都要通过获取锁才能进行对相同数据的操作。MySQL InnoDB中使用悲观锁
优点与不足:悲观并发控制实际上是”先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增 加产生死锁的机会;另外,在只读型事务处理中由于不会产生冲突,也没必要使用锁,这样做只能增加系统负载;还有会降低了并行性,一个事务如果锁定了某行数 据,其他事务就必须等待该事务处理完才可以处理那行数
- 共享锁是乐观锁的一种:共享锁指的就是对于多个不同的事务,对同一个资源共享同一个锁
- 排它锁:排它锁与共享锁相对应,就是指对于多个不同的事务,对同一个资源只能有一把锁。
乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机智的其实都是提供的乐观锁。 相反,如果经常发生冲突,上层应用会不断进行 retry,这样反而降低了性能,所以这种情况下用悲观锁比较合适
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁;
- 非公平锁:当线程要获取锁是,无视队列顺序直接抢锁,谁抢到就是谁的;
- 独占锁:当多个线程在挣钱锁的过程中,无论是读还是写,只能一个线程获取,其他线程阻塞等待
- 共享锁:允许多个线程同时获取共享资源,采取是乐观锁的机制,共享锁限制写写操作、读写操作, 但不会限制读读操作;
- 可重入锁:一个线程可以多次占用同一个锁,但是解锁时,需要执行相同次数的解锁操作
- 不可重入锁:一个线程不能多次占用同一个锁;
多个线程相互持有对方需要的资源,导致多个线程相互等待,无法继续执行后续任务
产生死锁的4个必要条件:
-
互斥条件:指进程对所分配到的资源进行排它性使用,一段时间资源只能一个线程占用,其他线程需要资源,需要请求等待,直到占有资源被释放;
-
请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而此时请求线程阻塞,但又对自己已获得的其他资源保持不放。
-
不可剥夺:指线程获得的资源,在未使用完之前,不能被剥夺,只能在使用完之后自己释放;
-
循环等待:一个等待一个,产生了一个闭环。
饥饿
指的是线程由于无法获取需要的资源而无法进行执行。
产生饥饿的主要原因
- 高优先级的线程不断抢占资源,低优先级的线程抢不到
- 某个线程一直不释放,导致其他线程无法获取资源
如何避免饥饿
- 使用公平锁分配资源
- 为程序分配足够的系统资源
- 避免持有锁的线程长时间占用锁
活锁
指的是多个线程同时抢占同一个资源,都主动将资源让个其他线程使用,导致这个资源在多个线程来回切换,导致线程因无法获取相应资源而无法继续执行的现象。
如何避免活锁
可以让多个线程随机等待一段时间后再次抢占资源,这样会大大减少线程抢占资源的冲突次数,有效避免活锁的产生。
锁的4种状态
-
无锁状态
没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但是只有一个线程能修改成功。
无锁总是假设对共享资源访问没有冲突的理想状态,无锁策略采用一种称为CAS的技术来保证线程执行的安全性,CAS是无锁技术的关键。
-
偏向锁
对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的是偏向第一个加锁线程,该线程是不会主动释放偏向锁,只有当其他线程尝试竞争偏向锁才会被释放。
偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停偏向锁的线程,然后判断锁对象是否处于被锁定状态,如果线程不处于活动状态,则将对象头设置为无锁状态,并撤销偏向锁。如果处于活动状态,升级为轻量级锁的状态。
-
轻量级锁
指当锁是偏向锁的时候,被第二个线程B访问,此时偏向锁就会升级为轻量级锁,线程B会通过自旋的形式尝试获取锁,线程不会阻塞,从而提升性能。
当前只有一个等待线程,则该线程通过自旋进行等待,但是当自旋超过一定次数,轻量级锁便会升级为重量级锁,当一个线程已持有锁,另一个线程在自旋,而此时第三个线程来访是,轻量级锁也会升级为重量级锁。
-
重量级锁
指当有一个线程获取锁以后,其余所有等待获取该锁的线程都会处于阻塞状态。
重量级锁通过对象内部的监听器实现,而其中监听器的本质是依赖于底层操作系统的Mutex Lock实现的,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。
它的主要特点可以总结为:线程复用,控制最大并发数,管理线程
- 可以降低资源消耗,通过重复使用已经创建的线程避免多次创建和销毁线程所带来的性能开销
- 可以提高响应速度,任务到达时,如果有空闲线程可以直接执行,而不需要等待线程创建时间
- 提高线程的可管理性,线程是稀缺资源,如果对于线程的创建和销毁不加以管理,不仅会消耗系统资源,并且会降低系统的稳定性,使用线程池可以对线程进行统一的分配、调节和监控
ThreadPoolExecutor 对象的7个参数
int corePoolSize
: 线程池中常驻的核心线程数,当线程池线程数达到该值时,就会将任务放入队列int maximumPoolSize
: 线程池中能容纳的同时执行的最大线程数,必须大于等于1long keepAliveTime
: 多余空闲线程的存活时间,当前线程数大于corePoolSize
且空闲时间达到该时间值时,多余线程会被销毁TimeUnit unit
:keepAliveTime
的时间单位BlockingQueue<Runnable> workQueue
: 任务队列,保存提交但尚未执行的任务ThreadFactory threadFactory
: 线程池中创建 工作线程的工厂,一般使用默认工厂RejectedExecutionHandler handler
: 拒绝策略,当队列满时且工作线程等于最大线程数并的处理策略
线程池的工作流程:
- 创建线程池后,等待任务提交
- 当调用
execute()
提交任务时,线程池做出如下判断:- 如果正在运行的线程数小于corePoolSize,立刻创建线程
- 如果正在运行的线程数等于corePoolSize,将任务放入队列
- 如果队列已满并且运行的线程数小于maximumPoolSize,创建非核心线程数来执行任务
- 如果队列已满并且运行的线程数等于maximumPoolSize,按照饱和拒绝策略来拒绝新任务
- 当一个线程执行完成后,会从队列中取下一个任务来执行
- 当线程没有运行超过keepAliveTime时,线程池会判断:
- 如果当前线程数大于corePoolSize,那么这个线程将会被销毁
线程池的4中拒绝策略:
AbortPolicy
:直接抛出RejectedExecutionException
异常,该策略为默认策略CallerRunsPolicy
:”调用者运行策略“,该策略即不会抛弃任务,也不会抛出异常,而是将某些任务回退至调用者,从而降低新的任务流量DiscardOldestPolicy
:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次尝试提交DiscardPolicy
:直接丢弃任务,不予处理也不抛出异常。如果允许任务丢失,这是最好的一种方案
以上拒绝策略均实现了RejectedExecutionHandler
接口
如何配置线程池
-
CPU密集型
CPU密集型任务需要大量的运算,CPU长期保持高负载,阻塞时间较少
那么对于CPU密集型任务,需要通常配置较少的线程数量,一般核心线程数设置为CPU核心数,减少线程上下文的切换
-
IO密集型
IO密集型任务需要大量的IO,也就意味着大量的阻塞,所以在单个线程上运行IO密集型任务会因为等待IO结束导致浪费大量的CPU运算能力
所以在IO密集型任务中使用多线程可以大大加速程序运行,可以配置较多的线程
参考公式为:核心线程数=CPU核心数/(1-阻塞系数)
阻塞系数:0.8~0.9
例如8核心的CPU,则设置核心线程数为8/(1-0.9)=80
创建线程池的7种方式:
- Executors.newFixedThreadPool:创建⼀个固定⼤⼩的线程池,可控制并发的线程数,超出的线程会在队列中等待; 2. Executors.newCachedThreadPool:创建⼀个可缓存的线程池,若线程数超过处理所需,缓存⼀段时间后会回收,若线程数不够,则新建线程;
- Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执⾏顺序;
- Executors.newScheduledThreadPool:创建⼀个可以执⾏延迟任务的线程池;
- Executors.newSingleThreadScheduledExecutor:创建⼀个单线程的可以执⾏延迟任务的线程池;
- Executors.newWorkStealingPool:创建⼀个抢占式执⾏的线程池(任务执⾏顺序不确定)【JDK1.8 添加】。
- ThreadPoolExecutor:最原始的创建线程池的⽅式,它包含了 7 个参数可供设置,上面有讲
-
基本介绍想
ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
-
常见使用场景
- 每个线程需要自己单独的实例
- 实例需要在多个方法中共享,但不希望被多线程共享
- 场景一:存储用户Session
- 场景二:数据库连接,处理数据库事务
- 场景三:数据跨层传递(controller,service,dao)
-
定义
与守护线程相对于的就是用户线程,用户线程可以理解为系统工作的线程,而守护线程守护的就是用户线程。当用户线程全部执行完毕,守护线程才会跟着结束。
CountDownLatch是java中一个协调多线程的工具类,假如多线程在执行后,需要等待所有都执行完再执行下一步,那么就可以使用CountDownLatch。
- RPC 是一种框架或者说一种架构,主要目标就是让远程服务调用更简单、透明,调用远程就像调用本地一样。
- 什么情况下用RPC
- 如果我们开发简单的应用,业务流程简单、流量不大,根本用不着 RPC。
- 当我们的应用访问量增加和业务增加时,发现单机已无法承受,此时可以根据不同的业务(划分清楚业务逻辑)拆分成几个互不关联的应用,分别部署在不同的机器上,此时可能也不需要用到 RPC 。
- 随着我们的业务越来越多,应用也越来越多,应用与应用相互关联调用,发现有些功能已经不能简单划分开,此时可能就需要用到 RPC。
- 比如,我们开发电商系统消息中间件rpc应用场景,需要拆分出用户服务、商品服务、优惠券服务、支付服务、订单服务、物流服务、售后服务等等,这些服务之间都相互调用,这时内部调用最好使用 RPC ,同时每个服务都可以独立部署,独立上线。
- RPC 架构主要包括三部分:
- 服务提供者启动后主动向服务注册中心(Registry)注册机器IP、端口以及提供的服务列表;
- 服务消费者启动时向服务注册中心(Registry)获取服务提供方地址列表。
- 服务注册中心(Registry)可实现负载均衡和故障切换。
- 进程是操作系统分配资源的最小单位,线程是CPU调度的最小单位。
- 一个进程可以包含多个线程。
- 进程与进程之间是相对独立,进程中的线程并不完全独立,可以共享进程中的堆内存、方法区内存、系统资源等;
- 进程上下文切换要比线程的上下文切换慢得多;
- 某个进程发生异常,不会对其他进程造成影响;但某个线程发生异常,可能会对此进程的其他线程产生影响;
-
线程组
线程组可以管理多个线程,顾名思义,把功能相同的线程放在一起,方便管理。
-
线程组与线程池的区别
- 线程组中线程可以跨线程修改数据,但线程组和线程组之间不可用跨线程修改;
- 线程池就是创建一定数量的线程,批量处理任务,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
- 线程池可以有效管理线程的数量,避免线程的无限创建,线程是很耗费系统资源,动不动就产生OOM并且会造成CPU过度切换,也有强大的拓展功能,比如延时定时线程池。
- 并行指多个线程在一段时间的每个时刻都在同时运行,并发指多个线程在一段时间内同时运行(不是同一时刻,一段时间内交叉执行)
- 并行的多个线程不会抢占系统资源,并发的多个线程会抢占系统资源;
- 并行是多cpu的产物,单个CPU中只有并发,没有并行;
-
可见性的定义:一个线程对共享变量的修改,另外一个线程能够立刻看到。
内存并不直接和CPU打交道,而是通过高速缓存与CPU打交道。
cpu <——> 高速缓存 <———> 内存
可见性问题都是由Cpu缓存不一致为并发编程带来,而其中的主要有下面三种情况:
- 线程交叉执行
- 重排序结合线程交叉执行
- 共享变量更新后的值没有在工作内存及主存间及时更新,请参考错误例子VisibilityDemo.java
- VisibilityDemo1加入volatile保证可见性,且只保证可见性;
- VisibilityDemo2换成Atomic相关类,保证原子性和可见性。
- VisibilityDemo3使用synchronized,Lock,保证可见性和原子性
-
Java内存模型对volatile关键字定义了一些特殊的访问规则,当一个变量被volatile修饰后,它将具备两种特性,或者说volatile具有下列两层语义:
- 第一、保证了不同线程对这个变量进行读取时的可见性。即一个线程修改了某个变量的值, 这个新值对其他线程来说是立即可见的。(volatile解决了线程间共享变量的可见性问题)。
- 使用 volatile 关键字会强制将在某个线程中修改的共享变量的值立即写入主内存。
- 使用 volatile 关键字的话, 当线程 2 进行修改时, 会导致线程 1 的工作内存中变量的缓存行无效(反映到硬件层的话, 就是 CPU 的 L1或者 L2 缓存中对应的缓存行无效);
- 由于线程 1 的工作内存中变量的缓存行无效,所以线程1再次读取变量的值时会去主存读取。基于这一点,所以我们经常会看到文章中或者书本中会说volatile 能够保证可见性。
- 第二、禁止进行指令重排序, 阻止编译器对代码的优化。
综上所述:就是用volatile修饰的变量,对这个变量的读写,不能使用 CPU 缓存,必须从内存中读取或者写入。
- 第一、保证了不同线程对这个变量进行读取时的可见性。即一个线程修改了某个变量的值, 这个新值对其他线程来说是立即可见的。(volatile解决了线程间共享变量的可见性问题)。
-
synchronized
-
作用域
- 是某个对象实例,修饰成员方法
- 是某个类的范围,修饰静态方法
-
作用于方法中的某个区块
- 表示只对这个区块的资源实行互斥访问。
-
synchronized关键字是不能继承的,也就是说,基类的方法
-
synchronized f(){ // 具体操作}
在继承类中并不自动是
synchronized f(){ // 具体操作}
而是变成了
f(){ // 具体操作}
综上3点所述:synchronized关键字主要有以下这3种用法:
- 修饰实例方法:作用于当前实例加锁,进入同步代码前要获得当前实例的锁
- 修饰静态方法:作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
- 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁
-
-
JVM关于synchronized的两条规定:
- 线程解锁前,必须把共享变量的最新值刷新到主内存
- 线程加锁时,将清空工作内存中共享变量的值,从而是使用共享变量时,需要从主内存中重新读取最新的值(注意:加锁与解锁是同一把锁)
- 我们在使用synchronized保证可见性的时候也要注意以下几点
- 无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象;而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。
- 每个对象只有一个锁(lock)与之相关联。Java 编译器会在 synchronized 修饰的方法或代码块前后自动加上加锁 lock() 和解锁 unlock(),这样做的好处就是加锁 lock() 和解锁 unlock() 一定是成对出现的,毕竟忘记解锁 unlock() 可是个致命的 Bug(意味着其他线程只能死等下去了)。
- 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制.
-
-
java堆
Java堆是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域唯一的目的就是存放实例对象。 Java堆是垃圾回收管理的主要区域,因此也有“GC堆”的叫法。 根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
-
方法区
方法区与Java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 方法区不需要连续的内存,可以选择固定大小或者可扩展,还可以选择不实现垃圾收集。方法区的内存回收的主要目标是针对常量池的回收和对类型的卸载。 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
-
程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码行号指示器。由于Java虚拟机的多线程是通过线程切换并分配处理器执行时间的方式来实现的,在任何一个确定的实可,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们成这类内存区域为“线程私有内存”。
程序计数器是在java虚拟机规范中唯一一个没有规定任何OutOfMemoryError情况的区域。
-
Java虚拟机栈
Java虚拟机栈也是线程私有的,它的生命周期与线程相同。 虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应这一个栈帧在虚拟机中从入栈到出栈的过程。 虚拟机栈中的局部变量表存放了编译期可知的各种基本数据类型(double,boolean,byte,char,short,int、long,float)、对象引用以及returnAddress类型。其中64位长度的long和double类型的数据会占用2个局部变量空间,其余数据类型只占用1个。局部变量表所需的内存空间在编译期完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间时完全确定的,在方法运行期间不会改变局部变量表的大小。 在java虚拟机规范中,对这个区域规定了两种异常情况:
- 如果线程请求的栈的深度大于虚拟机所允许的深度,将抛出StackOverFlowError异常;
- 如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
-
本地方法栈
本地方法栈与虚拟机栈的作用非常相似,虚拟机栈为执行java方法服务,本地方法栈为虚拟机中使用到的Native方法服务。在有的虚拟机中,比如Sun HotSpot虚拟机,直接把本地方法栈与虚拟机栈合二为一。 本地方法栈区域会抛出StackOverflowError和OutOfMemoryError异常。
-
运行时常量池
运行时常量池是方法区的一部分,class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池。当常量池无法再申请到内存时将抛出OutOfMemoryError异常
在前面的关于JVM的内存结构的图中,我们可以看到,其中Java堆和方法区的区域是多个线程共享的数据区域。也就是说,多个线程可能可以操作保存在堆或者方法区中的同一个数据。这也就是我们常说的“Java的线程间通过共享内存进行通信”。
Java内存模型是根据英文Java Memory Model(JMM)翻译过来的。其实JMM并不像JVM内存结构一样是真实存在的。他只是一个抽象的概念。中描述了,JMM是和多线程相关的,他描述了一组规则或规范,这个规范定义了一个线程对共享变量的写入时对另一个线程是可见的。
那么,简单总结下,Java的多线程之间是通过共享内存进行通信的,而由于采用共享内存进行通信,在通信过程中会存在一系列如可见性、原子性、顺序性等问题,而JMM就是围绕着多线程通信以及与其相关的一系列特性而建立的模型。JMM定义了一些语法集,这些语法集映射到Java语言中就是volatile、synchronized等关键字。
Java是一种面向对象的语言,而Java对象在JVM中的存储也是有一定的结构的。而这个关于Java对象自身的存储模型称之为Java对象模型。
每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass
,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc
对象,这个对象中包含了对象头以及实例数据。
java类保存在方法区,新建的对象保存在java堆区,而对象头指向方法区的java类。这就是一个简单的Java对象模型。
-
synchronized什么时候释放锁?
- 获取锁的线程执行完了该代码块,会调用monitorexit释放锁
- 线程执行出现异常
-
Lock是java实现的类,synchronized是java中的关键字
-
synchronized缺陷?
- Lock可以主动释放锁
- synchronized是被动的
-
Lock的实现
- ReadWriteLock,读可以使用readLock,写可以使用writeLock
- ReentrankLock,可重入锁,当然synchronzied也是可重入锁
- ReentrankReadWriteLock,可重入的读写锁
-
公平锁
- synchronized是非公平锁
- ReentrankLock默认是非公平锁,可以设置为公平锁
-
如何选择
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
是一个分段锁,采用segments数组+HashEntry数组+链表。底层一个Segments数组,存储一个Segments对象,每个segment是继承ReentrantLock互斥锁,具有加解锁的功能,一个Segments中储存一个Entry数组,存储的每个Entry对象又是一个链表头结点。
- put的时候:先对key做hash,找到segment数组中的位置index,然后竞争lock锁,如果获取到了锁,那就获取到了segment桶,然后再次hash确定存放的HashEntry数组的位置,然后插入数组到链表中,如果在链表中找到相同节点,则覆盖,没有就放到链表尾部。
- get操作:get操作不用加锁,先对key做hash,找到segment数组中的位置index,然后再次hash确定存放的HashEntry数组的位置,然后在该位置的链表查找值。
分段锁 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器的Segment的数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
ConcurrentHashMap是Node数组+链表+红黑树,和hashMap1.8版本类似
用Synchronzie同步代码块和cas原子操作维护线程安全。
Node数组使用来存放树或者链表的头结点,当一个链表中的数量到达一个数目时,会使查询速率降低,所以到达一定阈值时,会将一个链表转换为一个红黑二叉树,通告查询的速率。
-
主要属性
LOAD_FACTOR: 负载因子, 默认75%, 当table使用率达到75%时, 为减少table 的hash碰撞, tabel长度将扩容一倍。负载因子计算: 元素总个数%table.lengh TREEIFY_THRESHOLD: 默认8, 当链表长度达到8时, 将结构转变为红黑树。 UNTREEIFY_THRESHOLD: 默认6, 红黑树转变为链表的阈值。 MIN_TRANSFER_STRIDE: 默认16, table扩容时, 每个线程最少迁移table的槽位 个数。 MOVED: 值为-1, 当Node.hash为MOVED时, 代表着table正在扩容 TREEBIN, 置为-2, 代表此元素后接红黑树。 nextTable: table迁移过程临时变量, 在迁移过程中将元素全部迁移到nextTable 上。 sizeCtl: 用来标志table初始化和扩容的,不同的取值代表着不同的含义: 0: table还没有被初始化 -1: table正在初始化 小于-1: 实际值为resizeStamp(n) <<RESIZE_STAMP_SHIFT+2, 表明table正在扩容 大于0: 初始化完成后, 代表table最大存放元素 的个数, 默认为0.75*n transferIndex: table容量从n扩到2n时, 是从索引n->1的元素开始迁移, transferIndex代表当前已经迁移的元素下标 ForwardingNode: 一个特殊的Node节点, 其hashcode=MOVED, 代表着此时 table正在做扩容操作。扩容期间, 若table某个元素为null, 那么该元素设置为 ForwardingNode, 当下个线程向这个元素插入数据时, 检查hashcode=MOVED, 就 会帮着扩容。
-
构造方法:
并没有直接new出来一个Node数组,只检查了数值之后确定容量大小,会在第一次put操作的时候判断Node数组是否为空,才会调用intiTable方法进行初始化。
-
put操作:
判断Node数组是否为空,如果为空调用initTable方法进行初始化,方法里会用cas操作去修改sizeCtl属性的值,如果一个线程修改成-1(表示正在初始化)成功则对Node数组进行初始化。
初始化成功后,根据key的hash值&位运算找到Node数组的位置index,如果index位置上没有元素,则将根据这个key创建一个Node节点使用cas原子操作写入Node数组中,如果已经存在元素,则进入Synchronize同步代码,如果该节点hash不小于0,开始构建链表,判断链表中是否存在新建的Node,如果存在就覆盖,不存在就插入链表的尾部;如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点; 如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,并且容量达到64,则通过treeifyBin方法转化为红黑树存储;如果put的时候遇到了数组在扩容(node数组位置的元素是ForwardingNode节点,该节点的hash值为-1,判断hash==move),当前线程会去帮忙迁移,调用helpTransfer()协助扩容。
-
helpTransfer方法里主要做了如下事情:
- 检查是否扩容完成,对sizeCtrl = sizeCtrl+1, 然后调用transfer()进行真正的扩容。
-
扩容transfer:
-
扩容的整体步骤就是新建一个nextTab, size是之前的2倍, 将table上的非空元素
迁移到nextTab上面去。
-
-
-
get操作:
根据key的hash值&位运算找到Node数组的位置index,然后在该位置的链表中获取,如果Key的值< 0 ,说明是红黑树
-
为什么要用synchronized,cas不是已经可以保证操作的线程安全吗?
CAS也是适用一些场合的,比如资源竞争小时,是非常适用的,不用进行内核态和用户态之间 的线程上下文切换,同时自旋概率也会大大减少,提升性能,但资源竞争激烈时(比如大量线 程对同一资源进行写和读操作)并不适用,自旋概率会大大增加,从而浪费CPU资源,降低性 能
-
java的三个类加载器:
- Bootstrap ClassLoader最顶层的加载类,主要加载核心类库,比如说int类等 这个加载器是由
C++
编写的 - Extention ClassLoader 扩展的类加载器
- Appclass Loader 也称为
SystemAppClass
加载当前应用的classpath
的所有类
- Bootstrap ClassLoader最顶层的加载类,主要加载核心类库,比如说int类等 这个加载器是由
-
我自己的理解是当我加载一个
class
文件的时候,我先看下我之前是不是加载成功过这个class文件,也就是看下我的class
缓存里边有没有,如果有,就直接使用,如果没有,我就让我的父加载器去加载;父加载器的加载过程和我一样,也是先看自己缓存,如果缓存没有,父加载器再让自己的父加载器去加载,这样递归下去,直到让BootstrapClassLoader
加载,它也是先看缓存,缓存没有就通过方法加载;如果BootstrapClassLoader
也加载不到,那就让子加载器加载,也就是ExtentionClassLoader
,这个时候就不是找缓存了,而是通过方法加载,找到就返回,找不到就交给子加载器加载。最后如果都没找到,就抛出异常。- 一个
AppClassLoader
查找资源时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器。 递归,重复第1部的操作。 - 如果
ExtClassLoader
也没有加载过,则由BootstrapClassLoader
出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下,也就是sun.mic.boot.class
下面的路径。找到就返回,没有找到,让子加载器自己去找。 BootstrapClassLoader
如果没有查找成功,则ExtClassLoader
自己在java.ext.dirs
路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。ExtClassLoader
查找不成功,AppClassLoader
就自己查找,在java.class.path
路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。
- 一个
-
Java的动态编译就是在运行期直接编译.java文件,执行.class,并且能够获得相关的输入输出,甚至还能监听相关的事件。
-
创建或自动生成.java文件
-
调用JavaCompiler获取编译器,该类允许开发人员编译java文件为class文件
JavaCompiler compiler=ToolProvider.getSystemJavaCompiler();
-
获取文件管理器StandardJavaFileManager,用来管理要编译的.java文件。getStandardFileManager有3个参数,分别代表监听器、语言环境、字符集
StandardJavaFileManager fileManger = compiler.getStandardFileManager(null, null, null);
-
获取表示给定文件的文件对象
Iterable unils = fileManger.getJavaFileObjects(fileName)
-
获取编译任务的future接口CompilationTask,当调用它的call方法时,开始编译
CompilationTask t = compiler.getTask(null, fileManger, null, null, null, unils);``t.call();
-
调用URLClassLoader将编译的文件load内存,其中需要知道文件的位置,用URL记录,其中“file:/”表示本地文件。
URL[] urls = new URL[]{new URL("file:/"+System.getProperty("user.dir")+"/scr")};
URLClassLoader cl = new URLClassLoader(urls);
-
获取编译后的类
Class c = cl.loadClass("com.chensr.util.function.Function");
-
获取构造函数,构造方法如果有参数需要配置对应的参数类型
Constructor constructor = c.getConstructor(String.class);
Object b =constructor.newInstance("哈哈");
-
执行对应的方法
Method method=c.getDeclaredMethod("方法名","参数类型");
method.invoke(b,"参数值");
-
- GET在浏览器回退时是无害的,而POST会再次提交请求。
- GET产生的URL地址可以被Bookmark,而POST不可以。
- GET请求会被浏览器主动cache,而POST不会,除非手动设置。
- GET请求只能进行url编码,而POST支持多种编码方式。
- GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
- GET请求在URL中传送的参数是有长度限制的,而POST么有。
- 对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
- GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
- GET参数通过URL传递,POST放在Request body中。
HTTP协议中的两种发送请求的方法,所以GET和POST的底层也是TCP/IP