深拷贝和浅拷贝都是对象拷贝,而不是引用拷贝;
引用拷贝:创建一个指向对象的引用变量的拷贝。
对象拷贝:创建对象本身的一个副本。
深拷贝:深拷贝是一个整个独立的对象拷贝,深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。简而言之,深拷贝把要复制的对象所引用的对象都复制了一遍。
浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。即对象的浅拷贝会对“主”对象进行拷贝,但不会复制主对象里面的对象。”里面的对象“会在原来的对象和它的副本之间共享。简而言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象。
参考:Java深入理解深拷贝和浅拷贝区别
浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址。
对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值
浅拷贝是使用默认的 clone() 方法来实现(即不重写):sheep = (Sheep) super.clone();
深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,复制对象的所有基本数据类型的成员变量值为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象(包括对象的引用类型)进行拷贝。使用深拷贝的情况下,不会出现浅拷贝时释放同一个内存的错误。
简单来说就是一句话: 深拷贝和浅拷贝最根本的区别在于是否真正获取一个对象的复制实体,而不是引用。
java中如何实现深拷贝、浅拷贝深拷贝的方式有很多种,以下是三种方式
方法一 构造函数
方法二 重载clone()方法
方法三Serializable序列化
浅拷贝的实现方式主要有2种:
一、通过拷贝构造方法实现浅拷贝:
二、通过重写clone()方法进行浅拷贝:调用对象的 clone 方法,必须要让类实现 Cloneable 接口,并且覆写 clone 方法。
参考:Java的深拷贝与浅拷贝的几种实现方式
- 对于基本数据类型来说System.arraycopy() 方法是深拷贝。
- 对于引用数据类型来说 System.arraycopy() 方法是浅拷贝。
ArrayList.addAll() ArrayLis.clone()两个方法都是对数据的浅拷贝, *** 作的还是同一份内存。
深拷贝 必须new fltbuffer 来接收 bufferData.get(i)
浅拷贝 直接add bufferData.get(i)
序列化
JVM内存结构
【Java】JVM的内存组成
JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
- Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到运行时数据区中的方法区。
- Execution engine(执行引擎):执行字节码中的指令。
- Native Interface(本地接口):与native libraries交互,是与其它编程语言交互的接口。
- Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。
作用 :首先通过编译器把 Java 代码转换成字节码,类加载(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层 *** 作系统去执行,因此需要特定的解释器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
int ,Integer区别,在内存中的位置int是基本数据类型,Integer是其包装类;int 的包装类就是 Integer,从 Java 5 开始引入了自动装箱/拆箱机制,使得二者可以相互转换。
1.基本数据类型存放位置:
方法参数、局部变量存放在栈内存中的栈桢中的局部变量表
常量存放在常量池中
2.Integer存放位置: 常量池、堆内存
面向对象三大特性是封装、继承和多态
-
封装
所谓封装,就是将同一类事物的特征和功能包装在一起,只对外暴露需要调用的接口而已。典型的例子就是定义接口,在接口中,我们没有任何功能的实现,只是定义了一系列的抽象的方法声明。对于使用者来说,只需要知道接口定义了哪些方法,它是做什么用的就可以了。对于接口内部所要执行什么样的 *** 作,使用者无需了解。
封装的好处:
1、实现专业的分工
2、减少代码耦合
3、可以自由修改类的内部结构 -
继承
继承是Java中面向对象最显著的一个特征。继承是从已有的类中派生出新的类,新的类能够吸收已有类的属性和行为,并扩展新的能力。
Java中类是不支持多继承的,一个类只能有一个父类,父类是子类的抽象化,子类是父类的具体化;但接口可以支持多继承,一个接口可以继承多个其他接口。 -
多态
多态是三大特性中最重要的 *** 作,前面的封装和继承都是为多态服务的;
多态是同一个行为具有多个不同表现形式或形态的能力;
多态是同一个接口,使用不同的实例而执行不同 *** 作;
抽象类是用来捕捉子类的通用特性的,实现代码重用。接口是抽象方法的集合,利用接口可以达到 API 定义和实现分离的目的,提供程序的扩展性和可维护性。
从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
-
相同点
接口和抽象类都不能实例化
都位于继承的顶端,用于被其他类实现或继承
都包含抽象方法,其子类都必须重写这些抽象方法 -
不同点
备注:Java8中接口中引入默认方法和静态方法,以此来减少抽象类和接口之间的差异。现在,我们可以为接口提供默认实现的方法了,并且不用强制子类来实现它。
接口和抽象类各有优缺点,在接口和抽象类的选择上,必须遵守下面的几个原则:
抽象类用来定义某个领域的固有属性,即抽象类表示它是什么,接口用来定义某个领域的扩展功能,即接口表示它能做什么。
当需要为子类提供公共的实现代码时,应优先考虑抽象类。因为抽象类中的非抽象方法可以被子类继承,使实现功能的代码更简洁。
当注重代码的扩展性和可维护性时,应当优先采用接口。①接口与实现类之间可以不存在任何层次关系,接口可以实现毫不相关类的行为,比抽象类的使用更加方便灵活;②接口只关心对象之间的交互方法,而不关心对象所对应的具体类。接口是程序之间的一个协议,比抽象类的使用更安全、清晰。一般使用接口的情况更多。
(1)如果在一个子类继承的多个父类中拥有相同名字的实例变量,子类在引用该变量时将产生歧义,无法判断应该使用哪个父类的变量
(2)如果在一个子类继承的多个父类中拥有相同方法,子类中有没有覆盖该方法,那么调用该方法时将产生歧义,无法判断应该调用哪个父类的方法。
正因为有以上的致命缺点,所以java中禁止一个类继承多个父类;
在接口中不能有实例变量,只能有静态的常量,不能有具体的方法(包含方法体),只能有抽象方法,因此也就摒弃了多继承的缺点。
对于一个类实现多个接口的情况,因为接口只有抽象方法,具体方法只能由实现接口的类实现,在调用的时候始终只会调用实现类的方法(不存在歧义),因此不存在 多继承的第二个缺点;
而又因为接口只有静态的常量,但是由于静态变量是在编译期决定调用关系的,即使存在一定的冲突也会在编译时提示出错;
而引用静态变量一般直接使用类名或接口名,从而避免产生歧义,因此也不存在多继承的第一个缺点。对于一个接口继承多个父接口的情况也一样不存在这些缺点。
在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在 Java 中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。
线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取,线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。Java 5+中的 Executor 接口定义一个执行线程的工具。它的子类型即线程池接口是 ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类 Executors 中提供了一些静态工厂方法,生成一些常用的线程池,如下所示:
(1)newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
(2)newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。如果希望在服务器上使用线程池,建议使用 newFixedThreadPool方法来创建线程池,这样能获得更好的性能。
(3) newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于 *** 作系统(或者说 JVM)能够创建的最大线程大小。
(4)newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
线程池工作流程
线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
当调用 execute() 方法添加一个任务时,线程池会做如下判断:
如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
如果队列满了,而且正在运行的线程数量等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。
当一个线程完成任务时,它会从队列中取下一个任务来执行。
当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
常用的四种线程池
newCachedThreadPool——可缓存线程池
newFixedThreadPool————指定线程数量
newSingleThreadExecutor————单线程的Executor
newScheduleThreadPool——定时线程池
线程池七大参数
corePoolSize——核心线程最大数
maximumPoolSize——线程池最大线程数
keepAliveTime——空闲线程存活时间。
unit——空闲线程存活时间单位
workQueue——等待队列
threadFactory
handler——拒绝策略
使用Java命令启动应用的时候适当的配置你的参数能有效提高系统的性能。
Xms:是指程序启动时初始内存大小(此值可以设置成与-Xmx相同,以避免每次GC完成后 JVM 内存重新分配)。
Xmx:指程序运行时最大可用内存大小,程序运行中内存大于这个值会 OutOfMemory。
Xmn:年轻代大小(整个JVM内存大小 = 年轻代 + 年老代 + 永久代)。
XX:NewRatio:年轻代与年老代的大小比例,-XX:NewRatio=3 设置为3,则年轻代与年老代所占比值为1:3。
XX:SurvivorRatio:年轻代中Eden区与Survivor区的大小比值,-XX:SurvivorRatio=4,设置为4,则两个Survivor区与一个Eden区的比值为 2:4
XX:MaxPermSize:设置永久代大小。
XX:MaxTenuringThreshold:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
Xss:设置每个线程的堆栈大小。
IO密集型任务尽量加大线程数,因为io不占用cpu的资源
讲讲线程安全线程安全是编程中的术语,指某个方法在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。
Struts2 的 action 是多实例多线程的,是线程安全的,每个请求过来都会 new 一个新的 action 分配给这个请求,请求完成后销毁。
SpringMVC 的 Controller 是线程安全的吗?不是的,和 Servlet 类似的处理流程。
Struts2 好处是不用考虑线程安全问题;Servlet 和 SpringMVC 需要考虑线程安全问题,但是性能可以提升,不用处理太多的 gc,可以使用 ThreadLocal 来处理多线程的问题。
双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去加载,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载器无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载此类。
自下而上检查类是否已经被加载,自上而下尝试加载类
双亲委派模型工作流程:
当Application ClassLoader 收到一个类加载请求时,他首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器Extension ClassLoader去完成。
当Extension ClassLoader收到一个类加载请求时,他首先也不会自己去尝试加载这个类,而是将请求委派给父类加载器Bootstrap ClassLoader去完成。
Bootstrap ClassLoader尝试加载此类,如果Bootstrap ClassLoader加载失败,就会让Extension ClassLoader尝试加载。
Extension ClassLoader尝试加载此类,如果Extension ClassLoader也加载失败,就会让Application ClassLoader尝试加载。
Application ClassLoader尝试加载此类,如果Application ClassLoader也加载失败,就会让自定义加载器尝试加载。
如果均加载失败,就会抛出ClassNotFoundException异常。
双亲委派模型的好处:保证核心类库不被覆盖。如果没有使用双亲委派模型,由各个类加载器自行加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统将会出现多个不同的Object类, Java类型体系中最基础的行为就无法保证,应用程序也将会变得一片混乱。
集合:用于存储数据的容器。
集合和数组的区别:
- 数组是固定长度的;集合是可变长度的。
- 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
- 数组是Java语言中内置的数据类型,是线性排列的,执行效率和类型检查都比集合快,集合提供了众多的属性和方法,方便 *** 作。
联系:通过集合的toArray()方法可以将集合转换为数组,通过Arrays.asList()方法可以将数组转换为集合
谈谈List,Set,Map以及三者的区别Java 集合容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue,Map接口不是collection的子接口。
Collection集合主要有List、Set和Queue接口
- List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、linkedList 和 Vector。
- Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。常用实现类有 HashSet、linkedHashSet 以及 TreeSet。
- Queue/Deque,则是 Java 提供的标准队列结构的实现,除了集合的基本功能,它还支持类似先入先出(FIFO, First-in-First-Out)或者后入先出(LIFO,Last-In-First-Out)等特定行为。常用实现类有ArrayDeque、ArrayBlockingQueue、linkedBlockingDeque
Map是一个键值对集合,存储键和值之间的映射。Key无序,唯一;value 不要求有序,允许重复。Map没有继承Collection接口,从Map集合中检索元素时,只要给出键对象,就能返回对应的值对象。
常用实现类有HashMap、linkedHashMap、ConcurrentHashMap、TreeMap、HashTable
Map
- HashMap:JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8),但是数组长度小于64时会首先进行扩容,否则会将链表转化为红黑树,以减少搜索时间
- linkedHashMap:linkedHashMap 继承自 HashMap,它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,linkedHashMap 在上面结构的基础上,增加了一条双向链表,使得linkedHashMap可以保持键值对的插入顺序。
- HashTable:数组+链表组成的,数组是 HashTable 的主体,链表则是主要为了解决哈希冲突而存在的
- TreeMap:基于红黑树(自平衡的排序二叉树)实现。
hash碰撞与扩容导致
讲讲线程安全的HashMap四种线程安全的 hashmap
- hashtable采用synchronized方法上加锁,使用阻塞同步,效率低。
- collections.synchronizedMap(map)也是采用synchronized方法上加锁,使用阻塞同步,效率低。
- CopyonWriteMap (读写分离思想)
优点:写时不用加锁,即查询不加锁,在查询多,增删改少的情况下适合用。缺点(问题):内存占用和数据一致性 - ConcurrentHashMap采用锁分段技术,减小锁的粒度,效率高ConcurrentHashMap中是一次锁住一个桶。
可重入锁:可以再次进入方法 A,就是说在释放锁前此线程可以再次进入方法 A(方法 A 递归)。
不可重入锁(自旋锁):不可以再次进入方法 A,也就是说获得锁进入方法 A 是此线程在释放锁前唯一的一次进入方法 A。
公平锁:是指多个线程在等待同一个锁时,必须按照申请锁的先后顺序来一次获得锁。所用公平锁就好像在餐厅的门口安装了一个排队的护栏,谁先来的谁就站的靠前,无法进行插队,当餐厅中的人用餐结束后会把钥匙交给排在最前边的那个人,以此类推。公平锁的好处是,可以保证每个排队的人都有饭吃,先到先吃后到后吃。但是弊端是,要额外安装排队装置。
非公平锁:理解了公平锁,非公平锁就很好理解了,它无非就是不用排队,当餐厅里的人出来后将钥匙往地上一扔,谁抢到算谁的。但是这样就造成了一个问题,那些身强体壮的人可能总是会先抢到钥匙,而那些身体瘦小的人可能一直抢不到,这就有可能将一直抢不到钥匙,最后导致需要很长时间才能拿到钥匙甚至一直拿不到直至饿死。
相同点:两者都是可重入锁。
主要区别如下:
本质:synchronized 是关键字,ReetrantLock是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比 synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的变量
加锁和释放锁:ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动开启和释放锁;
作用域:ReentrantLock 只能给代码块加锁,而 synchronized 可以给方法、代码块加锁。
性能:早期版本 synchronized 在很多场景下性能较差,在后续版本进行了较多改进,在低竞争场景中表现可能优于 ReentrantLock。
底层实现:二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的 park 方法加锁,synchronized *** 作的是对象头中 mark word
乐观锁和悲观锁悲观锁:假定会发生并发冲突,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做 *** 作之前先上锁。再比如 jdk1.6 以前的同步原语 synchronized 关键字的实现也是悲观锁。
乐观锁:假设不会发生并发冲突,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制是乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子 *** 作类就是使用了乐观锁的一种实现方式 CAS。
乐观锁的实现方式:
1、版本号机制:一般是在数据表中加上一个版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读到的version值与当前数据库中的version值相等时才更新,否则重试更新 *** 作,直到更新成功。
2、CAS算法:java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS *** 作包含三个 *** 作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值 V 和 预期原值 A 的值是一样的,那么就将内存里面的值 V 更新成新值 B。CAS是通过无限循环来获取数据的,如果在第一轮循环中,a 线程获取地址里面的值被 b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行。
CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。
CountDownLatch类只提供了一个构造器:
CyclicBarrier 字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了。
CyclicBarrier类位于java.util.concurrent包下,CyclicBarrier提供2个构造器。
Semaphore翻译成字面意思为 信号量,Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。
Semaphore类位于java.util.concurrent包下,它提供了2个构造器。
acquire()用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。 release()用来释放许可。注意,在释放许可之前,必须先获获得许可。
根本区别:进程是 *** 作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
资源开销和内存分配:每个进程都有独立的代码和数据空间(程序上下文),进程之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:线程是进程的一部分。一个进程至少有一个线程,一个进程可以运行多个线程。
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序入口、顺序执行序列和程序出口。线程不能独立执行,必须依存在进程中,由进程提供多个线程执行控制,两者均可并发执行。
进程之于程序,犹如线程之于函数;
进程是程序的并发,线程是函数的并发;
同一进程中的线程之间的关系,犹如同一程序中函数的关系。
- 继承Thread类创建线程
- 实现Runnable接口创建线程
- 通过Callable和Future创建线程
- 通过线程池创建线程
start()方法 ;run()方法 ;isAlive()方法
每个线程都是通过某个特定Thread对象所对应的方法run()来完成其 *** 作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。也就是说start() 方法用于启动线程,run() 方法用于执行线程任务。run() 可以重复调用,而 start() 只能调用一次。
start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码;此时线程是处于就绪状态,并没有运行。然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。
run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接调用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。
- 新建(new):新创建了一个线程对象。
- 可运行(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。
- 运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
- 阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其再次进入到就绪状态,才有机会再次被 CPU 调用以进入到运行状态。
阻塞的情况分三种:
(一). 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态;(二). 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;(三). 其他阻塞:通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。 - 死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期,死亡的线程不可再次复生。
关于线程生命周期的不同状态,在 Java 5 以后,线程状态被明确定义在其公共内部枚举类型 java.lang.Thread.State 中,分别是:
- 新建(NEW),表示线程被创建出来还没真正启动的状态,可以认为它是个 Java 内部状态。
- 就绪(RUNNABLE),表示该线程已经在 JVM 中执行,当然由于执行需要计算资源,它可能是正在运行,也可能还在等待系统分配给它 CPU 片段,在就绪队列里面排队。
在其他一些分析中,会额外区分一种状态 RUNNING,但是从 Java API 的角度,并不能表示出来。 - 阻塞(BLOCKED),阻塞表示线程在等待 Monitor lock。比如,线程试图通过 synchronized 去获取某个锁,但是其他线程已经独占了,那么当前线程就会处于阻塞状态。
- 等待(WAITING),表示正在等待其他线程采取某些 *** 作。一个常见的场景是类似生产者消费者模式,发现任务条件尚未满足,就让当前消费者线程等待(wait),另外的生产者线程去准备任务数据,然后通过类似 notify 等动作,通知消费线程可以继续工作了。Thread.join() 也会令线程进入等待状态。
- 计时等待(TIMED_WAIT),其进入条件和等待状态类似,但是调用的是存在超时条件的方法,比如 wait 或 join 等方法的指定超时时间
- 终止(TERMINATED),不管是意外退出还是正常执行结束,线程已经完成使命,终止运行,也有人把这个状态叫作死亡。
在某些特定的情况下能
要使 volatile 变量提供理想的线程安全性,必须同时满足两个条件:
①对变量的写 *** 作不依赖于当前值。
②该变量没有包含在具有其他变量的不变式中。
其他情况下volatile并不能保证线程安全问题,因为volatile并不能保证变量 *** 作的原子性。
ThreadLocal 是一个本地线程局部变量工具类,在每个线程中都创建了一个 ThreadLocalMap 对象,简单说 ThreadLocal 就是一种以空间换时间的做法,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享。
原理:线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。
经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的 *** 作,不会出现 A 线程关了 B 线程正在使用的 Connection;还有 Session 管理 等问题。
进程间通信 线程间通信 说下信号量怎么实现Java实现线程间通信信号量
JVM中怎么定位内存泄漏可以添加-verbose:gc启动参数来输出Java程序的GC日志。通过分析这些日志,可以知道每次GC后内存是否有增加,如果在缓慢的增加的那,那就有可能是内存泄漏了(当然也需要结合当前的负载)。如果无法添加这个启动参数,也可以使用jstat来查看实时的gc日志。如果条件运行的化可以考虑使用jvisualvm图形化的观察,不过要是线上的化一般没这个条件。
当通过dump出堆内存,然后使用jvisualvm查看分析,一般能够分析出内存中大量存在的对象以及它的类型等。我们可以通过添加-XX:+HeapDumpOnOutOfMemoryError启动参数来自动保存发生OOM时的内存dump。
当确定出大对象,或者大量存在的实例类型以后,我们就需要去review代码,从实际的代码入手来定位到真正发生泄漏的代码。
线程间通信下 或者设置一个标记?
JVM垃圾回收细说一下Java的垃圾回收机制及常用的垃圾回收算法
什么是反射和反射原理JAVA反射机制是在程序运行过程中,对于任意一个类或对象,都能够知道这个类或对象的所有属性和方法,这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
反射为什么慢- 反射调用过程中会产生大量的临时对象,这些对象会占用内存,可能会导致频繁 gc,从而影响性能。
- 反射调用方法时会从方法数组中遍历查找,并且检查可见性等 *** 作会比较耗时。
- 反射在达到一定次数时,会动态编写字节码并加载到内存中,这个字节码没有经过编译器优化,也不能享受 JIT 优化。
- 反射一般会涉及自动装箱/拆箱和类型转换,都会带来一定的资源开销。
//压测机瓶颈原因
两台机器的性能不同,设置相同的线程数,会跑出不同的QPS。
增加线程数,但是QPS不增加,或者增加线程数,QPS不是等比例增加,此时就证明压测机出现瓶颈,原因有以下几方面
1、带宽影响:所有接口每秒响应数据,或者请求数据,超过了带宽可以传输的最大值。比如当前网速的下载速度是5M/s,而接口总的响应数据是6M/s,则有5M可以在一秒内响应成功,另外1M的接口只能等到下一秒
2、cpu性能影响:当总的响应数据或请求数据未超出带宽速率时,则有可能是压测机的CPU占用率太高,即CPU的性能已经不足以支撑更高的并发率导致QPS上不去。压测时可以同时查看CPU的占用率,如果一直持续在100%则说明已经达到了CPU的性能极限。
3、运行内存:压测机的运存太小,压测时无法存入更多的对象、变量、数据等,导致内存溢出,只能等待内存释放,才能继续存入。
4、磁盘容量:磁盘剩余空间不足,导致接口不能继续写入数据。
5、磁盘读写速度:压测的接口需要读取或者写入数据到本地文件。
http协议属于明文传输协议,交互过程以及数据传输都没有进行加密,通信双方也没有进行任何认证,通信过程非常容易遭遇劫持、监听、篡改,严重情况下,会造成恶意的流量劫持等问题,甚至造成个人隐私泄露(比如yhk卡号和密码泄露)等严重的安全问题。
我们的安全方案有token,加密(混合加密),签名,混淆,重复请求校验等
对于一般应用来说,基本上做到以下点就够了:
代码混淆,最直接的方式,可以保护加密解密签名算法
不直接使用userId等字段,而是有时效的临时token
密码,token等敏感数据配合时间戳,干扰字符进行加密
对整个请求体进行签名,防止被修改
服务端通过签名,时间戳进行完整性,时效性和重复校验
向权威CA机构申请SSL证书,服务器使用Https协议
HTTPS:是以安全为目标的HTTP通道,简单讲是HTTP的安全版,即HTTP下加入SSL层,HTTPS的安全基础是SSL,HTTPS就是从HTTP加上加密处理(一般是SSL安全通信线路)+认证+完整性保护
HTTPS协议的主要作用可以分为两种:一种是建立一个信息安全通道,来保证数据传输的安全;另一种就是确认网站的真实性。
HTTPS在传输数据之前需要客户端(浏览器)与服务端(网站)之间进行一次握手,在握手过程中将确立双方加密传输数据的密码信息。
浏览器输入URL后所有过程一、进行域名解析(获取IP地址)
二、应用层-浏览器发送HTTP请求
三、传输层-TCP或者UDP封装数据(HTTP请求报文)
四、网络层-IP协议封装IP地址,获取目的MAC(media access control address)地址
五、链路层-(又名数据链路层、网络接口层)建立TCP连接
把网络层交下来的IP数据添加首部和尾部,封装为MAC帧,根据目的MAC地址建立TCP连接(三次握手)
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)