Java相关

156 阅读8分钟

1、synchronized和volatile的区别

volatile具有可见性、有序性,不具备原子性。volatile不会让线程阻塞,响应速度比synchronized高,这是它的优点。

  • volatile不具备原子性synchronized具备原子性
  • volatile不会让线程阻塞synchronized会让线程阻塞

2、线程池

Executors类是java.util.concurrent提供的一个创建线程池的工厂类,使用该类可以方便的创建线程池,此类提供的几种方法,支持创建四种类型的线程池,分别是:newCachedThreadPoolnewFixedThreadPoolnewScheduledThreadPoolnewSingleThreadExecutor。还可以自己根据需求创建ThreadPoolExecutor

线程池方法 初始化线程池数 最大线程池数 线程池中线程存活时间 时间单位 工作队列
newCachedThreadPool 0 Integer.MAX_VALUE 60 SynchronousQueue
newFixedThreadPool 入参指定大小 入参指定大小 0 毫秒 LinkedBlockingQueue
newScheduledThreadPool 入参指定大小 Integer.MAX_VALUE 0 微秒 DelayedWorkQueue
newSingleThreadExecutor 1 1 0 毫秒 LinkedBlockingQueue

3、ThreadLocal

ThreadLocal用于保存某个线程共享变量,

4、ArrayList和LinkedList区别

  1. ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。 (LinkedList是双向链表,有next也有previous)

  2. 对于随机访问get和set,ArrayList绝对优于LinkedList,因为LinkedList要移动指针。

  3. 对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。

  4. ArrayList允许重复元素,允许 null 值,它不是线程安全的。

  5. LinkedList不是线程安全的,怎么解决线程不安全的问题:

    // 方法一、
    List<String> list = Collections.synchronizedList(new LinkedList<String>());
    // 方法二、
    // 将LinkedList全部换成ConcurrentLinkedQueue
    
    // 解决ArrayList线程不安全的问题
    // 将ArrayList替换成CopyOnWriteArrayList
    

5、HashMap

HashMap是基于哈希表的Map接口的非同步实现,并允许使用null值和null键。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。数组:存储区间连续,占用内存严重,寻址容易,插入删除困难; 链表:存储区间离散,占用内存比较宽松,寻址困难,插入删除容易;HashMap综合应用了这两种数据结构,实现了寻址容易,插入删除也容易。

HashMap的工作原理:HashMap是基于散列法(又称哈希法hashing)的原理,使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode用于找到bucket(桶)位置来储存Entry对象。”HashMap是在bucket中储存键对象和值对象,作为Map.Entry。并不是仅仅只在bucket中存储值。

Hash冲突:例如, 第一个键值对A进来。通过计算其key的hash得到的index=0。记做:Entry[0] = A。 第二个键值对B,通过计算其index也等于0, HashMap会将B.next =A,Entry[0] =B, 第三个键值对 C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方事实上存取了A,B,C三个键值对,它们通过next这个属性链接在一起。我们可以将这个地方称为桶。 对于不同的元素,可能计算出了相同的函数值,这样就产生了“冲突”。解决冲突的方法:

  1. 链地址法:将哈希表的每个单元作为链表的头结点,所有哈希地址为 i 的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。
  2. 开放定址法:即发生冲突时,去寻找下一个空的哈希地址。只要哈希表足够大,总能找到空的哈希地址。
  3. 再哈希法:即发生冲突时,由其他的函数再计算一次哈希值。
  4. 建立公共溢出区:将哈希表分为基本表和溢出表,发生冲突时,将冲突的元素放入溢出表。

HashMap 就是使用链地址法来解决冲突的(jdk8中采用平衡树来替代链表存储冲突的元素,但hash() 方法原理相同)。数组中的每一个单元都会指向一个链表,如果发生冲突,就将 put 进来的 K- V 插入到链表的尾部。

如何重新调整HashMap的大小

HashMap的扩容阈值(threshold = capacity* loadFactor 容量值范16~2的30次方),就是通过它和size进行比较来判断是否需要扩容。默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组(jdk1.6,但不超过最大容量),来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。

为什么HashMap在多线程中是非线程安全的

  1. 哈希碰撞:本来在HashMap中,发生哈希冲突是可以用链表法或者红黑树来解决的,但是在多线程中发生哈希冲突可能就直接给覆盖了。
  2. 扩容:HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。

怎么解决非线程安全的问题:采用另一个集合类ConcurrentHashMap。这个集合类兼顾了线程安全和性能。

6、Java多线程同步的意义及作用

link

为何要使用同步?java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查), 将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用, 从而保证了该变量的唯一性和准确性。

同步方式

  • 同步方法:即有***synchronized***关键字修饰的方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。***synchronized***关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权。

    public synchronized void add(int value) {
        sunCount += value;
        System.out.println(getDate() + " 存进:" + value);
    }
    
  • 同步代码块: 即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。运行效率上来说也比方法同步效率高,同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

    public void add(int value) {
        synchronized(object){
            sunCount += value;
            System.out.println(getDate() + " 存进:" + value);
        }
    }
    
  • 使用特殊域变量(volatile)实现线程同步

    a.volatile关键字为域变量的访问提供了一种免锁机制,

    b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,

    c.因此每次使用该域就要重新计算,而不是使用寄存器中的值

    d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量

    private volatile int count =0;
    
  • 使用重入锁实现线程同步

    ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。ReenreantLock类的常用方法有:ReentrantLock() : 创建一个ReentrantLock实例,lock() : 获得锁, unlock() : 释放锁 。ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁 。

    private Lock mLock = new ReentrantLock();
    public void add(int value) {
        mLock.lock(); // 上锁
        try {
            sunCount += value;
            System.out.println(getDate() + " 存进:" + value);
        } finally {
            mLock.unlock(); // 解锁
        }
    }
    
  • 使用局部变量实现线程同步ThreadLocal

    使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。现在明白了吧,原来每个线程运行的都是一个副本,也就是说存钱和取钱是两个账户,只是名字相同而已。

    ThreadLocal与同步机制的区别: a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。b.前者采用以"空间换时间"的方法,后者采用以"时间换空间"的