在构建文件的并发程序时,必须正确地使用线程和锁。但这只是一些机制,其核心在于要对状态访问操作进行管理,特别是对共享地和可变的状态的访问。即使某个程序省略了必要同步机制并且看上去似乎能正确执行,但仍可能在某个时刻发生错误

  • 共享
    变量可以由多个线程同时访问
  • 可变
    变量地值在其生命周期内可以发生变化

如果当多个线程访问同一个可变地状态变量时没有使用合适的同步,那么程序就会出现错误,有3种方式修复该问题:

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

从一开始就设计一个线程安全的类比以后再将该类修改为线程安全的类要容易得多。
在任何情况中,只有当类中仅包含自己的状态时,线程安全类才是有意义的

什么是线程安全性

线程安全性定义:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类时线程安全的

无状态(不包含任何域,也不包含任何对其他类中的域的引用)对象一定是线程安全的。
image.png
大多数Servlet都是无状态的

原子性

image.png
上图的++count并且原子性的,不会作为一个不可分割的操作来执行,其包括读取count的值,将值+1,然后将计算结果写入count

在并发编程中,由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,叫做竞态条件(Race Condition)

竞态条件

当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。正确的结果要取决于运气
竞态条件想要获得正确的结果必须取决于事件的发生时序
基于一种可能失效的观察结果来做出判断或者执行某个计算。这种类型的竞态条件称为“先检查后执行”:首先观察到某个条件为真,然后根据这个观察的结果采用相应的动作,但事实上,在观察到这个结果以及开始执行相应动作之间,观察结果可能会变得无效,从而导致各种问题

复合操作

要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是在修改状态的过程中
如果递增操作是原子操作,那么竞态条件就不会发生。
可以通过加锁机制确保原子性,或者通过package java.util.concurrent.atomic;包中的原子变量类,用于实现在数值和对象引用的源自状态转换。确保所有的访问操作都是原子的
image.png

加锁机制

尽管某些原子引用本身是线程安全的,但可能存在着竟态条件,可能会产生错误的结果。
在不变性条件中涉及多个变量时,各个变量之间并不是彼此独立的,而是某个变量的值会对其他变量的值产生约束,更新某一个变量时,需要在同一个原子操作中对其他变量同时进行更新
image.png

内置锁

Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。
同步代码块包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块

synchronized (lock)

    {
        //访问或修改由锁保护的共享状态
    }

Java的内置锁相当于一种互斥体(或互斥锁),只有一个线程能持有这种锁。
并发环境中的原子性与事务中的原子性有着相同的含义——一组语句作为一个不可分割的单元被执行。
用同步代码可能会产生性能问题,但不会产生线程安全问题
image.png

重入

当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。
由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功

/**
 * @Author jtao
 * @Date 2021/2/22 15:58
 * @Description
 */

public class Widget {
    public synchronized void doSomething() {
        //TODO
    }
}
/**
 * @Author jtao
 * @Date 2021/2/22 16:00
 * @Description
 */
//如果内置锁不是可重入的,那么这段代码将发生死锁
public class LoggingWidget extends Widget {
    @Test
    @Override
    public synchronized void doSomething() {
        System.out.println(toString() + ":calling doSomething");
        super.doSomething();
    }
}

如果内置锁不是可重入的,那么在调用super.doSomething方法在执行前都会获取Widget上的锁。如果内置锁不是可重入的,那么在调用super.doSomething方法时将无法获得Widget上的锁,因为这个锁已经被持有,从而线程将永远停顿下去,等待一个永远也无法获得的锁。重入则避免了这种死锁情况的发生
代码运行结果如下
image.png

用锁来保护状态

锁能使其保护的代码路径以串行形式(多个线程依次以独占的方式访问对象,而不是并发地访问)来访问,因此可以通过锁来构造一些协议以实现对共享状态的独占访问,只要始终遵循这些协议,就能确保状态的一致性。
对于可能呗多个线程同时访问的可变状态变量,不仅仅是写需要同步,读也需要即访问它时都需要持有同一个锁,在这种情况下,状态变量是由这个锁保护的
并非所有数据都需要锁的保护,只有被多个线程同时访问的可变数据才需要通过锁来保护。
当某个变量由锁来保护时,意味着每次访问这个变量时都需要首先获得锁,这样就确保在同一时刻只有一个线程可以访问这个变量。
虽然synchronized方法可以确保单个操作的原子性,但如果要把多个操作合并为一个复合操作,还是需要额外的加锁机制

活跃性与性能

简单粗粒度的方法能确保线程安全性,但付出的代价很高。
可以通过缩小同步代码块的作用范围,左到并发性和安全性。要确保同步代码块不要过小,并且不要将本应是原子的操作拆分到多个同步代码块中。应该尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,从而在这些操作的执行过程中,其他县城可以访问共享状态
image.png
如上图所示将同步锁的代码块进行缩小,在性能和安全性之间找到平衡

当执行时间较长的计算或者可能无法快速完成的操作时(I/O等),一定不要持有锁


这个家伙很懒,啥也没有留下😋