第三章:《并发控制的温柔陷阱》

3 篇文章 0 订阅

并发,像是魔术师的舞台,让多个任务在同一时间段内看似同时进行,虽然实际执行可能是交错的,但它巧妙地利用了CPU的切换时间,使得每个任务都能向前推进,提高了整体的响应速度。而并行,则是真正意义上的“同时”,如同多条赛道上的赛跑,多个任务能在不同的处理器核心上同时执行,这对于高度计算密集型的应用来说,无疑是性能提升的利器。

并发和并行的区别和联系

区别:

并发(Concurrency):指的是在一段时间内,多个任务或进程同时处于开始和结束之间的状态,它们可以交替使用系统的资源进行执行。在这个过程中,并不要求所有任务在同一时刻实际执行,而是通过任务切换,给用户造成“同时进行”的错觉。并发强调的是任务的交错执行和资源的共享分配,操作系统通过时间片轮转、优先级调度等方式来管理并发任务。

并行(Parallelism):是指在同一时刻,多个任务或进程能够真正同时执行,它们各自占用独立的处理器或处理器核心,实现物理上的同时处理。并行处理要求系统拥有多个执行单元(如多核CPU、多个GPU或分布式计算节点),能够将任务分解后,分配到不同的处理单元上同时运算,从而显著提高处理速度和效率。

**联系:**并发和并行都涉及到了多任务处理的概念,都是提高系统效率和响应速度的有效手段。在多核或多处理器系统中,并发是实现并行的基础,系统可以先通过并发管理多个任务,然后在可用的处理器核心上实现这些任务的并行执行。

在这里插入图片描述

通俗理解:

  • 并发就像是苦逼程序猿凯叔一个人在厨房里,同时准备几道菜。他可能先切好蔬菜放在一边,然后去搅拌汤料,接着回来炒菜,再回去检查汤的状态。虽然看起来他在同时处理多项任务,但实际上是他在不同任务之间迅速切换,同一时间只专注于一项工作。这种快速切换让你感觉好像所有事情都在“同时”进行。

  • 并行则是如果他有几位女朋友一起帮忙做饭,每个人负责一道菜。这样,切菜、煮汤、炒菜等任务就可以真正同时进行了。每个人都有自己的工作台和工具,他们不需要等待别人完成某个步骤再开始自己的工作,大大提高了做饭的效率。

总的来说,并发是“看起来同时做很多事”,实际上是在快速切换;而并行则是“真正同时做很多事”,每个任务都有独立的执行资源。在现代计算环境中,二者常常结合使用,以达到最佳的系统性能和用户体验。
在这里插入图片描述

锁住你的心: synchronized 的誓言

—— 如何使用synchronized关键字保证数据的一致性,如同承诺让爱情稳固。

首先提到多线程,synchronized是我们无法规避的一个工具,它通过提供一种简单而强大的同步机制,确保了多线程环境下的数据一致性、操作的正确性和程序的健壮性。他就像是一个保护我们的安全罩,又像是一把禁锢我们的枷锁,就像是结婚证,结过婚的想要逃出这个围城,而没有拿到的又渴望得到。

引入synchronized关键字是为了应对多线程环境下对共享资源的访问控制问题,以确保线程安全。以下是synchronized的作用和意义:

  1. 确保原子性:在多线程环境中,一个或一系列操作要么全部完成,要么都不执行,这就是原子性。synchronized可以确保被它保护的代码块或方法在同一时刻只能被一个线程执行,避免了因线程切换而导致的数据不一致问题,保证了操作的原子性。

  2. 实现互斥:当多个线程试图访问同一段代码或共享资源时,synchronized通过锁定机制确保每次只有一个线程可以进入临界区(即被同步的代码块或方法),其他线程则需等待锁的释放,从而实现了对共享资源的互斥访问,防止了数据竞争和脏读脏写现象。

  3. 保障可见性synchronized还确保了线程在释放锁之前对共享变量的修改能够立即对其他线程可见,这是因为退出synchronized代码块或方法时会有一个内存栅栏(memory barrier)操作,这强制线程将工作内存中的数据刷新回主内存,从而维护了数据的可见性。

  4. 避免死锁:虽然直接使用synchronized不当可能会引发死锁,但合理使用它可以作为一种同步机制,通过明确的锁顺序等策略,帮助开发者避免复杂的死锁问题,确保线程间的协作能够有序进行。

  5. 优化性能:虽然早期的synchronized是一个重量级锁,可能导致较高的性能开销,但随着Java的发展,JVM对synchronized进行了大量优化,如引入了偏向锁、轻量级锁和重量级锁的锁升级机制,以及锁粗化、锁消除等策略,极大地减少了锁操作的开销,使得其在许多场景下的性能与java.util.concurrent.locks.Lock接口的实现相差无几,甚至在某些情况下更优,因为synchronized的使用更为简洁,易于理解和维护。

    讲到synchronized如同承诺让爱情更加稳固,那么如何利用synchronized来稳固数据的一致性?类比于让爱情关系更加稳固一样:

    一对一的关注(对象锁)

    就像在恋爱中双方给予对方独一无二的关注,使用synchronized修饰非静态方法或同步代码块时,锁住的是当前实例对象。这意味着一次只有一个线程能够执行这段代码,就如同在关系中确保每次只专注于对方的需求,防止外界干扰,维护情感的纯真与深度。

    public class LoveStory {
        private int trustLevel; // 两人之间的信任级别
    
        public synchronized void increaseTrust() {
            trustLevel++; // 增强信任,如同共同经历加深感情
        }
    }
    
    共同的价值观(类锁)

    对于静态方法或指定类对象作为锁的情况,就像情侣间基于共同价值观和目标建立的稳固基础。无论哪个线程访问,都遵循相同的原则,保证了数据的一致性和关系的稳定性。

    public class LoveStory {
        private static int sharedMemories; // 共享记忆,两人共有的美好回忆
    
        public static synchronized void addMemory() {
            sharedMemories++; // 添加共同记忆,加深情感链接
        }
    }
    
    沟通的桥梁(代码块同步)

    在特定代码块使用synchronized,就好比在关系中针对特定问题进行深入沟通,确保信息的准确传递和理解。这既避免了不必要的误解,也保护了敏感话题不受外界打扰。

    public class LoveStory {
        private String secret; // 私密话题,需要特别保护的八卦信息
    
        public void shareSecret(String newSecret) {
            synchronized(this) {
                secret = newSecret; // 在保护下分享秘密,增强信任
            }
        }
    }
    

    使用synchronized关键字在多线程环境中维护数据一致性,就如同在爱情中维护双方的信任与承诺。它通过限制访问、确保同步和保护敏感区域,构建了一个稳定且可靠的环境,无论是对于程序中的数据还是现实中的情感关系,都是至关重要的。正如每一对情侣需要不断沟通、理解和尊重彼此,编程中的线程也需要通过synchronized这样的机制来协调操作,共同维护数据的和谐与一致。

    在这里插入图片描述

volatile的暧昧信息

—— 介绍volatile变量的作用,恋爱中那些让人捉摸不透的情感信号。

在并发编程中,引入volatile关键字主要是为了解决多线程环境下对共享变量的可见性和禁止指令重排序的问题,从而保证了在没有锁的轻量级并发控制中数据的一致性和正确性。volatile关键字在并发编程中的作用是提供了一种既能保证数据可见性又能禁止特定类型指令重排序的轻量级同步机制,它适用于那些需要高性能且仅需保证共享变量读写一致性的场景。下面是volatile的具体作用和意义:

  1. 确保可见性(Visibility):当一个变量被声明为volatile时,任何线程对这个变量的修改都会立即写入主内存中,同时任何访问该变量的线程都会从主内存中读取最新的值,而不是从各自的线程工作内存中读取,这样就确保了多线程环境下的可见性。即使没有使用锁,其他线程也能看到对volatile变量所做的修改,避免了缓存一致性问题和数据不一致的风险。
  2. 禁止指令重排序(Ordering):在没有同步的情况下,编译器和处理器为了优化性能,可能会对指令进行重排序,这在单线程环境下不会有问题,但在多线程环境下可能导致程序的行为不符合预期。volatile变量的写操作会在其后面的所有读操作之前完成,读操作会在其前面的所有写操作之后完成,这形成了一个所谓的“内存屏障(Memory Barrier)”,确保了特定操作的执行顺序,防止了因指令重排序导致的并发问题。
  3. 轻量级同步:相比于使用synchronized这样的重量级锁,volatile提供了一种较为轻量级的同步机制,它不会引起线程上下文的切换和调度开销,适合于读多写少的场景,比如标志位、状态标记等,能够在保证并发安全的同时提高程序的执行效率。
  4. 适用场景:尽管volatile在某些特定场景下非常有用,但它不能替代所有的同步机制。它不能保证复合操作的原子性,例如count++这样的操作实际上包含了读、改、写的三个子操作,使用volatile无法保证这三个操作的原子性。因此,volatile常用于状态标记(如单例模式的双重检查锁定中的初始化标志)、信号量等场景。

声明共享变量:当你有一个变量需要在多个线程间共享,并且其值可能被外部因素(如硬件状态、其他线程)改变时,可以将其声明为volatile类型。例如,在多线程环境中记录一个状态标志:

public class LoveSignal {
    private volatile boolean isHeartfelt; // 使用volatile确保所有线程看到的是最新情感状态
    //...
}
  1. 避免数据不一致:通过使用volatile,确保每次读取变量时都直接从主内存中获取最新值,写入时也直接写回主内存,避免了缓存不一致的问题。

  2. 不保证原子性:虽然volatile保证了可见性和一定程度的顺序性,但它并不能保证复合操作(如increment++)的原子性。因此,对于需要原子操作的场景,还需配合锁或其他同步机制。

    需要注意的是

  3. 捉摸不透的变化:就像在恋爱中,伴侣间微妙的情感变化可能瞬间即逝,难以捕捉,但又极其重要。volatile变量的状态也可能被外部因素迅速改变,对线程行为产生直接影响。

  4. 直接且即时的传达volatile变量的改变能立即反映到所有访问它的线程,就如同在恋爱中,当一方明确表达出某种情感(如通过一个眼神、一句话),另一方几乎瞬间就能感知到,无需通过复杂的推断。

  5. 表面现象下的深层含义:尽管volatile变量的直接读写看似简单,但它背后可能隐藏着复杂的逻辑或外部影响,正如恋爱中的一个微笑可能背后有着复杂的情感交织。理解这种信号需要更深层次的洞察和交流。

    代码示例:

    class ThreadVolatileDemo extends Thread {
    	public    boolean flag = true;
    	@Override
    	public void run() {
    		System.out.println("开始执行子线程....");
    		while (flag) {
    		}
    		System.out.println("线程停止");
    	}
    	public void setRuning(boolean flag) {
    		this.flag = flag;
    	}
    
    }
    
    public class ThreadVolatile {
    	public static void main(String[] args) throws InterruptedException {
    		ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();
    		threadVolatileDemo.start();
    		Thread.sleep(3000);
    		threadVolatileDemo.setRuning(false);
    		System.out.println("flag 已经设置成false");
    		Thread.sleep(1000);
    		System.out.println(threadVolatileDemo.flag);
    
    	}
    }
    

    这个运行结果是。。。。

    开始执行子线程....
    flag 已经设置成false
    false
    

    然后线程一直无法停止

原因:线程之间是不可见的,读取的是副本,没有及时读取到主内存结果。解决办法使用Volatile关键字将解决线程之间可见性, 强制线程每次读取该值的时候都去“主内存”中取值。

从专业的角度讲volatile和synchronized的区别有如下几点(敲黑板,划重点,面试必问点):

  1. 作用目标

    • volatile关键字主要关注于变量的可见性和顺序性。它确保了对volatile变量的修改能够立即被其他线程看到,且禁止了编译器和处理器对volatile变量相关的读写指令进行重排序,以维持执行的有序性。但它不提供原子性保证,即对于复合操作(如递增i++)无法保证操作的整体性。
    • synchronized关键字则提供了互斥锁机制,用于控制多个线程对共享资源的访问。它既能保证可见性,也能确保原子性,同时还维护了操作的顺序性。当一个线程获得了一个对象的监视器锁后,其他线程必须等待该锁被释放才能继续执行受保护的代码块。这有效地防止了并发访问导致的数据不一致问题。
  2. 使用场景

    • volatile适用于读多写少的场景,尤其是作为状态标记(如单例模式的双重检查锁定中的初始化标志)或者用于实现线程间的简单通信。
    • synchronized更适合于存在复杂交互和需要控制并发访问顺序的场景,如对集合类的并发修改、多个线程间的协作等。
  3. 性能影响

    • volatile操作相对轻量级,因为它不会引起线程的上下文切换和调度,但频繁的读写也会带来一定的性能开销。

    • synchronized由于涉及到线程的阻塞和唤醒,开销较大,但在JDK 1.6之后,通过偏向锁、轻量级锁和重量级锁的优化,性能有了显著提升,尤其在竞争不激烈的情况下。

    volatile就像是厨房里的一个小公告板,上面写着“盐已经用完”。一旦有人更新了这个信息(比如写上了“新盐已补充”),所有人都会立刻看到这个新信息,但公告板本身不能防止两个人同时去拿新盐(也就是不能保证原子性)。
    synchronized则是厨房门上的一把锁。如果门锁上了,其他人就必须在外面等,直到里面的人用完调料并且开门出来。这样就确保了每次只有一个厨师能进去操作,避免了混乱(既保证了可见性,也确保了原子性)。虽然这样可能需要等待,但大家都能安心地知道,当轮到自己进去的时候,调料一定是齐全的,而且不会有人突然闯进来搞乱你正在做的事情。

  • 18
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值