文章目录
- 5.原子性
- 5.1 volatile-问题
- 2.2 volatile解决
- 5.3 synchronized 解决
- 5.4 原子性
- 5.5 volatile关键字不能保证原子性
- 5.6 原子性 AtomicInteger
- 5.7 AtomicInteger-内存解析
- 5.8 AtomicInteger-源码解析
- 5.9 悲观锁和乐观锁
- 小结
5.原子性
5.1 volatile-问题
- 代码分析
package com.hcx.myvolatile;
public class Demo {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
t1.setName("小路同学");
t1.start();
MyThread2 t2 = new MyThread2();
t2.setName("小皮同学");
t2.start();
}
}
package com.hcx.myvolatile;
public class Money {
public static int money = 100000;
}
package com.hcx.myvolatile;
public class MyThread1 extends Thread{
@Override
public void run() {
while(Money.money == 100000){
}
System.out.println("结婚基金已经不是十万了");
}
}
public class MyThread2 extends Thread {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
Money.money = 90000;
}
}
-
运行结果
- 程序一直在线程1的While循环中跑,说明此时线程1认为共享数据money仍为100000,但是线程2在开启以后,抢到CPU的执行权,JVM调用run()方法,已经将共享数据修改为90000,此时线程1无法知道最新的数据。
2.2 volatile解决
以上案例出现的问题:
当线程2修改了共享数据时,B线程没有及时获取到最新的值,如果还在使用原先的值,就会出现问题。
- 1.堆内存是唯一的,每一个线程都有自己的线程栈。
- 2.每一个线程在使用堆里面变量的时候,都会先拷贝一份到变量的副本中。
- 3.在线程中,每一次使用是从变量的副本中获取的
以上案例出现的原因:
在开启线程1和线程2后,线程1先抢到CPU的使用权,将堆内存的数据拷贝到自己的变量副本中,然后从副本中获取共享数据,此时线程1中变量副本的值为100000,线程2后抢到CPU的执行权,然后JVM调用run()方法,将共享数据money的值改为90000,然后拷贝到自己的副本中,再从自己的副本中获取共享数据。所以线程1的数据为100000,线程2的数据为90000。造成了上述出现的情况。
Volatile关键字:强制线程每次在使用的时候,都会看一下共享区域最新的值。
代码实现:使用volatile关键字解决
package com.hcx.myvolatile;
public class Demo {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
t1.setName("小路同学");
t1.start();
MyThread2 t2 = new MyThread2();
t2.setName("小皮同学");
t2.start();
}
}
package com.hcx.myvolatile;
public class Money {
public static volatile int money = 100000;
}
package com.hcx.myvolatile;
public class MyThread1 extends Thread{
@Override
public void run() {
while(Money.money == 100000){
}
System.out.println("结婚基金已经不是十万了");
}
}
package com.hcx.myvolatile;
public class MyThread2 extends Thread {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
Money.money = 90000;
}
}
- 运行结果
5.3 synchronized 解决
-
synchronized解决:
- 1.线程获得锁
- 2.清空变量副本
- 3.拷贝共享变量最新的值到变量副本中
- 4.执行代码
- 5.将修改后变量副本中的值赋值给共享数据
- 6.释放锁
-
代码实现
package com.hcx.myvolatile2;
import com.hcx.myvolatile.MyThread2;
public class Demo {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
t1.setName("小路同学");
t1.start();
MyThread2 t2 = new MyThread2();
t2.setName("小皮同学");
t2.start();
}
}
package com.hcx.myvolatile2;
public class Money {
public static Object lock = new Object();
public static int money = 100000;
}
package com.hcx.myvolatile2;
public class MyThread1 extends Thread{
@Override
public void run() {
while(true){
synchronized (com.hcx.myvolatile2.Money.lock){
if(Money.money!=100000){
System.out.println("结婚基金已经不是十万了");
break;
}
}
}
}
}
package com.hcx.myvolatile2;
import com.hcx.myvolatile.Money;
public class MyThread2 extends Thread {
@Override
public void run() {
synchronized (com.hcx.myvolatile2.Money.lock) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
Money.money = 90000;
}
}
}
5.4 原子性
- 概述:所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体。
- 代码实现:
package com.hcx.threadatom;
public class AtomDemo {
public static void main(String[] args) {
MyAtomThread myAtomThread = new MyAtomThread();
for (int i = 0;i < 100; i++) {
new Thread(myAtomThread).start();
}
}
}
package com.hcx.threadatom;
public class MyAtomThread implements Runnable {
private volatile int count = 0;//送冰淇淋的数量
@Override
public void run() {
for (int i = 0;i < 100;i++) {
//1.从共享数据中读取数据到本线程中
//2.修改本线程栈中变量副本的值
//3.会把本线程栈中变量副本的值赋值给共享数据。
count++;
System.out.println("已经送了"+count +"个冰淇淋");
}
}
}
-
运行结果
-
思考:100个线程执行100次,最后应该打印结果为10000,为什么运行结果是9999呢?
- 假如现在有A、B两个线程,堆内存的共享数据此时为100,此时A线程抢夺到CPU的执行权,然后将共享数据100拷贝到自己的变量副本中,然后做count++,也就是将101赋值给了自己的变量副本,这个时候,CPU 的执行权被B线程抢走,现在A线程还没有将自己变量副本的值写回到共享数据中,所以B线程读取的值为共享数据的最新值,还为100,然后B线程执行跟A线程一样的操作,count++后,变量副本的值也为101,所以导致最后的运行结果为9999,而不是10000。
代码总结:count++ 不是一个原子性操作,他在执行的过程中,有可能被其他线程打断。
- 假如现在有A、B两个线程,堆内存的共享数据此时为100,此时A线程抢夺到CPU的执行权,然后将共享数据100拷贝到自己的变量副本中,然后做count++,也就是将101赋值给了自己的变量副本,这个时候,CPU 的执行权被B线程抢走,现在A线程还没有将自己变量副本的值写回到共享数据中,所以B线程读取的值为共享数据的最新值,还为100,然后B线程执行跟A线程一样的操作,count++后,变量副本的值也为101,所以导致最后的运行结果为9999,而不是10000。
-
综上,我们可以得出volatile关键字:
- 只能保证线程每次在使用共享数据的时候是最新值。
- 但是不能保证原子性。
5.5 volatile关键字不能保证原子性
- 解决方案:我们可以给 count++操作添加锁,那么count++操作就是临界区中的代码,临界区中的代码一次只能被一个线程去执行,所以count++就变成了原子操作。
package com.hcx.threadatom2;
import com.hcx.threadatom.MyAtomThread;
public class AtomDemo {
public static void main(String[] args) {
MyAtomThread myAtomThread = new MyAtomThread();
for (int i = 0;i < 100; i++) {
new Thread(myAtomThread).start();
}
}
}
package com.hcx.threadatom2;
public class MyAtomThread implements Runnable {
private volatile int count = 0;//送冰淇淋的数量
private Object lock = new Object();
@Override
public void run() {
for (int i = 0;i < 100;i++) {
//1.从共享数据中读取数据到本线程中
//2.修改本线程栈中变量副本的值
//3.会把本线程栈中变量副本的值赋值给共享数据。
synchronized (lock) {
count++;
System.out.println("已经送了"+count +"个冰淇淋");
}
}
}
}
- 运行结果
5.6 原子性 AtomicInteger
- 概述:Java 从JDK1.5开始提供了java.util.concurrent.atomic包(简Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。因为变量的类型有很多种,所以在Atomic包里一共提供了13个类,属于4种类型的原子更新方式,分别是原子更新基本类型、原子更新数组、原子更新引用和原子更新属性(字段)。本次我们只讲解使用原子的方式更新基本类型,使用原子的方式更新基本类型Atomic包提供了以下3个类:
- AtomicBoolean: 原子更新布尔类型
- AtomicInteger: 原子更新整型
- AtomicLong: 原子更新长整型
- 以上3个类提供的方法几乎一模一样,所以本节仅以AtomicInteger为例进行讲解,AtomicInteger的常用方法如下 :
- 代码实现
package com.hcx.threadatom3;
import java.util.concurrent.atomic.AtomicInteger;
public class MyAtomIntergerDemo1 {
//public AtomicInteger(): 初始化一个默认值为0的原子型Integer
//public AtomicInteger(int initialValue): 初始化一个指定值的原子型Integer
public static void main(String[] args) {
AtomicInteger ac = new AtomicInteger();
System.out.println(ac);
AtomicInteger ac2 = new AtomicInteger(10);
System.out.println(ac2);
}
}
package com.hcx.threadatom3;
import java.util.concurrent.atomic.AtomicInteger;
public class MyAtomIntergerDemo2 {
// int get(): 获取值
// int getAndIncrement(): 以原子方式将当前值加1,注意,这里返回的是自增前的值。
// int incrementAndGet(): 以原子方式将当前值加1,注意,这里返回的是自增后的值。
// int addAndGet(int data): 以原子方式将参数与对象中的值相加,并返回结果。
// int getAndSet(int value): 以原子方式设置为newValue的值,并返回旧值。
public static void main(String[] args) {
// AtomicInteger ac1 = new AtomicInteger(10);
// System.out.println(ac1.get());
//
// AtomicInteger ac2 = new AtomicInteger(10);
// int andIncrement = ac2.getAndIncrement();
// System.out.println(andIncrement);
// System.out.println(ac2.get());
// AtomicInteger ac3 = new AtomicInteger(10);
// int incrementAndGet = ac3.incrementAndGet();
// System.out.println(incrementAndGet);
// AtomicInteger ac4 = new AtomicInteger(10);
// int i = ac4.addAndGet(20);
// System.out.println(i);
// System.out.println(ac4.get());
AtomicInteger ac5 = new AtomicInteger(10);
int andSet = ac5.getAndSet(20);
System.out.println(andSet);
System.out.println(ac5.get());
}
}
5.7 AtomicInteger-内存解析
AtomicInteger原理:自旋锁+CAS算法
CAS算法:
我们来解释一下,首先还是一个堆内存,堆内存中有共享数据值为100,然后我开启了两个线程,一个线程为A线程,另一个线程为B线程,现在A线程跟B线程想要进行的事情是要将共享数据里边的值进行自增,也就是我们最后要把共享数据的值变成102,共享数据100相当于CAS算法中的内存值,假设A线程先抢到CPU的执行权,它接下来需要将内存值100读到自己的线程栈里边,读过来以后存到自己的变量副本中,此时这个值为旧的预期值,现在线程A需要进行自增, 是不是应该将101覆盖给了变量副本,那么变量副本里边现在变成了101,这个时候变量副本里边的值就是CAS算法中的要修改的值,此时B线程抢到了CPU的执行权,它同样将100读到变量副本当中,那么B线程,也有一个旧的预期值,然后线程B也进行了自增,将101覆盖给了变量副本,此时,B线程中要修改的值也为101,现在两个线程的自增都做完了,现在需要将101写到共享数据里面了,假设A线程先写, 它发现A里边的旧的预期值==内存值,表示这个内存值没有被其他线程操作过,所以就将A线程的101写到共享数据修改成功,我们B线程也需要往共享区域写,它发现旧的预期值!=内存值,表示有可能共享数据已经被其他线程操作过了,所以这个时候它修改失败,它需要将现在最新的内存值101再次读到自己的变量副本中,这个时候线程B的旧的预期值就变成101了,然后进行自增,并将102覆盖给变量副本,102就是要修改的值,然后将102写到堆内存的共享数据中。它发现第二次旧的预期值=内存值,所以修改成功。
5.8 AtomicInteger-源码解析
- 代码实现
package com.hcx.threadatom4;
import com.hcx.threadatom.MyAtomThread;
public class AtomDemo {
public static void main(String[] args) {
MyAtomThread myAtomThread = new MyAtomThread();
for (int i = 0;i < 100; i++) {
new Thread(myAtomThread).start();
}
}
}
package com.hcx.threadatom4;
import java.util.concurrent.atomic.AtomicInteger;
public class MyAtomThread implements Runnable {
// private volatile int count = 0;//送冰淇淋的数量
// private Object lock = new Object();
AtomicInteger ac = new AtomicInteger(0);
@Override
public void run() {
for (int i = 0;i < 100;i++) {
//1.从共享数据中读取数据到本线程中
//2.修改本线程栈中变量副本的值
//3.会把本线程栈中变量副本的值赋值给共享数据。
// synchronized (lock) {
// count++;
int count = ac.incrementAndGet();
System.out.println("已经送了"+count +"个冰淇淋");
// }
}
}
}
-
运行结果
-
源码解析
5.9 悲观锁和乐观锁
- synchronized和CAS的区别:
- 相同点:在多线程情况下,都可以保证共享数据的安全性。
- 不同点 :
- synchronized总是从最坏的角度出发,认为每次获取数据的时候,别人都可能修改。所以在每次操作共享数据之前,都会上锁(悲观锁)
- cas 是从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁。只不过在修改共享数据的时候,会检查一下,别人有没有修改过这个数据。如果别人修改过,那么我再次获取现在最新的值。如果别人没有修改过,那么我现在直接修改共享数据的值。(乐观锁)
小结
本篇文章介绍了 volatile 关键字的作用,通过一个用 volatile 关键字解决不了的送冰淇淋问题引出原子性的概念,介绍了JDK1.5使用原子的方式更新整数类型-AtomicInteger,并对AtomicInteger进行了内存分析和源码分析,最后还介绍了 synchronized和CAS的相同点与不同点,引出了悲观锁和乐观锁的概念。
更新不易,希望大家多多支持!!