一、jvm的内存模型:
堆、栈、方法区、本地方法栈、程序计数器。
Java 堆和方法区是 线程共享的。其他都是 线程私有的。
1)Java 堆存的是:对象实例及数组--GC堆(GC回收的地方)。
Java堆 = 老年代 + 新生代
新生代 = Eden + S0 + S1
虚拟机配置比例默认Eden:from :to = 8:1:1
2)方法区存的是:类信息(方法数据、构造函数、字段、常量池、普通方法、特殊方法)、静态变量。
jdk1.7--永久代,jdk1.8--元空间(默认21MB)。
1.7存在永久代;1.8没有永久代,而是元空间,元空间不占虚拟机内存,占本地内存空间。
3)虚拟机栈存的是:局部变量表(基本数据类型、对象的引用)、操作数栈、动态链接、方法出口。
4)本地方法区存的是:Native方法,线程私有。
5)程序计数器存的是:行号指示器,线程私有。
JVM优化:通过Java -server和java -client设置JVM的运行参数(-xint、-Xcomp、-Xmixed)。
内存相关从参数优化。
新生代垃圾回收参数优化。
老年代垃圾回收参数优化。
过程:性能监控、性能分析、性能调优。
基础性能的调优、数据库性能优化、应用架构优化、业务层面的优化。
jvm调优工具:jconsole、VisualVM、jps、jmap、jstat(看内存使用情况)。
Java垃圾回收(GC)机制:
①引用计数法,很难解决对象之间相互引用的情况。计数+1,-1
②可达性算法:基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的。
以GC Roots的对象为起始点,一层一层的引用链往下搜索,对象没有被引用时则标记然后回收掉!!!
垃圾收集算法:GC清除对象算法:年轻代(复制算法)年老代(标记-清除)(升级版:标记-整理)
①标记-清除(Mark-Sweep)算法:
分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。
缺点:效率不高,易产生内存碎片。
②复制(Copying)算法:回收新生代
它将可用的内存分为两块,每次只用其中一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次性清理掉。
缺点:内存缩小为原来的一半
③标记-整理(Mark-Compact)算法:回收老年代
让所有存活对象都向一端移动,然后直接清理掉边界以外的内存。
把存活的对象移到一边,以最后一个对象为边界,然后把外面的可回收对象全都回收掉。内存不会碎。
④分代收集算法:
大批对象死去、少量对象存活的(新生代),使用复制算法,复制成本低;对象存活率高、没有额外空间进行分配担保的(老年代),采用标记-清理算法或者标记-整理算法。
二、进程与线程
进程与线程的关系: 进程(Process)是操作系统分配资源的基本单位,一个进程拥有的资源有自己的堆、栈、虚存空间(页表)、文件描述符等信息。 线程是进程中执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一进程中的多个线程之间可以并发执行。
进程和线程的区别:
区别1:从属关系不同从属关系不同: 进程是正在运行程序的实例,进程中包含了线程,而线程中不能包含进程。
区别2:描述侧重点不同描述侧重点不同: 进程是操作系统分配资源的基本单位,而线程是操作系统调度的基本单位。
区别3:共享资源不同共享资源不同: 多个进程间不能共享资源,每个进程有自己的堆、栈、虚存空间(页表)、文件描述符等信息,而线程可以共享进程资源文件(堆和方法区)。
区别4:上下文切换速度不同上下文切换速度不同: 线程上下文切换速度很快(上下文切换指的是从一个线程切换到另一个线程),而进程的上下文切换的速度比较慢。
区别5:操纵者不同操纵者不同: 一般情况下进程的操纵者是操作系统,而线程的操纵者是编程人员。
总结:进程是操作系统分配资源的基本单位,而线程是操作系统调度的基本单位。一个进程中至少包含一个线程,线程不能独立于进程而存在。进程不能共享资源,而线程可以。线程可以看作是轻量级的进程,它们的主要区别体现在:从属关系、描述侧重点、共享资源、上下文切换速度和操纵对象等不同。 线程和进程的区别在于,子进程和父进程有不同的代码和数据空间,而多个线程则共享数据空间,每个线程有自己的执行堆栈和程序计数器为其执行上下文.多线程主要是为了节约CPU时间,发挥利用,根据具体情况而定. 线程的运行中需要使用计算机的内存资源和CPU。
并发编程:主要是通过多线程的技术将CPU的性能发挥到极致,提升系统性能,高并发的系统开发并发编程会显得尤为重要。并发编程会遇到的问题:内存泄露(内存没有办法被回收),线程安全,死锁。
线程同步的方式: 同步方法(synchronized)、同步代码块(synchronized)、使用特殊域变量(volatile)实现线程同步、 使用重入锁(ReenreantLock)实现线程同步、使用局部变量(ThreadLocal)实现线程同步、 使用阻塞队列实现线程同步、使用原子变量实现线程同步
三、线程与线程池
1、线程的状态:新建,就绪(start),运行(run),阻塞(等待/同步/睡眠),终止。
2、线程的实现方式:
①继承Thread类创建线程类,重写Thread类的run方法,创建Thread子类对象,调用线程对象start方法,启动线程。
②实现Runnable接口,重写Runnable接口的run方法,通过Thread类含参构造器创建线程对象,将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中,调用Thread类的start方法。
③通过Callable和Future创建线程,实现call()方法。
④使用线程池。
3、线程调度:Thread类中提供的。
线程的优先级等级:线程创建时继承父线程的优先级。低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用。
MAX_PRIORITY:10 最大
MIN _PRIORITY:1 最小
NORM_PRIORITY:5 正常
涉及的方法:
getPriority() :返回线程优先值。
setPriority(int newPriority) :改变线程的优先级。
4、线程分类:
守护线程:用来服务用户线程的,在start()方法前调用。thread.setDaemon(true)可以把一个用户线程变成一个守护线程。
用户线程:主线程
5、线程常用方法:sleep睡眠;yield让步;join合并;run方法结束。
6、线程池的创建方式:
单个线程: Executors.newSingleThreadExecutor();
缓存线程: Executors.newCachedThreadPool();
固定线程:Executors.newFixedThreadPool(2);
定时线程: Executors.newScheduledThreadPool(3);(父类中)
spring线程池: 线程池的启用有两种方式:配置文件或者注解@EnableAsync
7、线程安全与不安全:这就是锁!!!
安全(同步):同步机制
1)同步代码块:synchronized (对象){// 需要被同步的代码;}
2)同步方法:synchronized
区别: 锁块和锁方法的区别:
* 1:都是线程安全锁,可以解决并发问题
* 2:锁的范围不同,锁方法的范围大,锁块的范围小
不安全:在多个线程同时访问同一个对象时会发生数据错误、不完整等情况时,那就是线程不安全,不会发生上续错误时是线程安全的,一般采用锁机制。是不提供代码数据访问保护,可能出现多个线程先后访问更改数据造成所得的数据是脏数据!!!
8、线程的死锁:锁里套锁。A包含B,B包含A,然后相互调用。
解决的方法:
专门的算法、原则
尽量减少同步资源的定义
尽量避免嵌套同步。
9、synchronized 与 Lock 的对比:
synchronized: 底层使用指令码方式来控制锁的。Lock: 底层是CAS乐观锁。
Synchronized是关键字,内置语言实现,Lock是接口。
Lock是可以中断锁,Synchronized是非中断锁,必须等待线程执行完成释放锁。
Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
Lock只有代码块锁,synchronized有代码块锁和方法锁
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
10、volatile 关键字的作用:
只能保证可见性,无法保证原子性。线程可见,排队有序。
作用:禁止进行指令重排序;线程可见性。
四、锁
1、乐观锁与悲观锁:
悲观锁认为对于同一个数据的并发操作一定是会发生修改的,采取加锁的形式,悲观地认为,不加锁的并发操作一定会出问题。synchronized 关键字和 Lock 接口相关类。
乐观锁正好和悲观锁相反,它获取数据的时候,并不担心数据被修改,每次获取数据的时候也不会加锁,只是在更新数据的时候,通过判断现有的数据是否和原数据一致来判断数据是否被其他线程操作,如果没被其他线程修改则进行数据更新,如果被其他线程修改则不进行数据更新。Lock 是乐观锁的典型实现案例。
2、行锁与表锁:
表级锁,一般是指表结构共享锁,是不可对该表执行DDL操作,但对DML操作都不限制。 行级锁之前需要先加表结构共享锁。锁定整个表,限制对于其他用户对表的访问。 粒度最大 的一种锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。但触发锁冲突的概率最高,并发度最低。
行级锁,一般是指排它锁,即被锁定行不可进行修改,删除,只可以被其他会话select。行级锁之前需要先加表结构共享锁。对目前被修改的行进行锁定,其它用户可访问被锁定的行以外的行。 粒度最小 的一种锁,只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。并发度高,但加锁的开销也最大,加锁慢,会出现死锁。
3、分布式锁:通过互斥来保持一致性
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性。
Redis中可以使用SETNX命令实现分布式锁, watch和事务实现分布式锁,Redission实现分布式锁。
五、集合
1)ArrayList与LinkedList的区别:
1、ArrayList的实现是基于动态数组,LinkedList的实现是基于双向链表。
2、对于随机访问,ArrayList优于LinkedList,因为ArrayList直接通过数组下标直接找到元素;LinkedList要移动指针遍历每个元素直到找到为止。
3、对于插入和删除操作,LinkedList优于ArrayList(理论上),因为ArrayList在新增和删除元素时,可能扩容和复制数组;LinkedList实例化对象需要时间外,只需要修改指针即可。
4、LinkedList比ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
5、LinkedList有序,常用于栈(Stack)与队列(Queue)
相同点:两者都是非线程安全的,可以使用synchronized关键字是用来控制线程同步的。
ArrayList的扩容机制:初始 10 jdk8用的时候才给。调用 grow()方法进行扩容。
如果当前数组大小大于数组初识容量(比如初识容量为10,当添加第11个元素时)就会进行扩容,新的容量为旧的容量的1.5倍。
扩容时,会以新的容量创建一个原数组的拷贝,将原数组的数据拷贝过来,原数组就会被抛弃,会被GC回收。
2)HashMap与ConcurentHashMap的区别:
1、HashMap允许null键和null值,线程是不安全的,数据结构是数组+链表+红黑树,效率非常高,没有同步方式;
2、ConcurentHashMap不允许null键和null值,线程是安全的,数据结构是数组+链表+红黑树,效率高,同步方式分为:1.7版本是基于segment分段锁机制,基于ReentrantLock实现;1.8版本是基于CAS+synchronized实现,空节点插入使用CAS,有Node节点则使用synchronized加锁。
3、扩容机制:
①HashMap的扩容机制:
capacity 即容量,默认16。
loadFactor 加载因子,默认是0.75
threshold 阈值。阈值=容量*加载因子。默认12。当元素数量超过阈值时便会触发扩容。
如果某个桶内的元素超过8个,数组值大于64,二个同时满足,则会将链表转化成红黑树,加快数据查询效率。
②ConcurrentHashMap的扩容机制:transfer 方法!
计算每个线程可以处理的桶区间。默认 16.
初始化临时变量 nextTable,扩容 2 倍。
死循环,计算下标。完成总体判断。
1 如果桶内有数据,同步转移数据。通常会像链表拆成 2 份。