【Java面试】第五天

在这里插入图片描述

🌟个人主页:时间会证明一切.


如何将集合变成线程安全的

  1. 在调用集合前,使用synchronized或者ReentrantLock对代码加锁(读写都要加锁)
public class SynchronizedCollectionExample {
    private List<Integer> list = new ArrayList<>();

    public void add(int value) {
        synchronized (SynchronizedCollectionExample.class) {
            list.add(value);
        }
    }

    public int get(int index) {
        synchronized (SynchronizedCollectionExample.class) {
            return list.get(index);
        }
    }
}

  1. 使用ThreadLocal,将集合放到线程内访问,但是这样集合中的值就不能被其他线程访问了
public class ThreadLocalCollectionExample {
    private ThreadLocal<List<Integer>> threadLocalList = ThreadLocal.withInitial(ArrayList::new);

    public void add(int value) {
        threadLocalList.get().add(value);
    }

    public int get(int index) {
        return threadLocalList.get().get(index);
    }
}

  1. 使用Collections.synchronizedXXX()方法,可以获得一个线程安全的集合
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;

public class CollectionsSynchronizedExample {
    List<Integer> synchronizedList = Collections.synchronizedList(new ArrayList<Integer>());

    public void add(int value) {
        synchronizedList.add(value);  
    }

    public int get(int index) {
        return synchronizedList.get(index); 
    }
}

  1. 使用不可变集合进行封装,当集合是不可变的时候,自然是线程安全的
import com.google.common.collect.ImmutableList;

public class ImmutableCollectionExample {
    private ImmutableList<Integer> immutableList = ImmutableList.of(1, 2, 3);

    public int get(int index) {
        return immutableList.get(index);
    }
}

或者:

import java.util.List;

public class ImmutableJava9Example {
    private List<Integer> immutableList = List.of(1, 2, 3);

    public int get(int index) {
        return immutableList.get(index);
    }
}

创建线程有几种方式

在Java中,共有四种方式可以创建线程,分别是

  • 继承Thread类创建线程
  • 实现Runnable接口创建线程
  • 通过Callable和FutureTask创建线程
  • 通过线程池创建线程

其实,归根结底最终就两种,一个是继承Thread类,一个是实现Runnable接口,至于其他的。也是基于这两个方式实现的。但是有的时候面试官更关注的是实际写代码过程中,有几种方式可以实现。所以一般回答4种也没啥毛病。

Runnable和Callable区别

Runnable接口和Callable接口都可以用来创建新线程,实现Runnable的时候,需要实现run方法;实现Callable接口的话,需要实现call方法。

Runnable的run方法无返回值,Callable的call方法有返回值,类型为Object

Callable中可以够抛出checked exception,而Runnable不可以。

Callable和Runnable都可以应用于executors。而Thread类只支持Runnable。

Future

Future是一个接口,代表了一个异步执行的结果。接口中的方法用来检查执行是否完成、等待完成和得到执行的结果。当执行完成后,只能通过get()方法得到结果,get方法会阻塞直到结果准备好了。如果想取消,那么调用cancel()方法。

FutureTask是Future接口的一个实现,它实现了一个可以提交给Executor执行的任务,并且可以用来检查任务的执行状态和获取任务的执行结果。

FutureTask和Callable示例

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class FutureAndCallableExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        Callable<String> callable = () -> {
            System.out.println("Entered Callable");
            Thread.sleep(2000);
            return "Hello from Callable";
        };

        FutureTask<String> futureTask = new FutureTask<>(callable);
        Thread thread = new Thread(futureTask);
        thread.start();

        System.out.println("Do something else while callable is getting executed");
        System.out.println("Retrieved: " + futureTask.get());
    }
}

什么是ThreadLocal,如何实现的

ThreadLocal是java.lang下面的一个类,是用来解决java多线程程序中并发问题的一种途径;通过为每一个线程创建一份共享变量的副本来保证各个线程之间的变量的访问和修改互相不影响;

ThreadLocal存放的值是线程内共享的,线程间互斥的,主要用于线程内共享一些数据,避免通过参数来传递,这样处理后,能够优雅的解决一些实际问题。

比如一次用户的页面操作请求,我们可以在最开始的filter中,把用户的信息保存在ThreadLocal中,在同一次请求中,再使用到用户信息,就可以直接到ThreadLocal中获取就可以了。

ThreadLocal有四个方法,分别为:

  • initialValue
    • 返回此线程局部变量的初始值
  • get
    • 返回此线程局部变量的当前线程副本中的值。如果这是线程第一次调用该方法,则创建并初始化此副本。
  • set
    • 将此线程局部变量的当前线程副本中的值设置为指定值。许多应用程序不需要这项功能,它们只依赖于 initialValue() 方法来设置线程局部变量的值。
  • remove
    • 移除此线程局部变量的值。

ThreadLocal的实现原理

ThreadLocal中用于保存线程的独有变量的数据结构是一个内部类:ThreadLocalMap,也是k-v结构。
key就是当前的ThreadLocal对象,而v就是我们想要保存的值。

image.png

上图中基本描述出了Thread、ThreadLocalMap以及ThreadLocal三者之间的包含关系。

Thread类对象中维护了ThreadLocalMap成员变量,而ThreadLocalMap维护了以ThreadLocal为key,需要存储的数据为value的Entry数组。这是它们三者之间的基本包含关系,我们需要进一步到源码中寻找踪迹。

查看Thread类,内部维护了两个变量,threadLocals和inheritableThreadLocals,它们的默认值是null,它们的类型是ThreadLocal.ThreadLocalMap,也就是ThreadLocal类的一个静态内部类ThreadLocalMap。

在静态内部类ThreadLocalMap维护一个数据结构类型为Entry的数组,节点类型如下代码所示:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

从源码中我们可以看到,Entry结构实际上是继承了一个ThreadLocal类型的弱引用并将其作为key,value为Object类型。这里使用弱引用是否会产生问题,我们这里暂时不讨论,在文章结束的时候一起讨论一下,暂且可以理解key就是ThreadLocal对象。对于ThreadLocalMap,我们一起来了解一下其内部的变量:

// 默认的数组初始化容量
private static final int INITIAL_CAPACITY = 16;
// Entry数组,大小必须为2的幂
private Entry[] table;
// 数组内部元素个数
private int size = 0;
// 数组扩容阈值,默认为0,创建了ThreadLocalMap对象后会被重新设置
private int threshold;

这几个变量和HashMap中的变量十分类似,功能也类似。
ThreadLocalMap的构造方法如下所示:

/**
 * Construct a new map initially containing (firstKey, firstValue).
 * ThreadLocalMaps are constructed lazily, so we only create
 * one when we have at least one entry to put in it.
 */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 初始化Entry数组,大小 16
    table = new Entry[INITIAL_CAPACITY];
    // 用第一个键的哈希值对初始大小取模得到索引,和HashMap的位运算代替取模原理一样
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 将Entry对象存入数组指定位置
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    // 初始化扩容阈值,第一次设置为10
    setThreshold(INITIAL_CAPACITY);
}

从构造方法的注释中可以了解到,该构造方法是懒加载的,只有当我们创建一个Entry对象并需要放入到Entry数组的时候才会去初始化Entry数组。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值