首先map中有一个table名称大小为16的Entry数组,Entry是map接口中的一个内部接口,用来维护map类型key-value的键值,因此每当向map中存放一个key-value对的时候,都会实例化成Entry对象,这个Entry对象就会存储到上面的table数组里面,存放的时候,根据key的hashcode计算出来的hash值,进行数组内的索引值。
存放的时候首先对key进行检查,key如果是null值的话,则放置在table为0的位置上,因为null值的hashcode为0。
如果在索引的位置上没有元素则直接把entry对象放置到那个对应的索引上面,如果有的话,则调用key的equals方法进行内容的比较,相同的话,则直接进行覆盖,不相同的话,则代表hash冲突了,hashmap内部是通过链表的形式来进行解决的,在这个元素上进行迭代,直到找到Entry->next是null的,则把当前的对象放置在该节点上。
hashmap的性能参数主要有两个
容量:哈希表中桶的数量,也就是table数组的大小,默认16
加载因此:是指达到多少之前就会进行扩容,默认0.75
因此当哈希表中的条目超出了两者的乘积时,则就会对该结构进行扩容,因此16*0.75=12,当放置为12个元素的时候就会进行扩容操作,所以我们要放置大量的元素时要调整参数。或者上来就进行初始化操作。就会把数组大小扩展为2*16 = 32,即扩大一倍。
Arraylist内部是通过一个动态数组来实现的。它实现了Abstractlist,因此是一个数组的队列,提供了相关的添加、删除、遍历等功能,也实现了RandomAcceess等接口,提供了随机访问的功能。也实现了clone和序列化的接口,能够被克隆,并且支持序列化的操作。
在arraylist的内部有一个Object类型的ElementData的数组,和vector不同的是,它不是线程安全的,而vector和Copyonwritearraylist是线程安全的。Vector的线程安全通过了关键字synchronized来实现,而后者则通过ReentrantLock来实现,在读的效率上更高一些。
关于Arraylist中的动态扩容操作的话,list默认的初始化容量大小是10,Object类型的elementObject数组,扩容的过程是首先ensureCapacity对原数组进行扩充,创建一个新的数组,长度为原数组的1.5倍+1,然后通过调用arrays类的copyof方法对数组进行拷贝,最后把新数组的地址赋给elementdata引用类即可
也是一个自增长的数组操作,是线程安全的,通过关键字来实现的,与arraylist的区别是可以修改扩容的参数
set最大的特性就是不允许元素重复,内部是通过hashmap来实现的,如果我们看jdk源码的话,set里面也维护这一份hashmap的引用,map中对应的键值对对应的键是不能重复的,这个和set集合是保持一致的,于是就利用了map中键不能重复的这个特性来实现。在Hashset中键就是我们要存入的对象,值则是一个Object的常量,这样呢就可以确保我们所需要存储的信息是键,而键在hashmap中是不可以重复的。而在hashset中判断某个键是否存在,就是通过hashmap中判断键值是否存在,存在的话,返回的是一个object类型的常量PERSENT,否则不存在的话,返回的是一个null值。
与arraylist内部是动态数组来相比的话,linkedlist是以链表结构来实现的。因此对于get和set操作,arraylist要优于linkedlist,对于add和remove操作,linkedlist要有优势。
与hashmap的不同之处在于,hashmap内部也有一个Entry对象,而在LinkedHashMap内部同样有一个Entry对象,但是该entry对象继承了hashmap中的entry对象,它重新定义了数组中保存的元素Entry,该Entry除了保持Entry对象引用外,还保存了它上一个元素Before和下一个元素After的引用操作,从而在哈希表的基础上构成了双向链接表的操作。
java中的TreeSet和LinkedHashSet操作,首先两者放入的实体都是有序的,而LinkedHashSet支持放入的顺序操作,而TreeSet支持自动义的顺序,因此放入的实体需要自动的实现Comparable接口中的compareTo方法。至于TreeSet是底层使用了NavigableMap该map类操作。
hashtable,全锁类型,性能不是很高,而Concurrenthashmap,采用了局部锁的思想,内部维护另一个Segment类型的一个数组,其实一个segment就是一个hashtable的结构,segment内部又维护了一个链表的数据结构操作,因此我们了解到Concurrenthashmap定位一个元素需要两步操作,先定位到某个segment,然后定位到segment中的hashtable操作。
TreeMap用的哪种数据结构,时间复杂度多少
:TreeMap采用红黑树算法进行实现,又称红-黑二叉树。基本规则:
u 每个节点都只能是红色或者黑色
u 根节点是黑色
u 每个叶节点(NIL节点,空节点)是黑色的。
u 如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。
u 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
Treep操作,比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,其检索效率O(log n)
ReadLock、ReadWriteLock、WriteLock、ReentrantLock、ReentrantReadWriteLock
首先java中内置的关键字synchronized
1.首先执行代码块的时候,获取该代码块的锁,执行完后,然后释放该锁
2.线程执行异常的话,JVM会让线程自动释放该锁操作
1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
可重入锁的概念
假如一个方法中调用了另一个方法,两个方法都加入了锁,因此需要重新申请锁,假如此时该锁已经被第一个方法占用的话,则会导致堵塞的操作,那么此时采取可重入锁的话,就可以解决这个问题。java中的内置关键字和lock都具有可重入性,因此不会出现上述问题。
还有一些类似的公平锁的问题,意思是当多个线程在等待的时候,等待最长时间的线程会优先获得该锁,这就是公平锁。
ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、DelayQueue
ArrayBlockingQueue:是基于数组来实现的,必须需要指定数组的大小
LinkedBlockingQueue:是以链表来实现的,如果不指定大小的话,默认是Integer.MAX_VALUE
PriorityBlockingQueue:上面两个都是先进先出的队列,这个是按照元素的优先级进行排序的
DelayQueue:是一种阻塞延时的队列操作
下面是常见的队列的阻塞和非阻塞的方法
1.boolean offer():将Object类型的对象放入到队列中,如果队列有足够的空间,则返回true,否则返回false,该方法不阻塞当前的执行方法的线程,插入返回true,否则返回false
2.offer(long timeout):只不过是在特定的时间内插入,如果还不能插入则返回false
3.void put():把对象放置到队列中,如果没有足够的空间,则调用该线程的方法将会出于阻塞的状态,直到队列里面有空间为止
4.Object poll():取走首尾的对象,若不能立即取走,则可以等tme参数执行的时间,取不到时返回null
5.take:也是取走操作,但是在方法是阻塞的
java中实现线程有runnable接口,thread类,其实thread类也是实现了runnable接口操作,然后实现其run方法,最后调用thread类的start方法即可
java中提供了Executor框架,里面有newFixedThreadPool、newSingleThreadExecutor、newCachedThreadPool,无论是哪种内部都是对ThreadPoolExecutor的一种实现
newFixedThreadPool:返回了一个core和max一样大小的线程池,并且使用了linkedblockingqueue,当任务提交频繁的时候,队列可能会迅速膨胀,消耗系统的资源
newSingleThread:返回的是单线程池,只是简单的将线程池的数量设置为了1
newCachedThreadPool:缓存的线程池操作
JDK内置的拒绝策略如下
AbortPolicy:直接抛出异常,阻止系统正常运行
CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务
DiscardoLDestPolicy:丢弃最老的一个请求
DiscardPolicy:丢弃不能处理的任务
线程池大小
Ncpu=CPU的数量
Ucpu=目标CPU的使用率,0<=Ucpu<=1
W/C=等待时间与计算时间的比率。
Nthreads=Ncpu*Ucpu*(1+W/C)
像Vector、CopyOnWriteArraySet,CopyOnWriteArrayList都是线程安全的,首先CopyOnWriteArraySet不像hashSet内部是通过hashMap来实现的,而他是通过CopyOnWriteArrayList来实现的。
CopyOnWriteArrayList内部线程安全的机制:volatile关键字和互斥锁
CopyOnWriteArraylist的动态数组机制,它内部有一个Volatile数组来保持数据,在添加、修改、删除数据的时候,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给volatile数组。
如果要看CopyOnWriteArraylist的低层源码的话,就会发现,它不想vector用全加锁的方式,读也加锁,写也加锁。而是只是对写加了可重入锁,在写对象的时候,总是先获得锁,然后获得对象的一个副本,进行修改,最后将副本写回操作。
CopyOnWritearraylist的线程安全的机制,内部是通过volatile和互斥锁来实现的,首先是通过volatile数组来保存数据,这样的话,每一个数组来读取volatile数组的时候,总能看到其他线程对该volatile变量最后的写入,因此通过volatile数组,就提供了读取的数据总是最新额操作。而在写或者修改的时候,采用了互斥锁的机制,首先会获取互斥锁,修改完毕后,先将数据更新到volattile数组中,然后在释放互斥锁。这样就达到了保护数据的目的
AtomicInteger、Atomiclong、AtomicBoolean,内部通过自旋锁来实现的。互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁。也就是内部通过一个for循环一直在调度。
下面是它常用的一些方法
System. out.println(index.getAndSet(11));//设置新值,并返回旧值
System. out.println(index.compareAndSet(34, 45));//如果当前值为34,则设置为45
System. out.println(index.getAndIncrement());//当前值加1,并返回旧值
System. out.println(index.getAndDecrement());//当前值减一,并返回旧值
System. out.println(index.getAndAdd(45));//当前值增加45,并返回旧值
System. out.println(index.incrementAndGet());//当前值加1,并返回新值
System. out.println(index.decrementAndGet());//当前值减一,并返回新值
System. out.println(index.addAndGet(12));//当前值加12,并返回新值
旧的io操作是基于流的形式的,而在NIO中是基于块的形式,也就是把数据线读入到缓冲区中,缓冲区是一块连续的内存块,然后在读出到通道channel中。
常用的方法如下:
buffer.rewind():将position重置为0,并清除标志位,但是并不清除buffer中的数据,如果此时重新读入的话,会覆盖原有的
buffer.clear():只不过在rewind的基础上,将limit重置为了capacity的大小
buffer.reset():重置标志位的操作
buffer.mark():设置标志位的
buffer.duplicate:复制缓冲区,内容是共享的,但是各自维护自己的position、limit和mark操作
ByteBuffer. allocateDirect(1024);可以直接访问系统物理内存的类,普通的bytebuffer仍旧在堆上分配,其内存大小受到jvm的影响,而allocateDirect直接在物理内存中分配,并不占用堆空间,并且低层使用更接近操作系统的方法,执行起来更快。
左移运算符,num << 1,相当于num乘以2,左移动几就代表乘以2的几次方
右移运算符,num >> 1,相当于num除以2
重写equals 方法,在equals方法如果两个数组里的数据完全相同,顺序一样,就返回true。
java的关键字,变量修饰符。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。当一个对象被序列化的时候,transient型变量的值不包括在序列化的表示中,然而非transient型的变量是被包括进去的。
Throwable 是所有 Java 程序中错误处理的父类 ,有两种资类: Error 和 Exception 。
Error :表示由 JVM 所侦测到的无法预期的错误,由于这是属于 JVM 层次的严重错误 ,导致 JVM 无法继续执行,因此,这是不可捕捉到的,无法采取任何恢复的操作,顶多只能显示错误信息。
Exception :表示可恢复的例外,这是可捕捉到的。
Java 提供了两类主要的异常 :runtime exception 和 checked exception 。 checked 异常也就是我们经常遇到的 IO 异常,以及 SQL 异常都是这种异常。 对于这种异常, JAVA 编译器强制要求我们必需对出现的这些异常进行 catch 。所以,面对这种异常不管我们是否愿意,只能自己去写一大堆 catch 块去处理可能的异常。
但是另外一种异常: runtime exception ,也称运行时异常,我们可以不处理。当出现这样的异常时,总是由虚拟机 接管。比如:我们从来没有人去处理过 NullPointerException 异常,它就是运行时异常,并且这种异常还是最常见的异常之一。
Runnable和Callable的区别是,
(1)Callable规定的方法是call(),Runnable规定的方法是run().
(2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
(3)call方法可以抛出异常,run方法不可以
(4)运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
从java内存模型上进行分析,每个线程执行的时候都有有线程私有的内存空间,在内存空间里面会有局部变量的副本进行操作,而如果通过volatile来修饰的话,代表该线程执行完毕后,立即刷到主内存中去。
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速。比如,如果一个线程完成一个任务要100毫秒,那么用十个线程完成改任务只需10毫秒。Java在语言层面对多线程提供了卓越的支持,它也是一个很好的卖点。欲了解更多详细信息请点击这里。
线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。更多详细信息请点击这里。
ThreadLocal是Java里一种特殊的变量。每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。它是为创建代价高昂的对象获取线程安全的好方法,比如你可以用ThreadLocal让SimpleDateFormat变成线程安全的,因为那个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它,如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。首先,通过复用减少了代价高昂的对象的创建个数。其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。线程局部变量的另一个不错的例子是ThreadLocalRandom类,它在多线程环境中减少了创建代价高昂的Random对象的个数。查看答案了解更多。
除了以上列举的三种类加载器,还有一种比较特殊的类型 — 线程上下文类加载器。
几点思考
这也是我们在测试时为什么发现System.class.getClassLoader()结果为null的原因,这并不表示System这个类没有类加载器,而是它的加载器比较特殊,是BootstrapClassLoader,由于它不是Java类,因此获得它的引用肯定返回null。
一道面试题
答案:通常不可以,但可以采取另类方法达到这个需求。
解释:为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载。
但是,我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器放在一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。
原文:http://www.cnblogs.com/xingzc/p/5748970.html