xiaobaoqiu Blog

Think More, Code Less

Java Concurrency in Practice 2 : 线程安全

第二章:线程安全

1.什么是线程安全

当多个线程并发的访问一个类,如果不用考虑多个线程运行时的调度执行顺序,且不需要做额外的同步及代码调用时候的限制,这个类的结果依然是正确的,则可以称之为线程安全.

无状态的类是线程安全的,无状态指不包含域也没有引用其他类的域.如下面这个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class StateLess extends HttpServlet {

    /**
     * 从请求中获取两个数字,并计算两个数字的最大公约数,返回
     *
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Pair<Integer, Integer> number = extract(req);

        int result = computer(number.getLeft(), number.getRight());

        setToResponse(resp, result);
    }
}

2.原子性

常见的竞争条件1:检查再运行(check_then_act).检查再运行指使用潜在的过期值来做决策或者执行计算.如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Account {

    /**
     * 余额
     */
    private float balance;

    /**
     * 取款
     * @param count
     */
    public void withdrawing(float count) {
        if(balance > count){         //check
            doWithdrawing(count);     //act
        }
    }

    private void doWithdrawing(float count){
        balance -= count;
    }
}

常见的竞争条件2:读-改-写.如count++ 这种非原子的操作,其实包含三个原子操作

1
2
3
Read
Add
Write

什么是原子操作:

假设有操作A,在其他线程来看,执行操作A的线程执行时,要么操作A执行完成,要么一点都没有执行.则称之为原子操作.

如果前面的例子是原子的,则不会出现检查再运行和读-改-写的竞争条件.

为例保证线程安全,操作必须原子的执行.

比如count++,我们可以使用AtomicInteger:

1
count.getAndIncrement();

3.锁

java提供了强制原子性的内置锁机制:synchronized块.一个synchronized块包含两部分:1.锁对象的引用;2.锁对象保护的代码块;

1
2
3
synchronized(lock){
  // code
}

每个java对象都代表一个互斥锁(称之为内部锁或者监视器锁),因此每个java对象都可以扮演lock的角色.对于方法级别的synchronized,获取的是方法所在对象的锁.对于静态方法,从Class对象上获取锁.

当一个线程请求另外一个线程持有的锁时候,请求线程将会被阻塞.但是,内部锁是可以重入(Reentrancy)的,即线程请求它自己占有的锁的时候,是会成功的.这表示锁的请求是基于每个线程(per-thread)的,而不是基于每个调用(per-invocation)的.

4.用锁来保护状态

对于可以被多个线程访问的可变状态变量,如果所有访问它的线程在执行时都占有同一个锁,则称这个变量是由这个锁保护的.

并不是所有数据都需要锁的保护,只有哪些被多个线程访问的可变数据.

如果类的不变约束涉及到多个变量,则需要用同一个锁来保护这多个变量.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Account {

    /**
     * 余额
     */
    private float balance;

    /**
     * 操作次数
     */
    private int count = 0;

    /**
     * 取款
     * @param value
     */
    public void withdrawing(float value) {
        synchronized (this){    //同一个锁保护多个可变变量
            if(balance > value){
                doWithdrawing(value);
            }

            count++;
        }
    }
}

5.活跃度和性能

为了达到安全的目的,我们完全可以将方法设置为synchronized,但是这样带来的问题是:我们期房方法能提供并发访问,但是为了安全,实际上变成的串行访问.

因此在使用synchronized的时候需要考虑安全(不能妥协),简单性和性能.

比如网络或者IO这种耗时的操作器件,不应该占用锁.