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;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,018评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,046评论 2 372
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,215评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,303评论 1 266
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,181评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,171评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,577评论 3 384
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,260评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,546评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,630评论 2 311
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,390评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,260评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,633评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,920评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,210评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,569评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,767评论 2 335

推荐阅读更多精彩内容

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