程序员

Java Concurrency In Practice 第二章

2017-10-08  本文已影响0人  namelessEcho

线程是CPU调度的最小单位,与进程不同,它们拥有相同的地址和fd描述符,操作系统的基本调度单元是线程。进程为线程提供了独立的地址(通过vm)和独立的资源,文件句柄,是实体单元。

总的来说,多线程的安全性问题主要是对于多线程之间的可变共享变量如何进行操作的问题。共享意味着可以由多个线程共同访问,可变意味着变量可以在这个过程中发生改变。所以可能会产生的竞争条件。必须使用同步来保证各个线程之间对于这些共享变量的协调问题。

不包含类的实例域或者静态域的方法是无状态的(stateless),由于方法内的局部变量是栈上的,栈是线程私有的,其他线程无法主动访问线程的栈。如果不主动发布一个局部变量到外部的话,那么该方法总是线程安全的,因为这些不发布的局部变量只在栈内部。如果方法想要发布局部变量到其他线程的话1.如果是一个基本类型的引用,由于方法之间的参数传递是值传递,外部方法只能得到这个基本类型的一个拷贝,Java的语言特性保证了该应用是线程安全的。因为其他对象无法实际访问到它(只是得到值的拷贝)。如果是指向对象的引用,那么通过参数传递外部线程可以得到在栈上的某个实际对象的应用,所以必须要进行必要的线程安全措施。

例如,来看一下书上的这个例子就可以明白什么是无状态了。因为这个线程没有引用外部线程的方法,亦没有将局部引用变量发布到线程之外,其service方法只有两个线程私有的局部变量BigInteger i 和 BigInteger[] factors ,所以这个线程一定是线程安全的。


@ThreadSafe

public class StatelessFactorizer implements Servlet {

public void service(ServletRequest req, ServletResponse resp) {

BigInteger i = extractFromRequest(req);

BigInteger[] factors = factor(i);

encodeIntoResponse(resp, factors);

}

}

这里,我们可以考虑一下StatelessFactorizer类在如果加入一个实例域count,会发生什么。考虑这样一件事,对于一个实例域来说,它并不是线程私有的,作为类的实例域,他被分配到线程们共享的堆上。对于线程来说,如果是一个PUBLIC的域,那么所有线程都拥有访问他的权限,而对于一个private变量来说,访问限制是在类层次上,不是对象层次上的。只要是调用了privat域所属类的方法的线程都可以访问它。

@NotThreadSafe 
public class UnsafeCountingFactorizer implements Servlet { 
 private long count = 0; 
 public long getCount() { return count; } 
 public void service(ServletRequest req, ServletResponse resp) { 
 BigInteger i = extractFromRequest(req); 
 BigInteger[] factors = factor(i); 
 ++count; 
 encodeIntoResponse(resp, factors); 
 } 
} 

现在问题出现了,主要在于count++这里,对于多个线程来说很可能出现竞争问题。最主要的是count++虽然看上去是一个操作,但他实际上由读取-自加-写回三步组成。所以很可能出现两个线程同时读取,同时自加,最后同时写回,这样count只自加了一次。对于这种问题通常称为竞争状态(Race-Condition)。必须提出一种手段,当线程在修改时不允许其他线程对这个共享域的访问,并且要保证一旦值发生了改变,所有线程都必须能看到这种改变,这就是原子性和可见性这同步的两大要素。

Java通过同步快的形式来实现内部锁,内部锁的对象是this自身。

1.内部锁。(Intrinsic Locks)


synchronized (lock) {

// Access or modify shared state guarded by lock

}

通过传入lock对象进入synchronized块来实现同步。当执行线程进入到同步块时自动获得内部锁对象。离开时自动释放。

2.重入(Reentrancy)

重入意味着java中的内部锁持有单位是进程本身(one lock per thread)而不是POSIX标准中的每一次方法调用(one lock per tinvocation).这意味着JVM将锁与线程以及计数相关联。当计数是零,锁被认为不被任何线程持有。当一个线程请求一个未被持有的锁时,此时如果计数是零,则JVM会记录此时的线程,并将计数写成1。如果不是零,那么JVM会检查这个进程是否是锁的持有者,如果是,JVM会允许对于锁的再次获取,并将计数自加,换句说JAVA允许持有锁的对象对于锁的重复进入。如果否,那么会吊起进程直到持有锁的进程释放锁再进行锁的分配。

下面来看一个例子,这个例子在基于调用的锁下回造成死锁。例如:在Widget的doSomething方法中调用子类的doSomething,此时父类的方法持有一个独立的锁,子类的方法也持有一个独立的锁,在子类的方法中我们调用了父类的方法,对于基于调用的锁来说,由于父类的锁的存在,在想要调用父类的方法时,子类会被挂起,由于父类在等待子类方法的结束,而子类由于父类的锁的存在被挂起,这样造成了这个进程无法再继续执行,陷入了死锁。


public class Widget {

public synchronized void doSomething() {

}

}

public class LoggingWidget extends Widget {

public synchronized void doSomething() {

System.out.println(toString() + ": calling doSomething");

super.doSomething();

}

}

3.性能和同步的折中。
通常情况下不需要对所有块都进行同步保护,这样会大大的降低性能,也违背了使用多线程的初衷。一般只在出现了共享变量,并对其进行了操作的块才需要添加同步,这样我们在性能和正确性上进行了折中。要注意的一点是不止是setter方法,getter方法也需要同步,才能保证读到的不是一个无效的值,这涉及到线程的可见性问题。一般编译器会对没有同步保护和没有volatile说明的变量进行优化到cache或者register,对于多核来说,不同线程属于不同的核,他们的cache和register内的值对于各个其他核是不可见的。如果一个setter方法改变了值,而其getter方法没有同步,那么其他线程可能读到的是一个setter方法之前的过期值。使用同步可以保证我读到的是最新的。当然这里也可以使用Volatile进行可见性声明。

上一篇下一篇

猜你喜欢

热点阅读