发布与逸出
发布一个对象的意思是使对象能够在当前作用域外的代码中使用。
当某个不应该被发布的对象被发布了则成为逸出(比如再对象构造之前就发布该对象)。
注意发布一个对象,它的非私有的属性也同样会被发布。
比如一种通过内部类发布对象,这时就会将本类的this引用发布出去(内部类能够调用外部类的属性),所以不要在构造函数中发布某内部对象,这时可能外部对象尚未构造完全。
比如下面的代码就会与预期的不一样。
class Test {//绑定监听器并做一些其他操作
private int vv;
Test(Out j) throws InterruptedException {
System.out.println(Thread.currentThread().getName()+"进入Test构造方法");
j.register((x)->{
System.out.println(x);
doSomething();
});
Thread.sleep(100);//模拟其他初始化操作
vv= 88;
System.out.println(Thread.currentThread().getName()+"即将退出Test构造方法");
}
private void doSomething(){
System.out.println(vv+"干点什么");
}
}
class Out {//类似监听器类
public static List<F> fs = Collections.synchronizedList(new ArrayList());
public void register(F i){
fs.add(i);
System.out.println(Thread.currentThread().getName()+"注册成功");
}
}
interface F{//实现某些行为的接口
void move(String s);
}
测试代码:
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(()->{
try {
new Test(new Out());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
service.execute(()->{
while (Out.fs.size()!=1) {
}
Out.fs.get(0).move("aaaaaa");
});
service.shutdown();
}
就会发现输出结果是这个亚子的:
发现vv未被正确初始化。如果之后再开个线程这时就被正确初始化了,也就是说在结果出来之前,处于正确与不正确的叠加态 (薛定谔的初始化)。
那么该怎么正确处理呢?很简单,将发布对象调整到某个方法当中,这时,对象肯定已经初始化完全了。
public Test newInstance(Out t){
t.register((x)->{
System.out.println(x);
doSomething();
});
return this;
}
线程封闭
如果不使用共享数据,自然就不会出现线程不安全的情况,这个方法就是这样的原理,将对象封闭到一个线程中。这就需要对象不要从该线程逸出。主要用于读数据再进行一些中间操作。(如Swing、JDBC)
Ad-hoc线程封闭
维护线程封闭性的职责完全由程序实现来承担,特别的脆弱,一般不用。比如使用volatile变量,只要保证了只有单个线程对共享的volatile执行写操作就可避免竞态条件的产生。
栈封闭
栈封闭是线程封闭的一种特例,只能通过局部变量才能访问对象,而局部变量本身就是属于当时执行该方法的线程的。比Ad-hoc线程封闭更易于维护,更健壮。我决定这样的好处就在于可以个线程独立的执行一些内部处理操作。
class StackFB{
public int addTheList(Collection<String> loginIPs){
Set<String> ips;
int numIP = 0;
ips = new HashSet<>();
ips.addAll(loginIPs);
for(String s:ips){
IP.add(s);//add方法还是得加锁,因为用了共享数据且有竞争
numIP++;
}
return numIP;
}
}
TreadLocal
该类能使得线程中的某个值和保存值的对象关联起来。它提供了get、set等访问接口或方法,这些方法为每个使用该变量的线程提供一份独立的副本。通常用于防止对可变的单实例变量或全局变量的共享。
当某线程初次调用ThreadLocal.get方法时,就会调用initialValue来获取初始值,ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置,当线程终止后,这些值会被作为垃圾回收。
将一个单线程应用移植到多线程环境时,通过将共享的全局变量转换为ThreadLocal对象,可以维持线程安全性。
大佬的源码级讲解(俺受益匪浅)
不变性
满足同步需求的另一种方法是使用不变性对象。如果某个对象在被创建后其状态不能被修改便被称为不变性对象。不变性对象一定是线程安全的!
不可变对象需要满足:
- 对象创建后状态就不被修改
- 对象的所有域都是final类型
- 对象是正确创建的(创建期间this引用没有逸出)
Final域
除非更高的的可见性需求,那么所有域都应该声明为私有,除非需要某个域可变,否则全申明为final域。
从代码可以看出,这个类将其内部变量全部final化了,而且也没有发布任何内在对象。有点妙啊
public class OneValueCache{
private final BigInteger num;
private final BigInteger[] fac;
public OneValueCache(BigInteger i,BigInteger[] bs){
num = i;
fac = bs==null?null: Arrays.copyOf(bs,bs.length);
}
public BigInteger[] getFac(BigInteger i){
if(num==null||!num.equals(i)){
return null;
}
return Arrays.copyOf(fac,fac.length);
}
}
不过不知道为啥俺用来测试的Servlet加载得很慢,有大佬帮忙看看吗?
public class StatelessFactorizer extends HttpServlet {
private int num;
private static final long serialVersionUID = 554L;
private volatile OneValueCache cache ;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
resp.setContentType("text/html");
PrintWriter writer = resp.getWriter();
writer.println("<html><head>Test</head><body><form method = 'post'><table><tr><input name='number'/></tr><tr>" +
"<input type='submit'/></tr></table>" +
"</form><br/>"+(num++)+"</body></html>");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
BigInteger b = extractFromRequest(req);
BigInteger[] factor = cache==null?null:cache.getFac(b);
if(factor==null){
factor = factor(b);
cache = new OneValueCache(b,factor);
}
encodeIntoResponse(resp,factor);
}
private void encodeIntoResponse(ServletResponse servletResponse, BigInteger[] factors) throws IOException {
PrintWriter writer = servletResponse.getWriter();
writer.print("<html><head>jieguo</head><body><table>");
int count = 1;
for(BigInteger i:factors){
writer.println("<tr><td>第"+(count++)+"对<td><td>"+i+"<td></tr>");
}
writer.print("</table><a href='/test2/stateless'>返回</a></body></html>");
}
private BigInteger[] factor(BigInteger i) {
Set<BigInteger> bigSet = new HashSet<>();
for(int j=1;!bigSet.contains(BigInteger.valueOf(j));j++){
if(i.mod(BigInteger.valueOf(j)).equals(BigInteger.ZERO)){
bigSet.add(BigInteger.valueOf(j));
bigSet.add(i.divide(BigInteger.valueOf(j)));
}
}
return bigSet.toArray(new BigInteger[0]);
}
private BigInteger extractFromRequest(ServletRequest servletRequest) {
return new BigInteger(servletRequest.getParameter("number"));
}
}
正确发布一个对象可以采用以下方式:
- 在静态初始化函数中初始化一个对象引用
- 将对象的引用保存到volatile类型的域或者AtomicReferance对象中
- 将对象的引用保存到某个正确构造对象的final类型域中
- 将对象的引用保存到一个由锁保护的区域中
按对象的可变性可以采用以下发布方式:
- 不可变对象可以通过任意机制发布
- 事实不可变对象(就是技术上可变但是创建后并不会改变的对象)必须通过安全方式来发布
- 可变对象必须通过安全方式来发布,并且是线程安全的或者由某个锁锁起来。
并发程序中使用和共享变量可采取以下实用策略: - 线程封闭:只由一个线程拥有。
- 只读共享:允许多个线程并发访问,但不可修改它。包括不可变对象和事实不可变对象
- 线程安全共享:线程安全的对象再其内部实现同步。
- 保护对象:只有持有特定的锁才能访问。