前言
一大部分Java程序员可能都没有办法写出一个好的多线程代码让代码执行结果与预期相同。为了解决这个问题。出现了很多线程框架、类库与工具。这个问题其实就是线程安全问题。来了解一下线程安全问题。
线程安全问题出现的原因
多个线程,多线程之间有竞争或者同步的机制,并且能够共享资源。共享资源的时候他们同时会读写这组共享的资源。读写资源,就会总成竞争,也就是我们常说的静态条件。操作竞态条件的的代码块,我们称为临界区。在临界区内,大家都想操作相同的资源。如果没有好的同步控制,就会导致线程之间。相互操作的时候有部分人拿到的资源是错误的或者不完整的。也就是 其他线程拿到值修改完还没有同步到主内存中。这样也就导致了多线程环境下并发安全的问题。
进一步对这个过程进行分析,有三个性质可以保证和分析线程的三个问题。
- 原子性 :在整个操作过程中不会被打断,不会被终止,这些操作要么执行,要么不执行。例如 :对基本数据类型的读取和赋值。
x = 10; // 原子性
y = x; // 两个操作 先读取 ,在赋值
x ++; // 两个操作 先读取 ,在++
x = x + 1; // 两个操作 先读取 ,在+1
- 可见性 :jvm内部 , 两个线程同时去操作一个变量,这时候两个线程同时拥有这个变量的副本。 对该变量的即时修改,都是先发生在当前线程所持有的副本上。然后再被刷到主内存。Java中使用volatile 关键字来保证可见性。当变量被 volatile 修饰时,会保证所修改的值都会被立即刷新到主内存中。这时候有其他线程来访问,一定会从主内存拿到最新的值。但值得注意的是,它并不能保证原子性。 syn 和 lock 同步机制也可以保证可见性,在同步代码块执行完毕后,同步代码块中修改的值会立即刷新到主内存中。也保证了可见性。
- 有序性 :Java允许编译器和处理器进行指令重排序来优化程序的执行性能。这在单线程下并不会有什么影响。但是如果在多线程环境下,就会影响到并发执行的正确性volatile关键字可以保证一定的有序性(当然 同步机制也可以) --〉 happens-before 原则
synchronized
synchronized 的作用是给 同步代码块、同步方法上加锁,单独对象加锁。其实加锁是在对应的对象头(对象头的标志位)上加锁。
不同加锁方式的区别:锁的粒度不同。粒度指的是,在同步代码块中,代码是同步执行的,其他则是并发执行,提高了程序的并发性。同样的,有时候可以做锁分离。用多把锁锁不同的代码块。不同的方法,不同代码块,可以锁定不同对象,那么在同一时间多个线程可以拿到多把锁。并发的粒度又提高了。
volatile
- 每次读取 都强制从主内存刷数据。
- 使用场景 :单个线程写,多个线程读
- 能不用就不用,不能确定也不用。
- 替代方案:Atomic 原子操作类。
volatile 内存屏障 : x = 1 的修改结果 和 y = 2 的修改结果 一定是 volatile 下面的代码块可以看到的。如果发生了指令重排,也不会把 前面两条语句重排到后面两条语句之间。
x = 1;
y = 2;
volatile int c = 3;
x = 4;
y =5;
final 关键字 (写代码最大化final 是一个好习惯 )
被final 修饰的东西,就基本只是可读的,这种东西跨线程就是安全的。在写代码的时候,在很多地方写上final 是非常有利的。
final的安全发布,在构造函数返回时,final域的最新值被保证其他线程是可见的。