java并发编程 • 核心 • 状态管理

992 阅读4分钟

并发编程的核心是管理状态,而不是使用线程封闭技术和锁,它们只是管理状态的方式。在Java中,状态主要体现在共享可变对象,共享意味着该对象会被多线程访问,可变意味着该对象不是一个常量,具有多种状态

线程安全

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的,线程安全类始终能表现出正确的行为。

无状态对象

共享可变对象的状态主要体现在它的变量上,如果它的变量是一个引用类型,那么这个状态会延续到这个引用类型所拥有的变量。一个没有变量的类,它的对象一定是线程安全的,因为它根本没有状态可言;一个提供了变量但是没有提供改变方法的类,它的对象也是线程安全的,因为它不能被改变;因此无状态的对象一定是线程安全的。

原子性操作

一旦在程序中引入了状态,那么在多线程环境下没有做同步处理就会导致不正确的结果,例如上一篇文章所讲到的不正确的执行时序问题,也就是业界常说了竞态条件,最常见的竞态条件就是先检查、后执行,如下:当线程A判断boo为false的时候,线程B将boo改为了true,此时,线程A没有结束运行,而是继续运行,结果与期望相悖。

// 线程不安全
public class TestSync {
    private Boolean boo = false;

    public void changeFlag() {
        boo = true;
    }

    public void checkFlag() {
        if (!boo) {
            System.out.print("继续运行");
        } else {
            System.out.print("结束运行, current time = " + new Date());
        }
    }
} 

在这个过程包含两步操作,判断boo状态以及根据boo状态执行相应的指令,称之为复合操作,在没有实现同步机制的情况下,由于状态在执行中会发生改变,多线程环境下总会出现线程安全问题。所以需要引入原子操作, 原子操作没有中间状态的概念,这个动作要么执行完成,要么没有执行(执行失败回滚)。

加锁

为了使复合操作变成原子性操作,避免线程安全问题,引入了加锁机制,最常见的也就是关键字Synchronized,Java定义每一个对象都可以用做一个实现同步的锁,被称为内置锁,这也是为在使用锁的时候不需要开发者去显示地创建。Synchronized Block 由两部分组成,一个是作为锁的对象引用,另一个是作为锁保护的代码块;Java内置锁是一种互斥锁,线程在进入同步代码块之前自动获得锁,并在退出同步代码块时自动释放锁,被锁保护的代码块将由原子方式执行。

将上面的代码做如下修改,在TestSync对象被多线程共享的情况下(系统中只有一个TestSync对象),不仅将检查、执行的代码片段使用synchronized锁了起来,而且更改boo类型的change()方法也被锁起来了,这样在A线程执行checkFlag()方法的时候,B线程无法访问changeFlag()方法,便可以在操作中控制boo的状态,避免竞态提哦案件。

// 程序中只有一个对象,线程安全
public class TestSync {
    private Boolean boo = false;

    public synchronized void changeFlag() {
        boo = true;
    }

    public synchronized void checkFlag() {
        if (!boo) {
            System.out.print("继续运行");
        } else {
            System.out.print("结束运行, current time = " + new Date());
        }
    }
} 

锁分两种,一种是类锁,顾名思义是在类上加锁,另一种是对象锁。在多线程环境下,各自的线程分别实例化这个对象(系统中有多个TestSync对象,各个对象锁互不干扰),那么对象锁将无法保证状态的一致性,checkFlag()和changeFlag()可以被同时访问。如果,checkFlag()和changeFlag()被类锁加锁,那么,在一定时间内只有一个线程可以获得类锁并执行其中一段代码,代码如下:

// 程序中有多个对象,线程安全
public class TestSync {
    private Boolean boo = false;

    public void changeFlag() {
        synchronized (TestSync.class) {
            boo = true;
        }
    }

    public void checkFlag() {
        synchronized (TestSync.class) {
            if (!boo) {
                System.out.print("继续运行");
            } else {
                System.out.print("结束运行, current time = " + new Date());
            }
        }
    }
}

相当于在changeFlag()和checkFlag()上加了static,并在一定时间内由类去互斥访问,类锁一般用于静态方法。

活跃性与性能

  1. 并发编程代码太差,代码并发的数量,不仅会受到处理 资源的限制,还会受到应用程序本身的限制;
  2. 尽可能将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,一定不要对执行时间长的代码加锁;
  3. 同步代码块的大小需要合理,不能太细(在获取和释放锁等操作上需要一定的开销,影响性能),不能太长,影响代码简单性(代码混在一块,难以理解),权衡利弊;