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

5次阅读

共计 2505 个字符,预计需要花费 7 分钟才能阅读完成。

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())先于被中断线程的代码。

正文完
 0