天问的私人面试宝典01

日期类Date

常用的数据结构有:

数组,链表,栈,堆,队列,树,图,散列表

运算符

左移运算符  <<

在数字没有溢出的前提下,对于正数和负数,左移一位都相当于乘以2的1次方,左移n位就相当于乘以2的n次方。

高效算出  2*8 = 2<<3;

右移运算符 >>

右移一位相当于除2,右移n位相当于除以2的n次方。

成员变量和局部变量的区别:

    1:成员变量直接定义在类中。

       局部变量定义在方法中,参数上,语句中。

    2:成员变量在这个类中有效。

      局部变量只在自己所属的大括号内有效,大括号结束,局部变量失去作用域。

    3:成员变量存在于堆内存中,随着对象的产生而存在,消失而消失。

      局部变量存在于栈内存中,随着所属区域的运行而存在,结束而释放。

static:★★★ 关键字,是一个修饰符,用于修饰成员(成员变量和成员函数)。

特点:

    1,想要实现对象中的共性数据的对象共享。可以将这个数据进行静态修饰。

    2,被静态修饰的成员,可以直接被类名所调用。也就是说,静态的成员多了一种调用方式。类名.静态方式。

    3,静态随着类的加载而加载。而且优先于对象存在。

  弊端:

    1,有些数据是对象特有的数据,是不可以被静态修饰的。因为那样的话,特有数据会变成对象的共享数据。这样对事物的描述就出了问题。所以,在定义静态时,必须要明确,这个数据是否是被对象所共享的。

    2,静态方法只能访问静态成员,不可以访问非静态成员。

      因为静态方法加载时,优先于对象存在,所以没有办法访问对象中的成员。

    3,静态方法中不能使用this,super关键字

      因为this代表对象,而静态在时,有可能没有对象,所以this无法使用。

    4,主函数是静态的。

成员变量和静态变量的区别:

    1,成员变量所属于对象。所以也称为实例变量。

      静态变量所属于类。所以也称为类变量。

    2,成员变量存在于堆内存中。

      静态变量存在于方法区中。

    3,成员变量随着对象创建而存在。随着对象被回收而消失。

      静态变量随着类的加载而存在。随着类的消失而消失。

    4,成员变量只能被对象所调用 。

      静态变量可以被对象调用,也可以被类名调用。

    所以,成员变量可以称为对象的特有数据,静态变量称为对象的共享数据。

集合Collection

子接口set 和list接口,list是一个有序的集合,可以包含重复的元素,提供了暗索引访问的方式;set中不能包含重复的元素。

ArrayList
- 数组存放数据
- 访问效率高
- 增删数据效率可能降低

 内部数组默认初始容量是10。如果不够会以1.5倍容量增长。

ArrayList扩容:ArrayList相当于在没指定initialCapacity时就是会使用延迟分配对象数组空间,当第一次插入元素时才分配10(默认)个对象空间。假如有20个数据需要添加,那么会分别在第一次的时候,将ArrayList的容量变为10 ;之后扩容会按照1.5倍增长。也就是当添加第11个数据的时候,Arraylist继续扩容变为10*1.5=15;当添加第16个数据时,继续扩容变为15 * 1.5 =22个

可变长度数组的原理:

    当元素超出数组长度,会产生一个新数组,将原数组的数据复制到新数组中,再将新的元素添加到新数组中。

    ArrayList:是按照原数组的50%延长。构造一个初始容量为 10 的空列表。

    Vector:是按照原数组的100%延长。

LinkedList
- 双向链表
- 两端效率高
- 如果只在两端操作数据,用双向链表

Vector:ArrayList , Vector , LinkedList 是 List 的实现类
ArrayList 是线程不安全的, Vector 是线程安全的,这两个类底层都是由数组实现的
LinkedList 是线程不安全的,底层是由链表实现的

HashSet、AbstractSet无序;TreeSet有序(二叉排序法)

HashSet:底层是哈希表,包装了HashMap,相当于向HashSet中存入数据时,会把数据作为K,存入内部的HashMap中。当然K仍然不许重复。

HashMap
- 哈希表
- HashMap 使用 Entry[] 数组存放数据
- 初始长度 16
- 翻倍增长
- 存放数据的运算过程:
- key.hashCode() 获得键的哈希值计算下标 i
- 新建 Entry 实例封装键值对
- 把 Entry 实例放入 i 位置
- 如果是空位置直接方法
- 如果已经有数据,依次用 equals() 比较键是否相等
- 有相等的键,覆盖值
- 没有相等的键,用链表连接在一起
- 加载因子、负载率到 0.75
- 容量翻倍
- 所有数据重新执行哈希运算放入新数组
- jdk1.8
- 链表长度到8,转成红黑树
- 树上的数据减少到6,转回链表

Map 是键值对集合
HashTable 和 HashMap 是 Map 的实现类
HashTable 是线程安全的,不能存储 null 值
HashMap 不是线程安全的,可以存储 null 值

HashMap比HashTable效率更高

LinkedHashMap

结合双向链表的哈希表
有序的哈希表
ConcurrentHashMap

        对整个桶数组进行分割分段,在每一个段上都用Lock锁进行了行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更高,HashMap键值对允许有null值,ConcurrentHashMap不允许null值。ConcurrentHashMap,它内部细分了若干个小的 HashMap,称之为段(Segment)。 默认情况下一个 ConcurrentHashMap 被进一步细分为 16 个段。

多线程并发访问安全的哈希表
实现了分段锁
 

String、StringBuilder、Stringbuffer的区别?

String:不可变字符串;一经创建不可更改;底层的字符数组有private final修饰;

StringBuilder:可变字符串,线程不安全、效率高;没有重写equals方法,用的是Object的equals
StringBuffer:可变字符串,线程安全、效率低;没有重写equals方法。

StringBuilder、StringBuffer:

1、  封装了char[]数组

2、  是可变的字符序列

3、  提供了一组可以对字符内容修改的方法

4、  常用append()来代替字符串做字符串连接

5、  内部字符数组默认初始容量是16:initial capacity of 16 characters

6、  如果大于16会尝试将扩容,新数组大小原来的变成2倍+2,容量如果还不够,直接扩充到需要的容量大小。int newCapacity = value.length * 2 + 2;

线程

--并发和并行的区别
            --并发:是多个软件抢占一个CPU资源,发生了抢占现象
            --并行:是多个CPU,每个CPU执行一个任务,不抢
解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
解释二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。

创建线程

1.继承Thread类

2.实现Runnable接口

两种方式最终都是调用Thread类的start()方法来启动线程的。第一种方式是继承Thread类,因Java是单继承,如果一个类继承了Thread类,那么就没办法继承其它的类了,在继承上有一点受制,有一点不灵活,第二种方式就是为了解决第一种方式的单继承不灵活的问题,所以平常使用就使用第二种方式。Runnable如果要访问当前线程,则必须使用Thread.currentThread()方法。

PS(以下两种非创建线程,类似Spring实例化对象,实例化对象:new、反射、克隆、反序列化)

Callable / Future

Callable 有返回值的线程,能取消线程,可以判断线程是否执行完毕

future 是取餐条,当需要执行结果时,使用取餐条获取结果

线程常用方法

1 .start()start和run的区别:系统通过调用线程类的start()方法启动一个线程,此时该线程处于就绪状态,而非运行状态,也就意味着这个线程可以别JVM调用执行,执行的过程中,JVM通过调用想成类的run()方法来完成实际的操作,当run()方法结束后,线程也就会终止。
如果直接调用线程类的run()方法,就会被当做一个普通函数调用,程序中仍然只有一个主程序,也就是说start()方法能够异步调用run()方法,但是直接调用run()方法却是同步的,也就无法达到多线程的目的。

2. sleep() sleep和wait的区别:都是线程阻塞方法。

        Sleep:线程类Thread类的方法,调用的时候需传参(毫秒),不会释放对象锁

        wait:Object类的方法,不需要传参,需调用notify或notifyall唤醒线程,调用后会释放掉他所占用的锁,从而使线程所在对象中的其他synchronized数据可被别的线程使用。

3. yield()   

static voidyield()
          暂停当前正在执行的线程对象,并执行其他线程。

sleep与yield的区别:   1.sleep()给其他线程运行机会时,不考虑线程的优先级,因此会给低优先级的线程以运行的机会,而               yield()方法只会给相同优先级或更高优先级的线程以运行的机会。
         2.sleep()方法会转入阻塞状态,所以,执行sleep()方法的线程在指定的时间内不会被执行,而yield()方法只是使当前线程重新回到           可执行状态,所以执行yield()方法的线程很可能在进入到可执行状态后马上又被执行。
4.join

 voidjoin()
          等待该线程终止。
 voidjoin(long millis)
          等待该线程终止的时间最长为 millis 毫秒。
 voidjoin(long millis, int nanos)
          等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。

5.interrupt

 voidinterrupt()
          中断线程。
static booleaninterrupted()
          测试当前线程是否已经中断。

synchronized:互斥锁(悲观锁,有罪假设)

        每个对象都有一个monitor(锁标记),当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池。任何一个对象系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程,因此叫做互斥锁。

        synchronized(对象){}   争夺执行对象的锁

        synchronized  void  f(){}   争夺当前实例(this)的锁

        static synchrinized void f(){}   争夺类对象的锁

        生产者、消费者模型:线程间数据传递的方式;线程之间使用一个数据集合来传递,生产者存入数据、消费者取出数据

        等待和通知:

                没有数据时消费者等待;

                生产者放入数据是发出通知;

                wiat()  notify() notifyall();

                等待和通知方法必须在synchronized内调用;

                等待和通知的对象必须是同一个对象

线程工具

        1、线程池

        ExecutorService - 线程池
                Executors - 辅助创建线程池的工具类
                newFixedThreadPool(5) 最多5个线程的线程池
                newCachedThreadPool() 创建足够多的线程,使执行的任务不会等待
                newSingleThreadExecutor() 单线程线程池
        执行任务的方法
                execute(Runnable) 向线程池丢任务
                submit(Runnable) 返回一个 Future 对象,可以等待任务结束
                submit(Callable) 返回一个 Future 对象,可以异步的获得任务执行结果
        Callable / Future
                callable 任务有返回值,可以抛出异常
                future 是取餐条,当需要执行结果

        2、ThreadLocal

Lock

ReentrantLock - 可重入锁

ReadWriteLock - 读写锁         

        对读锁优化,读锁是共享锁

synchronized与lock:

需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁会自动释放,而是用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况), 要把互斥区放在try内,释放锁放在finally内! 

与互斥锁相比,读-写锁允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)从理论上讲,与互斥锁定相比,使用读-写锁允许的并发性增强将带来更大的性能提高。

Lock消耗内存更多。

volatile  多线程环境下,频繁的修改数据应添加该关键字

        可见性:不同CPU之间监听数据的修改,对数据修改可见

        禁止指令重拍:cpu可能会对代码执行进行优化,对代码顺序重新排序,加 volatile 可以禁用指令重排优化

final变量

final关键字可以修饰变量、方法和类,我们这里只讨论final修饰的变量。final变量的特殊之处在于:

        final 变量一经初始化,就不能改变其值。

        这里的值对于一个对象或者数组来说指的是这个对象或者数组的引用地址。因此,一个线程定义了一个final变量之后,其他任意线程都可以拿到这个变量。但有一点需要注意的是,当这个final变量为对象或者数组时,

        1、虽然我们不能将这个变量赋值为其他对象或者数组的引用地址,但是我们可以改变对象的域或者数组中的元素。

        2、线程对这个对象变量的域或者数据的元素的改变不具有线程可见性。
 

死锁

概念:两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞现象,若无外力作用,他们无法推进下去。

        多个操作者M>=2,争夺多个资源N>=2,M>=N

        争夺资源顺序不对。

特点:,线程活着但是不工作了,不会报异常。

解决方案:

        1.定位。

1、Jstack命令

jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息。 Jstack工具可以用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。

2、JConsole工具

Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。而且本身占用的服务器内存很小,甚至可以说几乎不消耗。

     

CAS:

CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。它体现的是一种乐观锁的思想。

两步操作,一比较二交换,如何保证这两步操作的原子性?

        由现代处理器保证,处理器在实现上提供了CAS指令。

        CAS ( 内存地址 V, 旧值 ,新值 )

        执行CAS指令的时候,CPU到内存地址 V中取出当前的value,和传入的旧值进行比较,如果相同,就把新值写到内存地址V中去。不一致,就重来,也叫自旋。不会死循环。

获取共享变量时,为了保证变量的可见性,需要使用volatile修饰。结合CAS和volatile可以实现无锁并发,用于竞争不激烈,多核CPU场景。

        因为没有使用synchronized ,所以线程不会陷入阻塞,提升效率。但如果竞争激烈,可以想到 充实必然发生,反而效率会受影响。

        CAS底层依赖于一个Unsafe 类来直接调用操作系统底层的CAS指令。Unsafe 对象不能直接调用。只能通过反射获得。

CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
AS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。

  1. 循环时间长开销很大。CAS 通常是配合无限循环一起使用的,我们可以看到 getAndAddInt 方法执行时,如果 CAS 失败,会一直进行尝试。如果 CAS 长时间一直不成功,可能会给 CPU 带来很大的开销。
  2. 只能保证一个变量的原子操作。对多个变量操作时,CAS 目前无法直接保证操作的原子性。但是我们可以通过以下两种办法来解决:1)使用互斥锁来保证原子性;2)将多个变量封装成对象,通过 AtomicReference 来保证原子性。
  3. ABA问题。

    CAS 的使用流程通常如下:1)首先从地址 V 读取值 A;2)根据 A 计算目标值 B;3)通过 CAS 以原子的方式将地址 V 中的值从 A 修改为 B。

    但是在第1步中读取的值是A,并且在第3步修改成功了,我们就能说它的值在第1步和第3步之间没有被其他线程改变过了吗?

    如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。

AQS:

AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。

AQS:也就是队列同步器,这是实现 ReentrantLock 的基础。

AQS 有一个 state 标记位,值为1 时表示有线程占用,其他线程需要进入到同步队列等待,同步队列是一个双向链表。

当获得锁的线程需要等待某个条件时,会进入 condition 的等待队列,等待队列可以有多个。

当 condition 条件满足时,线程会从等待队列重新进入同步队列进行获取锁的竞争。

ReentrantLock 就是基于 AQS 实现的,如下图所示,ReentrantLock 内部有公平锁和非公平锁两种实现,差别就在于新来的线程是否比已经在同步队列中的等待线程更早获得锁。

和 ReentrantLock 实现方式类似,Semaphore 也是基于 AQS 的,差别在于 ReentrantLock 是独占锁,Semaphore 是共享锁。

ReentrantLock里面有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。

它有公平锁FairSync和非公平锁NonfairSync两个子类。

ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。

非公平锁性能比公平锁更好。
 

它是用来构建锁或其他同步组件的基础框架,内部通过一个int类型的成员变量state来控制同步状态,当state=0时,则说明没有任何线程占有共享资源的锁,当state=1时,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待,AQS内部通过内部类Node构成FIFO的同步队列来完成线程获取锁的排队工作,同时利用内部类ConditionObject构建等待队列,当Condition调用wait()方法后,线程将会加入等待队列中,而当Condition调用signal()方法后,线程将从等待队列转移动同步队列中进行锁竞争。注意这里涉及到两种队列,一种的同步队列,当线程请求锁而等待的后将加入同步队列等待,而另一种则是等待队列(可有多个),通过Condition调用await()方法释放锁后,将加入等待队列。
 

        

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值