Java线程安全总结

1、概念

进程和线程都是一个时间段的描述,是CPU工作时间段的描述。两者颗粒度不同。
进程是CPU资源分配的最小单位,可以理解为一个应用程序。
线程是CPU调度的最小单位,是建立在进程的基础上的一次程序运行单位。

2、三个核心

原子性

一个操作,要么全部执行,要不么全部不执行。
简单的说,就是在一个线程对共享变量进行操作时,阻塞其他线程对该变量的操作。

可见性

当线程操作某个变量时,顺位为:
1、将变量从主内存拷贝到工作内存中。
2、执行代码,操作共享变量值。
3、将工作内存的数据刷新到主内存中。
多个线程并发访问共享变量时,一个线程对共享变量的操作,其他线程能够立刻看到。
每个线程读取共享变量时,都会将该变量加载进其对应CPU的高速缓存里,修改该变量后,CPU会立即更新该缓存,但并不一定会立即将其写回主内存(实际上写回主内存的时间不可预期)。此时其它线程(尤其是不在同一个CPU上执行的线程)访问该变量时,从主内存中读到的就是旧的数据,而非第一个线程更新后的数据。

顺序性

程序的执行顺序按照代码的先后顺序执行。

int a,b;
a++;
b++;
if(b==1){
  print(a);
}

在理想情况下,当b=1时,a=1,但是实际情况中,JVM在执行代码的过程中,并不一定按照代码的顺序执行,有可能先执行b++,后执行a++。
happens-before 原则
1、程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
2、锁定规则:一个unlock操作一定发生在lock操作之前。
3、volatile变量规则:对一个变量的写操作先行发生于后面的读操作。
4、传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。
5、线程启动规则:Thread对象的所有操作都发生在start()之后。
6、线程中断规则:线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
7、线程终结规则:线程中所有的操作都先行发生于线程的终止检测。
8、对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。

对于程序次序规则,应该理解为jvm保证最终执行的结果与程序顺序执行的结果一致。jvm有可能对不存在数据依赖性的指令进行重排序。实际上,这个规则是用来保证单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。

3、线程状态

  • INIT : 线程对象进行new初始化后,此时还未调用start()。
  • NEW : 线程对象调用start()方法后,进去可运行状态。如果处于RUNABLED状态的线程调用yield()后,会释放占用的资源,重新进入NEW状态。
  • RUNABLED : 线程获取到CPU时间片,进入运行状态。
  • BLOCKED : 线程调用sleep()或者join()方法后,进去阻塞状态,此时线程不释放所占有的系统资源。当sleep()结束或者join()等到其他线程到来,当前线程进入RUNABLED状态。
  • TIME WAITING : 线程进入到RUNABLED状态,还未开始运行的时候,发现要获取的资源处于同步状态,该线程就会进入TIME WAITING状态,等待资源释放;当前线程使用wait()方法后,进入TIME WAITING状态,只有在获得notify()或者notifyAll()通知后,才会进入WAITING状态。

4、关键字

synchronized

当synchronized修饰一个方法或者代码块的时候,保证同时只有一个线程可以访问该方法或代码块。保证了线程的执行顺序性和可见性。

synchronized(锁){
     临界区代码
}
public void synchronized method(){
    方法体
}

synchronized修饰代码块,锁就是这个对象;
synchronized修饰方法,锁就是这个class。
理论上所有对象都可以成为锁,但是能被多个线程共享的锁才有意义。
每个锁对象有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了即将获取锁的线程,阻塞队列存储被阻塞的线程。
java内置锁是可重入锁,子类可以获得父类的锁资源。
synchronized是一种悲观锁,会导致其他所有需要锁的线程挂起,等待持有锁的线程释放锁。这样的锁对性能不够友好。

volatile

volatile关键字可以保证可见性,当使用volatile来修饰某个共享变量时,会保证该变量的修改会立刻更新到主内存中,并且将其他缓存中对该变量的缓存设置为无效,其他线程需要重新从主内存读取该变量。
volatile关键字可以禁止进行指令重排序。
单线程下

x = 1; //语句1
y=0;  //语句2
volatile flag = true; //语句3
x = 2; //语句4
y = 4; //语句5

使用volatile修饰flag后,jvm在进行指令重排序时,不会将语句4,5放在语句3之前,也不会将语句1,2放在语句3之后。
多线程下

//线程1
object = loadObject(); //语句1
init = true; //语句2
//线程2
while(!init){
  ...
}
doSomething(object);

在多线程的情况下,线程1有可能先执行语句2,假如此时线程1进入阻塞,线程2开始执行,但此时语句1还没执行,obejct没有被初始化,导致程序出错。
这里用volatile修饰init,可以保证语句1先执行。
原理
加入volatile关键字时,会多出一个lock前缀指令。
lock前缀指令相当于一个内存屏障,有3个功能:
(1)、确保指令重排序时不会把内存屏障之后的指令排在内存屏障之前,也不会把内存屏障之前的指令排在内存屏障之后。
(2)、强制将对缓存的修改操作立即写入主内存。
(3)、如果是写操作,会导致其他CPU中对应的缓存行无效。

Lock

java.util.concurrent.lock中的lock框架是锁定的一个抽象,它允许把锁定的实现作为java类。它拥有与synchronized相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和中断锁等候的一些特性。此外,它在激烈争用的情况下具有更加的性能。
不要忘了在finally中释放lock

读写锁

ReentrantReadWriteLock

悲观锁和乐观锁

共享锁和排他锁

CAS

Compare And Swape,比较并交换。目前CAS被广泛应用于硬件层面的并发操作。
乐观锁的机制就是CAS,乐观锁就是每次不加锁,假设没有冲突的去完成某项操作,如果因为冲突失败就重试,直到成功为止。
CAS操作包含三个操作数--内存位置V,预期原值A和新值B。如果内存位置的值与预期原值匹配,那么将该位置替换为新值。否则,处理器不作任何处理。
利用CPU的CAS指令,同时借助JNI来完成JAVA的非阻塞算法。其他原子操作都是利用类似的特性完成的。而整个JUC都是建立在CAS的基础上的。
缺点
CAS虽然具有很高效的原子操作,但是CAS仍然存在三大问题。
(1)、ABA问题。如果一个值原来是A,变成了B,又变成了A,那么使用CAS检查时会认为它的值没有发生变化,但是实际上发生了变化。解决思路就是加版本号,1A-2B-3A。
(2)、循环时间长开销大。CAS是自旋锁,如果长时间不成功,会给CPU带来非常大的执行开销。
(3)、只能保证一个共享变量的原子操作。

Double-Check

在单例模式的懒汉式中,存在双重检查,这种方式在多线程中是不安全的。

public Singleton getResource(){
    if (resource == null){   //语句1
        synchronized(this){
            if (resource == null) {  //语句2
                  resource = new Singleton(); //语句3
            }
        }
    }
    return resource;
}

假设线程1执行到了语句1,执行了new Resource()指令,但是还未给resource赋值,此时线程1阻塞,线程2开始执行,在判断resource == null是,因为已经分配了内存空间(未赋值),该语句为false,就返回了未完成初始化的resource,造成程序错误。
改进方法
(1)、在方法上加synchronized。

public synchronized Singleton getResource(){
    if (resource == null){  
          resource = new Singleton(); 
    }
    return resource;
}

(2)、使用volatile

private valotile Singleton resource = null;
public synchronized Singleton getResource(){
    if (resource == null){  
          resource = new Singleton(); 
    }
    return resource;
}

你可能感兴趣的:(Java线程安全总结)