实战java高并发程序设计第一章

本文深入探讨了并发编程中的关键概念,包括同步与异步、并发与并行、临界区、阻塞与非阻塞、死锁、饥饿与活锁,以及不同级别的并发策略。同时,文章详细介绍了JMM内存模型,阐述了原子性、可见性和有序性的重要性,并通过实例说明了这些特性在实际应用中的表现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 基本概念

同步(Synchronous)和异步(Asynchronous)
并发(Conncurrency)和并行(Parallelism)
临界区
阻塞(Blocking)与非阻塞(Non-Blocking)
死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock)

同步(Synchronous)和异步(Asynchronous)
在这里插入图片描述
并发(Conncurrency)和并行(Parallelism)
在这里插入图片描述

  • 临界区
    临界区:公共资源或共享数据,可被多个线程使用,但每次只能有一个线程使用,其他线程需等待
  • 阻塞(Blocking)与非阻塞(Non-Blocking)
    阻塞:一个线程占用了临界区资源,其他线程就会在就必须在临界区等待,等待会导致线程挂起,这就是阻塞
    非阻塞:没有一个线程可以妨碍其他线程执行,所有线程都会不断尝试向前
  • 死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock)
    死锁:A线程中占用a资源,并且尝试去获取b资源,同时B线程中占用着b资源,并且尝试去获取a资源,此时谁也无法进行下去
    ,导致死锁
  • 饥饿:资源竞争激烈时,某个线程长时间无法获取到资源,产生饥饿
    活锁:两个线程竞争同一资源时相互谦让,导致两个线程一直在谦让而无法正常工作

2. 并发级别

阻塞
无饥饿(Starvation-Free)
无障碍(Obstruction-Free)
无锁(Lock-Free)
无等待(Wait-Free)

  • 阻塞

使用synchronized关键字或重入锁等时,会阻塞其他线程获取临界区资源

  • 无饥饿(Starvation-Free)

使用公平锁时,先到先得,不会产生饥饿;非公平锁,由于竞争激烈,或者某些线程优先级高导致低优先级的线程有可能产生饥饿

  • 无障碍(Obstruction-Free)

无障碍是最弱的非阻塞调度,两个线程均可修改临界区数据,一旦检测到数据不安全,即对自己修改的数据进行回滚,确保数据安全,冲突严重时所有线程会不停回滚从而造成系统无法正常工作.
一般会配合"一致性标记"一起使用,操作前读取一致性标记,修改后再次读取此标记,若不一致则表示数据不安全

  • 无锁(Lock-Free)

无锁的并行都是无障碍的.无锁的并发必然有一个线程能够在有限步内完成操作离开临界区.一般都会包含无穷循环,且可能产生饥饿

  • 无等待(Wait-Free)

无等待在无所的基础上更进一步,要求所有线程必须在有限步内访问完临界区,这样就不会引起饥饿问题。读线程都是无等待的,写数据时,采用RCU(Read Copy Update)策略,先取得原数据副本,再修改副本数据,修改完后在合适的机会写回数据

3.JMM内存模型

JMM的技术点围绕多线程的原子性、可见性和有序性来建立的

- 原子性(Atomicity)

32位虚拟机中操作long型变量是非原子性的,可能造成数据不安全

public class MultiThreadLong {
    public volatile static long t=0;
    public static class ChangeT implements Runnable{
        private long to;
        public ChangeT(long to){
            this.to=to;
        }
        @Override
        public void run() {
            while(true){
            MultiThreadLong.t=to;     //赋值临界区的t
            Thread.yield();            //让出资源
            }
        }
    }
    public static class ReadT implements Runnable{
        @Override
        public void run() {
            while(true){
             long tmp=MultiThreadLong.t;
             if(tmp!=111L && tmp!=-999L && tmp!=333L && tmp!=-444L)
                 System.out.println(tmp);    //打印非正常值
            Thread.yield();            //让出资源
            }
        }
    }
    
    public static void main(String[] args) {
        new Thread(new ChangeT(111L)).start();
        new Thread(new ChangeT(-999L)).start();
        new Thread(new ChangeT(333L)).start();
        new Thread(new ChangeT(-444L)).start();
        new Thread(new ReadT()).start();
        //输出:
        //-4294966963
        //4294966852
        //-4294966963
    }
}

- 可见性

可见性是指一个线程修改某一个共享变量的值时,其他线程是否能够立即知道这个修改
原因:缓存优化或者硬件优化问题(内存读写不会立即触发,而先进入一个硬件队列);指令重排和编辑器的优化
在这里插入图片描述
- 有序性

并发时,程序的执行可能出现乱序,给人感觉就是:写在前面的代码,会在后面执行

public class OrderExample {
    int a=0;
    boolean flag=false;
    public void writer(){
        a=1;
        flag=true;          //这一步不一定在a=1之后
    }

    public void reader(){
        if(flag){
            int i=a+1;      //当flag=true时,a不一定为1,也可能未执行a=0
        }
    }
}

在这里插入图片描述

为什么要指令重拍呢?
完全是出于性能考虑,一条指令可以分为以下几步:

  • .取指IF。
  • ·译码和取寄存器操作数ID。
  • ·执行或者有效地址计算EX。
  • ·存储器访问MEM。
  • ·写回WB。

通过指令重排可以减少cpu流水线停顿,提升巨大效率

Happen-Before原则:

  • 程序顺序原则:一个线程内保证语义的串行性。
  • volatile规则:volatile变量的写先于读发生,这保证了volatile变量的可见性。
  • 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前。
  • 传递性:A先于B,B先于C,那么A必然先于C。
  • 线程的start()方法先于它的每一个动作。
  • ·线程的所有操作先于线程的终结(Thread.join())。
  • 线程的中断(interrupt())先于被中断线程的代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值