并发编程笔记

同步问题

互斥与同步: 同类进程竞争同一资源彼此为互斥关系,不同类型进程为同步关系。(生产者与消费者之间是同步关系,消费者之间是互斥关系)

临界区 ( Critical Section ) 与临界资源: 同一时刻只允许一个进程使用的资源称为临界资源,临界区中代码为对外呈现原子性的操作临界资源的非原子操作。

竞争条件: 在并发编程中,不恰当的执行时序造成的不正确的结果。比如:

  • 先检查后执行 ( Check-Then-Act )

原子性: 要不执行成功要不不执行,无中间状态。(复合原子操作不具有原子性)

内存可见性

当一个线程修改了对象的状态之后,其他线程能够看到发生的状态变化。(多核)

在Java中

在没有同步的情况下,编译器、处理器以及运行时都有可能对操作的执行顺序进行一些意想不到的调整,在缺乏足够同步的多线程程序中,要想对内存操作执行顺序进行判断,几乎无法得出正确的结论。

volatile 变量的操作不会被其他内存操作一起重排序,也不会被缓存在寄存器等地方,因此读取该类型的数据总会返回最新写入的值,可以用来确保变量的更新操作会同步更新到其他线程上。此外,加锁的含义不仅仅局限于互斥行为,还包括内存可见性,为了确保所有线程都能看到共享变量的最新值,所有执行读、写操作的线程都必须在同一个锁上同步。

什么情况使用volatile?
对变量的写入操作不依赖变量当前的值,或仅由单个线程更新该变量的值(不产生竞争条件)

此外,对于非 volatile 类型的 long 和 double 变量, JVM 将64位读写操作分解为两个 32 位操作

线程安全

线程不安全:并行访问同一个可变的状态变量时( 共享 Shared + 可变 Mutable )。有三种方法修复这个问题:

  • 不要在线程之间共享该状态变量
  • 将状态变量修改为不可变的变量
  • 在访问状态变量时使用同步

线程安全

当多个线程访问某个对象时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在调用代码中不需要任何额外的同步或协调,这个对象都表现出正确的行为,那么就称这个对象是线程安全的。( 在线程安全类中封装了必要的同步机制,因此无需才去进一步的同步措施 )

  • 无状态对象一定是线程安全的

实现线程安全

  • 线程封闭 ( swing )
  • 不可变 ( 只读共享 )
    • 不可变对象一定是线程安全的
    • Java中,满足以下条件时对象是不可变的
      • 对象创建后其状态就不能修改
      • 对象的所有域都是 final 的
      • 对象是正确创建的 (在对象创建期间,this 引用没有逸出)
      • (此外,不可变对象内部可以使用可变对象来管理他们的状态 《Java并发编程实战》p39)
  • 使用同步
    • 使用线程安全对象 ( 线程安全共享 )
    • 保护对象 ( 对象只能通过持有特定的锁访问 )

死锁

产生条件

  • 互斥
  • 锁不可剥夺
  • 请求与保持
  • 环路等待

锁顺序死锁

  • 如果所有线程以固定的顺序来获得锁,那么程序中就不会出现锁顺序死锁问题。
  • 动态的锁 ~ 顺序死锁 (🔒*2)
    • 锁顺序 利用hashcode排序保证加锁顺序一致性
    • 加时赛锁 (Tie-Breaking) 可保证每次只有一个线程以位置顺序获得这两把锁

在协作对象中发生死锁

  • 如果在持有锁的时候调用某个外部方法,那么将会出现活跃性问题( 死锁 ),因为在这个外部方法中可能会获取其他的锁。
  • 开放调用(Open Call)。如果在调用某个方法时候不需要持有锁,那么这种调用被称为开放调用。(具体做法是在java中如果方法需要调用外部对象,那么就不要声明为synchronized,在代码中使用synchronized this)

资源死锁

  • 多个线程互相持有彼此正在等待的锁而有不释放自己持有的锁
  • 线程饥饿死锁

死锁的避免与诊断

  • 如果程序每次至多获取一个锁那么就不会产生顺序死锁,但通常不现实。两阶段策略:找出在什么地方获取多个锁,确保他们在整个程序中获取锁的顺序都保持一致。 破坏环路等待
  • 使用支持超时的 死锁→活锁 破坏请求与保持

同步工具

信号量

使用二进制信号量去处理并行程序的竞争条件,关键在于提供一些工具、策略去管控程序进入临界区、监测程序离开临界区。

P(S) 代表获取进入临界区的权限(S是信号量),当程序进入临界区后发生了任务切换,另一个任务也执行 P(S) ,那么第二个任务将会被阻塞并被操作系统置为等到状态。不久之后,又到了第一个任务执行的时候了,当执行到 V(S) 的时候,代表着该任务离开了临界区,这时第二个任务才可以拥有进入临界区的权限。

此外,信号量可以用比1大的初始值,这使得使用临界区可以同时管控多个资源。比如用来处理停车场问题,用信号量的初值代表一开始的空位,当有车停进来的时候数值递减,如果信号量的值为0又要新的车要停进来,那么获取该信号量的任务将会被阻塞指导有任务释放信号量。(一辆车离开了停车场)。

互斥锁

互斥锁更加的简单,只有两种状态,上锁状态和未上锁状态。(LockUnLock )语义。

然而就状态来看,互斥锁和二进制信号量拥有一样的状态空间,那么其区别在哪里呢?

观察一些现有系统的有关信号量和互斥锁的文档,不难发现,互斥锁强调锁的持有者。也就是其他任务不能解锁另一个任务持有的的互斥锁,解铃还须系铃人的感觉。

  • 比如在 pthread 中 “If the mutex type is PTHREAD_MUTEX_NORMAL […] If a thread attempts to unlock a mutex that it has not locked or a mutex which is unlocked, undefined behavior results.”,解锁其他线程持有的锁是未定义的操作。
  • Java中 ReentrantLockunlock 方法的文档中也提到

    如果调用unlock的线程并没有持有该锁,会触发异常。

JAVA内建同步

synchronized

1
2
3
synchronized (lock) {
...
}

每个 Java 对象有一个内置锁( 可重入 ),线程在进入同步代码块时会自动获得锁,并在退出时自动释放锁。( 也是获得内置锁唯一的方法 )

tips

  • 每个共享的可变变量都应该只由一个锁来保护
  • 除非需要更高的可见性,否则应将所有的域都声明为死有的
  • 除非需要某个域是可变的,否则应将其声明为final域

线程安全容器

JAVA内存模型

…todo…

部分内容参考:

  • 操作系统高分笔记
  • Java 并发编程实战

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×