Java

2018-04-12  本文已影响17人  一纸砚白

equals和hashcode

public boolean equals(Object obj){

    return this == obj

}

Object类中默认的实现方式是  :  return this == obj  。那就是说,只有this 和 obj引用同一个对象,才会返回true。

关于hashCode方法,一致的约定是:重写了euqls方法的对象必须同时重写hashCode()方法。

对象的散列码(hashcode)是为了更好的支持基于哈希机制的Java集合类,例如 HashTable, HashMap, HashSet 等。

1.equal()相等的两个对象他们的hashCode()肯定相等,也就是用equal()对比是绝对可靠的。

2.hashCode()相等的两个对象他们的equal()不一定相等,也就是hashCode()不是绝对可靠的。

所有对于需要大量并且快速的对比的话如果都用equal()去做显然效率太低,所以解决方式是,每当需要对比的时候,首先用hashCode()去对比,如果hashCode()不一样,则表示这两个对象肯定不相等(也就是不必再用equal()去再对比了),如果hashCode()相同,此时再对比他们的equal(),如果equal()也相同,则表示这两个对象是真的相同了,这样既能大大提高了效率也保证了对比的正确性。

重写hashCode(),要把重要字段(equals中衡量相等的字段)参入散列运算,每一个重要字段都会产生一个hash分量,为最终的hash值做出贡献(影响)



创建线程的4种方式

1. 继承Thread类创建线程类

2. 通过Runable接口创建线程类

3. 通过Callable和FutureTask创建线程

     a. 创建Callable接口的实现类,并实现call()方法;

    b. 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callback对象的call()方法的返回值;

    c. 使用FutureTask对象作为Thread对象的target创建并启动新线程;

    d. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

                   1.继承Thread类,并复写run方法,创建该类对象,调用start方法开启线程。

                   2.实现Runnable接口,复写run方法,创建Thread类对象,将Runnable子类对象传递给Thread类对象。调用start方法开启线程。

                    第二种方式好,将线程对象和线程任务对象分离开。降低了耦合性,利于维护

                   3.创建FutureTask对象,创建Callable子类对象,复写call(相当于run)方法,将其传递给FutureTask对象(相当于一个Runnable)。

                     创建Thread类对象,将FutureTask对象传递给Thread对象。调用start方法开启线程。这种方式可以获得线程执行完之后的返回值。

4. 通过线程池创建线程

      线程池传送门

cup,进程,线程之间的关系


volatile和synchronized

volatile是一个修饰符,而synchronized则作用于一段代码或者方法。volatile只是在线程内存和main memory(主内存)间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。                                                            volatile特点:

1.保证了不同线程对该变量操作的内存可见性;

2.禁止指令重排序

Java内存模型有三个特性:原子性、可见性、有序性。

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通synchronized和Lock来实现。

可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。 Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

有序性:即程序执行的顺序按照代码的先后顺序执行。在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

synchronized与Lock的区别

两者区别:

1.首先synchronized是java内置关键字,在jvm层面,Lock是个java类;

2.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;

3.synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;

4.用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;

5.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)

6.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。 

ReenTrantLock独有的能力:

1. ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。

2.  ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

3.  ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。

wait和sleep的区别

HashMap

源码分析+面试问答+实现原理                                                                   

  Java1.8中hashmap采用的是数组+链表+红黑树,在链表过长的时候可以通过转换成红黑树提升访问性能。大多数情况下,结构都以链表的形式存在。

1.8以后HashMap结构

1.8以前hashMap用数组加链表,链表过长查找速度为O(N),所以加入了红黑树。  HashMap 包含如下几个构造器:                                                          1.HashMap():构建一个初始容量为 16,负载因子为 0.75 的 HashMap。  2.HashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 的 HashMap。                                                                                            3.HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。

简单地说,HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。

HashMap的resize(rehash)                                                                              HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。                                                                那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组的loadFactor时,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

ConcurrentHashMap

源码分析

HashMap结构类似于

HashMap

实现了同步的HashTable也是这样的结构,它的同步使用锁来保证的,并且所有同步操作使用的是同一个锁对象。这样若有n个线程同时在get时,这n个线程要串行的等待来获取锁。

ConcurrentHashMap中对这个数据结构,针对并发稍微做了一点调整。它把区间按照并发级别(concurrentLevel),分成了若干个segment(相当于一个HashTable)。默认情况下内部按并发级别为16来创建。对于每个segment的容量,默认情况也是16。当然并发级别(concurrentLevel)和每个段(segment)的初始容量都是可以通过构造函数设定的。创建好默认的ConcurrentHashMap之后,它的结构大致如下图:

ConcurrentHashMap

看起来只是把以前HashTable的一个hash bucket创建了16份而已。

继续看每个segment是怎么定义的:

static final class Segment extends ReentrantLock implements Serializable{

Segment继承了ReentrantLock,表明每个segment都可以当做一个锁。这样对每个segment中的数据需要同步操作的话都是使用每个segment容器对象自身的锁来实现。只有对全局需要改变时锁定的是所有的segment。这种做法,就称之为“分离锁(lock striping)”

CocurrentHashMap的操作(get、put、remove)

Segment的get操作是不需要加锁的。因为volatile修饰的变量保证了线程之间的可见性

Segment的put操作是需要加锁的,在插入时会先判断Segment里的HashEntry数组是否会超过容量(threshold),如果超过需要对数组扩容,翻一倍。然后在新的数组中重新hash,为了高效,CocurrentHashMap只会对需要扩容的单个Segment进行扩容

CocurrentHashMap获取size的时候要统计Segments中的HashEntry的和,如果不对他们都加锁的话,无法避免数据的修改造成的错误,但是如果都加锁的话,效率又很低。所以CoccurentHashMap在实现的时候,巧妙地利用了在累加过程中发生变化的几率很小的客观条件,在获取count时,不加锁的计算两次,如果两次不相同,在采用加锁的计算方法。采用了一个高效率的剪枝防止很大概率地减少了不必要额加锁。

HashEntry类结构。

staticfinalclassHashEntry {

    final K key;

    final int hash;

    volatile V value;

    final HashEntry next;

    ......

}

除了 value,其它成员都是final修饰的,也就是说value可以被改变,其它都不可以改变,包括指向下一个HashEntry的next也不能被改变。

put方法的大致过程

put方法

因为每个HashEntry中的next也是final的,没法对链表最后一个元素增加一个后续entry所以新增一个entry的实现方式只能通过头结点来插入了。

remove方法的大致过程

remove方法

假设我们的链表元素是:e1-> e2 -> e3 -> e4 我们要删除 e3这个entry,因为HashEntry中next的不可变,所以我们无法直接把e2的next指向e4,而是将要删除的节点之前的节点复制一份,形成新的链表。

get方法

publicV get(Object key) {

    inthash = hash(key); // throws NullPointerException if key null

    returnsegmentFor(hash).get(key, hash);

}

它没有使用同步控制,交给segment去找,再看Segment中的get方法:

V get(Object key, inthash) {

        if(count != 0) { // read-volatile // ①

            HashEntry e = getFirst(hash);

            while(e != null) {

                if(e.hash == hash && key.equals(e.key)) {

                    V v = e.value;

                    if(v != null)  // ② 注意这里

                        returnv;

                    returnreadValueUnderLock(e); // recheck

                }

                e = e.next;

            }

        }

        returnnull;

}

它也没有使用锁来同步,只是判断获取的entry的value是否为null,为null时才使用加锁的方式再次去获取。

1) 在get代码的①和②之间,另一个线程新增了一个entry

如果另一个线程刚好new 这个对象时,当前线程来get它。因为没有同步,就可能会出现当前线程得到的newEntry对象是一个没有完全构造好的对象引用。

这里同样有可能一个线程new这个对象的时候还没有执行完构造函数就被另一个线程得到这个对象引用。

所以才需要判断一下:if (v != null)如果确实是一个不完整的对象,则使用锁的方式再次get一次。

2) 在get代码的①和②之间,另一个线程修改了一个entry的value

value是用volitale修饰的,可以保证读取时获取到的是修改后的值。

3) 在get代码的①之后,另一个线程删除了一个entry

假设我们的链表元素是:e1-> e2 -> e3 -> e4 我们要删除 e3这个entry,因为HashEntry中next的不可变,所以我们无法直接把e2的next指向e4,而是将要删除的节点之前的节点复制一份,形成新的链表。

类加载过程

  类加载的全过程:加载、验证、准备、解析、初始化这5个过程。

类的生命周期

        加载:程序运行之前jvm会把编译完成的.class二进制文件加载到内存,供程序使用,用到的就是类加载器classLoader ,这里也可以看出java程序的运行并不是直接依靠底层的操作系统,而是基于jvm虚拟机。如果没有类加载器,java文件就只是磁盘中的一个普通文件。

                在加载阶段虚拟机需要完成以下3件事:

                1)通过一个类的全限定名来获取定义此类的二进制字节流

                2)将这个字节流所代表的静态存储结构转化为方法区的运行时结构

                3)在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区的数据访问入口。

        验证:验证是连接阶段的第一步,这一阶段的目的是确保Class对象的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全>。主要有:1,文件格式验证。2,元数据验证。3,字节码验证。4,符号引用验证。

        准备:准备阶段就是正式为类变量分配内存并设置类变量的初始值的阶段,这些内存都将在方法区中进行分配。

        解析:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

        初始化:初始化阶段是真正开始执行类中定义的java程序代码。

内部类

问:什么是内部类呢

答:内部类( Inner Class )就是定义在另外一个类里面的类。与之对应,包含内部类的类被称为外部类。

问:那为什么要将一个类定义在另一个类里面呢?清清爽爽的独立的一个类多好啊!!

答:内部类的主要作用如下:

1. 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类

2. 内部类的方法可以直接访问外部类的所有数据,包括私有的数据

3. 内部类所实现的功能使用外部类同样可以实现,只是有时使用内部类更方便

问:内部类有几种呢?

答:内部类可分为以下几种:

成员内部类、静态内部类、局部内部类、匿名内部类

上一篇下一篇

猜你喜欢

热点阅读