二进制与十进制的转换
二进制是01表示数字的数制,基数是2,逢2进1。
Java十进制及二进制表示对比:
十进制:0 1 2 3 4 5 6 7 8 9
二进制:0000 0001 0010 0011 0100 0101 0110 0111 1000 1001
**规律:**除2取余并倒着写,直到被除数小于权值。比如 7 0111
十进制转换为二进制:位数上的数字*2的位数次方的和,比如 9 = 1X2的0次方+0X2的1次方+0X2的2次方+1X2的3次方 = 1+8 = 9;
程序实现如下:
public class DecimalUtils{
/**
* 十进制转换为二进制
* 正数:数字除2 取余倒着写,直到商为0
* 负数:正数的二进制取反 0-1 1-0 ,然后+1
*
* @return
*/
public static String decimal2Binary(int decimal){
String result = "";
if(decimal == 0){
return "0000";
}
if(decimal>0){
while(decimal!=0){
int i = decimal %2;
result = i+result;
decimal = decimal/2;
}
}
return result;
}
/**
* 二进制转化为十进制: 每个位数上的数字 * 2的(该位数-1)次方 ,也可以使用字符串实现
* 使用 字符串实现可以避免int类型的限制
* @param binary
* @return
*/
public static int binary2Decimal(int binary){
int decimal = 0;
int i = 0; //位数
while(true){
int temp = binary % 10;
decimal = temp * Math.pow(2,p);
binary = binary/2;
p++;
}
return decimal;
}
}
//Test
public class Test{
public static void main(String[] agrs){
System.out.println("二进制转换为十进制结果是:"+DecimalUtils.decimal2Binary(123)); //1111011
System.out.print("十进制转换为二进制结果是:"+DecimalUtils.binary2decimal(1111));//15
}
}
特殊的运算符记录:
<< 左移:向左移动两位 5<<2=20。 5:0101==》左移两位:10100==>20. >> 右移:向右移动两位 5>>2=1。 右移两位 0001===》1
:无符号移动两位(不区分正负),只对int类型有用。 5>>>2 = 1;
位运算符:应用于int long char short byte
& :如果对应的数字都为1 则是 1 ,否则是 0 例: 6 & 2 = 2 6: 0110 2: 0010;=》0010
| :如果对应的数字都是0 ,则是0 ,否则是1。例: 6|2 = 4
^ : 如果对应的数字相同则是0 ,否则是 1。 例:6^2 = 4
~:按位反转,即 0 变 1 ,1变 0 。 例:~6 = -7 0000 0000 0000 0110 ==》反转:1111 1111 1111 1001 高位 = 1 是负数。
- int 装箱拆箱的特殊性
public static void main(int args){
Integer i2 = new Integer(0);
int j = i2.intValue();
j+=3;
Integer b = new Integer(j);
System.out.print(b.intValue());
}
new Integer() 是对象地址,两个Integer对象不相等
Integer i1= new Integer(100);
Integer i2 = new Integer(100); i1=i2 -->false
int i3 = 100; i1=i3—>true ,i1 会自动拆箱,两个int比较
Integer i4= 100; i1 = i4 —>false,i1指向的是堆中的数据,i4指向的是常量池。
Integer i5 = 100; i4 = i5 —>true
但是如果是:
Integer i6 = 128 ;
Integer i7 = 128 ; i6=i7;—>false. 源码中 (超出 -127-128 )后拆箱操作会重新new 一个Integer,地址不一致。
- 常量池相等问题: String s1 = “hello”; String s2 = s1; String s3 = “hello”; s1=s2 —> true s1=s3–> true ,都是指向的常量池 。但是如果是 String s3 = new String(“hello”); 则为false. String s3 = new String(“hello”);创建的对象数量是 1个或2个,在堆中 创建s3 ,如果常量池有 “hello”则不创建,否则创建。
- run()和start()方法的区别?如果在run()方法上加上synchronized调用顺序有什么变化?
Start()方法启动一个线程,使线程处于就绪状态,并没有执行,调用start()方法后不用等run方法执行完成即可继续执行下面代码。
run()方法,线程处于运行状态,等run()方法执行完毕后才可继续执行下面的代码。
在run()方法增加synchronize之后是防止多个线程争夺资源。
hashmap及hashtable的区别?
见链接:
final
,finally
,finalize
的区别?一个变量被 final
修饰,变量的初始化在该类的构造函数里面,这样写对吗?
-
final
:是一个修饰符,用于控制控制一个成员、方法或一个类是否被修改、重写或继承等功能。一个类被final声明时无法派生子类,不能被作为父类被继承。一个方法使用final修饰改方法无法被重写,一个成员使用final修饰。其初始化只能在两个地方,一是定义的时候,在定义的时候同时赋值,二是在构造函数的时候,二者只能选其一,在定义的时候没有赋值,可以在构造函数中赋值,但之后引用该成员变量,只能读取,不能修改。 -
finally
:一般用于异常处理,一般在异常处理中,使用finally进行补充,实现清除操作。finally块的代码一定会被执行,不管有没有异常,所以finally可以维护对象的内部状态,并可以清理非内存资源。finally在try{}catch{}中可有可无,一般用于关闭文件流操作时,关闭数据库操作。 -
finalize
:是一个方法,在Java中,允许使用finalize()方法在垃圾回收器清理对象之前做清理工作。这个方法是有垃圾回收器在确定一个对象没有引用时对这个对象调用。它是在object内定义的类,可以被所有类重写。
Arraylist 和LinkedList 的区别,及源码分析
相同点
- ArrayList和LinkedList 都实现了List接口,都是元素的容器,存放对象的引用。可以对对象进行增删改查和排序操作。但是他们除了实现List接口也实现了其他接口,因此造就了他们的差异。
- List接口是Collection的子 类。
- ArrayList 和LinkedList都可以实现队列、栈的数据结构,但是因为LinkedList实现了对列的接口,所以用LinkedList实现队列和栈比较合适。
- ArrayList 和LinkedList都是线程不安全的。
ArrayList
- ArrayList 内部是使用数组实现的,数组是静态分配内存,所有有一个初始化大小,初始化大小是10 ,
private static final int DEFAULT_CAPACITY = 10;
- 在进行插入元素时会判断是否需要扩容,扩容的步长是原容量的0.5倍,扩容采用复制的方式,需要将插入位置之后的元素进行后移(System.arrayCopy方法实现),所以越前面的元素开销越大,因此 ArrayList 的插入和删除操作比较慢,时间复杂度是O(n)。
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
ArrayList 删除元素时,将元素置空,方便GC进行回收,并不改变原有的长度。
- ArrayList 实现了RandomAccece接口,对元素的随机访问速度非常快。因此ArrayList的修改和查找效率比较快,可以通过数组下标进行查找,时间复杂度是O(1)
- 所以在需要大量查询和修改元素时使用ArrayList比较合适。
LinkedList
- LinkedList的内部实现结构是双向链表,链表是动态分配内存。它不仅存储对象还需要存储两个引用,一个指向前一个元素,一个指向后一个元素。
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
- LinkedList在插入元素和删除元素时只需要修改元素的指针指向即可,不需要额外扩容,所以LinkedList的插入和删除效率比较高,时间复杂度是O(1)。
LinkedList批量插入元素是通过for循环数组,ArrayList是通过System.arrayCopy复制操作。
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index); // 检查在0-size之间
Object[] a = c.toArray(); // 转换成数组,方便进行各种循环操作
int numNew = a.length;
if (numNew == 0) // 没有需要新增的元素
return false;
Node<E> pred, succ;
if (index == size) { // 最链表尾部增加节点
succ = null; // 后一个节点 是空
pred = last; // 前一个节点就是队尾
} else {
succ = node(index); // 取出index节点,作为后一个节点
pred = succ.prev;
}
// 循环操作 增加节点
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null); // 用前一个节点和增加的元素创建一个节点
if (pred == null) // 是头节点
first = newNode;
else // 不是头节点,修改指针
pred.next = newNode;
pred = newNode; // 迭代
}
if (succ == null) {
last = pred;
} else { // 在队中插入,更新节点指针
pred.next = succ;
succ.prev = pred;
}
// 更新 size
size += numNew;
modCount++;
return true;
}
- LinkedList没有实现RandomAccece接口,在查找和修改元素时需要移动指针,效率比较慢,时间复杂度是O(n)。
// 查询index 节点时,把链表分两半查找,是从后开始查询还是前面开始查询,提高查询效率
/**
* Returns the (non-null) Node at the specified element index.
*/
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
// 查询都需要遍历元素查找
public int indexOf(Object o) {
int index = 0;
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
- 所以在插入和删除的情况比较多的时候使用LinkedList比较合适。
单向链表和双向链表的区别
- 单链表只有一个指向下一个节点的指针,也就是只能next.
- 双向链表有一个向前的指针和一个向后的指针,可以通过prev()读取前一个元素。 因此双向链表也会比单链表更占内存。
HashMap 的原理?发生hash碰撞的时候如何解决?什么时候开始扩容?扩容的过程?
- HashMap是基于hashing原理,先对键调用hashcode方法,计算并返回用来寻找Map数组bucket位置的hashcode,在这个bucket(桶)位置存储Node对象(Map.Node)
- put 的过程:
- 计算Key 的hash 值,然后再计算出索引
- 如果没有相同的hash值,直接放入桶中
- 如有相同的hash值(hash碰撞),放入到同一个桶中,以链表的方式连接到后面
- 当链表的长度=8 的时候转为红黑树,当红黑树节点少于6的时候转为链表
- 当容量(16)*加载因子(0.75)桶满了的时候就需要扩容。扩容调用resize 方法,扩容为原来的两倍,扩容使用for循环将原所有元素转移到新的桶中。
/**
* Adds a new entry with the specified key, value and hash code to
* the specified bucket. It is the responsibility of this
* method to resize the table if appropriate.
*
* Subclass overrides this to alter the behavior of put method.
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) { //超过负载因子的时候 需要扩容
resize(2 * table.length);
hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
// 扩容的过程:扩容为原table的2倍,扩容过程是创建一个新长度的hashmapEntry,并通过for循环将原存储对象转移到新的 table中
void resize(int newCapacity) {
HashMapEntry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
HashMapEntry[] newTable = new HashMapEntry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
- 有什么办法可以减少碰撞?
- 扰动函数:扰动即hash方法内部的算法实现,目的是使的不同的对象返回不同的hashcode。原理是两个不相等的对象返回不同的hashcode,碰撞的几率就会小点,存在链表的结构会比较少,查找时也比较快。
- 使用不可变的声明final的对象 因为不可变性能够缓存不同键的hashcode ,减少碰撞的发生,所以String,Interge 适合做键,因为String 不仅是final修饰的,而且实现了equals和hashcode方法。
- HashMap的hash函数怎么实现的?
-计算key的hashcode —h
对h无符号向右位移 16位 i
h和i做异或运算。使得高位也可以参与hash,更大程度上减少了碰撞率
static final int hash(Object var0) {
int var1;
return var0 == null ? 0 : (var1 = var0.hashCode()) ^ var1 >>> 16;
}
- 为什么不使用二叉树而使用红黑树?
- 二叉树在特殊情况下会变为线性结构的,这跟原来的链表的查找一样,查询的时候会比较慢。而红黑树在插入新数据时会通过左旋右旋变色等操作保持平衡,引入红黑树是为了解决链表深度查询问题,加快查找速度,但是为了保持平衡有一定的开销,如果在链表节点少的时候查找的开销不如保持红黑树的开销大,所以是链表和红黑树结合使用。 - 红黑树的总结
- 每个节点非红即黑
- 根节点必须是黑色
- 如果节点是红色,则它的子节点必须是黑色(反之不一定)
- 从根节点到叶子节点或空节点的每条路径必须包含相同的黑色节点。 - 当多线程对hashmap进行扩容时会发生什么问题?
当hashmap需要扩容时,有两个线程同时尝试进行resize操作,重新new 一个长度为原来2倍的HashTableEntry,并遍历原数组将原数组重新hash到新数组。在多线程的情况下会发生死循环的问题(多线程使用currentHashMap). 多线程进行扩容和转移数据,最后一只有一个线程会赋值给table,会造成数据丢失。 - 多线程的操作下发生的死循环是如何产生的?
总结:当有两个线程同时对HashMap进行扩容时,线程A先行完成了扩容, 并将原Map的链表重新散列到自己的表中,并且链表变成了倒序,线程B在进行扩容的时候,又进行自己的散列,将原来倒序的链表又正序过来,形成了一个环形链表,当get一个链表不存在元素时就会发生死循环,CPU占比100%。在JDK1.8中,链表扩容转为红黑树,没有相关问题。 - HashMap 和HashTable,HashSet、LinkedHashMap、TreeMap的区别
- HashMap 是随机访问的,获取数据的顺序完全是随机的。具有很快的访问速度
- LinkedHashMap是HashMap的子类,保存了记录插入记录的顺序,先得到的数据肯定是先插入的,如果获取数据的顺序和插入数据的顺序一样可以使用LinkedHashMap.
- TreeMap实现了SortedMap接口,能够把它保存的记录根据键排序,默认是生序排序,也可以指定排序的比较器。如果需要得到的数据是排过序的可以使用TreeMap.
- HashMap初始化大小是16,加载因子是0.75,允许键值为null,线程不安全。扩容是原大小的2倍
- HashTable初始化大小是11,加载因子是0.75,是Dictionary的子类,键值不允许是null,内部都使用使用Synchronized,保证线程安全。扩容是原来的2倍+1
- HashSet是一个保存了一个固定key值的HashMap.
Java 多线程
进程和线程的区别
- 进程是一个可以独立运行环境,是一个程序或应用,一个进程至少有一个线程,不同的进程使用不同的内存空间。线程是一个进程中的执行任务,所有线程公用同一个内存空间。
sleep 和wait()、yield() 方法的区别
- 两者sleep 和 wait 都是暂停当前线程,OS将执行时间分配给其他线程,两者都是final方法,无法被重写。
- sleep 是Thread类的方法,wait是Object 方法。
- sleep不会释放锁,wait会释放锁,必须由其他线程调用notify来唤醒。
- yield方法和sleep方法一样是让出CPU权限,但yield方法只能让拥有相同优先级或更高优先级的线程获取CPU 执行时间的机会,而wait()方法不考虑优先级;yield也不会释放锁。
- 调用sleep方法之后线程进入阻塞状态,yield方法之后线程依旧处于ready状态,只需要等待重新获取CPU的执行时间即可。
上下文切换
- 上下文切换就是在运行一个线程的过程中中转到到另一个线程:切换线程需要保存线程的状态。上下文切换中会记录程序计数器、CPU寄存器等状态数据。
- 总结:上下文切换就是存储和恢复CPU状态的过程,它使得线程可以从中断点继续执行。
你如何确保main()方法所在的线程是Java程序最后结束的线程?
可以使用joint()方法确保所有线程在main 方法退出前结束。
同步方法和同步块,哪个是更好的选择?
同步块是更好的选择,因为同步方法会锁住整个对象,即使这个类中有多个不相关联的方法,也需要等待获得对象锁。
什么是ThreadLocal?
ThreadLocal是创建线程的本地变量,对象的所有线程公用它的全局变量,所以他是线程不安全的,我们可以使用同步技术,但是当我们不需要使用同步技术的时候,可以使用ThreadLocal变量。
volatile的作用?如果已经加了synchronize为什么还要加volatile?或者说volatile修饰的int i,多个线程同时进行 i++ 操作,这样可以实现线程安全吗?
- volatile可以保证可见性,在java中为了加快程序运行效率,部分变量的操作是在该线程的寄存器或缓存中操作的,之后才同步到主存中,但是使用volatile修饰的变量直接读取主存。
class MyThread extends Thread{
private volatile boolean isStop = false;
@Override
public void run() {
super.run();
while (!isStop){
System.out.print("do ....");
}
}
public void setStop(){
isStop = true;
}
}
在调用了setStop设置isStop的值后,在while里可以立即读取到isStop修改后的值。
- Volatile禁止指令重排:指令重排指 处理器为提高代码运行效率,会对输入的代码进行优化,它不保证执行的代码顺序和写入的代码顺序一致,但会保证执行的结果一致。在单线程下没问题,但在多线程并发下可能有问题。
而使用volatile修饰的变量的读/写操作是,其前面的操作肯定已经完成且其结果对后面的操作可见。
示例:
//在线程1中
context = loadContext(); //语句1
inited = true; //语句2
//在线程2中
while(!inited){
sleep();
}
doSomething(context);
分析由于指令重排,有可能执行语句2再执行语句1 ,导致cotext没有初始化,而在线程2中用未初始化的context执行会出现空指针。
//修改 在inited 加上volatile修饰,执行到inited的时候context一定执行过了
//在线程1中
context = loadContext(); //语句1
volatile inited = true; //语句2
//在线程2中
while(!inited){
sleep();
}
doSomething(context);
因此如果已经加了synchronize为什么还要加volatile,最主要防止指令重排出现的空指针。
- synchronize可作用于一段代码或方法,既可以保证可见性又可以保证原子性。
可见性:体现在使用synchronize或Lock修饰的一段代码能保证同一时刻只有一个线程获取锁执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存中。
原子性:要么不执行,要么执行到底。比如i++ 就不是原子性操作,分为了3步,i= 0 ,i+1 ,然后将+1 后的结果写入内存。期间有可能被中断,因此不是原子操作。
**分析:**自增操作不是原子操作,volatile不能保证原子性。当线程A读取到i= 0 就切换到了线程B,线程B读取到的i= 0 ,此时线程A对i++ ,此时线程B也进行i++,因为之前线程B读取到的i= 0 ,此时线程B对i自增为1 ,但其实线程A已经对i自增为1了,因此最终的结果比应该自增的结果小。
private volatile int i = 0;
private void add() {
i++;
}
public static void main(String[] args){
final MyClass myClass = new MyClass();
for (int i = 0; i < 10; i++) {
new Thread(){
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
myClass.add();
}
}
}.start();
//等前面线程执行完毕
while (Thread.activeCount()>1){
Thread.yield();
}
System.out.print("i++ = "+i);
}
**修改如下:**加锁,对自增操作只允许一个线程操作。
private int i = 0;
private synchronize void add() {
i++;
}
总结:
- volatile和synchronize都是线程同步。
- v保证可见性和禁止指针重排,不保证原子性,syn保证可见性和原子性。
- 在性能上syn保证只有一个线程执行,效率较低,某些情况下volatile的性能会比synchronize高。
- Volatile仅修饰在变量上,synchronize可以修饰在类、方法、变量上。
- volatile 不会阻塞线程,synchronize会阻塞线程。