接触 Android 开发也有一段时间了,前段时间便开始想抽空整理一些知识点,通过笔记整理的方式减少自己重复学习的时间成本和提高自身的效率。
目前先是总结了部分 Java 的知识点,这就是本文的主要分享内容。 想特意申明的一点是,这个总结更多的是从本人自己的编程基础和侧重点出发,所以在内容上会有选择性的忽略以及侧重点,参考的博客和图文有很多,没办法一一列出,如果有引用不当的部分会立即删除,望大家见谅。 知识点目录可以再右边侧边栏查看跳转。
另外,之后会整理的知识点还会有 Android SDK、Android 源码、其他的一些计算机基础以及常见的面试题等几个部分,往后的一个月时间里会陆续补充更新,在 Github 上创建了项目,想关注的欢迎 star 。
区域 | 说明 |
---|---|
程序计数器 | 每条线程都需要有一个程序计数器,计数器记录的是正在执行的指令地址,如果正在执行的是Natvie 方法,这个计数器值为空(Undefined) |
java虚拟机栈 | Java方法执行的内存模型,每个方法执行的时候,都会创建一个栈帧用于保存局部变量表,操作数栈,动态链接,方法出口信息等。一个方法调用的过程就是一个栈帧从VM栈入栈到出栈的过程 |
本地方法栈 | 与VM栈发挥的作用非常相似,VM栈执行Java方法(字节码)服务,Native方法栈执行的是Native方法服务。 |
Java堆 | 此内存区域唯一的目的就是存放对象实例,几乎所有的对象都在这分配内存 |
方法区 | 方法区是各个内存所共享的内存空间,方法区中主要存放被JVM加载的类信息、常量、静态变量、即时编译后的代码等数据 |
指令 | 说明 |
---|---|
invokeinterface | 用以调用接口方法 |
invokevirtual | 指令用于调用对象的实例方法 |
invokestatic | 用以调用类/静态方法 |
invokespecial | 用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法 |
类加载器 | 说明 |
---|---|
BootstrapClassLoader | Bootstrap类加载器负责加载rt.jar中的JDK类文件,它是所有类加载器的父加载器。Bootstrap类加载器没有任何父类加载器,如果你调用String.class.getClassLoader(),会返回null,任何基于此的代码会抛出NUllPointerException异常。Bootstrap加载器被称为初始类加载器 |
ExtClasssLoader | 而Extension将加载类的请求先委托给它的父加载器,也就是Bootstrap,如果没有成功加载的话,再从jre/lib/ext目录下或者java.ext.dirs系统属性定义的目录下加载类。Extension加载器由sun.misc.Launcher$ExtClassLoader实现 |
AppClassLoader | 第三种默认的加载器就是System类加载器(又叫作Application类加载器)了。它负责从classpath环境变量中加载某些应用相关的类,classpath环境变量通常由-classpath或-cp命令行选项来定义,或者是JAR中的Manifest的classpath属性。Application类加载器是Extension类加载器的子加载器 |
工作原理 | 说明 |
---|---|
委托机制 | 加载任务委托交给父类加载器,如果不行就向下传递委托任务,由其子类加载器加载,保证java核心库的安全性 |
可见性机制 | 子类加载器可以看到父类加载器加载的类,而反之则不行 |
单一性机制 | 父加载器加载过的类不能被子加载器加载第二次 |
常见的Error | ||
---|---|---|
OutOfMemoryError | StackOverflowError | NoClassDeffoundError |
常见的Exception | ||
---|---|---|
常见的非检查性异常 | ||
ArithmeticException | ArrayIndexOutOfBoundsException | ClassCastException |
IllegalArgumentException | IndexOutOfBoundsException | NullPointerException |
NumberFormatException | SecurityException | UnsupportedOperationException |
常见的检查性异常 | ||
IOException | CloneNotSupportedException | IllegalAccessException |
NoSuchFieldException | NoSuchMethodException | FileNotFoundException |
try { Class cls = Class.forName("com.jasonwu.Test"); //获取构造方法 Constructor[] publicConstructors = cls.getConstructors(); //获取全部构造方法 Constructor[] declaredConstructors = cls.getDeclaredConstructors(); //获取公开方法 Method[] methods = cls.getMethods(); //获取全部方法 Method[] declaredMethods = cls.getDeclaredMethods(); //获取公开属性 Field[] publicFields = cls.getFields(); //获取全部属性 Field[] declaredFields = cls.getDeclaredFields(); Object clsObject = cls.newInstance(); Method method = cls.getDeclaredMethod("getModule1Functionality"); Object object = method.invoke(null); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } 复制代码
public class CustomManager { private Context mContext; private static final Object mLock = new Object(); private static CustomManager mInstance; public static CustomManager getInstance(Context context) { synchronized (mLock) { if (mInstance == null) { mInstance = new CustomManager(context); } return mInstance; } } private CustomManager(Context context) { this.mContext = context.getApplicationContext(); } } 复制代码
public class CustomManager { private Context mContext; private volatile static CustomManager mInstance; public static CustomManager getInstance(Context context) { // 避免非必要加锁 if (mInstance == null) { synchronized (CustomManger.class) { if (mInstance == null) { mInstacne = new CustomManager(context); } } } return mInstacne; } private CustomManager(Context context) { this.mContext = context.getApplicationContext(); } } 复制代码
public class CustomManager{ private CustomManager(){} private static class CustomManagerHolder { private static CustomManager INSTANCE = new CustomManager(); } public static CustomManager getInstance() { return CustomManagerHolder.INSTANCE; } } 复制代码
静态内部类的原理是:
当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。getInstance()方法并没有多次去new对象,取的都是同一个INSTANCE对象。
虚拟机会保证一个类的 <clinit>()
方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 <clinit>()
方法,其他线程都需要阻塞等待,直到活动线程执行 <clinit>()
方法完毕
缺点在于无法传递参数,如Context等
当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步,因此在读取volatile类型的变量时总会返回最新写入的值。
当一个变量定义为 volatile 之后,将具备以下特性:
AtomicInteger 中主要实现了整型的原子操作,防止并发情况下出现异常结果,其内部主要依靠JDK 中的unsafe 类操作内存中的数据来实现的。volatile 修饰符保证了value在内存中其他线程可以看到其值得改变。CAS操作保证了AtomicInteger 可以安全的修改value 的值。
HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存Entry对象。当两个对象的hashcode相同时,它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用链表存储对象,这个Entry会存储在链表中,当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。
如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。
为什么String, Interger这样的wrapper类适合作为键?
因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。
当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
在 Java 中,每个对象都会有一个 monitor 对象,这个对象其实就是 Java 对象的锁,通常会被称为“内置锁”或“对象锁”。类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的 Class 对象锁。每个类只有一个 Class 对象,所以每个类只有一个类锁。
Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。
悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现。乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。
在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。
而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。
自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。
当前线程拥有其他线程需要的资源,当前线程等待其他线程已拥有的资源,都不放弃自己拥有的资源。
本次的分享就到这啦,喜欢的话可以点个赞:+1:或关注。如有错误的地方欢迎大家在评论里指出。
本文为个人原创,转载请注明出处。