您的位置:首页 > 编程语言 > Java开发

《java并发编程实战》随笔——第二章 线程安全性

2015-12-13 18:43 387 查看
这是《java并发编程实战》第一部分基础知识的第一章。

线程安全性

书中balabala了一堆,其实线程安全性最核心的就是正确性,正确性的含义就是,类的行为与其规范完全一致,通常我们说某个类是线程安全的其实就是说:

当多个线程访问这个类时,这个类始终都能表现出正确的行为。

举个例子,现在要模拟实现一个c++的vector类,它有一个方法push_back(const T &item), 表示将一个新成员添加到vector中,其内部实现可想而知是维护了一个数组和两个长度变量,结构大致是这样:

class vector {

int length_; // 数组总长度

int count_; //有限元素个数

T *array_;

}

每当有元素push进来时,vector都会更新count_, 将其自增, 你可能想到了这个count_++并不是原子操作,多线程环境下会出问题咯,如果编码时不考虑这一点,那最终实现的这个vector类就是一个非线程安全类。

ps: 虽然看的是java并发编程实战,但主要目的是学习其处理问题的核心思想,所以博客中我还是尽量用自己更熟悉的c/c++或伪代码来示例,见谅。

原子性

考虑一个web程序,它接受客户端请求,在服务端对客户输入的整数进行因式分解,最终结果返给客户端,这是一个很简单的程序,为了提高并发性能,可能需要用若干个线程处理不同的客户端请求。

线程的处理逻辑类可能是这样的:
class Computer {
void compute_facotor(int n, vector<int> &fs)
{
// do_compute
}
}

因为每个线程是状态无关的,它的逻辑就是简单接受客户端输入的数字并将计算结果放入数组fs,过程不涉及任何共享变量,即使并发也不会问题。

假设现在新增了需求,服务器需要统计客户端连接的数量,此时需要添加一个全局变量n_clients,compute_facotor的逻辑要修改为:

class Computer {
void compute_facotor(int n, vector<int> &fs)
{
n_cliens++;
  // do_compute
...
  }
}
基于前面的铺垫,你可能又发现了,自增操作不是原子的,在多线程的环境下统计出来的客户端数量可能不准,除非我们能通过某种机制保证n_clients++是原子的。

竞态条件

当多个线程访问同一资源时,如果对访问顺序敏感,就称存在竞态条件,考虑经常出现在面试中的单例类:

class SingleInstance {
SingleInstance *single;
SingleInstance():single(NULL) {}
public:
SingleInstance *get_instance() {
if (!single) {
singe = new SingleInstance();
}
return single;
} // end get_instance
} // end classget_instance包含一个竞态条件,当A线程看到single为空,需要创建一个新的对象,B线程同样需要判断single是否为空,此时的single是否为空取决于不可预测的时序,包括线程的调度方式以及A线程创建新对象需要的时间:
1.此时single为空,B也去创建新对象,非期望

2.此时single不空,期望

书中有一段话是:

”为了避免竞态条件,就要在修改该变量时禁止其他线程使用该变量,从而确保其它线程只能在修改操作完成之前或者之后使用该变量,而不是修改的过程中“

ps:1.这里用的是”使用“,包括读取和写入

        2.上面这段书中原话个人觉得对"禁止"时机的叙述有点问题,应该是使用该变量时禁止其它线程使用该变量,而不是修改该变量时禁止,试想下两个线程同时进入if(!single)的情况,还是会出问题。

具体到客户端的例子,我们有两个办法保证n_clients++的原子性:

1. 使用内置的线程安全的数据结构类(书中使用java的AutomicLong)

2. 在自增前加锁,确保拿到锁之后不会有其它线程修改该n_clients

剩余

第二章剩余内容都是一些java语言提供的特性,比如synchronized关键字实现代码块or函数的原子操作

ps: 通常不会对整个大函数加锁,否则可能会有性能问题,使用不当甚至可能导致比单线程更差的性能,比如统计客户端连接数的例子,实际只需要对关键的n_clients++加锁,耗时的因式分解不访问共享变量,因此不需要加锁。

总结

讲了一堆废话,其实总结下很简单:

1. 并发的坑:由于不同线程对共享变量不确定的访问时序造成了非期望的结果。

2. 怎么填坑:访问共享变量时加锁啊。

其中2又是一个很大的专题,属于说起来简单做起来难的那类,里面涉及大学问,下一章见。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: