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;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,465评论 11 349
  • Java8张图 11、字符串不变性 12、equals()方法、hashCode()方法的区别 13、...
    Miley_MOJIE阅读 9,128评论 0 11
  • 浅谈java内存模型 不同的平台,内存模型是不一样的,但是jvm的内存模型规范是统一的。其实java的多线程并发问...
    流浪java阅读 2,910评论 1 3
  • 一、多线程 说明下线程的状态 java中的线程一共有 5 种状态。 NEW:这种情况指的是,通过 New 关键字创...
    Java旅行者阅读 10,185评论 0 44
  • 夏雨温温似玉钩, 叩我闲窗忆旧游。 星散南海十年路, 月映长江千里流。 清晨寂寂鹦鹉院, 午后喧喧佛牙楼。 路遥酒...
    南风小帅阅读 2,880评论 1 2