Volatile
volatile的特性
- 禁止指令重排
- 可见性(对volatile修饰的成员变量进行修改,会立即同步到内存中,其他线程是可见的)
Volatile的可见性
* 写一个Volatile变量时,Java内存模型会把线程对应的本地内存中的共享变量刷新到主内存中。

* 当读一个volatile变量时,Java内存模型会把线程对应的本地内存设置为无效,线程接下来将从主内存中读取共享变量。

Volatile的重排序
1. 当第二个操作为volatile写操作时,不管第一个操作时什么,都不能进行重排序。这个规则确保volatile写之前的所有操作都不会重排序到volatile之后。
2. 当第一个操作为volatile读操作时,不管第二个操作是什么,都不能进行重排序。这个规则确保volatile读之后的所有操作都不会被重排到volatile之前。
3. 当第一个操作时volatile写操作时,第二个操作时volatile读操作,不能进行重排序。
也就是说两个volatile变量操作不能进行重排序。
内存屏障
- 内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。(也就是让一个CPU处理单元中的内存状态对其它处理单元可见的一项技术。)
内存屏障可以被分为以下几种类型:
LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。
在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
内存屏障阻碍了CPU采用优化技术来降低内存操作延迟,必须考虑因此带来的性能损失。为了达到最佳性能,最好是把要解决的问题模块化,这样处理器可以按单元执行任务,然后在任务单元的边界放上所有需要的内存屏障。采用这个方法可以让处理器不受限的执行一个任务单元。合理的内存屏障组合还有一个好处是:缓冲区在第一次被刷后开销会减少,因为再填充改缓冲区不需要额外工作了。
happens-before原则
- 程序次序规则:一个线程内,按照代码顺序,书写在前的操作先行发生于书写在后面的操作。
- 锁定规则:一个unlock操作先行发生于后面对同一个所的lock操作
- volatile变量原则:对一个变量的写操作先行发生于对后面对这个变量的读操作
- 传递规则:如果操作A先与操作B,而操作B又先于操作C,则可以得出操作A先行发生于操作C。
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断时间的发生。
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值手段检测线程已经终止执行
- 对象终结规则:一个对象的初始化先行发生于他的finalize()方法。
Java是如何实现跨平台的
Java号称一次编译到处执行,一次编译指的是.java文件通过编译器编译为.class文件,也就是字节码文件。JVM虚拟机负责将字节码文件在不同的操作系统实现运行,也就是在.class文件与操作系统低层之间,JVM就像是一个适配器,不同的操作系统对应不同的JVM。
简述扫描微信二维码登录的流程
- 用户A访问微信网页版,微信服务器会为这个会话生成一个唯一的ID。
- 用户A打开自己的微信并扫描这个二维码,并提示用户是否确认登录。
- 确认登录之后,手机上的微信客户端将微信账号和扫描这个得到的ID一起提交到服务器
- 服务器将这个ID和用户A的微信号绑定在一起,并通知网页版微信,这个ID对应的微信号为用户A,记载用户A的微信信息。
Java线程有那些状态,这些状态是如何转变的
- 当一个线程执行了start方法后,不代表这个线程就会立即被执行,只代表这个线程处于可运行的状态,最终由OS的线程调度来决定哪个可运行状态下的线程被执行。
- 一个线程一次被选中执行是有时间限制的,这个时间段叫做CPU的时间片,当时间片用完但线程还没有结束时,这个线程又会变为可运行状态,等待OS的再次调度;在运行的线程里执行Thread.yeild()方法同样可以使当前线程变为可运行状态。
- 在一个运行中的线程等待用户输入、调用Thread.sleep()、调用了其他线程的join()方法,则当前线程变为阻塞状态。
- 阻塞状态的线程用户输入完毕、sleep时间到、join的线程结束,则当前线程由阻塞状态变为可运行状态。
- 运行中的线程调用wait方法,此线程进入等待队列。
- 运行中的线程遇到synchronized同时没有拿到对象的锁标记、等待队列的线程wait时间到、等待队列的线程被notify方法唤醒、有其他线程调用notifyAll方法,则线程变成锁池状态。
- 锁池状态的线程获得对象锁标记,则线程变成可运行状态。
- 运行中的线程run方法执行完毕或main线程结束,则线程运行结束。
*需要注意的是Java多线程的阻塞状态,分为三种:
- 等待阻塞, obj.wait()方法,进入等待队列中
- 同步阻塞, 获取synchronized等同步锁时,进入锁池中
- 其他阻塞, Thread.sleep(), t.join(), IO请求等、*
List接口、Set接口、Map接口的区别
- List和Set接口继承自Collection接口,而Map和Collection是一个级别的接口。
Collection表示一组对象,这些对象也称为collection的元素;一些 collection允许有重复的元素,而另一些则不允许;一些collection是有序的,而另一些则是无序的;JDK中不提供此接口的任何直接实 现,它提供更具体的子接口(如 Set 和 List)实现;Map没有继承Collection接口,Map提供key到value的映射;一个Map中不能包含相同key,每个key只能映射一个value;Map接口提供3种集合的视图,Map的内容可以被当做一组key集合,一组value集合,或者一组key-value映射;
- List接口:元素有放入顺序,元素可重复,可为空值
List是一种有序的Collection,可以通过索引访问集合中的数据。所有的索引返回的方法都有可能抛出一个IndexOutOfBoundsException异常,subList(int fromIndex, int toIndex)返回的前闭后开。
所有的List中可以有相同的元素,例如Vector中可以有 [ tom,koo,too,koo ];
所有的List中可以有null元素,例如[ tom,null,1 ];
基于Array的List(Vector,ArrayList)适合查询,而LinkedList(链表)适合添加,删除操作;
实现类:
- LinkedList:低层基于链表实现,随机查询性能较差,增加删除节点很快。
- ArrayList:低层基于数组实现的,随机查询性能高,增加删除效率很差,线程不安全的类。
- Vector:低层基于数组实现的,随机查询性能高,增加删除效率很差,线程安全的类。
- Set接口: 元素无放入顺序,元素不可重复(注意:元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的)
Set具有与Collection完全一样的接口。实际上Set就是Collection,只是行为不同。(这是继承与多态思想的典型应用:表现不同的行为)Set不保存重复的元素。
存入Set的每个元素都必须是唯一的,因为Set不保存重复元素。加入Set的元素必须定义equals()方法以确保对象的唯一性。
实现类:
- HashSet: 为快速查找设计的Set。低层为HashMap实现,存入HashSet的对象必须定义hashCode()。
- LinkedHashSet: 具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入的次序)。于是在使用迭代器遍历Set时,结果会按元素插入的次序显示。
- TreeSet: 保存次序的Set, 底层为树结构。使用它可以从Set中提取有序的序列。
- Map接口:key-value形式的元素
Map接口有三个实现类:HashMap,HashTable,LinkeHashMap
- HashMap非线程安全,高效,支持null;
- HashTable线程安全,低效,不支持null
- SortedMap有一个实现类:TreeMap
Cookie和Session的区别
cookie是一种发送到客户浏览器的文本串句柄,并保存在客户机硬盘上,可以用来某个WEB站点会话间持久化的保持数据。
session指的是访问者从到达某个特定主页到离开位置的哪段时间,Session其实是利用Cookie进行信息处理,当用户首先进行了请求后,服务端就在用户浏览器上创建了一个Cookie,当这个Session结束时,Cookie过期。这个Cookie的唯一目的是为每一个用户提供唯一的身份认证。
cookie数据存放在客户的浏览器上,session数据放在服务器上,cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗,如果主要考虑到安全应当使用session。session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,如果主要考虑到减轻服务器性能方面,应当使用COOKIE。单个cookie在客户端的限制是3K,就是说一个站点在客户端存放的COOKIE不能3K。将登陆信息等重要信息存放为SESSION;其他信息如果需要保留,可以放在COOKIE中
相同之处是cookie和Session都可以用来跟踪浏览器用户身份的会话方式。
区别:
Cookie | Session |
---|---|
Cookie保存在客户端 | session数据保存在服务器端 |
浏览器使用的是Cookie,则所有数据都会保存在浏览器端,比如当你登录之后,服务器设置了Cookie用户名,当你再次请求服务器的时候,浏览器会将用户名一块发送给服务器。 | web端使用的是Session,则所有数据都保存在服务器上,客户端每次请求服务器的时候会发送当前会话的SessionID,服务器根据当前SessionID判断相应的用户数据标志,以确定用户是否登录。 |
Cookie容易被伪造,安全性不高 | Session安全性较高 |
cookie的有效期是存储的时候设置好的 | session的有效期由服务器决定 |
equals和hashCode方法
equals方法是用来判断两个对象是否相等,Object类中的equals方法相等的含义是两个引用指向同一个对象,很多类比如String,Integer等类重写equals方法,认定两个对象的值相等就是相等的。equals方法的性质有以下几个:
- 自反性,对于任意不为null的引用值x,x.equals(x)一定时true
- 对称性,x.equals(y)为true,y.equals(x)一定为true
- 传递性,x.equals(y)是true,同时y.equals(z)是true,那么x.equals(z)一定是true
- 一致性,如果用于equals比较的对象信息没有被修改的话,多次调用时x.equals(y)要么一致地返回true要么一致地返回false。
重写equals方法时,hashCode方法也要被重写,因为两个对象相等,其hashCode一定要相等
hashCode给对象返回一个HashCode值,主要用于hash tables,比如HashMap。
- 一个对象没有被更改,则其hashCode返回值不会改变。
- 两个对象相等,则其hashCode一定相等。
- 一般来讲,Object类定义的hashCode方法对于不同的对象返回不同的哈希值。
集合中使用
当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。所以这里存在一个冲突解决的问题。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。
相等的对象必须有相同的哈希码
如果两个对象的hashCode相同,他们不一定相等。
换句话说,equals()方法不相等的两个对象,hashcode()有可能相等(我的理解是由于哈希码在生成的时候产生冲突造成的)。反过来,hashcode()不等,一定能推出equals()也不等;hashcode()相等,equals()可能相等,也可能不等。
Java中CAS算法
乐观锁与悲观锁
- 乐观锁会直接进行操作了,失败了重试。
- 悲观锁会先加锁,加锁成功才去操作。synchronized为悲观锁
- 悲观锁不管对象存在不存在竞争,都会对其进行加锁,当并发量很高时,其性能开销会很大。而乐观锁是基于冲突检验的,也就是每个线程都直接去进行操作,计算完成后检测是否与其他线程存在共享数据竞争,如果没有则直接进行修改,如果存在竞争则重复执行操作和检验,直至成功为止,CAS自旋等待。
CAS算法:Compare And Swap
CAS算法设计三个操作数,内存值,预期值,新值。并且仅当预期值和内存值相等时才将内存值换成新值。这样处理的逻辑为,首先检查某块内存的值是否跟之前读取的一样,如果不一样则表示此内存值已经被别的线程更改过,舍弃本次操作,否则说明期间没有其他线程对此内存值进行操作,可以把新值设置给此块内存。
CAS是原子性的,原子性由CPU硬件指令实现保证的。
乐观锁避免了悲观锁独占对象的现象,同时提高了并发性能。
乐观锁的缺点:
- 乐观锁只能保证一个共享变量的原子操作。
- 长时间自旋会造成开销大,如果CAS长时间不成功而一直自旋,会给CPU带来很大的开销
- 存在ABA问题,CAS的核心思想是通过对比内存值与预期值是否一样而判断内存值是否被改过,但这个判断逻辑不严谨,比如内存原值为A,后来一条线程将其修改为B,最后被改为A,则CAS内存值并没有发生改变,但是实际上有被其他线程改过。这种情况对依赖过程值的运算结果影响很大。解决思路是引入版本号,每次变量更新都把版本号加一。
手写单例模式
- 懒汉
- 饿汉
- 双重校验
- 静态内部类
- 枚举
comparable与comparator的区别
Comparable是一个内比较器,实现了Comparable接口的类有一个特点,就是这些类是可以和自己比较的,至于具体和另一个实现了Comparable接口的类如何比较,则依赖compareTo方法的实现,compareTo方法也被称为自然比较方法。compareTo方法的返回值包括:
- 正数,比较者大
- 0,相等
- 负数,被比较者大
Comparetor是一个外比较器,两种情况下可以实现Comparator接口的方式:
- 一个对象不支持自己和自己比较,但是又想进行两个对象的比较。
一个对象实现了Comparable接口,但是开发者认为compareTo方法中的比较方式并不是自己想要的Comparator接口里面的compare方法,Comparator接口里面有一个compare方法,方法有两个参数T o1和T o2,是泛型的表示方式,分别表示待比较的两个对象,方法返回值和Comparable接口一样是int,有三种情况:
- o1大于o2,返回正整数
- o1等于o2,返回0
- o1小于o3,返回负整数
Arrays和Collections对于sort的不同实现原理
Arrays.sort():该算法是一个经过调优的快速排序,此算法在很多数据集上提供N*log(N)的性能,这导致其他快速排序会降低二次型性能。
Collections.sort():该算法是一个经过修改的合并排序算法。此算法可提供保证的N*log(N)的性能,此实现将指定列表转储到一个数组中,然后再对数组进行排序,在重置数组中相应位置处每个元素的列表上进行迭代。这避免了由于试图原地对链接列表进行排序而产生的n2log(n)性能。
Java中Object常用方法
- clone()
- equals()
- hashCode()
- finalize()
- getClass()
- notify()
- notifyAll()
- toString()
对Java中多态的理解
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
多态的定义:指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)
Java实现多态有三个必要条件:继承、重写、父类引用指向子类对象。
继承:在多态中必须存在有继承关系的子类和父类。
重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
父类引用指向子类对象:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
实现多态的技术称为:动态绑定(dynamic binding),是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。
多态的作用:消除类型之间的耦合关系。