Bootstrap

大数据面试题整理1

Java语言特性

1. static关键字

  1. 最主要作用:方便在没有创建对象的情况下来进行调用(方法/变量)。被static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问。
  2. 权限:静态方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问静态成员方法和静态成员变量。
  3. static变量也称为静态变量,静态变量和非静态变量的区别:
    1. 静态变量被所有对象共享,在内存中只有一个副本,在类初次加载的时候才会初始化
    2. 非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响
    3. static成员变量初始化顺序按照定义的顺序来进行初始化
  4. static能作用于局部变量么?static是不允许用来修饰局部变量
  5. 能通过this访问静态成员变量吗?静态成员变量虽然独立于对象,但是不代表不可以通过对象去访问,所有的静态方法和静态变量都可以通过对象访问(只要访问权限足够)。

2. string,stringgbuffer,stringbuilder区别:

  1. 总:
    1. String:不可变字符串;
    2. StringBuffer:可变字符串、效率低、线程安全;
    3. StringBuilder:可变字符序列、效率高、线程不安全;
  2. 初始化上的区别,String可以空赋值,后者不行,报错

3. final,finally,finalize区别:

final       用于申明属性,方法和类,表示属性不可变,方法不可以被覆盖,类不可以被继承。
finally     是异常处理语句结构中,表示总是执行的部分。  
finallize   表示是object类一个方法,在垃圾回收机制中执行的时候会被调用被回收对象的方法。允许回收此前未回收的内存垃圾。所有object都继承了.

final表示不可变,finally表示必须执行的语句,finalize表示垃圾回收时执行的代码。

4. == 和equals区别:

  • 基本数据类型(也称原始数据类型) :byte,short,char,int,long,float,double,boolean。他们之间的比较,应用双等号(==),比较的是他们的值。
  • 引用数据类型:当他们用(==)进行比较的时候,比较的是他们在内存中的存放地址(确切的说,是堆内存地址)。
  • == 的作用:
      基本类型:比较的就是值是否相同
      引用类型:比较的就是地址值是否相同
    equals 的作用:
      引用类型:默认情况下,比较的是地址值。
    注:不过,我们可以根据情况自己重写该方法。一般重写都是自动生成,比较对象的成员变量值是否相同

Java集合

  1. HashTable 和 HashMap区别。

2. HashMap和CurrentHashMap区别:

在ConcurrentHashMap中,就是把Map分成了N个Segment,put和get的时候,都是现根据key.hashCode()算出放到哪个Segment中。

3. HashMap解决hash冲突的方法

解决方案:HashMap的数据结构是:数组Node[]与链表Node中有next Node.

HashMap中的实际数据结构

(1)如果上述的 persons.put(“1”,”jack”);persons.put(“2”,”john”); 同时计算到的hash值都为123,那么jack先放在第一列的第一个位置Node-jack,persons.put(“2”,”john”);执行时会将Node-jack的next(Node) = Node(john),Jack的下个节点将指向Node(john)。

(2)那么取的时候呢,persons.get(“2”),这个时候取得的hash值是123,即table[123],这时table[123]其实是Node-jack,Key值不相等,取Node-jack的next下个Node,即Node-John,这时Key值相等了,然后返回对应的person.
其他的哈希冲突解决方法:线性探测,平方探测,二次哈希,拉链法。

4. hashmap大小为啥是2的幂次?

HashMap是根据key的hash值决定key放到哪个桶中,通过tab[i = (n - 1) & hash]公式计算得出

这样做的好处在于:

  • &运算速度快,至少比%取模运算快

  • 能保证索引值肯定在HashMap的容量大小范围内

  • (n - 1) & hash的值是均匀分布的,可以减少hash冲突

2^n能保证均分。

Java%/操作比&慢10倍左右,因此采用&运算会提高性能。

(通过限制length是一个2的幂数,h & (length-1)h % length结果是一致的。)

5. HashMap扩容

resize()

 HashMap在底层数据结构上采用了数组+链表+红黑树

hashMapåå­ç»æå¾

如果存在Hash碰撞就会以链表的形式保存,把当前传进来的参数生成一个新的节点保存在链表的尾部(JDK1.7保存在首部)。而如果链表的长度大于8那么就会以红黑树的形式进行保存。

第一种:使用默认构造方法初始化HashMap。从前文可以知道HashMap在一开始初始化的时候会返回一个空的table,并且thershold为0。因此第一次扩容的容量为默认值DEFAULT_INITIAL_CAPACITY也就是16。同时threshold = DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR = 12。

第二种:指定初始容量的构造方法初始化HashMap。那么从下面源码可以看到初始容量会等于threshold,接着threshold = 当前的容量(threshold) * DEFAULT_LOAD_FACTOR。

第三种:HashMap不是第一次扩容。如果HashMap已经扩容过的话,那么每次table的容量以及threshold量为原有的两倍。
 

负载因子loadFactor保持在0.75f是在时间跟空间上达到一个平衡,实际上也就是说0.75f是效率相对比较高的

6. Jdk1.8对Hashmap的改进

结构上:

示æå¾

向HashMap添加数据(成对 放入 键 - 值对)

示æå¾

为什么不直接采用经过hashCode()处理的哈希码 作为 存储数组table的下标位置?

示æå¾

为什么采用 哈希码 与运算(&) (数组长度-1) 计算数组下标?

示æå¾

为什么在计算数组下标前,需对哈希码进行二次处理:扰动处理?

示æå¾

7. HashMap的key适用什么数据类型?

Map 的 key 和 value 都不允许是 基本数据 类型

1、HashMap 的 key 和 value 都可以是 null

2、Map 的 key 和 value 都 不允许 是 基本数据类型

3、HashMap 的 key 可以是 任意对象,但如果 对象的 hashCode 改变了,那么将找不到原来该 key 所对应的 value

8. ArrayList和LinkedList的区别

1.ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
2.对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
3.对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。

9. Hashset的底层实现

       对于HashSet而言,它是基于HashMap实现的,集成AbstractSet 实现Set接口,HashSet底层使用HashMap来保存所有元素,因此HashSet 的实现比较简单,相关HashSet的操作,基本上都是直接调用底层HashMap的相关方法来完成。

/**

 * @param e 将添加到此set中的元素。
 * @return 如果此set尚未包含指定元素,则返回true。
 */
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

由于 HashMap 的 put() 方法添加 key-value 对时,当新放入 HashMap 的 Entry 中 key 与集合中原有 Entry 的 key 相同(hashCode()返回值相等,通过 equals 比较也返回 true),新添加的 Entry 的 value 会将覆盖原来 Entry 的 value(HashSet 中的 value 都是PRESENT),但 key 不会有任何改变,因此如果向 HashSet 中添加一个已经存在的元素时,新添加的集合元素将不会被放入 HashMap中,原来的元素也不会有任何改变,这也就满足了 Set 中元素不重复的特性。

Java多线程

1. 进程和线程的区别

进程与线程的区别
(1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。

(2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行。

(3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。

(4)系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。
 

2. java多线程的6种实现方式

(1)继承Thead类。继承时,重写Thread类的run方法即可。

(2)实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target。

这2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。

Callable和Future,返回值是Object,所以返回的结果可以放在Object对象中。

(3) 通过Callable和FutureTask创建线程

Future提供了三种功能:

  1)判断任务是否完成;

  2)能够中断任务;

  3)能够获取任务执行结果。

因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask。

a:创建Callable接口的实现类 ,并实现Call方法

b:创建Callable实现类的实现,使用FutureTask类包装Callable对象,该FutureTask对象封装了Callable对象的Call方法的返回值

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

d:调用FutureTask对象的get()来获取子线程执行结束的返回值

public class ThreadDemo03 {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Callable<Object> oneCallable = new Tickets<Object>();
        FutureTask<Object> oneTask = new FutureTask<Object>(oneCallable);

        Thread t = new Thread(oneTask);

        System.out.println(Thread.currentThread().getName());

        t.start();

    }

}

class Tickets<Object> implements Callable<Object>{

    //重写call方法
    @Override
    public Object call() throws Exception {
        // TODO Auto-generated method stub
        System.out.println(Thread.currentThread().getName()+"-->我是通过实现Callable接口通过FutureTask包装器来实现的线程");
        return null;
    }   

  ExecutoreService提供了submit()方法,传递一个Callable,或Runnable,返回Future。

 (4) 通过线程池创建线程。

public class ThreadDemo05{

    private static int POOL_NUM = 10;     //线程池数量

    /**
     * @param args
     * @throws InterruptedException 
     */
    public static void main(String[] args) throws InterruptedException {
        // TODO Auto-generated method stub
        ExecutorService executorService = Executors.newFixedThreadPool(5);  
        for(int i = 0; i<POOL_NUM; i++)  
        {  
            RunnableThread thread = new RunnableThread();

            //Thread.sleep(1000);
            executorService.execute(thread);  
        }
        //关闭线程池
        executorService.shutdown(); 
    }   

}

class RunnableThread implements Runnable  
{     
    @Override
    public void run()  
    {  
        System.out.println("通过线程池方式创建的线程:" + Thread.currentThread().getName() + " ");  

    }  
}  

3. 线程安全的定义

线程安全可以简单理解为一个方法或者一个实例可以在多线程环境中使用而不会出现问题。

在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。如,同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件。实际上,这些问题只有在一或多个线程向这些资源做了写操作时才有可能发生,只要资源没有发生变化,多个线程读取相同的资源就是安全的。
定义:

1.无状态的一定是线程安全的。这个很好理解,因为所谓线程不安全也就是一个线程修改了状态,而另一个线程的操作依赖于这个被修改的状态。
2.只有一个状态,而且这个状态是由一个线程安全的对象维护的,那这个类也是线程安全的。比如你在数据结构里只用一个AtomicLong来作为计数器,那递增计数的操作都是线程安全的,不会漏掉任何一次计数,而如果你用普通的long做++操作则不一样,因为++操作本身涉及到取数、递增、赋值 三个操作,某个线程可能取到了另外一个线程还没来得及写回的数就会导致上一次写入丢失。
3.有多个状态的情况下,维持不变性(invariant)的所有可变(mutable)状态都用同一个锁来守护的类是线程安全的。这一段有些拗口,首先类不变性的意思是指这个类在多线程状态下能正确运行的状态,其次用锁守护的意思是所有对该状态的操作都需要获取这个锁,而用同一个锁守护的作用就是所有对这些状态的修改实际最后都是串行的,不会存在某个操作中间状态被其他操作可见,继而导致线程不安全。所以这里的关键在于如何确定不变性,可能你的类的某些状态对于类的正确运行是无关紧要的,那就不需要用和其他状态一样的锁来守护。因此我们常可以看到有的类里面会创建一个新的对象作为锁来守护某些和原类本身不变性无关的状态。


4. 线程安全产生的原因及解决方案

原因:

1、多个线程在操作共享的数据。

2、操作共享数据的线程代码有多条。

当一个线程在执行操作共享数据的多条代码过程中,若其它线程参与了运算,就会导致线程安全问题的产生。

线程安全问题的解决方案(2个):

方式一:同步代码块

    格式:synchronize(锁对象){

                      需要被同步的代码

               }

    同步代码块需要注意的事项:

        1.锁对象可以是任意的一个对象;

        2.一个线程在同步代码块中sleep了,并不会释放锁对象;

        3.如果不存在线程安全问题,千万不要使用同步代码块;

        4.锁对象必须是多线程共享的一个资源,否则锁不住。

    例子:三个窗口售票

class SaleTicket extends Thread{
     static int num = 50;//票数  非静态的成员变量,非静态的成员变量数据是在每个对象中都会维护一份数据的。
     public SaleTicket(String name) {
        super(name);
    }
    
    @Override
    public void run() {
        while(true){
            //同步代码块
            synchronized ("锁") {                
                if(num>0){
                    System.out.println(Thread.currentThread().getName()+"售出了第"+num+"号票");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    num--;
                }else{
                    System.out.println("售罄了..");
                    break;
                }
            }            
        }
    }            

public class Demo4 {    
    public static void main(String[] args) {
        //创建三个线程对象,模拟三个窗口
        SaleTicket thread1 = new SaleTicket("窗口1");
        SaleTicket thread2 = new SaleTicket("窗口2");
        SaleTicket thread3 = new SaleTicket("窗口3");
        //开启线程售票
        thread1.start();
        thread2.start();
        thread3.start();        
    }    
}


方式二:同步函数(同步函数就是使用synchronized修饰一个函数)

    同步函数注意事项:

        1.如果函数是一个非静态的同步函数,那么锁对象是this对象;

        2.如果函数是静态的同步函数,那么锁对象是当前函数所属的类的字节码文件(class对象);

        3.同步函数的锁对象是固定的,不能由自己指定。

    例子:两夫妻取钱

class BankThread extends Thread{
    static int count = 5000;
    public BankThread(String name){
        super(name);
    }
    
    @Override  //
    public synchronized  void run() {
        while(true){
            synchronized ("锁") {                
                if(count>0){
                    System.out.println(Thread.currentThread().getName()+"取走了1000块,还剩余"+(count-1000)+"元");
                    count= count - 1000;
                }else{
                    System.out.println("取光了...");
                    break;
                }
            }
        }
    }
 
public class Demo1 {
 
    public static void main(String[] args) {
        //创建两个线程对象
        BankThread thread1 = new BankThread("老公");
        BankThread thread2 = new BankThread("老婆");
        //调用start方法开启线程取钱
        thread1.start();
        thread2.start();    
    }
    
}
推荐使用:同步代码块

    原因:

        1.同步代码块的锁对象可以由我们自由指定,方便控制;

        2.同步代码块可以方便的控制需要被同步代码的范围,同步函数必须同步函数的所有代码。


5. Volatile关键字

被volatile修饰的共享变量,就具有了以下两点特性:

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

2 . 禁止指令重排序

可见性: 当一个变量被volatile修饰时,那么对它的修改会立刻刷新到主存,当其它线程需要读取该变量时,会去内存中读取新值。而普通变量则不能保证这一点。

有序性: 对一个volatile域的写,happens-before于后续对这个volatile域的读。

JMM

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存

当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。

底层实现:

如果把加入volatile关键字的代码和未加入volatile关键字的代码都生成汇编代码,会发现加入volatile关键字的代码会多出一个lock前缀指令。

lock前缀指令实际相当于一个内存屏障,内存屏障提供了以下功能:

1 . 重排序时不能把后面的指令重排序到内存屏障之前的位置 2 . 使得本CPU的Cache写入内存 3 . 写入动作也会引起别的CPU或者别的内核无效化其Cache,相当于让新写入的值对别的线程可见。

6. volatile和synchronized的区别

volatile和synchronized的区别
1.volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
2.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
3.volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
4.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
5.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化


7. synchronized 和lock区别

类别synchronizedLock

 

存在层次

Java的关键字,在jvm层面上是一个类
锁的释放1、以获取锁的线程执行完同步代码,释放锁  2、线程执行发生异常,jvm会让线程释放锁在finally中必须释放锁,不然容易造成线程死锁
锁的获取假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待
锁状态无法判断可以判断
锁类型可重入 不可中断  非公平可重入 可判断 可公平(两者皆可)
性能少量同步大量同步

 

一、synchronized和lock的用法区别

synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。

lock:需要显示指定起始位置和终止位置。一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类做为对象才能保证锁的生效。且在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。

二、synchronized和lock性能区别

synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。

说到这里,还是想提一下这2中机制的具体区别。据我所知,synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。

而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。

8. synchronized底层实现

1. synchronized的三种应用方式:

  1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  2. 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

synchronized(this): 

这里写图片描述

涉及两条指令:(1)monitorenter

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。

3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

(2)monitorexit

执行monitorexit的线程必须是objectref所对应的monitor的所有者。

指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个
monitor 的所有权。

通过这两段描述,我们应该能很清楚的看出synchronized的实现原理,synchronized的语义底层是通过一个monitor的对象来完成。
其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

同步方法

public synchronized void method() {
         System.out.println("Hello World!");
}

这里写图片描述

从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现)。

相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符

JVM就是根据该标示符来实现方法的同步的:当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

9. sleep和wait区别

1、每个对象都有一个锁来控制同步访问,Synchronized关键字可以和对象的锁交互,来实现同步方法或同步块。sleep()方法正在执行的线程主动让出CPU(然后CPU就可以去执行其他任务),在sleep指定时间后CPU再回到该线程继续往下执行(注意:sleep方法只让出了CPU,而并不会释放同步资源锁!!!);wait()方法则是指当前线程让自己暂时退让出同步资源锁,以便其他正在等待该资源的线程得到该资源进而运行,只有调用了notify()方法,之前调用wait()的线程才会解除wait状态,可以去参与竞争同步资源锁,进而得到执行。(注意:notify的作用相当于叫醒睡着的人,而并不会给他分配任务,就是说notify只是让之前调用wait的线程有权利重新参与线程的调度);

2、sleep()方法可以在任何地方使用;wait()方法则只能在同步方法或同步块中使用;3、sleep()是线程线程类(Thread)的方法,调用会暂停此线程指定的时间,但监控依然保持,不会释放对象锁,到时间自动恢复;wait()是Object的方法,调用会放弃对象锁,进入等待队列,待调用notify()/notifyAll()唤醒指定的线程或者所有线程,才会进入锁池,不再次获得对象锁才会进入运行状态;

;