Java内存模型

Java内存模型(Java Memory Model, JMM)是定义在Java虚拟机(JVM)中如何处理多线程间共享变量读写的规范。
它为Java并发编程提供了一套规则,确保程序在多核处理器上的正确执行。

Java内存模型的主要目标:
定义程序中变量的访问规则,即在什么条件下,一个线程对共享变量的修改对另外一个线程可见。
提供一种机制来控制多线程编程中的同步访问,以避免线程之间的数据不一致。

Java内存模型的主要组件:
主内存(Main Memory):
主内存是所有线程共享的内存区域,存放所有Java对象实例以及类的静态字段。
当线程需要读取变量时,它会从主内存中获取这些变量的值。类似地,当线程需要写入变量时,变量值最终会被更新回主内存。主内存也是线程间共享变量的媒介,它保持了线程间的变量可见性。

工作内存(Working Memory)
工作内存与主内存相对应,是线程私有的内存区域。
每个线程都有自己的工作内存,它包含了线程使用到的变量的主内存副本。

当线程操作变量时,它实际是在自己的工作内存中进行操作。
例如,当线程执行一个计算任务时,它将读取的变量从主内存复制到工作内存,进行运算处理后,再将结果更新回主内存。
这个过程保证了线程在执行时拥有所需数据的快速访问,因为访问工作内存比访问主内存要快。

主内存与工作内存的交互

主内存和工作内存之间的交互关系由JMM的内存一致性模型规定。以下是这种交互的几个核心操作:

锁定(Lock)和解锁(Unlock):当一个线程需要同步访问共享变量时,它会通过锁定来确保其他线程不能同时修改该变量。
读取(Read)和加载(Load):线程从主内存读取变量,然后加载到工作内存中。
使用(Use)和赋值(Assign):线程可以操作工作内存中的变量,执行计算。
存储(Store)和写入(Write):线程将变更后的变量值存储在工作内存中,然后写入到主内存。

同步机制:
为了保证主内存与工作内存中变量值的一致性,JMM提供了同步机制。这些机制包括volatile关键字、synchronized块、锁机制以及原子变量操作等。

volatile变量
对volatile变量的读写操作直接作用于主内存,确保变量的可见性和禁止指令重排序。(后面有代码解释)

synchronized块
进入synchronized块时,会清空工作内存中的变量值,强制重读主内存。
退出synchronized块时,会将工作内存中的共享变量的最新值刷新回主内存。

原理:

当线程进入synchronized方法或代码块时,它将自动获得锁。出现以下两种情况之一:
如果锁是可用的(即当前没有其他线程持有),线程就会获得锁并继续执行。
如果锁不可用,则线程将被阻塞(挂起执行)直到锁被释放。
当线程离开synchronized方法或代码块时,无论是通过正常的执行路径还是通过抛出异常的方式,它将自动释放锁。

作用域:(文章后有代码解释)
Synchronized 方法:
用synchronized关键字修饰方法分为两种情况:
实例方法:
锁定的是调用该方法的对象实例(this)。
当一个线程进入一个对象的同步实例方法时,它自动获得那个对象的锁,那么其他线程就无法同时进入这个对象的任何其他同步实例方法。
静态方法:
锁定的是这个类的所有对象共用的类对象(Class对象)。
如果一个线程访问类的同步静态方法,它持有的是类锁,因此其他线程不能同时执行这个类的任何同步静态方法。
Synchronized 代码块:
synchronized还可以用来修饰代码块,而不是整个方法。
这允许锁定任何对象,为线程提供更细粒度的控制,从而减小锁的范围,提高效率。
同步某个对象:
可以指定锁定一个给定的对象。
不同的代码块可以锁定不同的对象,这样它们就可以并行执行。
同步类对象:
可以锁定类的Class对象,跟静态同步方法类似,这样线程在访问类的任何其他同步代码块时都必须等待释放类锁。
同步一个资源: 为了保护非同步方法中的临界资源,可以创建一个特定的对象来作为锁。


获取锁时,清空工作内存;释放锁时,将修改刷新回主内存。
原理:
当某个线程(或进程)需要访问共享资源时,它会尝试获得与该资源关联的锁:
如果锁是可用的,即没有其他线程持有该锁,那么请求锁的线程会获得该锁并进入其临界区,这是一段只允许单个线程进入的代码。
如果锁不可用,即已被另一个线程持有,请求锁的线程可能会进入等待状态,直到锁被释放。
一旦线程完成了对共享资源的操作,它将释放锁,这样其他线程就可以请求并获得锁来访问资源。

锁的类型
互斥锁(Mutex):互斥锁是最简单的锁类型,用于保证同一时间只有一个线程可以持有锁。这保证了对共享资源的独占访问。
读写锁(Read-Write Lock):读写锁允许多个线程同时读取资源,但在写入时需要独占访问。当锁处于写模式时,其他线程既不能读也不能写。
自旋锁(Spinlock):自旋锁是一种忙等待锁,当线程试图获得锁而锁已被其他线程持有时,线程将循环等待,直到锁变得可用。
递归锁(Recursive Lock):递归锁允许同一个线程多次获得锁。该线程必须释放锁与其获得次数相同的次数才能真正释放该锁。
条件变量(Condition Variable):条件变量不是锁本身,而是建立在锁之上的同步原语,允许线程在某些条件尚未满足时挂起,并且当条件满足时,允许其他线程唤醒它。

性能和死锁
锁是一个强大的工具,但如果不当使用,可能会导致性能问题或死锁。以下是一些要点:
锁粒度:精细的锁(锁定小的资源区域)可能导致更高的并发度,而粗粒度的锁(锁定大的资源区域)可能导致较低的并发度。
死锁:当两个或多个线程在等待对方释放锁时,可能发生死锁。这是一种情况,线程永远等待一个永远不会发生的事件。
锁竞争:当多个线程频繁请求相同的锁时,会发生锁竞争,可能会导致性能下降。

原子变量操作:
CAS(Compare-And-Swap)比较和替换
是一种同步机制,用于在多线程环境中安全地更新共享资源
CAS操作涉及三个操作数:
内存位置(V):
这是CAS操作要更新的变量的地址。
预期原值(A):这是我们预期在内存位置V中找到的值。
新值(B):如果内存位置V的当前值与预期原值A匹配,应将V的值更新为这个新值B。

这是CAS的基本工作原理:
读取当前值,比较当前值,条件更新

Compare-And-Swap(CAS)操作通常返回一个布尔值,表示操作是否成功。

CAS操作的基本工作原理是这样的:
它将内存位置的当前值与一个旧的预期值进行比较。
如果当前值与预期值相匹配,就将该内存位置更新为一个新的给定值。
CAS操作会返回一个布尔值,指示更新是否成功执行。

线程锁 AQS 
是Java中用于构建锁和其他同步组件的一个框架(ReadWriteLock读写锁)

AQS使用一个int类型的变量来表示同步状态,并通过一个内部的FIFO队列来管理那些在同步状态上等待的线程(被包装成Node)。

同步状态:AQS内部有一个volatile int类型的变量来表示同步状态。

FIFO队列 (First In, First Out)在FIFO队列中,插入操作(入队)通常发生在队列的尾部,而移除操作(出队)则发生在队列的头部。这就意味着,所有的元素都将按照它们被加入队列的顺序来移除。

AQS使用了一个内部类Node来表示等待队列中的每一个元素,每个Node包含了线程的引用、等待状态和连接前后节点的链接。AQS通过头尾节点的引用管理队列。

获取操作:线程尝试获取资源,如果成功,则执行任务;如果失败,它将会被包装成节点加入队列。

释放操作:释放当前持有的资源,并且可能会唤醒队列中的后续节点(线程)。

两种同步模式:

独占模式(Exclusive):
这种模式下,一个时间点只能有一个线程成功地获取同步状态。
其他任何线程都无法获取同步状态,直到它被当前拥有它的线程释放。这是实现互斥锁的一种方式。

共享模式(Shared):
在这种模式下,同步状态可以被多个线程同时获取。
例如,在实现一个计数信号量或读写锁的“读”锁时,通常会使用共享模式,因为多个线程应该能够同时读取。

代码解释:

下面是一个使用volatile关键字的简单Java代码示例。在这个例子中,有两个线程:一个用来修改一个volatile变量的值,另一个用来读取这个变量的值。使用volatile保证了当变量值被修改时,其他线程能够立即看到这个更改。
public class VolatileExample {
    // 定义一个volatile变量
    private volatile boolean active;
    public boolean isActive() {
        return active;
    }
    public void setActive(boolean active) {
        this.active = active;
    }
    public void runExample() {
        // 创建一个线程修改active变量的值
        Thread thread1 = new Thread(() -> {
            while (!isActive()) {
                // 在这个循环中等待active变量变为true
            }
            System.out.println("Thread 1 sees active is true and exits the loop");
        });
        // 创建另一个线程设置active变量的值为true
        Thread thread2 = new Thread(() -> {
            // 模拟一些初始化工作,花费一些时间
            try {
                Thread.sleep(1000); // 1秒后设置active为true
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Thread 2 setting active to true");
            setActive(true);
        });
        // 启动两个线程
        thread1.start();
        thread2.start();
    }
    public static void main(String[] args) {
        new VolatileExample().runExample();
    }
}

在这个例子中,active是一个volatile变量。Thread 1在一个循环中等待active变为true。Thread 2在睡眠一秒后将active设置为true。

如果active不是volatile变量,Thread 1可能会无限循环,因为它可能不会看到Thread 2对active的更改。但是,因为active被声明为volatile,JVM保证了active的更改对所有线程立即可见。所以,当Thread 2修改了active后,Thread 1能够看到这个更改并退出循环。

Synchronized 实例方法:
在这个例子中,increment()和getCount()都是同步的实例方法,它们锁定的是Counter对象的实例。
public class Counter {
    private int count = 0;
    // Synchronized instance method
    public synchronized void increment() {
        count++;
   }
    public synchronized int getCount() {
        return count;
    }
}

Synchronized 静态方法:
这里,staticIncrement()和getStaticCount()是同步的静态方法,它们锁定的是StaticCounter类的Class对象。
public class StaticCounter {
    private static int staticCount = 0;
    // Synchronized static method
    public static synchronized void staticIncrement() {
        staticCount++;
    }
    public static synchronized int getStaticCount() {
        return staticCount;
    }
}

Synchronized 代码块 - 同步某个对象:
在这里,我们使用synchronized块锁定一个私有的lock对象,而不是整个方法。
public class BlockCounter {
    private int count = 0;
    private final Object lock = new Object();
    public void increment() {
        // Synchronized block using a private lock object
        synchronized (lock) {
            count++;
        }
    }
    public int getCount() {
        synchronized (lock) {
            return count;
        }
    }
}

Synchronized 代码块 - 同步类对象:
对于静态变量,我们可以使用synchronized代码块并锁定Class对象,如BlockStaticCounter.class。
public class BlockStaticCounter {
    private static int staticCount = 0;
    public void staticIncrement() {
        // Synchronized block using the class object as the lock
        synchronized (BlockStaticCounter.class) {
            staticCount++;
        }
    }
    public int getStaticCount() {
        synchronized (BlockStaticCounter.class) {
            return staticCount;
        }
    }
}

Synchronized 代码块 - 同步一个资源:
在这个例子中,我们专门为计数器的增量操作创建了一个锁对象resourceLock。
public class ResourceCounter {
    private volatile int count = 0;
    private final Object resourceLock = new Object();
    public void safeIncrement() {
        // Synchronized block using a resource-specific lock
        synchronized (resourceLock) {
            count++;
        }
    }
    public int getCount() {
        // No need to synchronize if you're only reading a single atomic value
        return count;
    }
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,794评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,050评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,587评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,861评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,901评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,898评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,832评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,617评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,077评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,349评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,483评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,199评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,824评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,442评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,632评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,474评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,393评论 2 352

推荐阅读更多精彩内容