《Java 并发编程实战》学习笔记一

《Java 并发编程实战》学习笔记

01 | 可见性、原子性和有序性问题:并发编程Bug的源头

  1. 可见性:一个线程对共享变量的修改,另一个线程能立刻看到

缓存导致的可见性问题:多核时代每颗 CPU 都有自己的缓存,这时 CPU 缓存(三级缓存)与内存的数据一致性就容易出现问题了,当多个线程在不同的 CPU 上执行时,这些线程操作的是不同的 CPU 缓存。比如,线程 A 操作的是 CPU-1 上的缓存,而线程 B 操作的是 CPU-2 上的缓存,很明显,这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了。
多核 CPU 的缓存与内存关系图

  1. 原子性:一个或者多个操作在 CPU 执行的过程中不被中断的特性

线程切换带来的原子性问题:高级语言里一条语句往往需要多条 CPU 指令完成,例如count += 1这句代码,至少需要三条 CPU 指令。
指令 1:首先,需要把变量 count 从内存加载到 CPU 的寄存器;
指令 2:之后,在寄存器中执行 +1 操作;
指令 3:最后,将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。
操作系统做任务切换,可以发生在任何一条 CPU 指令执行完,是的,是 CPU 指令,而不是高级语言里的一条语句。对于上面的三条指令来说,我们假设 count=0,如果线程 A 在指令 1 执行完后做线程切换,线程 A 和线程 B 按照下图的序列执行,那么我们会发现两个线程都执行了 count+=1 的操作,但是得到的结果不是我们期望的 2,而是 1。
非原子操作的执行路径示意图

  1. 有序性:指的是程序按照代码的先后顺序执行
public class Singleton {  
	/* 单例模式-双重检测锁
	 *一个new操作在字节码层面对应三步操作
     * 1、开辟空间
     * 2、初始化
     * 3、变量赋值
     * cpu可能会重排需2、3步。如果先执行了第3步,则会返回一个没有初始化的对象,
     * 发生空指针异常。增加volatile关键字,防止重排序
     */
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getInstance() {  
	    if (singleton == null) {  
	        synchronized (Singleton.class) {  
		        if (singleton == null) {  
		            singleton = new Singleton();  
		        }  
	        }  
	    }  
	    return singleton;  
	}  
}

编译优化带来的有序性问题:我们假设以上代码中singleton变量没有被volatile关键字修饰,线程 A 先执行 getInstance() 方法,当执行完“变量赋值”时恰好发生了线程切换,切换到了线程 B 上;如果此时线程 B 也执行 getInstance() 方法,那么线程 B 在执行第一个判断时会发现 instance != null ,所以直接返回 instance,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。

02 | Java内存模型:看Java如何解决可见性和有序性问题

  1. 为什么定义Java内存模型?
    Java语言规范引入了Java内存模型,通过定义多项规则对处理器的乱序执行、编译器的重排序,还有内存系统的重排序等进行限制,主要是针对可见性和有序性。
  2. Java内存模型涉及的几个关键词:锁、volatile字段、final修饰符与对象的安全发布。

锁,锁操作是具备happens-before关系的,解锁操作happens-before之后对同一把锁的加锁操作。实际上,在解锁的时候,JVM需要强制刷新缓存,使得当前线程所修改的内存对其他线程可见。

volatile字段,volatile字段可以看成是一种不保证原子性的同步但保证可见性的特性,其性能往往是优于锁操作的。但是,频繁地访问volatile字段也会出现因为不断地强制刷新缓存而影响程序的性能的问题。

final修饰符,final修饰的实例字段则是涉及到新建对象的发布问题。当一个对象包含final修饰的实例字段时,其他线程能够看到已经初始化的final实例字段,这是安全的。

  1. Happens-Before的7个规则:
    (1).程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
    (2).管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而"后面"是指时间上的先后顺序。
    (3).volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的"后面"同样是指时间上的先后顺序。
    (4).线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
    (5).线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
    (6).线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
    (7).对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
  2. Happens-Before的1个特性:传递性(如果A先于B,B先于C,那么A一定先于C)。
  3. Java内存模型底层怎么实现的?主要是通过内存屏障(memory barrier)禁止重排序的,即时编译器根据具体的底层体系架构,将这些内存屏障替换成具体的 CPU 指令。对于编译器而言,内存屏障将限制它所能做的重排序优化。而对于处理器而言,内存屏障将会导致缓存的刷新操作。比如,对于volatile,编译器将在volatile字段的读写操作前后各插入一些内存屏障。

03 | 互斥锁(上):解决原子性问题

由01节已知,原子性问题出现的原因是因为线程切换,单核cpu可以通过禁止线程中断来防止切换线程,从而达到一个线程不间断执行;但是多核cpu场景下,虽然可以通过禁止线程中断来保证cpu上的线程连续执行,但是无法保证多个线程同时修改同一个共享变量,这时我们就需要加锁来保证多个线程对共享变量的修改是互斥的,这样就可以保证原子性了。

锁是一种通用的技术方案,Java 语言提供的 synchronized 关键字,就是锁的一种实现。synchronized 关键字可以用来修饰方法,也可以用来修饰代码块,它的使用示例基本上都是下面这个样子:

class X {
  // 修饰非静态方法
  synchronized void foo() {
    // 临界区
  }
  // 修饰静态方法
  synchronized static void bar() {
    // 临界区
  }
  // 修饰代码块
  Object obj = new Object()void baz() {
    synchronized(obj) {
      // 临界区
    }
  }
}  

当修饰静态方法的时候,锁定的是当前类的 Class 对象,在上面的例子中就是 Class X;
当修饰非静态方法的时候,锁定的是当前实例对象 this。

另外,加锁时需要考虑锁定的对象和受保护资源的关系

04 | 互斥锁(下):如何用一把锁保护多个资源?

  1. 没有关联关系的资源
    1. 应该采用【细粒度锁对共享资源进行精细化管理】(也就是一个锁对应一个受保护的共享资源),这样可以提高并发性能。
    2. 如果非要使用同一把锁去保护多个没有关联关系的资源的话,首先要明白这是不会出现并发问题的,但是这样【锁的范围太大了,粒度太粗了,把本可以并行执行的操作搞成了串行了,会降低并行性能】

    例:一个账户Account对象有提现和修改密码两个方法,如果对两个方法都加synchronized关键字,锁的对象是这个this对象,那么提现和修改密码这两个操作就是串行的会导致性能太差。如果我们用两个锁分别对提现和修改密码加锁,那么这两个功能就可以并行了

  2. 保护有关联关系的多个资源
    这时候就需要锁能覆盖所有受保护资源。

    例:
    在这里插入图片描述假设有 A、B、C 三个账户,余额都是 200 元,我们用两个线程分别执行两个转账操作:账户 A 转给账户 B 100 元,账户 B 转给账户 C 100 元,最后我们期望的结果应该是账户 A 的余额是 100 元,账户 B 的余额是 200 元, 账户 C 的余额是 300 元。如果单独对A->B和B->C的转账加锁的话,两个转账逻辑互不影响,同时转账时可能会出现B的余额为300或者100的情况。这时我们就要对两个转账使用同一把锁进行保护。

05 | 一不小心就死锁了,怎么办?

  1. 死锁发生的必要条件:
    1)互斥,共享资源 X 和 Y 只能被一个线程占用;
    2)占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
    3)不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
    4)循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。

  2. 避免发生死锁(哲学家就餐问题)
    只要我们破坏造成死锁的其中一个条件,就可以成功避免死锁的发生。
    1)互斥这个条件我们没有办法破坏,因为我们用锁为的就是互斥。
    2)“占用且等待”->一次性申请所有的资源,这样就不存在等待了。

    “同时申请”这个操作是一个临界区,我们也需要一个角色(Java 里面的类)来管理这个临界区,我们就把这个角色定为 Allocator(必须是单例,只能由一个人来分配资源)。它有两个重要功能,分别是:同时申请资源 apply() 和同时释放资源 free()。

    3)“不可抢占”->占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。

    提供tryLock(long, TimeUnit) 方法,在一段时间尝试获取锁。使用超时机制,申请不到资源时,不阻塞,继续执行后续代码,从而可以释放已有资源

    4)“循环等待”->可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。

    需要对资源进行排序,然后按序申请资源。

06 | 用“等待-通知”机制优化循环等待

  1. 线程的生命周期(本部分属于前置知识拓展,课程中安排在第09节)
  2. wait()和sleep()的异同
    不同点:
    1)wait会释放所有锁而sleep不会释放锁资源.
    2)wait只能在同步方法和同步块中使用,而sleep任何地方都可以.
    3)wait无需捕捉异常,而sleep需要.(都抛出InterruptedException ,wait也需要捕获异常)
    4)wait()无参数需要唤醒,线程状态WAITING;wait(1000L);到时间自己醒过来或者到时间之前被其他线程唤醒,状态和sleep都是TIME_WAITING
    相同点:都会让渡CPU执行时间,等待再次调度!
  3. wait()、notify()、notifyAll() 都是在 synchronized{}内部被调用的。如果在 synchronized{}外部调用,或者锁定的 this,而用 target.wait() 调用的话,JVM 会抛出一个运行时异常:java.lang.IllegalMonitorStateException。
  4. notify() 是会随机地通知等待队列中的一个线程,而 notifyAll() 会通知等待队列中的所有线程。

07 | 安全性、活跃性以及性能问题

并发编程中我们需要注意的问题有很多,主要有三个方面,分别是:安全性问题、活跃性问题和性能问题。

  1. 安全性:
    数据竞争: 多个线程同时访问一个数据,并且至少有一个线程会写这个数据。
    竞态条件: 程序的执行结果依赖程序执行的顺序。也可以按照以下的方式理解竞态条件: 程序的执行依赖于某个状态变量,在判断满足条件的时候执行,但是在执行时其他变量同时修改了状态变量。

    if (状态变量 满足 执行条件) {
      执行操作
    }
    
  2. 活跃性:
    死锁:略
    活锁:虽然没有发生阻塞,但仍会存在执行不下去的情况。我感觉像进入了某种怪圈(例如同时争抢同一个资源,大家一起释放之后,间隔了相同时间,又一起抢)。解决办法,等待随机的时间,例如Raft算法中重新选举leader。
    饥饿:线程因无法访问所需资源而无法执行下去的情况。解决办法,一是保证资源充足,二是公平地分配资源,三就是避免持有锁的线程长时间执行

  3. 性能:
    核心就是在保证安全性和活跃性的前提下,根据实际情况,尽量降低锁的粒度。即尽量减少持有锁的时间。JDK的并发包里,有很多特定场景针对并发性能的设计。还有很多无锁化的设计,例如MVCC,TLS,COW等,可以根据不同的场景选用不同的数据结构或设计。

    性能方面的度量指标有很多有三个指标非常重要,就是:吞吐量、延迟和并发量。
    吞吐量:指的是单位时间内能处理的请求数量。吞吐量越高,说明性能越好。
    延迟:指的是从发出请求到收到响应的时间。延迟越小,说明性能越好。
    并发量:指的是能同时处理的请求数量,一般来说随着并发量的增加、延迟也会增加。所以延迟这个指标,一般都会是基于并发量来说的。例如并发量是 1000 的时候,延迟是 50 毫秒。

08 | 管程:并发编程的万能钥匙

  1. 管程(Monitor),很多 Java 领域的同学都喜欢将其翻译成“监视器”,指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。管程是一种概念,任何语言都可以通用。
    在并发编程领域,有两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作。这两大问题,管程都是能够解决的。

    管程解决互斥问题,将共享变量及其对共享变量的操作统一封装起来。
    管程解决同步问题,引入了管程模型。

  2. 管程模型:Hasen 模型、Hoare 模型和 MESA 模型

    Hasen 模型、Hoare 模型和 MESA 模型的一个核心区别就是当条件满足后,如何通知相关线程。管程要求同一时刻只允许一个线程执行,那当线程 T2 的操作使线程 T1 等待的条件满足时,T1 和 T2 究竟谁可以执行呢?
    Hasen 模型里面,要求 notify() 放在代码的最后,这样 T2 通知完 T1 后,T2 就结束了,然后 T1 再执行,这样就能保证同一时刻只有一个线程执行。
    Hoare 模型里面,T2 通知完 T1 后,T2 阻塞,T1 马上执行;等 T1 执行完,再唤醒 T2,也能保证同一时刻只有一个线程执行。但是相比 Hasen 模型,T2 多了一次阻塞唤醒操作。
    MESA 管程里面,T2 通知完 T1 后,T2 还是会接着执行,T1 并不立即执行,仅仅是从条件变量的等待队列进到入口等待队列里面。这样做的好处是 notify() 不用放到代码的最后,T2 也没有多余的阻塞唤醒操作。但是也有个副作用,就是当 T1 再次执行的时候,可能曾经满足的条件,现在已经不满足了,所以需要以循环方式检验条件变量。
    有一个编程范式,就是需要在一个 while 循环里面调用 wait()。这个是 MESA 管程特有的。
    while(条件不满足) { wait();}

  3. 在java中,每个加锁的对象都绑定着一个管程(监视器)
  4. 线程访问加锁对象,就是去拥有一个监视器的过程。如一个病人去门诊室看医生,医生是共享资源,门锁锁定医生,病人去看医生,就是访问医生这个共享资源,门诊室其实是监视器(管程)。
  5. 所有线程访问共享资源,都需要先拥有监视器。就像所有病人看病都需要先拥有进入门诊室的资格。
  6. 监视器至少有两个等待队列。一个是进入监视器的等待队列一个是条件变量对应的等待队列。后者可以有多个。就像一个病人进入门诊室诊断后,需要去验血,那么它需要去抽血室排队等待。另外一个病人心脏不舒服,需要去拍胸片,去拍摄室等待。
  7. 监视器要求的条件满足后,位于条件变量下等待的线程需要重新在门诊室门外排队,等待进入监视器。就像抽血的那位,抽完后,拿到了化验单,然后,重新回到门诊室等待,然后进入看病,然后退出,医生通知下一位进入。

09 | Java线程(上):Java线程的生命周期

  1. 通用的线程生命周期
    通用线程的生命周期:初始状态,可运行状态,运行状态,休眠状态,终止状态
    通用的线程周期
    1)初始状态,指的是线程已经被创建,但是还不允许分配 CPU 执行。这个状态属于编程语言特有的,不过这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层面,真正的线程还没有创建。
    2)可运行状态,指的是线程可以分配 CPU 执行。在这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配 CPU 执行。
    3)当有空闲的 CPU 时,操作系统会将其分配给一个处于可运行状态的线程,被分配到 CPU 的线程的状态就转换成了运行状态
    4)运行状态的线程如果调用一个阻塞的 API(例如以阻塞方式读文件)或者等待某个事件(例如条件变量),那么线程的状态就会转换到休眠状态,同时释放 CPU 使用权,休眠状态的线程永远没有机会获得 CPU 使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态。
    5)线程执行完或者出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束了。

  2. Java 中线程的生命周期

    java.lang.Thread类中的State枚举:
    NEW(初始化状态),
    RUNNABLE(可运行 / 运行状态),
    BLOCKED(阻塞状态),
    WAITING(无时限等待),
    TIMED_WAITING(有时限等待),
    TERMINATED(终止状态)

    与通用线程生命周期的区别:Java 语言里把可运行状态和运行状态合并了,并把休眠状态细化成阻塞状态,无时限等待和有时限等待。因为可运行和运行状态这两个状态在操作系统调度层面有用,而 JVM 层面不关心这两个状态,因为 JVM 把线程调度交给操作系统处理了。
    在这里插入图片描述

    java线程生命周期状态的流转:
    1)RUNNABLE 与 BLOCKED 的状态转换:只有一种场景会触发这种转换,就是线程等待 synchronized 的隐式锁。当等待的线程获得 synchronized 隐式锁时,就又会从 BLOCKED 转换到 RUNNABLE 状态。

    线程调用阻塞式 API 时,在操作系统层面,线程是会转换到休眠状态的,但是在 JVM 层面,Java 线程的状态会依然保持 RUNNABLE 状态。因为JVM 层面并不关心操作系统调度相关的状态

    2)RUNNABLE 与 WAITING 的状态转换
    (1) 获得 synchronized 隐式锁的线程,调用无参数的 Object.wait() 方法。调用notify()或者notifyAll()来唤醒。
    (2) 调用无参数的 Thread.join() 方法。当前线程会阻塞,线程的状态会从RUNNABLE状态转换为WAITING状态。等待的线程执行完时,当前线程又会从 WAITING 状态转换到 RUNNABLE。
    (3) 调用 LockSupport.park() 方法。当前线程会阻塞,线程的状态会从 RUNNABLE 转换到 WAITING。调用 LockSupport.unpark(Thread thread) 可唤醒目标线程,目标线程的状态又会从 WAITING 状态转换到 RUNNABLE。
    3)RUNNABLE 与 TIMED_WAITING 的状态转换
    (1) 调用带超时参数的 Thread.sleep(long millis) 方法;
    (2) 获得 synchronized 隐式锁的线程,调用带超时参数的 Object.wait(long timeout) 方法;
    (3) 调用带超时参数的 Thread.join(long millis) 方法;
    (4) 调用带超时参数的 LockSupport.parkNanos(Object blocker, long deadline) 方法;
    (5) 调用带超时参数的 LockSupport.parkUntil(long deadline) 方法。

    4)NEW 到 RUNNABLE 的状态转换
    调用线程对象的 start() 方法
    5)RUNNABLE 到 TERMINATED的状态转换
    (1) run()执行完成。
    (2) stop():杀死线程,如果线程持有ReentrantLock锁,被stop()的线程不会自动调用ReentrantLock的unlock()去释放锁,但是会释放synchronized隐式锁。(方法已过时,不推荐使用

    interrupt()
    a. 被中断的线程通过异常方式获得通知。 当线程A处于WAITING、TIMED_WAITING状态时,如果其他线程调用线程A的interrupt(),会使线程A返回到RUNNABLE状态,同时触发InterruptedException异常。 当线程处于RUNNABLE状态时,如果阻塞在InterruptibleChannel上,触发ClosedByInterruptException,如果阻塞在Selector上,就会触发Selector异常。
    b. 主动监测:可以通过isInterrupted()检测自己是不是被中断了。

10 | Java线程(中):创建多少线程才是合适的?

  1. 为什么要使用多线程?
    使用多线程,本质上就是提升程序性能。也就是从度量的角度,主要是降低延迟,提高吞吐量。

    延迟指的是发出请求到收到响应这个过程的时间;延迟越短,意味着程序执行得越快,性能也就越好。
    吞吐量指的是在单位时间内能处理请求的数量;吞吐量越大,意味着程序能处理的请求越多,性能也就越好。
    这两个指标内部有一定的联系(同等条件下,延迟越短,吞吐量越大),但是由于它们隶属不同的维度(一个是时间维度,一个是空间维度),并不能互相转换。

  2. 多线程的应用场景
    要想“降低延迟,提高吞吐量”,对应的方法呢,基本上有两个方向,一个方向是优化算法,另一个方向是将硬件的性能发挥到极致。前者属于算法范畴,后者则是和并发编程息息相关了。那计算机主要有哪些硬件呢?主要是两类:一个是 I/O,一个是 CPU。简言之,在并发编程领域,提升性能本质上就是提升硬件的利用率,再具体点来说,就是提升 I/O 的利用率和 CPU 的利用率。
  3. 创建多少线程合适?(理论公式,此处核数指的是逻辑核数)
    (1)CPU密集型(I/O 操作执行的时间相对于 CPU 计算来说都非常长)计算:
    a. 线程数量 = CPU核数
    b. 线程数量 = CPU核数 + 1 (工程上)
    (2)I/O密集型(CPU操作执行的时间相对于 I/O 计算来说都非常长)计算
    a. 最佳线程数 = 1 + (I/O耗时 / CPU耗时)
    b. 最佳线程数 = CPU核数 * [ 1 + (I/O耗时 / CPU耗时) ]

11 | Java线程(下):为什么局部变量是线程安全的?

  1. 方法的执行
    当调用方法A的时候,CPU 要先找到方法A的地址,然后跳转到这个地址去执行代码,最后 CPU 执行完方法A之后,要能够返回。首先找到调用方法的下一条语句的地址,再跳转到这个地址去执行。
    cpu找调用方法的参数和返回地址执行通过 CPU 的堆栈寄存器。CPU 支持一种栈结构,因为这个栈是和方法调用相关的,因此经常被称为调用栈。例如,有三个方法 A、B、C,他们的调用关系是 A->B->C(A 调用 B,B 调用 C),在运行时,会构建出下面这样的调用栈。每个方法在调用栈里都有自己的独立空间,称为栈帧,每个栈帧里都有对应方法需要的参数和返回地址。当调用方法时,会创建新的栈帧,并压入调用栈;当方法返回时,对应的栈帧就会被自动弹出。也就是说,栈帧和方法是同生共死的。
    局部变量(的引用)就是放到了调用栈里,局部变量的作用域是方法内部,也就是说当方法执行完,局部变量就没用了(无引用的对象会被GC处理),局部变量应该和方法同生共死。
    每个线程都有自己独立的调用栈。因为每个线程都有自己的调用栈,局部变量保存在线程各自的调用栈里面,不会共享,所以自然也就没有并发问题。
    调用栈结构
    线程封闭:仅在单线程内访问数据,由于不存在共享,所以即便不同步也不会有并发问题。JDBC连接池就用到了线程封闭技术。

  2. 递归有可能导致栈溢出
    栈溢出原因:因为每调用一个方法就会在栈上创建一个栈帧,方法调用结束后就会弹出该栈帧,而栈的大小不是无限的,所以递归调用次数过多的话就会导致栈溢出。而递归调用的特点是每递归一次,就要创建一个新的栈帧,而且还要保留之前的环境(栈帧),直到遇到结束条件。所以递归调用一定要明确好结束条件,不要出现死循环,而且要避免栈太深。
    解决方法:(1)简单粗暴,不要使用递归,使用循环替代。缺点:代码逻辑不够清晰;(2)限制递归次数;(3)使用尾递归,尾递归是指在方法返回时只调用自己本身,且不能包含表达式。编译器或解释器会把尾递归做优化,使递归方法不论调用多少次,都只占用一个栈帧,所以不会出现栈溢出。

12 | 如何用面向对象思想写好并发程序?

  1. 封装共享变量
    将共享变量作为对象属性封装在内部,对所有公共方法制定并发访问策略。
    对于这些不会发生变化的共享变量,建议你用 final 关键字来修饰。
  2. 识别共享变量间的约束条件
    在设计阶段,我们一定要识别出所有共享变量之间的约束条件,如果约束条件识别不足,很可能导致制定的并发访问策略南辕北辙。共享变量之间的约束条件,反映在代码里,基本上都会有 if 语句,所以,一定要特别注意竞态条件
  3. 制定并发访问策略
    (1)避免共享:避免共享的技术主要是利于线程本地存储以及为每个任务分配独立的线程。
    (2)不变模式:这个在 Java 领域应用的很少,但在其他领域却有着广泛的应用,例如 Actor 模式、CSP 模式以及函数式编程的基础都是不变模式。
    (3)管程及其他同步工具:Java 领域万能的解决方案是管程,但是对于很多特定场景,使用 Java 并发包提供的读写锁、并发容器等同步工具会更好。
    除了这些方案之外,还有一些宏观的原则需要你了解。这些宏观原则,有助于你写出“健壮”的并发程序。这些原则主要有以下三条。
    (1)优先使用成熟的工具类:Java SDK 并发包里提供了丰富的工具类,基本上能满足你日常的需要,建议熟悉它们,用好它们。
    (2)迫不得已时才使用低级的同步原语:低级的同步原语主要指的是 synchronized、Lock、Semaphore 等,这些虽然感觉简单,但实际上并没那么简单,一定要小心使用。
    (3)避免过早优化:安全第一,并发程序首先要保证安全,出现性能瓶颈后再优化。
  • 14
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值