并发三大特性:
- 原子性:CAS和Automic类可以实现简单的原子性,对于复杂的操作可以使用synchronized和lock来实现
- 可见性:当多个线程访问同一个变量时,其中一个线程修改了变量的值,其他的线程可以立即看到修改的值。即立刻刷新修改的值到主存而不是工作内存。
- 有序性:程序执行的顺序按照代码的先后顺序执行,禁止进行指令冲排序。指令重排序是jvm为了优化指令,提高程序运行效率,在不影响单线程程序执行的情况下提高程序的并行度。但在多线程的情况下,有些代码顺序的改变有可能造成程序的不正确。
代码实例:
- (1) 加上volatile修饰,并不能保证原子性
运行结果不等于300000
// volatile 只具有可见性和有序性,但不保证原子性
public static volatile int num = 0;
//使用CountDownLatch来等待计算线程执行完
static CountDownLatch countDownLatch = new CountDownLatch(30);
public static void main(String []args) throws InterruptedException {
//开启30个线程进行累加操作
for(int i=0;i<30;i++){
new Thread(){
@Override
public void run(){
for(int j=0;j<10000;j++){
num++;//自加操作
}
countDownLatch.countDown();
}
}.start();
}
countDownLatch.await();
System.out.println(num);
}
- (2)使用AtomicInteger来计数,可以保证原子性,
- 并配合CountDownLatch来确保所有线程执行完毕,可以看到执行结果等于300000
//使用原子操作类
public static AtomicInteger num = new AtomicInteger(0);
//使用CountDownLatch来等待计算线程执行完
static CountDownLatch countDownLatch = new CountDownLatch(30);
public static void main(String []args) throws InterruptedException {
//开启30个线程进行累加操作
for(int i=0;i<30;i++){
new Thread(){
@Override
public void run(){
for(int j=0;j<10000;j++){
num.incrementAndGet();//原子性的num++,通过循环CAS方式
}
countDownLatch.countDown();
}
}.start();
}
//等待计算线程执行完
countDownLatch.await();
System.out.println(num);
}
(3)volatile为什么没有原子性
volatile保证了读写一致性。但是当线程2已经使用旧值完成了运算指令,且将要回写到内存时,是不能保证原子性的。
具体化:使用git开发项目时存在主干和分支,有一个全项目都使用的枚举类,
所以甲修改了该类立即提交主干,并通知组内成员:“你们使用这个类时需要在主干
上拉取一下”,但是此时乙在旧版本开发完毕并且正在提交这个类,导致了冲突。
(4)volatile防止指令重排
普通变量仅仅会保证在该方法执行过程中所有依赖赋值结果的地方都能得到正确的结果,
而不能保证变量赋值操作的顺序域代码中的执行顺序一致。
被volatile修饰的变量,会加一个lock前缀的汇编指令。若变量被修改后,
会立刻将变量由工作内存回写到主存中。那么意味了之前的操作已经执行完毕。这就是内存屏障。
(5)内存屏障
内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。
内存屏障的两个作用:
1.阻止屏障两侧的指令重排序;
2.强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
- 对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新的主内存加载数据;
- 对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。
volatile 性能:
volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中
插入许多内存屏障指令来保证处理器不发生乱序执行。