并发编程之思想篇

--- 本文是《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/

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值