多线程并发总结一 基础

平日主要是开发企业信息系统,所以使用到多线程的时候不多,但是相关的知识又很多很复杂,所以就在这里做一些总结吧。

我主要做Java为主,这里记录的会是很多跟Java相关,但是我想多线程的问题,很多又是相通的。

先说说多线程的难度吧,这个可以跟多线程相关问题形成的原因有关

  • 首先它是因为有共享的可变变量
  • 因为代码的执行顺序是可以在编译或执行时优化的,所以以为已经执行的代码,其实还没有执行

通过各种锁的机制可以解决并发问题,但是又有以下的情况需要注意的

  • 如果同步的范围太大,那么并发带来的性能提升将不存在
  • 如果同步范围缩小,容易因为没有覆盖全需要覆盖的代码而引进Bug
  • 如果有多个锁需要获取,且获取锁的顺序在不同的代码段不是一致的,那么就可能引起死锁。
  • 另外,还有其他的一些相关的问题就是活锁,饿死和阻塞。

所以加锁需要加的范围刚刚好,且多个锁的获取顺序必须一致来避免加锁。

举几个Java的例子,比较容易出错的。

int a=0;

public void increase() {
    a++
}

这块代码并不满足原子性,所以当有多个1000个线程运行上面的代码之后,我们得到的结果并不一定等于1000.

换一下,变成下面的样子

volatile int a = 0;

public void increase() {
    a++;
}

在多线程的情况下还是会出错的,volatile并没有让a++变成原子,只是让写完以后可以立即的被其他的线程读出来,以及对volatile这个变量的写操作可以确保之前的所有操作都已经被完成。在Java的内存模型里面,这个叫做Happen Before的机制。

多线程并发总结一 基础_第1张图片

来源:http://gee.cs.oswego.edu/dl/jmm/cookbook.html

上图标成了No的那些地方就是在内存中顺序不能被编译器或者运行时优化的边界。可以看出,进入锁或者读取volatile变量都会让所有之前写的代码确保在其之前运行,比较有趣的一点是,离开锁或者写volatile变量只能确保其之前的代码已经执行,但是不能确保其后面的代码摆到其之前执行。

所以有人问这样子的话volatile有什么用。的确,volatile一般用于把性能的最后一滴榨干用的,比较经典的一个场景就是读数对实时性要求非常高的时候,其他的情况可以考虑使用其他的同步机制。

Java还有另外的一个关键词也跟内存同步有关的,就是final,final可以保证从变量读取其对象的时候,对象已经创建成功了。

比如有一个下面的代码

class Obj {
	Integer i = new Integer(1);
}


public class Test {
	public Obj o1;
	public volatile Obj o2;
	public final Obj o3;
	public Test () {
		o3 = new Obj ();
		o1 = new Obj ();
		o2 = new Obj ();
	}
	
	static Test test;
	
	static void calledByThread1() {
            test = new Test();
	}

        static void calledByThread2() {
            if (test != null) {
                System.out.println(test.o1.i);
                System.out.println(test.o2.i);
                System.out.println(test.o3.i);
            }
        }
}

在千钧一发的情况下面,上面这个Test对象的o2和o3都是不安全的。

因为Java不会保证分配内存地址并赋于给一个变量是发生在完成这个对象的创建之后的,所以第二个线程尽管验证了test变量不为空的情况下再去访问test对象的属性也不能保证是安全的。

在这里,只有 o3 的访问是安全的,因为它是有final的属性,Java可以保证其被访问的时候已经被初始化完成。

 

比较典型的可以提供Happen Before分界的Java语境有

单线程确保happen-before:这个是最基本的语境,不会在单个线程的情况下,后面的语句执行于前面的语句之前。
LockMonitor,锁监控进入和离开相关:参考上面的对应表。
volatile的读写:这里需要纠正一下网上很多其他的网站都声明,volatile变量的写只是优先于其他针对这个变量的操作,其实是不太正确的,volatile的读,可以确保到这行代码前面的所有操作已经完成。上面也提及过,一点比较有趣的地方是volatile write后面的操作可以被转移到write之前执行的。
happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
线程start:线程启动优先于所有此线程的操作,这个很好理解。
线程interupt, terminate, join:此线程的所有操作都发生在中断和终结之前。
 

public class MyTest {

    Integer a;
    Integer b;
    volatile Integer c;
    
    public void setValue(Integer a, Integer b, Integer c) {
        this.a = a;
        this.b = b;
        this.c = c;
    }
    
    public String getValue() {
        if (c!=null) {
            return a.toString() + b.toString();
        }
        return null;
    }
}

上面的这个例子是安全的,因为对于volatile的写操作可以确保前面的其他操作已经完成。

public class MyTest {

    Integer a;
    Integer b;
    volatile Integer c;

    public void setValue(Integer a, Integer b, Integer c) {
        this.c = c;
        this.a = a;
        this.b = b;
    }

    public String getValue() {
        String ret = null;
        if (c==null) {
            assertNull(a);
            assertNull(b);
        }
        return ret;
    }
}

反过来,这个例子就不是安全的,因为普通操作可以被优化到volatile的写操作的前面的,但是我在本地却模拟不到出错的情景。这个也是多线程问题难处理的另一个原因,他跟编译器和运行时也有关系,不是所有的情况都能在本地被模拟出来的。我们写程序的时候要确保安全就要严格按照规范来写。

 

你可能感兴趣的:(多线程和并发)