JVM
JVM也是一个软件,不同的平台有不同的版本。我们编写的Java源码,编译后会生成一种 .class 文件,称为字节码文件。Java虚拟机就是负责将字节码文件翻译成特定平台下的机器码然后运行。也就是说,只要在不同平台上安装对应的JVM,就可以运行字节码文件,运行我们编写的Java程序。
而这个过程中,我们编写的Java程序没有做任何改变,仅仅是通过JVM这一”中间层“,就能在不同平台上运行,真正实现了”一次编译,到处运行“的目的。
JVM是一个”桥梁“,是一个”中间件“,是实现跨平台的关键,Java代码首先被编译成字节码文件,再由JVM将字节码文件翻译成机器语言,从而达到运行Java程序的目的。
注意:编译的结果不是生成机器码,而是生成字节码,字节码不能直接运行,必须通过JVM翻译成机器码才能运行。不同平台下编译生成的字节码是一样的,但是由JVM翻译成的机器码却不一样。
所以,运行Java程序必须有JVM的支持,因为编译的结果不是机器码,必须要经过JVM的再次翻译才能执行。即使你将Java程序打包成可执行文件(例如 .exe),仍然需要JVM的支持。
注意:跨平台的是Java程序,不是JVM。JVM是用C/C++开发的,是编译后的机器码,不能跨平台,不同平台下需要安装不同版本的JVM。
事务的四大特征
- 原子性(Atomicity):表示事务内不可分割,要么都成功,要么都失败。
- 一致性(Consistency):要么都成功,要么都失败.失败了,要对前面的操作进行回滚。
- 隔离性(Isolation):一个事务开启了,不能受其它事务的影响。
- 持久性(Durability):持续性,表示事务开始了,就不能终止.事务提交后,将数据序列化到数据库。
ORM
对象关系映射(Object Relational Mapping,简称ORM)模式是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术。简单来说,将程序中的兑现自动持久化到关系数据库中。那么,到底如何实现持久化呢?一种简单的反感是采用硬编码的方式(jdbc操作sql方式),为每一种可能的数据库访问操作提供单独的方法。这种方法存在很多缺陷,所以使用ORM框架(为了解决面型对象与关系数据库存在的互不匹配的现象的框架)来解决。
同步代码块、同步方法
- 当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
- 然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
- 尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
- 当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
- 从尺寸上讲,同步代码块比同步方法小。你可以把同步代码块看成是没上锁房间里的一块用带锁的屏风隔开的空间。
- 同步代码块还可以人为的指定获得某个其它对象的key。就像是指定用哪一把钥匙才能开这个屏风的锁,你可以用本房的钥匙;你也可以指定用另一个房子的钥匙才能开,这样的话,你要跑到另一栋房子那儿把那个钥匙拿来,并用那个房子的钥匙来打开这个房子的带锁的屏风。记住你获得的那另一栋房子的钥匙,并不影响其他人进入那栋房子没有锁的房间。同步代码块可以指定钥匙这一特点有个额外的好处,是可以在一定时期内霸占某个对象的key。你所取得的那把钥匙不是永远不还,而是在退出同步代码块时才还。
- 无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。
- 每个对象只有一个锁(lock)与之相关联。
- 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
- 定义private 的instance变量+它的 get方法,而不要定义public/protected的instance变量。如果将变量定义为public,对象在外界可以绕过同步方法的控制而直接取得它,并改动它。这也是JavaBean的标准实现方式之一。
- 如果instance变量是一个对象,如数组或ArrayList什么的,那上述方法仍然不安全,因为当外界对象通过get方法拿到这个instance对象的引用后,又将其指向另一个对象,那么这个private变量也就变了,岂不是很危险。 这个时候就需要将get方法也加上synchronized同步,并且,只返回这个private对象的clone()――这样,调用端得到的就是对象副本的引用了。
get 和 post 请求
- 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中。
- GET产生一个TCP数据包;POST产生两个TCP数据包。
但是GET和POST只是HTTP协议中的两种发送请求的方法,HTTP的底层是TCP/IP。所以GET和POST的底层也是TCP/IP,也就是说,GET/POST都是TCP链接。GET和POST能做的事情是一样一样的。你要给GET加上request body,给POST带上url参数,技术上是完全行的通的。
html 和 jsp
html的优点:
- 开发过程中前端与后端脱离,交互通过JSON传输来实现
- 跨平台能力更强,依托于浏览器的支持
- 使后台数据接口能够得到复用
html的缺点:
- 开发难度大,考虑浏览器的兼容性
- 页面请求过多
- 属于后加载,无法被爬虫爬到
- 接口代码需要新增很多
- 无法直接显示java实体类对象,需要转换为json格式
jsp的优点:
- 可被爬虫爬到
- 减少请求次数
- 不用考虑浏览器的兼容性
jsp的缺点:
- 增大了服务器的压力
- 前端与后端未脱离,拖慢开发进度
- 过于依赖java运行环境
- 复用较低
锁
1、公平锁/非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。 对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。 对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS(AbstractQueuedSynchronizer)的来实现线程调度,所以并没有任何办法使其变成公平锁。
2、可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。说的有点抽象,下面会有一个代码的示例。 对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁。 对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。
synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000);
}
3、独享锁/共享锁
- 独享锁是指该锁一次只能被一个线程所持有。
- 共享锁是指该锁可被多个线程所持有。
对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。 读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。 独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。 对于Synchronized而言,当然是独享锁。
4、互斥锁/读写锁
上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
- 互斥锁在Java中的具体实现就是ReentrantLock
- 读写锁在Java中的具体实现就是ReadWriteLock
5、乐观锁/悲观锁
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
- 悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
- 乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。 悲观锁在Java中的使用,就是利用各种锁。 乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
6、分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
7、偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
- 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
- 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
- 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
8、自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。 典型的自旋锁实现的例子,可以参考自旋锁的实现
Hibernate 和 MyBatits
相同点:
都是java中的orm框架,屏蔽jdbc的api底层访问细节,使我们不用域jdbc的挨批打交道,就可以完成对数据的持久化操作。jdbc的api编程流程固定,还将sql与java代码混在在一起,经常需要拼凑sql语句,细节繁琐,开发不方便。
Mybatis的好处:
屏蔽jdbc api的底层访问细节,将sql语句域java代码分离,提供将结果集自动封装为实体对象和对象的集合的功能,queryForList返回对象集合。用queryForObject返回单个对象。提供自动将实体对象的属性传递给sql语句的参数。
Hibernate的好处:
Hibernate是一个全自动的偶人吗映射工具,它可以自动生成sql语句,执行并返回java结果。
不同点:
- Hibernate要比MyBatits功能强大很多,因为Hibernate自动生成sql语句。
- Mybatis需要我们自己在xml配置文件中写sql语句,Hibernate我们无法直接控制该语句,我们就无法去写特定的高效的sql,对于一些不太复杂的sql查询,Hibernate可以很好帮我们完成。但是,对于特别复杂的查询,Hibernate就很难适应了,这时候用MyBatits就是不错的选择,因为MyBatits还是由我们自己写sql语句。mybatis可以处理复杂语句,而Hibernate不能。
- Mybatis要比Hibernate简单的多,MyBatits是面向sql的,不用考虑对象间一些复杂的映射关系。
集合
HashMap内部实现原理
1、initialCapacity:初始容量。指的是 HashMap 集合初始化的时候自身的容量。可以在构造方法中指定;如果不指定的话,总容量默认值是 16 。需要注意的是初始容量必须是 2 的幂次方。
2、size:当前 HashMap 中已经存储着的键值对数量,即 HashMap.size()
3、loadFactor:加载因子。所谓的加载因子就是 HashMap (当前的容量/总容量) 到达一定值的时候,HashMap 会实施扩容。加载因子也可以通过构造方法中指定,默认的值是 0.75 。举个例子,假设有一个 HashMap 的初始容量为 16 ,那么扩容的阀值就是 0.75 * 16 = 12 。也就是说,在你打算存入第 13 个值的时候,HashMap 会先执行扩容。
4、threshold:扩容阀值。即 扩容阀值 = HashMap 总容量 * 加载因子。当前 HashMap 的容量大于或等于扩容阀值的时候就会去执行扩容。扩容的容量为当前 HashMap 总容量的两倍。比如,当前 HashMap 的总容量为 16 ,那么扩容之后为 32 。
5、table:Entry 数组。我们都知道 HashMap 内部存储 key/value 是通过 Entry 这个介质来实现的。而 table 就是 Entry 数组。
6、在 Java 1.7 中,HashMap 的实现方法是数组 + 链表的形式。上面的 table 就是数组,而数组中的每个元素,都是链表的第一个结点。
7、如果 table 数组为空时先创建数组,并且设置扩容阀值;如果 key 为空时,调用 putForNullKey 方法特殊处理;计算 key 的哈希值;根据第三步计算出来的哈希值和当前数组的长度来计算得到该 key 在数组中的索引;遍历该数组索引下的整条链表,如果之前已经有一样的 key ,那么直接覆盖 value ;如果该 key 之前没有,那么就进入 addEntry 方法。下面就来看一下 addEntry 方法。
8、在 Java 1.8 中,如果链表的长度超过了 8 ,那么链表将转化为红黑树;发生 hash 碰撞时,Java 1.7 会在链表头部插入,而 Java 1.8 会在链表尾部插入;在 Java 1.8 中,Entry 被 Node 代替(换了一个马甲)。
step1:如果传入的key为空,那么返回空键对应的值(HashMap允许一个null键)。
step2:把key的hashcode经过一个hash算法,得到一个hash值。
step3:根据上一步得到的hash值定位数组的下标。
step4:上一步定位到数组的下标的位置如果有元素,取第一个元素,判断他们的hash值是否相同,如果相同,再判断两个对象是否相等。如果hash值不同,或hash值相同,却不相等,判断第二个元素。直到判断完所有的元素。
step5:把当前元素作为数组下标位置的第一个元素,把之前的元素设为新插入元素的next节点,判断是否超过threshold,超过则扩容(addEntry逻辑说明)。
我们把key看成是一个人,key的hashCode看成是人的姓名,value看成是电话号码。假如我现在要存张三的电话号码,那么,对应的:
step2:把姓名按照首字母划分,得到hash值为Z。
step3:找到Z所在的位置。
step4:判断Z位置处第一个元素(假设Z位置处只有一个赵六),赵六的hash值也为Z,所以,继续比较到底是不是我要找的张三,ok,不是,判断Z位置处不存在第二个元素,执行step5。
step5:在Z位置处插入张三和他的电话号码,把赵六移到他的后面。