《java并发编程实战》随笔——第二章 线程安全性
2015-12-13 18:43
387 查看
这是《java并发编程实战》第一部分基础知识的第一章。
当多个线程访问这个类时,这个类始终都能表现出正确的行为。
举个例子,现在要模拟实现一个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++或伪代码来示例,见谅。
线程的处理逻辑类可能是这样的:
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)的情况,还是会出问题。
1. 使用内置的线程安全的数据结构类(书中使用java的AutomicLong)
2. 在自增前加锁,确保拿到锁之后不会有其它线程修改该n_clients
ps: 通常不会对整个大函数加锁,否则可能会有性能问题,使用不当甚至可能导致比单线程更差的性能,比如统计客户端连接数的例子,实际只需要对关键的n_clients++加锁,耗时的因式分解不访问共享变量,因此不需要加锁。
1. 并发的坑:由于不同线程对共享变量不确定的访问时序造成了非期望的结果。
2. 怎么填坑:访问共享变量时加锁啊。
其中2又是一个很大的专题,属于说起来简单做起来难的那类,里面涉及大学问,下一章见。
线程安全性
书中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又是一个很大的专题,属于说起来简单做起来难的那类,里面涉及大学问,下一章见。
相关文章推荐
- net.sf.json在处理json对象转换为普通java实体对象时的问题和解决方案
- Spring MVC Controller单例陷阱
- JAVA泛型学习笔记
- 《spring in action》笔记(一)
- ubuntu,jdk安装成功后,点击eclipse,提示信息A Java RunTime Environment (JRE) or Java Development Kit (JDK)
- Java IO
- Java----之“关键字”
- 圣思园java se培训总结(50)(类集的总结)
- spring MultiActionController 简单配置
- Ubuntu make 及多版本JDK安装与切换
- 浅析Java中的访问权限控制
- Java Nio 三、Java NIO Channel
- 浅谈Java中的对象和引用
- java webservice 用户验证 (服务端 + 客户端)
- java web开发 高并发处理
- java学习感悟
- spring beans源码解读
- JAVA环境变量配置的作用
- commons之集合与队列JAVA140
- Spring-Aop详细教程