人工智能及大数据Java线程安全面试题你真的了解吗.docx
- 文档编号:14645702
- 上传时间:2023-06-25
- 格式:DOCX
- 页数:9
- 大小:31.43KB
人工智能及大数据Java线程安全面试题你真的了解吗.docx
《人工智能及大数据Java线程安全面试题你真的了解吗.docx》由会员分享,可在线阅读,更多相关《人工智能及大数据Java线程安全面试题你真的了解吗.docx(9页珍藏版)》请在冰点文库上搜索。
人工智能及大数据Java线程安全面试题你真的了解吗
Java线程安全面试题,你真的了解吗?
多个线程不管以何种方式访问某个类,并且在主调代码中不需要进行同步,都能表现正确的行为。
线程安全有以下几种实现方式:
不可变
不可变(Immutable)的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。
只要一个不可变的对象被正确地构建出来,永远也不会看到它在多个线程之中处于不一致的状态。
多线程环境下,应当尽量使对象成为不可变,来满足线程安全。
不可变的类型:
∙final关键字修饰的基本数据类型
∙String
∙枚举类型
∙Number部分子类,如Long和Double等数值包装类型,BigInteger和BigDecimal等大数据类型。
但同为Number的原子类AtomicInteger和AtomicLong则是可变的。
对于集合类型,可以使用Collections.unmodifiableXXX()方法来获取一个不可变的集合。
public class ImmutableExample {
public static void main(String[] args) {
Map
Map
unmodifiableMap.put("a", 1);
}
}
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.Collections$UnmodifiableMap.put(Collections.java:
1457)
at ImmutableExample.main(ImmutableExample.java:
9)
Collections.unmodifiableXXX()先对原始的集合进行拷贝,需要对集合进行修改的方法都直接抛出异常。
public V put(K key, V value) {
throw new UnsupportedOperationException();
}
互斥同步
synchronized和ReentrantLock。
非阻塞同步
互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。
互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。
无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。
1.CAS
随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略:
先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。
这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。
乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。
硬件支持的原子性操作最典型的是:
比较并交换(Compare-and-Swap,CAS)。
CAS指令需要有3个操作数,分别是内存地址V、旧的预期值A和新值B。
当执行操作时,只有当V的值等于A,才将V的值更新为B。
2.AtomicInteger
J.U.C包里面的整数原子类AtomicInteger的方法调用了Unsafe类的CAS操作。
以下代码使用了AtomicInteger执行了自增的操作。
private AtomicInteger cnt = new AtomicInteger();
public void add() {
cnt.incrementAndGet();
}
以下代码是incrementAndGet()的源码,它调用了Unsafe的getAndAddInt()。
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
以下代码是getAndAddInt()源码,var1指示对象内存地址,var2指示该字段相对对象内存地址的偏移,var4指示操作需要加的数值,这里为1。
通过getIntVolatile(var1,var2)得到旧的预期值,通过调用compareAndSwapInt()来进行CAS比较,如果该字段内存地址中的值等于var5,那么就更新内存地址为var1+var2的变量为var5+var4。
可以看到getAndAddInt()在一个循环中进行,发生冲突的做法是不断的进行重试。
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!
pareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
3.ABA
如果一个变量初次读取的时候是A值,它的值被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。
J.U.C包提供了一个带有标记的原子引用类AtomicStampedReference来解决这个问题,它可以通过控制变量值的版本来保证CAS的正确性。
大部分情况下ABA问题不会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。
无同步方案
要保证线程安全,并不是一定就要进行同步。
如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。
1.栈封闭
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。
public class StackClosedExample {
public void add100() {
int cnt = 0;
for (int i = 0; i < 100; i++) {
cnt++;
}
System.out.println(cnt);
}
}
public static void main(String[] args) {
StackClosedExample example = new StackClosedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> example.add100());
executorService.execute(() -> example.add100());
executorService.shutdown();
}
100
100
2.线程本地存储(ThreadLocalStorage)
如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。
如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。
其中最重要的一个应用实例就是经典Web交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多Web服务端应用都可以使用线程本地存储来解决线程安全问题。
可以使用java.lang.ThreadLocal类来实现线程本地存储功能。
对于以下代码,thread1中设置threadLocal为1,而thread2设置threadLocal为2。
过了一段时间之后,thread1读取threadLocal依然是1,不受thread2的影响。
public class ThreadLocalExample {
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal();
Thread thread1 = new Thread(() -> {
threadLocal.set
(1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadLocal.get());
threadLocal.remove();
});
Thread thread2 = new Thread(() -> {
threadLocal.set
(2);
threadLocal.remove();
});
thread1.start();
thread2.start();
}
}
1
为了理解ThreadLocal,先看以下代码:
public class ThreadLocalExample1 {
public static void main(String[] args) {
ThreadLocal threadLocal1 = new ThreadLocal();
ThreadLocal threadLocal2 = new ThreadLocal();
Thread thread1 = new Thread(() -> {
threadLocal1.set
(1);
threadLocal2.set
(1);
});
Thread thread2 = new Thread(() -> {
threadLocal1.set
(2);
threadLocal2.set
(2);
});
thread1.start();
thread2.start();
}
}
它所对应的底层结构图为:
每个Thread都有一个ThreadLocal.ThreadLocalMap对象。
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
当调用一个ThreadLocal的set(Tvalue)方法时,先得到当前线程的ThreadLocalMap对象,然后将ThreadLocal->value键值对插入到该Map中。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map !
= null)
map.set(this, value);
else
createMap(t, value);
}
get()方法类似。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map !
= null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e !
= null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocal从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。
在一些场景(尤其是使用线程池)下,由于ThreadLocal.ThreadLocalMap的底层数据结构导致ThreadLocal有内存泄漏的情况,应该尽可能在每次使用ThreadLocal后手动调用remove(),以避免出现ThreadLocal经典的内存泄漏甚至是造成自身业务混乱的风险。
3.可重入代码(ReentrantCode)
这种代码也叫做纯代码(PureCode),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。
可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 人工智能 数据 Java 线程 安全 试题 了解