从写一个单例开始究竟能问多深及终极解决方案
ddatsh
写一个线程安全的单例模式的例子
线程安全的实现有很多种,根据业务场景可以new一个实例作为私有静态成员变量,这样程序一启动,实例就生成,私有化构造函数,利用公用的静态函数getInstance返回实例
这种预加载的是能保证线程安全的但是如果不是确定会被使用,会造成内存的浪费,所以可以将实例放到私有静态类中作为成员变量
写一种利用锁机制来保证的懒加载方法
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
从这个例子上我能想到的知识点主要有三个
- volatile 关键字,可深入到Java VM内存相关
- synchronized 关键字,可深入到Java锁机制,高并发相关
- new 关键字,可深入到Java VM类加载机制相关
是否真的理解自己写的代码
- 这个程序是怎么保证线程安全的?
将类的构造方法私有起来,外部调用进行初始化的时候只能通过调用getSingleton这个静态方法来获得实例,静态方法是整个Java虚拟机中只有一个实例
在创建的时候首先进行非空判断,这时候如果实例不存在,对整个类进行加锁同步,为了避免过程中非空状态的改变,同步块内再进行一次判断,如果不存在实例则创建实例返回
使用volatile关键字,下次访问这个方法就能直接看到实例的最新非空状态,直接返回实例
-
volatile 起到了什么作用?
用在多线程中来同步变量
Java对象在堆中分配空间。但Java有主内存和线程自己独有的内存拷贝
没有volatile修饰的局部变量,线程运行中访问工作内存中的变量值,修改对于主内存不是立即可见
而volatile修饰的值在线程独有的工作内存中无副本,线程直接和主内存交互,修改对主内存立即可见
-
synchronized起到了什么作用?
锁定对象,限制当前对象只能被一个线程访问
-
synchronized里你传Singleton.class这个参数,起到什么作用,换成别的行不行?
对当前类加锁,使得这个代码块一次只能被一个线程访问。这里Singleton.class可以换成一个常量字符串或者自己定义一个内部静态Object
-
那传Singleton.class,常量字符串,自己定义一个内部静态Object有区别吗?
因为这是一个静态方法,相当于一个概念上的类锁,所以在这里起到的效果是一样的
但是如果是原型模式,或者直接每个类都是new出来的,实例不同的话,在一个非静态方法里加这三种锁,这时是一个对象锁,因为Singleton.class或者是静态的一个Object或者是JVM只存一份的字符串常量,这些对象线程间是共享的,会对所有的实例的同步块都加同一把锁,每个实例访问到此对象的同步代码块都会被阻塞
但是如果这时synchronized的参数是this,或者是内部new出来的一个内部非静态Object,则各个实例拥有不同的锁,访问同一个代码相同同步块也是互不干扰。只有实例内部使用了同一个对象锁才会同步等待
-
synchronized关键字实现同步的原理
Java虚拟机中用监视器锁来实现
每个对象都有一个监视器锁,当监视器锁被占用时就会处于锁定状态
线程执行一条叫monitorenter的指令来获取监视器锁的所有权。如果此监视器锁的进入数为0,则线程进入并将进入数设置为1,成为线程所有者
如果线程已经拥有该锁,因为是可重入锁,可以重新进入,则进入数加1.如果线程的监视器锁被其他线程占用,则阻塞直到此监视器锁的进入数为0时才能进入该锁
线程执行一条叫monitorexit的指令来释放所有权。执行monitorexit的必须是线程的所有者。每次执行此指令,线程进入数减1,直到进入数为0。监视器锁被释放
-
可重入锁是什么概念,有不可重入锁吗?
jdk1.5引入了concurrent包,里面有Lock接口,它有一个实现叫ReentrantLock
广义的可重入锁也叫递归锁,是指同一线程外层函数获得锁之后,内层还可以再次获得此锁
可重入锁的设计是为了避免死锁
sun的corba里的mutex互斥锁是一种不可重入锁的实现
自旋锁也是一种不可重入锁,本质上是一种忙等锁,CPU一直循环执行"测试并设置"直到可用并取得该锁,在递归的调用该锁时必然会引起死锁
另外,如果锁占用时间较长,自旋锁会过多的占用CPU资源,这时使用基于睡眠原理来实现的锁更加合适
-
concurrent包,它里面有哪些锁的实现?
常用的有ReentrantLock,它是一种独占锁。ReadWriteLock接口也是一个锁接口,和Lock接口是一种关联关系,它返回一个只读的Lock和只写的Lock。读写分离,在没有写锁的情况下,读锁是无阻塞的,提高了执行效率,它是一种共享锁。ReadWriteLock的实现类为ReentrantReadWriteLock。ReentrantLock和ReentrantReadWriteLock实现都依赖于AbstractQueuedSynchronizer这种抽象队列同步器
-
锁还有其他维度的分类吗?
还可以分为公平锁和非公平锁
非公平锁是如果一个线程尝试获取锁时可以获取锁,就直接成功获取。公平锁则在锁被释放后将锁分配给等待队列队首的线程
-
AQS是什么?
一个简单的框架,这个框架为同步状态的原子性管理,线程的阻塞和非阻塞以及排队提供了一种通用机制
表现为一个同步器,主要支持获取锁和释放锁。获取锁的时候如果是独占锁就有可能阻塞,如果是共享锁就有可能失败。如果是阻塞,线程就要进入阻塞队列,当状态变成可获得锁就修改状态,已进入阻塞队列的要从阻塞队列中移除。释放锁时修改状态位及唤醒其他被阻塞的线程
AQS本质是采用CHL模型完成了一个先进先出的队列。对于入队,采用CAS操作,每次比较尾节点是否一致,然后插入到尾节点中。对于出队列,因为每个节点缓存了一个状态位,不满足条件时自旋等待,直到满足条件时将头节点设置为下一个节点
-
AWS 队列的数据结构
双向链表
-
AQS是一种通用机制,那它还有哪些应用?
可重入锁ReentrantLock和ReentrantReadWriteLock之外,还用于不可重入锁Mutex的实现
java并发包中的同步器如:Semphore,CountDownLatch,FutureTask,CyclicBarrier都是采用这个机制实现的
通过new对象创建出来的,还可不可以采用别的方法来创建对象
class类的newInstance方法,Constructor构造器类的newInstance方法,克隆方法和反序列法方法
类加载的时候,自己定义了一个类和java自己的类类名和命名空间都一样,JVM加载的是哪一个呢?
调用的是java自身的,根据双亲委派模型,最委派Bootstrap的ClassLoader来加载,找不到才去使用Extension的ClassLoader,还找不到才去用Application的ClassLoader,这种机制利于保证JVM的安全
java的反射机制是什么概念?
在运行状态中,对于任何一个类,都能够知道它所有的属性和方法
对于任何一个对象,都能够调用它的任何一个方法和属性。这种动态的获取信息和动态调用对象的方法的功能就是java的反射机制。它是jdk动态代理的实现方法
其他的动态代理实现?
还有cglib动态代理
两种动态代理哪个比较好呢?
spring AOP 同时使用了这两种动态代理,因为他们各有优劣
jdk动态代理是利用java内部的反射机制来实现,在生成类的过程中比较高效
cglib动态代理则是借助asm来实现,可以利用asm将生成的类进行缓存,所以在类生成之后的相关执行过程中比较高效
但是jdk的动态代理前提是目标类必须基于统一的接口,所以有一定的局限性