并发编程基础 一

Java内存模型

Java内存模型,即:JVM。当程序执行并行操作时,如果数据的访问和操作不加以控制,那么必然对程序的正确性造成破坏。因此,我们需要在深入了解并行机制的前提下,再定义一种规则,来保证多个线程间可以有效地、正确地协同工作。而JMM就是为此而生的.

>     什么是并发和并行?

并行和并发都是多任务处理的概念,但它们的含义不同。

并行是指两个或多个任务在同一时刻执行,即在同一时刻有多个任务在同时进行。在计算机领域,多核 CPU 可以实现并行处理,即多个 CPU 内核同时执行不同的任务。在并行处理中,任务之间相互独立,不需要等待其他任务的完成。

并发是指两个或多个任务在同一时间段内执行,即在同一时间段内有多个任务在交替进行。在计算机领域,单核 CPU 可以通过轮流执行各个任务来实现并发处理。在并发处理中,任务之间可能会相互影响,需要考虑任务的顺序和优先级,也需要考虑任务之间的同步和通信问题。

简单来说,如果是多个任务同时执行,就是并行;如果是多个任务交替执行,就是并发。并行处理通常需要多核 CPU 来支持,可以提高处理速度;而并发处理可以在单核 CPU 上实现,但需要考虑任务之间的同步和通信问题。
 

JVM的关键技术点都围绕着多线程的原子性可见性有序性来创建的。

1.原子性(Atomicity)

定义:是指一个操作是不可中断的。即使是多线程一起执行的时候,一个操作一旦开始,就不会被其他线程锁干扰。

在 Java 中,我们可以使用 synchronized 关键字或者 java.util.concurrent.atomic 包中的原子类来保证操作的原子性。

在 Java 中,读取或写入一个变量(除了 long 和 double 类型)都是原子的。比如,以下的操作都是原子的:

(注:32 位 JVM 对 long、double 赋值操作为非原子操作,64 位 JVM 对 long、double 赋值操作为原子操作,不同 JVM 有不同实现,大致符合上述原则)

int i = 0;
i++;
i = i + 1;

但是,虽然 i++ 是一个原子操作,但在多线程环境下,如果没有合适的同步措施,它仍然可能导致线程安全问题。例如:

public class Counter {
    private int count = 0;

    public void incrrment() {
        count++;   //这里是非线程安全的
    }

    public int getCount() {
        return count;
    }
}

在这个例子中,increment 方法是非线程安全的。因为 count++ 这个操作虽然是原子的,但它包含了三个步骤:读取 count 的值,将值增加 1,然后将新的值写回 count。在这三个步骤中,如果有线程切换的话,就可能导致数据的不一致。

在 Java 内存模型中,对基本数据类型的变量(除了 long 和 double 比较特殊)的读取和写入操作都被保证是原子的。即,在执行这些操作时,不会被线程调度机制中断。换句话说,它们是不可分割的,要么完全执行,要么完全不执行。

然而,即使一个操作是原子性的,如果它不是线程安全的,那么在并发环境中,仍然可能存在问题。参考上述 Counter 示例。

increment() 方法中的 count++ 是一个原子操作,但它不是线程安全的。因为 count++ 是由获取 count,增加其值,并写回新值这三个步骤组成的。在这三个步骤之间,如果发生线程切换,可能导致数据的不一致性。

为了解决这个问题,Java 提供了 synchronized 和 java.util.concurrent.atomic 包中的类如 AtomicInteger,来实现更复杂操作的原子性。

如何使用 synchronized、Lock、Atomic 类来保证原子性

synchronized关键字,

确保同一时刻最多只有一个线程执行该代码段,从而保证了代码的原子性。

public class Synchronized {
    private int sync = 0;

    synchronized void increment() {
        sync++;
    }
    synchronized int getSync(){
        return sync;
    }
}

在上面的例子中,使用synchronized关键字修饰了increment()和getSync()方法,这就说明,当一个线程进入increment()方法以后,其他在试图进入increment()或getSync()方法的线程就会被堵塞,直到第一个进入的线程退出这两个方法之一。

Lock接口

Java 中的 java.util.concurrent.locks.Lock 是一个接口,它定义了控制多线程访问共享资源的方式。它与 synchronized 不同的地方在于 Lock 提供了更多的灵活性和更好的性能。

public class LockDemo {
    private int c = 0;
    private Lock lock = new ReentrantLock();

    void increment() {
        lock.lock();
        try {
            c++;
        }finally {
            lock.unlock();
        }
    }

    int getC() {
        return c;
    }
}

在以上示例中,我们使用了 ReentrantLock,它实现了 Lock 接口。在调用 increment() 方法时,我们先获取锁,然后尝试执行 count++,不论 count++ 是否成功,我们最后都会释放锁。

Atomic类

public class AtomicDemo {
    private AtomicInteger c = new AtomicInteger(0);

    void increment(){
        c.incrementAndGet();
    }

    int getc(){
        return c.get();
    }
}

在以上示例中,我们使用了 AtomicInteger 类来实现计数器。其中 incrementAndGet() 方法会以原子方式将当前值加 1,并返回更新后的值。

2.可见性(Visibiliity)

定义:是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改

为了提高处理速度,每个线程都会在 CPU 中维护一份共享变量的本地缓存,而线程对共享变量的所有操作都会在自己的本地缓存中进行。如果线程 A 更改了一个共享变量,线程 B 有可能看不到线程 A 的修改,这就是所谓的可见性问题。

public class VisibilityDemo {
    private boolean flag = true;

    public void run(){
        while (flag){
            //do something
        }
    }

    public void stop(){
        flag = false;
    }
}

在上述代码中,如果 run 和 stop 分别在两个不同的线程中调用,那么理论上 stop 能够使 run 方法的循环结束。然而,由于可见性问题,run 方法中看到的 flag 可能仍然是 true,因此循环可能永远不会结束。

解决这个问题最常用的方法是使用volatile关键字

private volatile boolean flag = true;

在这段代码中,我们将 flag 声明为 volatile。这告诉 JVM,每次在使用 flag 之前,都需要从主内存中读取它的值,而且每次更改 flag 的值后,都需要立即写回主内存。这样,就能保证所有线程看到的 flag 值是一致的。

除了 volatile 关键字,我们还可以使用 synchronized 关键字来保证共享变量的可见性。

synchronized 是 Java 提供的一种内置的同步机制。当一个线程进入一个 synchronized 方法或者代码块时,它会获取一个锁。当它退出 synchronized 方法或者代码块时,或者线程因为 wait() 方法等待,或者线程死亡,它都会释放这个锁。

这个锁不仅确保了同一时刻只有一个线程能执行 synchronized 代码块中的内容,而且还能保证 synchronized 代码块中的所有变量都同步到主内存,以及每次进入 synchronized 代码块时,都从主内存中拉取最新的变量值。

public class SynchronizedDemo {
    private int count = 0;

    public synchronized void increment(){
        count++;
    }

    public synchronized int getCount(){
        return count;
    }
}

在这个示例中,increment() 和 getCount() 都是 synchronized 方法,这意味着每次只有一个线程能调用这些方法,这保证了 count 的可见性和原子性。

3.有序性(Ordering)

因为指令流水线的存在,CPU才能真正高效的执行。但是,流水线总是害怕被中断的。流水线满载时,性能确实相当不错,但是一旦中断,所有的硬件设备都会进入一个停顿期,再次满载又需要几个周期,因此,性能损失会比较大。所以,我们必须要想办法尽量不让流水线中断

public class Example {
    private int a = 0;
    private boolean flag = false;

    public void writer(){
        a = 1;       //1
        flag = true; //2
    }

    public void reader(){
        if (flag){  //3
            int i = a * a;//4   
        }
    }
}

在这个例子中,我们期望的执行顺序是 writer 方法先执行(1,2),然后 reader 方法再执行(3,4)。但是,由于指令重排,writer 方法中设置 flag = true 的语句(2)可能会在设置 a = 1 的语句(1)之前执行。这样,当 reader 方法执行到检查 flag 的语句(3)时,可能会发现 flag 已经是 true 了,然后它去读取 a 的值,发现 a 还是 0,这就出现了我们不期望的结果

可以使用 volatile、synchronized 关键字和 Lock 接口来控制代码的执行顺序,从而避免由于指令重排导致的问题

synchronized 是 Java 提供的一种互斥同步的方法。当我们在代码块上使用 synchronized 时,Java 就会对这段代码进行加锁,确保同一时刻只有一个线程能执行这段代码。

public class Example {
    private int a = 0;
    private boolean flag = false;

    public synchronized void writer(){
        a = 1;       //1
        flag = true; //2
    }

    public synchronized void reader(){
        if (flag){  //3
            int i = a * a;//4
        }
    }
}

volatile 是 Java 提供的一种轻量级的同步机制。它可以确保变量的修改对所有线程立即可见,并阻止指令重排序。

public class Example {
    private int a = 0;
    private volatile boolean flag = false;

    public void writer(){
        a = 1;       //1
        flag = true; //2
    }

    public void reader(){
        if (flag){  //3
            int i = a * a;//4
        }
    }
}
happens-before 原则

定义:如果一个操作 A happens-before 另一个操作 B,那么在 A 完成之前,所有对共享变量的修改,必须对执行操作 B 的线程可见。此外,操作 A 和操作 B 的执行顺序也将被严格保证(也就是说,操作 A 不会在操作 B 之后执行)。

  1. 程序顺序规则:在一个线程中,按照代码的顺序,写在前面的操作先行发生于写在后面的操作。(一个线程内保证语义的串行性。)
  2. 锁定规则:一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
  3. volatile 变量规则:对一个 volatile 域的写操作,先行发生于后面对这个 volatile 域的读操作。
  4. 传递性:A先于B,B先于C,那么A必然先于C。
  5. 线程启动规则:Thread 对象的 start 方法先行发生于此线程的每一个动作。
  6. 线程结束规则:线程中的所有操作都先行发生于对此线程的 join 的返回。(Thread.join())
  7. 中断规则:对线程 interrupt 方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
  8. 对象的构造函数执、结束先于finalize()方法。
  • 22
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值