Java线程安全面试题
线程安全有以下几种实现方式:
Immutable
不可改变(Immutable)的对象一定是线程安全的,不需要实现线程安全措施。只要正确构造不可变对象,您就不会在多个线程中看到它们处于不一致的状态。在多线程环境中,应尽可能使对象不可变,以保证线程安全。
不可变类型:
由关键字修饰的基本数据类型
String
枚举类型
Number 的一些子类,如 Long 和 Double Numeric Pack 等。数据类型。但是原子类 AtomicInteger 和 AtomicLong 都是数字,可以互换。
对于集合类型,可以使用 () 方法获取不可变集合。
public class ImmutableExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
Map<String, Integer> unmodifiableMap = (map);
("a", 1);
}
}
“主”线程上的异常 java.lang.UnsupportedOperationException
at java.util.Collections$()
at ImmutableExample.main()
() 首先复制原始集合,任何需要修改集合的方法都会抛出异常。 。 。
publication V put(K, value V) {
throw new UnsupportedOperationException();
}
互斥锁同步
同步和ReentrantLock。
非阻塞同步
互斥同步的主要问题是线程阻塞和唤醒带来的性能问题,所以这种同步也称为阻塞同步。
互斥同步是一种悲观并发策略。人们相信,只要不采取正确的同步步骤,问题就总会发生。无论是否存在共享数据的竞争,都必须加锁(这里讨论的是概念模型,实际上虚拟机优化了很大一部分不必要的加锁)、用户态内核态转换、锁计数器维护和Ensure有阻塞的线程需要唤醒等操作。
- CAS
随着硬件指令集的发展,我们可以采用基于冲突检测的乐观并发策略:先执行操作,如果没有其他线程竞争共享数据,则操作成功,否则,步骤 - 采取补偿步骤(继续尝试,直到成功)。这种乐观并发策略的很多实现都不需要线程被阻塞,因此这种同步操作称为非阻塞同步。
乐观锁需要两个操作步骤,并且冲突检测是原子的。互斥同步不再用于保证这一点,只能在硬件中完成。硬件支持的最典型的原子操作是:比较和交换(CAS)。 CAS指令需要3个操作数,分别是V的内存地址、A的预期旧值和B的新值。执行操作时,只有当V的值相等时,才会将V的值更新为B A. 包从 Unsafe 类调用 CAS 操作。
以下代码使用AtomicInteger来执行增量操作。
private AtomicInteger cnt = new AtomicInteger();
public void add() {
();
}
下面的代码是incrementAndGet('t)的源码,获取callsAndGet('t)。
public final intincrementAndGet() {
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 = (var1, var2);
} while(!(var1, var2, var5, var5 + var4));
return var5;
}
- ABA
如果读取 B 时变量的值为 A,如果稍后该值发生更改,则该值会更改回到A,CAS操作会错误地认为曾经发生过变化。
包提供了一个标记为AtomicStampedReference的原子引用类来解决这个问题,它可以通过控制变量值的版本控制来保证CAS的正确性。大多数情况下,ABA问题不会影响程序的正确并发性。如果需要解决 ABA 问题,切换到传统的互斥同步可能比原子类更高效。
无同步方案
为了保证线程安全,不需要同步。如果该方法不涉及数据共享,则不应需要同步步骤来确保正确性。
- 堆是封闭的
当多个线程以同样的方式访问局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机的堆中,成为线程私有的。 ? (参见本地存储)
如果一段代码中所需的数据必须与其他代码共享,那么请确保引用该数据的代码能够保证在同一个线程中执行。如果可以保证的话,我们可以限制同一个线程的连接数据的查看次数。这样,我们就可以保证在不同步的情况下,线程之间不存在数据争用的问题。
满足此特性的应用并不常见。大多数使用消费队列的架构模式(例如“生产者-消费者”模型)将尝试在单个线程中消费产品。最重要的应用示例之一是经典Web交互模型中的“Thread-per-Request”处理方法。这种处理方法的广泛应用使得许多Web服务器应用程序都可以使用线程。本地存储解决线程安全问题。
可以使用类来实现线程本地存储功能。
以下代码,在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(() -> {
(1);
try {
(1000);
} catch (InterruptedException e) {
();
}
(());
();
});
Thread thread2 = new Thread(() -> {
(2);
();
});
();
();
}
}
1
理解ThreadLocal,请看下面的代码:
class♷ Public Class
对应的基本结构图为: 每个线程有一个对象。
/* 与该线程关联的ThreadLocal值。该映射由 ThreadLocal 类管理
- 。 */
threadLocals = null;
调用ThreadLocal的set(T value)方法时,首先从当前线程获取一个ThreadLocalMap对象,然后将ThreadLocal->value键值对插入到Map中。
public void set(Value T) {
Thread t = ();
ThreadLocalMap map = getMap(t);
if (map != null)
(this, value);
else
createMap(t, value);
}
get() 方法相同。
public T get() {
Thread t = ();
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由于底层数据结构的原因可能会出现内存泄漏的情况。每次使用ThreadLocal后都必须手动调用delete(),以避免经典的ThreadLocal内存泄漏。泄密甚至可能会导致您自己的业务中断。
- 可重入代码
此类代码也称为纯代码。它可以在执行过程中随时中断代码并切换到其他代码执行(包括递归调用),并且控制权返回后,原程序不会导致错误。
可重入代码具有一般特征,例如不依赖于堆栈上存储的数据和公共系统资源,所有使用的状态变量都通过参数传递,并且不调用不可重入方法。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。