--- 本文是《java并发编程实战》的读书笔记。
读完这本书后,反而会总结出4个问题,而这些问题也可以作为笔记的开端:
问题1:什么样的类是线程安全的
问题2:什么样的类是线程不安全的
问题3:如何设计线程安全的类
问题4:如何使用线程不安全的类
下面将一一解释
一、在访问共享的可变状态时需要正确的管理
1、什么样的对象是线程安全的
(1)无状态对象
public class StatelessServlet implements Servlet {
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse){}
}
比如StatelessServlet是一种没有状态的对象,因为没有域,直白点就是没有类属性共享
2、什么样的对象是线程不安全的
(1)存在竞态条件
public class StateServlet implements Servlet {
private int count;
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
++count;
}
}
也就是StatelessServlet所说的,因为多了count这个竞态条件(域),count可被所有线程修改
二、如何共享和发布对象,从而使他们能够安全的由多个线程同时访问
发布:使对象能够在当前作用域之外的代码中使用
逸出:当某个不应该发布的对象被发布时
1、场景一:
private static Set<Secret> knownSecrets;
public void initialize(){
knownSecrets = new HashMap<>();
}
任何代码都可以遍历这个集合,并获得这个新Secret对象的引用
2、场景二:
public class UnsafeState{
private String[] states = new String[]{"AK", "AL"...};
public String[] getStates(){return states}
}
任何调用者都可以修改这个数组的内容
3、场景三:
public class ThisEscape {
// 隐式地使this引用逸出(不要这么做)
public ThisEscape(EventSource source) {
source.registerListener(new EventListener(){
public void onEvent(Event e) {
doSomething(e);
}
} );
}
private void doSomething(Event e) { }
interface EventSource {
void registerListener(EventListener e);
}
interface EventListener {
void onEvent(Event e);
}
interface Event {
}
}
当ThisEscape 发布Event的时候,也隐含的发布了ThisEscape 实例本身。当从对象的构造函数中发布对象是,实质只是发布了尚未完成构造的对象。
还有改进如下:
三、实现线程安全性方式
1、线程封闭技术
如果仅在单线程内访问数据,就不需要同步,这种叫线程封闭
2、栈封闭
线程封闭的一种特例。只能通过局部变量才能访问对象
如:业务逻辑代码放在方法内,
3、ThreadLocal类
最佳实践:
ThreadLocal 无法解决共享对象的更新问题,ThreadLocal 对象建议使用 static修饰。这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量 ,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量。【阿里手册】
4、不可变对象一定是线程安全的
(1)对象在创建后,其状态就不能被修改。
(2)对象的所有域都是final类型
(3)对象是正确创建的(在对象的创建期间,this引用没有逸出)
如ThreeStooges就是线程安全的
public final class ThreeStooges{
private final Set<String> stooges = new HashSet<>();
public ThreeStooges(){
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
}
public boolean isStooge(String){return stooges.contains(name);}
}
四、设计线程安全的类
3个基本要素:
(1)找到构成对象状态的所有变量
(2)找出约束状态变量的不可变条件
(3)建立对象的并发访问管理策略
这3个要素是设计线程安全类的关键思想
public final class Counter{
private long value = 0;
public synchoronized long getValue(){return value}
public synchoronized long increment(){
if (value == Long.MAX_VALUE) {
throw new IllegalStateException("counter overflow");
}
return ++value;
}
}
1、实例限制
将数据封装在对象内部,把对数据的访问限制在对象的方法上。
使线程不安全的类,可以安全地用于多线程环境。
public class PersonSet{
private final Set<Person> mySet = new HashSet<>();
public synchronized void addPerson(Person p) {mySet.add(p);}
public synchronized boolean containsPerson(Person p){return mySet.contains(p);}
}
如Collections.synchronizedList() 所有数据操作都限制在方法上,并且方法里加锁
2、监视器模式
Java内置锁(如synchronized )称为监视器
public class PrivateLock{
private final Object myLock = new Object();
Widget widget;
void someMehod(){
synchronized(myLock) {// 访问或修改Widget的状态}
}
}
通过私有锁保护状态,对应还有公有锁
3、委托线程安全
(1)基于监视器模式
(2)基于线程安全委托
这种方式将线程安全委托给了ConcurrentMap类型的locations, 由于Point类时不可变的,所以如果想要实时更新点的位置, 可以调用getLocations()函数, 这样更新会立刻显示; 如果不需要发生变化的车辆位置, 可以调用getLocationsAsStatic()函数, 在这里采用了__浅拷贝__的方式(因为Point不可变, 复制Map的结构即可, 不需要复制它的内容)
五、死锁问题
因为线程安全涉及到用锁,只要使用锁就会涉及到死锁
1、锁顺序死锁
public class LeftRightDeadlock{
private final Object left = new Object();
private final Object right = new Object();
public void leftRight(){
synchronized(left) {
synchronized(right) {
doSomething();
}
}
}
public void rightLeft(){
synchronized(right) {
synchronized(left) {
doSomething();
}
}
}
}
因为加锁和释放锁的顺序并不一致,导致死锁
2、资源死锁(资源死锁本质也是一种锁顺序死锁)
(1)当线程持有和等待的目标变为资源时
public class ThreadDeadlock{
ExecutorService exec = Executors.newSingleThreadExecutor();
public class RenderPageTask implements Callable<String>{
public String call() throws Execption{
Future<String> header, footer;
header = exec.submit(new LoadFileTask("header.html"));
footer = exec.submit(new LoadFileTask("footer.html"));
String page = renderBoby();
// 出现死锁 - 任务在等待子任务的结果
return header.get() + page + footer.get();
}
}
}
(2)如果一个任务需要连接到两个数据库,并且两个资源并不是按相同顺序进行调用的,线程A可能持有数据库D1的连接,并等待连接到数据库D2,而线程B持有D2的连接并等待D1的连接。(线程池越大,这种情况发生的可能性就越小,如果每个线程池都有N个连接,死锁的发生需要N套循环等待的线程,并且偶发时序多次发生)
3、避免死锁
确保多个线程获得多个锁时,使用一致的顺序
总结:
最重要的是上面的设计线程安全3个基本要素,用通俗的理解,就是找到引发线程不安全的变量或代码,然后使之线程安全访问(这里可以理解为加锁,或者线程独享,比如放在方法体内)。
推荐书籍
Java并发编程实战 https://book.douban.com/subject/10484692/
Java并发编程的艺术 https://book.douban.com/subject/26591326/