Java中,在调用类的静态成员,或新建该类的对象等之前,类一定要先装入Java虚拟机中,这是勿庸置疑的。但虚拟机怎样把类装载进来的呢?要经过三步:装载(Load),链接(Link),初始化(Initializ)。其中链接又可分为校验(Verify),准备(Prepare),解析(Resolve)三步。
ClassLoader就是用来装载的。通过指定的className,找到二进制码,生成Class实例,放到JVM中。 ClassLoader从顶向下分为 Bootstrap ClassLoader、Extension ClassLoader、System ClassLoader以及User-Defined ClassLoader(分叉,可以多个)。如下图。
这是Tomcat装载器的例子:
装载过程从源码清析可见:
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 先检查是否已被当前ClassLoader装载。 Class c = findLoadedClass(name); if (c == null) { try { if (parent != null) { // 如果没被当前装载,则递归的到父中装载。 c = parent.loadClass(name, false); } else {// 装载器树已到顶,还没找到的话就到Bootstrap装载器中找。注意:虽Bootstrap是所有加载器的根,但它是C++实现的,不可能放到子的"parent"中,因此,第二层装载器是所有的根了。 c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { // 如果祖先都无法装载,则用当前的装载。子类可在findClass方法中调用defineClass,把从自定义位置获得的字节码转换成Class。 c = findClass(name); } } if (resolve) { // Links the specified class. resolveClass(c); // 注 } return c; }注:
resolveClass(c)方法的注释是链接类,而不只是解析,从该即可看出。调用resolveClass时语义上是去链接,是否真的链接了我不是很清楚,但可以肯定的是没有初始化。当A类中有static B b=new B()时,最晚会在初始化时去装载B。如果改成static B b=null,那么把B.class删掉后,即使A已经链接,初始化过了,但也不会报错,也就是A所引用的B类没有被加载过。解析时难道没有真的去装入它所引用的B类?还是链接时,没有执行解析的步骤? 问题的关键就是 1.对“解析”的理解,解析时是否会去装载B类?2. JVM在链接时是否执行了解析?(毕竟有资料说,解析是可选步骤)
链接就是把load进来的class合并到JVM的运行时状态中。 链接 是三个阶段中最复杂的一个。可以把它分成三个主要阶段:校验。 对二进制字节码的格式进行校验,以确保格式正确、行为正确。准备。 准备类中定义的字段、方法和实现接口所必需的数据结构。比如会为类中的静态变量赋默认值(int等:0, reference:null, char:’\u0000’)。解析。 装入类所引用的其他所有类。可以用许多方式引用类:超类接口字段方法签名方法中使用的本地变量
Initialization of a class consists of executing its static initializers and the initializers forstatic fields (class variables) declared in the class. Initialization of an interface consists of executing the initializers for fields (constants) declared there.
类的初始化包括:执行静态区块和静态方法的初始化。比如下面这两种代码都会被执行,包括new B()。
static{ …}
static B b=new B();
接口中不允许有static initializer(也就是static{…}),所以对于接口,只会执行静态字段的初始化。
初始化前,装载,链接一定已经执行过!
类初始化前,它的直接父类一定要先初始化(递归),但它实现的接口不需要先被初始化。类似的,接口在初始化前,父接口不需要先初始化。
什么情况下,类的初始化会被触发?
A class or interface type T will be initialized immediately before the first occurrence of any one of the following: T is a class and an instance of T is created.T is a class and a static method declared by T is invoked.A static field declared by T is assigned.A static field declared by T is used and the field is not a constant variable (§4.12.4).T is a top-level class, and an assert statement (§14.10) lexically nested within T is executed.
当使用类的字段时,即便可以通过子类或子接口访问该字段,但只有真正定义该字段的类会被触发初始化。如下例。
class Super { static int taxi = 1729; } class Sub extends Super { static { System.out.print("Sub "); } } class Test { public static void main(String[] args) { System.out.println(Sub.taxi); }}只会输出“1729”,不会输出”Sub”,也就是说,Sub其实没有被初始化。
这两方法都可以通过一个给定的类名去定位和加载这个类名对应的 java.long.Class 类对象,区别如下:
初始化 Class.forName()会对类初始化,而loadClass()只会装载或链接。可见的效果就是类中静态初始化段及字节码中对所有静态成员的初始工作的执行(这个过程在类的所有父类中递归地调用). 这点就与ClassLoader.loadClass()不同. ClassLoader.loadClass()加载的类对象是在第一次被调用时才进行初始化的。 你可以利用上述的差异. 比如,要加载一个静态初始化开销很大的类, 你就可以选择提前加载该类(以确保它在classpath下), 但不进行初始化, 直到第一次使用该类的域或方法时才进行初始化
类加载器可能不同 Class.forName(String) 方法(只有一个参数), 使用调用者的类加载器来加载, 也就是用加载了调用forName方法的代码的那个类加载器。当然,它也有个重载的方法,可以指定加载器。 相应的, ClassLoader.loadClass()方法是一个实例方法(非静态方法), 调用时需要自己指定类加载器, 那么这个类加载器就可能是也可能不是加载调用代码的类加载器(调用代用代码类加载器通getClassLoader0()获得)
参考资料: 1. http://java.sun.com/docs/books/jls/third_edition/html/execution.html#44487 这章是“Execution”,很详细讲了类装载过程,很权威! 2. http://www.ibm.com/developerworks/cn/java/j-dclp1/ 类装入问题解秘,不错。 3. http://baike.baidu.com/view/160708.htm 百度百科
思想:遍历数组,将当前元素插入到之前已经排序的部分的适当位置。
最优复杂度:当输入数组就是排好序的时候,复杂度为O(n),而快速排序在这种情况下会产生O(n^2)的复杂度。
最差复杂度:当输入数组为倒序时,复杂度为O(n^2)
思想:通过两两交换,像水中的泡泡一样,小的先冒出来,大的后冒出来
最坏运行时间:O(n^2)
最佳运行时间:O(n^2)(当然,也可以进行改进使得最佳运行时间为O(n))
改进版冒泡排序
思想:加flag岗哨
最佳运行时间:O(n)
最坏运行时间:O(n^2)
思想:每一次从后边未排序的部分中选择最小的放到前边已经排序的部分
最好情况时间:O(n^2)。
最坏情况时间:O(n^2)。
思想:运用分治的思想解决排序问题,将对一个数组的排序分解为将一个个小数组的排序,最后合并
最坏情况运行时间:O(nlgn)
最佳运行时间:O(nlgn)
思想:快速排序是对冒泡排序的一种改进。它的基本思想是:通过一躺排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一不部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
最坏运行时间:当输入数组已排序时,时间为O(n^2),当然可以通过随机化来改进(shuffle array 或者 randomized select pivot),使得期望运行时间为O(nlgn)。
最佳运行时间:O(nlgn)
思想:利用最大堆和最小堆的数据结构,最大堆(堆顶是整个堆的最大值),最小堆(堆顶是整个堆的最小值)利用大顶堆(小顶堆)堆顶记录的是最大关键字(最小关键字)这一特性,使得每次从无序中选择最大记录(最小记录)变得简单。
其基本思想为(大顶堆):
1)将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
2)将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
3)由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
特性:unstable sort、In-place sort。
最优时间:O(nlgn)
最差时间:O(nlgn)
思想:类似桶排,对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。例如,如果输入序列中只有17个元素的值小于x的值,则x可以直接存放在输出序列的第18个位置上。当然,如果有多个元素具有相同的值时,我们不能将这些元素放在输出序列的同一个位置上,因此,上述方案还要作适当的修改。
特性:stable sort、out-place sort。
最坏情况运行时间:O(n+k)
最好情况运行时间:O(n+k)
思想:当d为常数、k=O(n)时,效率为O(n)我们也不一定要一位一位排序,我们可以多位多位排序,比如一共10位,我们可以先对低5位排序,再对高5位排序。
引理:假设n个b位数,将b位数分为多个单元,且每个单元为r位,那么基数排序的效率为O[(b/r)(n+2^r)]。
当b=O(nlgn),r=lgn时,基数排序效率O(n)
特性:stable sort、Out-place sort。
最坏情况运行时间:O((n+k)d)
最好情况运行时间:O((n+k)d)
思想:基数排序是先映射到大小为10的计数数组中,然后再映射到大小等于待排序数组长度的临时数组中.而桶排序就是直接整个足够大的临时数组,把待排序的元素全部映射过来.其索引为待排序元素数值.所以为了确定临时数组的大小得先算出数组中最大数.
特性:out-place sort、stable sort。
最坏情况运行时间:当分布不均匀时,全部元素都分到一个桶中,则O(n^2),当然[算法导论8.4-2]也可以将插入排序换成堆排序、快速排序等,这样最坏情况就是O(nlgn)。
最好情况运行时间:O(n)
通过面向对象的方法进行异常处理,把各种不同的异常进行分类,并提供了良好的额接口,在java中,每一个异常都是一个对象,他是Throwable类或者其他子类的实例,当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个兑现过的方法可以捕获到这个异常并进行处理。
try执行一段程序,如果出现异常,系统会抛出(throws)一个异常,这时候你可以通过他的类型来捕获(catch)他,或者最后(finally)由缺省处理器来捕捉
throw用来抛出异常,throws哟on过来标明一个成员函数可能抛出的异常。finally是无论如何都会执行的。
字节流和字符流,字节流继承自InputStream,OutputStream,字符流继承自InputStreamReader,OutputStreamWriter。
Clone有缺省行为,super.clone();负责产生正确大小的空间,并逐位复制。
有两种定义形式,dtd文档类型定义和SchemaXML模式; XML Schema 和DTD都用于文档验证,但二者还有一定的区别,本质区别是:Scheme本身是xml的,可以被XML解析器解析,这也是从DTD上发展Schema的根本目的。 另外, XML Schema 是内容开放模型,可扩展,功能性强,而DTD可扩展性差。 XML Schema 支持丰富的数据类型,而 DTD不支持元素的数据类型,对属性的类型定义也很有限。 XML Schema 支持命名空间机制,而DTD不支持。 XML Schema 可针对不同情况对整个XML 文档或文档局部进行验证;而 DTD缺乏这种灵活性。 XML Schema 完全遵循XML规范,符合XML语法,可以和DOM结合使用,功能强大;而DTD 语法本身有自身的语法和要求,难以学习。
有DOM文档对象模型,SAX(Simple API for XML),STAX等。 DOM:文档驱动,处理大行文件时,其性能下降的非常厉害,这个问题是由DOM的树结构所造成的,这种结构占用的内存较多,而且DOM必须在解析文件之前把整个文档装入内存,适合对XML的随机访问。 SAX:不同于DOM,SAX是事件驱动型的XML解析方式。他顺序读取XML文件,不需要一次全部装在整个文件,当遇到像文件开头,文档结束,或者标签开头与标签结束时,他会触发一个事件,用户通过在其回调事件中写入处理代码来处理XML文件,适合对XML的顺序访问,且是只读的。当前浏览器不支持SAX SAXParserFactory factory = SAXParserFactory.newinstance(); SAXParser saxparser = factory.newSAXParser(); //创建SAX解析器 MyHandler handler = new MyHandler(); //创建事件处理器 saxParser.parse(new File(“Sax_1.xml”),handler); //绑定文件和事件处理者
STAX:Streaming API for XML ,是用“JavaTM”语言处理XML的最新标准,STAX与其他方法的区别就在于应用程序能够把XML作为一个文件流来处理。STAX允许应用程序把代码这些事件逐个拉出来,而不用提供在解析器方便时从解析器中接受事件的处理程序。
同步的实现当然是采用锁了,java中使用锁的两个基本工具是 synchronized 和 Lock。
synchronized 用在方法和代码块上有什么区别呢?
synchronized 用在方法签名上(以test为例),当某个线程调用此方法时,会获取该实例的对象锁,方法未结束之前,其他线程只能去等待。当这个方法执行完时,才会释放对象锁。其他线程才有机会去抢占这把锁,去执行方法test,但是发生这一切的基础应当是所有线程使用的同一个对象实例,才能实现互斥的现象。否则synchronized关键字将失去意义。
(但是如果该方法为类方法,即其修饰符为static,那么synchronized 意味着某个调用此方法的线程当前会拥有该类的锁,只要该线程持续在当前方法内运行,其他线程依然无法获得方法的使用权!)
synchronized 用在代码块的使用方式:synchronized(obj){//todo code here}
当线程运行到该代码块内,就会拥有obj对象的对象锁,如果多个线程共享同一个Object对象,那么此时就会形成互斥!特别的,当obj == this时,表示当前调用该方法的实例对象。即使用synchronized代码块,可以只对需要同步的代码进行同步,这样可以大大的提高效率。
小结:
使用synchronized 代码块相比方法有两点优势:
1、可以只对需要同步的使用
2、与wait()/notify()/nitifyAll()一起使用时,比较方便
wait() 与notify()/notifyAll()
这三个方法都是Object的方法,并不是线程的方法!
wait():释放占有的对象锁,线程进入等待池,释放cpu,而其他正在等待的线程即可抢占此锁,获得锁的线程即可运行程序。而sleep()不同的是,线程调用此方法后,会休眠一段时间,休眠期间,会暂时释放cpu,但并不释放对象锁。也就是说,在休眠期间,其他线程依然无法进入此代码内部。休眠结束,线程重新获得cpu,执行代码。wait()和sleep()最大的不同在于wait()会释放对象锁,而sleep()不会!
notify(): 该方法会唤醒因为调用对象的wait()而等待的线程,其实就是对对象锁的唤醒,从而使得wait()的线程可以有机会获取对象锁。调用notify()后,并不会立即释放锁,而是继续执行当前代码,直到synchronized中的代码全部执行完毕,才会释放对象锁。JVM则会在等待的线程中调度一个线程去获得对象锁,执行代码。需要注意的是,wait()和notify()必须在synchronized代码块中调用。
notifyAll()则是唤醒所有等待的线程。
为了说明这一点,举例如下:
public class Consumer implements Runnable { private Integer count; public Consumer(Integer count) { this.count = count; } @Override public void run() { while (count > 0) { synchronized (ThreadDemo.obj) { if (count > 0) { System.out.println("当前剩余:" + count + ",消费了一个"); count --; } try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } ThreadDemo.obj.notify(); try { ThreadDemo.obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } public class Producer implements Runnable { private Integer count; public Producer(Integer count) { this.count = count; } @Override public void run() { while (true) { synchronized (ThreadDemo.obj) { System.out.println("当前剩余:" + count + ",生产了一个"); count ++; ThreadDemo.obj.notify(); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } try { ThreadDemo.obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } public class ThreadDemo { public static final Object obj = new Object(); public static void main(String[] args) { ThreadDemo data = new ThreadDemo(); Integer count = 15; new Thread(new Consumer(count)).start(); new Thread(new Producer(count)).start(); } }这里使用static obj作为锁的对象,当线程Produce启动时(假如Produce首先获得锁,则Consumer会等待),打印“A”后,会先主动释放锁,然后阻塞自己。Consumer获得对象锁,打印“B”,然后释放锁,阻塞自己,那么Produce又会获得锁,然后…一直循环下去,直到count = 0.这样,使用Synchronized和wait()以及notify()就可以达到线程同步的目的。
除了wait()和notify()协作完成线程同步之外,使用Lock也可以完成同样的目的。
ReentrantLock 与synchronized有相同的并发性和内存语义,还包含了中断锁等候和定时锁等候,意味着线程A如果先获得了对象obj的锁,那么线程B可以在等待指定时间内依然无法获取锁,那么就会自动放弃该锁。
但是由于synchronized是在JVM层面实现的,因此系统可以监控锁的释放与否,而ReentrantLock使用代码实现的,系统无法自动释放锁,需要在代码中finally子句中显式释放锁lock.unlock();
使用建议:
在并发量比较小的情况下,使用synchronized是个不错的选择,但是在并发量比较高的情况下,其性能下降很严重,此时ReentrantLock是个不错的方案。
是引用不变,而不是对象不变,我们在使用引用对象的时候,可以利用final增大引用对象的作用域。
当我们通过io或者网络传递对象的时候,id作为标识,将成为反序列化对象时候的检验标识,如果id不同,那么代表不是同一个类。