在现代计算机架构下,为了充分利用CPU多核心的优势,我们需要在应用程序中使用并发编程技术。然而,并发编程在保证线程安全性和正确性方面也存在许多挑战和难点。本文将详细介绍Java并发编程中的四个关键字:ThreadLocal、Volatile、Synchronized和Atomic,分别介绍它们的作用、使用方法、实现原理以及注意事项。
1. ThreadLocal
ThreadLocal:线程变量,ThreadLocal中的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。它可以在每个线程上创建一个独立的副本,使得每个线程都可以访问自己的副本,而不会与其他线程的副本冲突。
既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
在多线程环境下,线程之间的共享数据可能会导致线程不安全。例如,在Web应用程序中,一个对象通常会被多个请求的线程同时访问。如果这个对象是可变的,那么它的状态可能会在两个线程之间冲突,从而产生错误的结果。在这种情况下,可以使用ThreadLocal来解决线程安全问题。
以下是一个简单的示例:
public class MyThreadLocal {
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void set(Integer value) {
threadLocal.set(value);
}
public static Integer get() {
return threadLocal.get();
}
public static void remove() {
threadLocal.remove();
}
}
在上述示例中,我们创建了一个线程本地变量threadLocal,它保存了一个整数值。我们还提供了三个方法:set、get和remove。使用set方法可以将当前线程的副本设置为指定的值;get方法可以返回当前线程的副本;remove方法可以从当前线程中删除该变量的值。
1.1 内存泄漏
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
就是JVM创建的对象永远都无法访问到,但是GC又不能回收对象所占用的内存。
由于ThreadLocal对象是弱引用,如果外部没有强引用指向它,它就会被GC回收,导致Entry的Key为null,如果这时value外部也没有强引用指向它,那么value就永远也访问不到了,按理也应该被GC回收,但是由于Entry对象还在强引用value,导致value无法被回收,这时「内存泄漏」就发生了,value成了一个永远也无法被访问,但是又无法被回收的对象。
如何避免?
使用ThreadLocal时,一般建议将其声明为static final的,避免频繁创建ThreadLocal实例。
尽量避免存储大对象,如果非要存,那么尽量在访问完成后及时调用remove()删除掉。
1.2使用场景demo
ThreadLocal使用场景用来解决数据库链接、Session管理等。
一般对Web应用,请求层调用底层接口。收到请求到返回响应所经过的所有程序调用都属于同一个线程,所以在线程内定义ThreaLocal变量,每个线程保存自己的缓存信息。
定义拦截器,请求发起的时候保存用户信息。
2. Volatile
在多线程环境下,由于缓存一致性协议的存在,Java的内存模型可能会导致线程安全问题。例如,在一个线程中更新了某个变量的值,但是在另一个线程中无法立即看到这个变量的新值。在这种情况下,可以使用Volatile关键字来解决线程可见性问题。
当我读变量时,总是能读到它的最新值,这里最新值是指不管其它哪个线程对该变量做了写操作,都会立刻被更新到主存里,我也能从主存里读到这个刚写入的值。也就是说volatile关键字可以保证可见性以及有序性。
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。
Volatile关键字用于修饰变量,在多线程环境下保证变量的可见性。当一个变量被声明为Volatile时,在任何时刻都保证所有线程都能够读取该变量的最新值。以下是一个简单的示例:
public class MyVolatile {
private volatile boolean flag = false;
public void setFlag(boolean value) {
this.flag = value;
}
public boolean isFlag() {
return this.flag;
}
}
在上述示例中,我们创建了一个布尔型的flag变量,并使用Volatile关键字修饰它。这样,即使多个线程对flag变量进行操作,也可以保证每个线程都能够读取到最新的值。
需要注意的是,虽然Volatile关键字可以解决线程可见性问题,但它并不能解决线程安全性问题。如果变量本身不具备原子性,则仍然需要使用其他方式来保证线程安全。
普通变量与volatile的区别是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。可以说volatile保证了多线程操作时变量的可见性。
2.1特性
-
保证可见性
-
不保证原子性
-
禁止指令重排,确保程序的有序性
2.2线程安全
volatile变量的运算在并发下是不安全的,因为Java里面的运算并非原子操作,所以需要加锁synchronized。
2.3 使用场景demo
volatile用来指定具有一个状态转换的标志变量。
比如全局只需要初始化一次,可以增加状态标记,demo:
public class MyTest {
volatile boolean initFlag = false;
public void init() {
initFlag = true;
}
public void do() {
while (!initFlag) {
// do work
}
}
3. Synchronized
在多线程环境下,由于线程之间的相互竞争,可能会导致线程安全问题。例如,在一个线程中更新了某个变量的值,但是在另一个线程中也对该变量进行了操作,从而导致了数据不一致的情况。在这种情况下,可以使用synchronized关键字来解决线程安全问题。
synchronized关键字用于修饰方法或代码块,将它们标记为同步的。当一个线程进入到一个被synchronized修饰的方法或代码块时,会自动获取该对象的锁(也称为监视器锁)。只有获得了锁的线程才能够执行该方法或代码块,其他线程则需要等待直到锁被释放才能执行。以下是一个简单的示例:
public class MySynchronized {
private int count = 0;
public synchronized void increment() {
this.count++;
}
public int getCount() {
return this.count;
}
}
在上述示例中,我们创建了一个MySynchronized类,其中包含一个整型变量count和两个方法:increment和getCount。increment方法使用synchronized关键字修饰,以确保只有一个线程能够修改count变量的值;getCount方法则不需要同步,因为它只是读取变量的值。
需要注意的是,虽然synchronized关键字可以解决线程安全问题,但它的效率较低,因为它会导致多个线程之间的竞争,从而降低了程序的并发性能。因此,在实际开发中,应该尽可能地避免使用synchronized关键字。
3.1 使用场景demo
两个线程同时访问同一个对象的同步方法等等场景
比如单例场景下:
public class Student {
private static Student instance;
public static Student getInstance(){
if(instance. == null){
synchronized(Student.class){
if(instance. == null){
instance = new Student();
}
}
}
}
}
4. Atomic
在多线程环境下,由于多个线程同时访问同一个变量,可能会导致线程安全问题。例如,在一个线程中更新了某个变量的值,但是在另一个线程中也对该变量进行了操作,从而导致了数据不一致的情况。在这种情况下,可以使用Atomic类来解决线程安全问题。
Atomic类是Java中提供的一组原子性操作类,包括AtomicBoolean、AtomicInteger、AtomicLong等。这些类提供了一组原子性操作方法,如get、set、compareAndSet等,以确保对共享变量的操作具有原子性和可见性。
4.1 使用场景demo
public class MyAtomic {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
在上述示例中,我们创建了一个MyAtomic类,其中包含一个AtomicInteger类型的变量count和两个方法:increment和getCount。increment方法使用AtomicInteger提供的incrementAndGet方法来递增count变量的值;getCount方法则直接返回count变量的值。
需要注意的是,尽管Atomic类可以保证线程安全性和原子性操作,但它不一定能够解决所有线程安全问题。如果需要进行复杂的操作或者多个操作之间存在依赖关系,则可能需要使用其他技术来保证线程安全。
5. 总结
在Java并发编程中,ThreadLocal、Volatile、Synchronized和Atomic关键字都是非常重要的工具。ThreadLocal用于创建线程本地变量,以避免线程安全问题;Volatile用于保证变量的可见性;Synchronized用于修饰方法或代码块,以实现线程安全;Atomic类用于提供原子性操作
6.volatile VS synchronized
volatile是Java虚拟机提供的最轻量级的同步机制,是轻量级的synchronized。它比synchronized的使用和执行成本会更低,因为它不会引起线程上下文的切换和调度。
1)Synchronized保证内存可见性和操作的原子性
加锁----清空内存----在主存中拷贝最新副本----执行+修改--------刷回主存-------释放锁
2)Volatile只能保证内存可见性
a.每次读取的时候都会CAS
b.每次写完都会store回主存
3)Volatile不需要加锁(忙等待,做自旋),比Synchronized更轻量级,并不会阻塞线程(volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。)
4)volatile标记的变量不会被编译器优化(通过volatile保证了有序性),而synchronized标记的变量可以被编译器优化(如编译器重排序的优化). (synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性)所以根据before-happen原则,也可以保证有序性。
5)volatile是变量修饰符,仅能用于变量,而synchronized是一个方法或块的修饰符。
volatile本质是在告诉JVM当前变量在寄存器中的值是不确定的,使用前,需要先从主存中读取,因此可以实现可见性。而对n=n+1,n++等操作时,volatile关键字将失效,不能起到像synchronized一样的线程同步(原子性)的效果
7.ThreadLocal VS Synchronized
ThreadLocal比直接使用Synchronized同步机制解决线程问题更简单,更方便,且程序拥有更高的并发性。
本文详细介绍了Java并发编程中的ThreadLocal、Volatile、Synchronized和Atomic关键字,阐述了它们的作用、使用方法和注意事项,以及它们在解决线程安全和内存一致性问题上的应用。
192

被折叠的 条评论
为什么被折叠?



