面试题答案
一键面试变量与常量在多线程场景下的表现
- 可见性
- 变量:在多线程环境下,一个线程对变量的修改,另一个线程可能无法及时看到。这是因为每个线程可能会将变量缓存到自己的工作内存中,而不是直接操作主内存。例如,线程A修改了一个普通变量
var num = 0
,线程B可能仍然读取到旧的值,因为线程B的工作内存中的num
值没有及时更新。 - 常量:Kotlin中的常量(使用
val
声明)一旦初始化后就不能再修改。由于其不可变性,在多线程环境下不存在可见性问题,所有线程看到的常量值都是一致的。例如,val constant = 10
,所有线程看到的constant
值都是10。
- 变量:在多线程环境下,一个线程对变量的修改,另一个线程可能无法及时看到。这是因为每个线程可能会将变量缓存到自己的工作内存中,而不是直接操作主内存。例如,线程A修改了一个普通变量
- 原子性
- 变量:普通变量的操作通常不是原子性的。例如,对一个整型变量
var num
进行自增操作num++
,它实际上包含了读取、增加和写入三个步骤,在多线程环境下可能会出现数据竞争。比如线程A读取num
的值为10,线程B也读取num
的值为10,然后两个线程分别进行自增操作并写入,最终num
的值可能是11而不是预期的12。 - 常量:由于常量不可变,不存在原子性问题。
- 变量:普通变量的操作通常不是原子性的。例如,对一个整型变量
优化方案
-
使用
volatile
关键字- 适用场景:适用于变量的读写操作不依赖于当前值,并且主要关注变量的可见性问题。例如,用于控制线程的启动、停止等标志位。
- 优点:保证变量的可见性,当一个线程修改了
volatile
修饰的变量,其他线程能立即看到修改后的值。 - 缺点:不保证操作的原子性。例如,对
volatile
修饰的整型变量进行自增操作,仍然可能出现数据竞争问题。
示例代码:
class VolatileExample { private volatile var flag = false fun startThread() { flag = true } fun checkFlag(): Boolean { return flag } }
-
使用
Atomic
系列类- 适用场景:适用于需要保证原子性操作的场景,如计数器等。
- 优点:提供了原子性操作,能避免数据竞争问题。例如
AtomicInteger
的incrementAndGet
方法能原子性地对值进行自增。 - 缺点:相比普通变量,
Atomic
系列类的操作相对复杂一些,并且在某些场景下性能可能略低于普通变量(但在多线程竞争场景下性能优势明显)。
示例代码:
import java.util.concurrent.atomic.AtomicInteger class AtomicExample { private val counter = AtomicInteger(0) fun increment() { counter.incrementAndGet() } fun getValue(): Int { return counter.get() } }
-
使用
synchronized
关键字- 适用场景:适用于需要保证一段代码块线程安全的场景,无论是变量的可见性还是原子性都能保证。例如,多个线程对同一个对象的不同方法进行操作,并且这些操作存在数据依赖关系时。
- 优点:全面保证线程安全,包括可见性和原子性。
- 缺点:由于是同步锁机制,可能会导致性能瓶颈,特别是在高并发场景下,因为同一时间只有一个线程能进入同步代码块。
示例代码:
class SynchronizedExample { private var num = 0 fun increment() { synchronized(this) { num++ } } fun getValue(): Int { synchronized(this) { return num } } }