我们都知道,进程是个自封闭的运行环境,它有自己完整的一套运行时资源,特别是有自己的内存地址空间。进程中的线程共享进程的资源,如内存地址空间,文件句柄等。但线程又有自己的计数器、栈、本地变量,现代操作系统大多以线程,而非进程,作为基本调度单元。在大多平台中,JVM以单进程方式执行,Java线程共享JVM进程的内存和文件句柄。在CPU环境下,多线程可以提高CPU的利用率。而在单CPU下,可以在发生I/O的时候继续计算。
但是多线程下,可能发生安全性问题。那怎么解决安全性问题?ps,还有活跃性问题,这个问题还不太明白。
一,互斥性
先来看一下例子
public class UnSafeState {
private int val;
public int getNext(){
return val++;
}
}
val++是个复合操作,在两线程访问时,会发生什么问题?
A线程: val=5 ------> 5+1=6 --------> val=6
B线程: val=5 -------> 5+1=6 ------> val=6
两条线程获取到的val都是6,结果是不正确的。
线程安全性问题的根源在于,多线程共享JVM进程的内存地址空间。当多个线程访问某个类的对象时,对象始终都能表现出正确的行为,称这个类是线程安全的。对象的状态指存储在变量中的数据。对象安全是指对象在多线程下状态是正确的。对象分为有状态和无状态的。
1. 无状态对象一定是线程安全的
public class StatelessValue {
public int ran10(){
return new Random().nextInt(10);
}
}
该类没有实例变量,也就是对象是无状态的,多线程时对象没有状态可以改变,线程安全。
2. 对象的状态不可变
public class UnchangableValue{
private final double PI = 3.14;
public double roundArea(double r){
return PI*r*r;
}
}
对象的唯一状态PI是个不可变常量,多线程下也无法改变这个状态,线程安全。
3. 单状态可变
状态可变主要问题在于竞态条件,即读取-修改-写入这三步,他们都不是原子性的。
class UnSafeCount {
private long count = 0;
public long getCount(){
return count;
}
public void service(){
System.out.println("I do service for you");
count++;
}
}
这个例子里两个线程操作,可能由于线程执行顺序的不同,结果错误。我们可以使用原子类和内置锁来保证线程安全
内置锁保证线程安全
public class SafeCount {
private long count = 0;
public synchronized long getCount(){
return count;
}
public void service(){
System.out.println("I do service for you");
synchronized(this){
count++;
}
}
}
原子类保证线程安全
public class SafeCount {
private AtomicLong count = new AtomicLong(0);
public long getCount(){
return count.get();
}
public void service(){
System.out.println("I do service for you");
count.incrementAndGet();
}
}
另外一个例子就是单例模式
public class UnsafeSingleton{
private static UnsafeSingleton instance = null;
private UnsafeSingleton(){
}
public static UnsafeSingleton getInstance(){
if(null == instance){
instance = new UnsafeSingleton();
}
return instance;
}
}
线程A判断instance是null,进入if条件语句,此时时间片切换到线程B,B同样判断instance为null,也进入条件语句,这种情况下,线程A和线程B分别创建了一个对象,不再满足单例模式。要做到线程安全,必须对instance加锁。
public class SafeSingleton{
private static SafeSingleton instance = null;
private SafeSingleton(){
}
public static SafeSingleton getInstance(){
synchronized(instance){
if(null == instance){
instance = new SafeSingleton();
}
}
return instance;
}
}
4. 多状态可变
多状态可变分为两类,一类是状态是相互独立的,只要保证对每个状态的操作是原子性的,就能保证是线程安全
public class User{
private int age;
private double weight;
public synchronized int increaseAge(){
return age++;
}
public synchronized double increaseWeight(){
return weight++;
}
}
但是对于多个相互依赖的状态,就需要对多个状态同时加锁
public class CachingFactorizer{
private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();
public void service(BigInteger bInt){
if(bInt.equals(lastNumber.get())){
System.out.println("Results exist.");
doWithFactors(lastFactors);
}else{
lastFactors = extractNumbers(bInt);
lastNumber.set(bInt);
}
}
}
这个类的对象有两个状态,两个状态相互依赖,分别是上一次因式分解的数和其因子。当A线程里要进行因式分解的数等于上一次的数字,那么进入if条件语句,此时轮动线程B的时间片,其要进行因式分解的数不为上一次的数,进入else语句,因式分解,并修改上一次因式分解的数和因子为现在的数字和因子。B线程结束。A线程继续执行,对因子处理。结果不正确,因为这个因子已经变成线程B保存下来的数的因子,而不是线程A本线程需要处理的数的因子。虽然对这个对象的两个状态的操作分别是原子性的,但是结果不正确了,所以对多状态同时加锁。对service方法加内置锁就能达到目的。
public class CachingFactorizer{
private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();
public synchronized void service(BigInteger bInt){
if(bInt.equals(lastNumber.get())){
System.out.println("Results exist.");
doWithFactors(lastFactors);
}else{
lastFactors = extractNumbers(bInt);
lastNumber.set(bInt);
}
}
}
值得一提的是,一个对象的状态包括的不仅仅是本身的状态,如果一个对象的状态中有其他对象的饮用,那么必须保证对另一个对象的状态的操作也是原子性的。
二,内存可见性
我们不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改状态,而且希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化,但是在多线程环境里,我们无法确保执行读操作的线程能适时地看到其他线程写入的值。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。加锁机制既可以确保原子性,又可以确保可见性。
public class NoVisibility{
public static int number;
public static boolean ready;
public static class ReadyThread extends Thread{
public void run(){
while(!ready){
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("number is " + number);
}
}
public static void main(String[] args){
new ReadyThread().start();
number = 3;
ready = true;
}
}
这个程序的问题可能是ReadyThread线程可能他永远看不到ready的true值。而可能读取到的number值为0。因为这是分别在两个线程对两个状态进行操作。在JVM里,每条线程都有自己的空间,保存程序计数器,变量等。主线程对变量的操作可能只保存在线程内部。同样地,ReadyThread线程可能读取到的也只是线程空间内的值,这两个线程空间的数据并没有同步。
public class UInteger{
private int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
同样地,A线程调用了setValue方法修改了value的值,线程B调用getValue可能读取到的是以前的失效的值,这里需要两个方法都同步才能保证看到正确的值,仅仅一个方法同步是无法保证的。
public class SafeInteger{
private int value;
public synchronized int getValue() {
return value;
}
public synchronized void setValue(int value) {
this.value = value;
}
}
一个失效值,至少以前是正确的,但如果是对double和long进行非同步操作,线程可能读取到的是一个随机值,这是完全不可以接受的。因为在JVM里,对变量的读取和写入操作要求是原子性的,但是对double和long的操作是分解为两个32位操作而不是一个原子操作。
内存可见性可以通过volatile和同步锁来保证。
public class SafeInteger{
private volatile int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}