volatile是一种轻量级的线程同步机制 他主要有三个特性
保证可见性,
import java.util.concurrent.TimeUnit;
/**
* 验证volatile关键字可以保证可见性
*/
class ShareDate {
volatile int num = 1;
public void setNum(){
num = 50;
}
}
public class TestVol {
public static void main(String[] args) {
ShareDate shareDate = new ShareDate();
new Thread(()->{
System.out.println("线程"+Thread.currentThread().getName()+"没改之前num的值"+shareDate.num);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
shareDate.setNum();
System.out.println("线程"+Thread.currentThread().getName()+"改之后num的值"+shareDate.num);
}, "A").start();
while (shareDate.num == 1){
// main线程在这里一直循环等待
}
System.out.println(Thread.currentThread().getName()+"任务结束");
}
}
JMM关于同步的规定,
1.线程解锁前,必须把共享变量的值刷新回主内存
2.线程加锁前,必须把主内存的新值拷贝到自己的工作内存中
3.加锁解锁是同一把锁
ps JMM是一个抽象的规范,但java的堆栈是确实存在的东西,他们之间的联系我认为,JMM的主内存可以理解为java的堆,因为只有堆中数据共享,而各自的共工作内存可以理解为栈但又不全是,因为线程是由操作系统在创建,而各自线程的数据最终都要放到cpu的寄存器参与运算,另外内存和cpu之间还有缓存,所以这个各自的工作内存我感觉就很宽泛,毕竟JMM就是一个抽象的规范。另外线程之间的通信也要通过主内存。
不保证原子性,
原子性就是在某个线程做某个业务时中间不可以被分割或者加塞需要保证完整,要么都成功,要么都失败。即不可分割,完整。
就我个人的理解,加上周阳的讲解,比如以20个线程每个线程执行100次加1操作,结果不是2000。因为n++这个操作不是原子的,n++在编译成汇编语言之后是4条指令,分别是从主内存取出,然后加一,然后写回,看上去可以成功但如果同一时间很多个线程操作的话volatile 虽然保证可见性,每次加1如果在并发不高的情况下可以通知到,如果并发太高就通知不到,可以用生活中例子来理解。我们在生活中经常遇到我们要做什么事的时候别人突然叫住我们,我们即便是反应过来了,但是我们的动作已经出去了。大概就是这个道理。具体细节没有讲,应该是更深入的东西。
/**
* 验证volatile关键字不能保证原子性
*/
class ShareDate1 {
volatile int num = 1;
public void add() {
num = num + 1;
}
}
public class TestVol1 {
public static void main(String[] args) {
ShareDate1 shareDate = new ShareDate1();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
shareDate.add();
}
}).start();
}
while (Thread.activeCount() > 2){
// main线程和GC线程
Thread.yield(); //让这个放弃cpu权限到就绪状态
}
System.out.println(shareDate.num);
}
}
import java.util.concurrent.atomic.AtomicInteger;
/**
* 使用原子整形类保证每次++操作是原子的
*/
class ShareDate1 {
// volatile int num = 1;
AtomicInteger num = new AtomicInteger();
public void add() {
num.getAndIncrement();
}
}
public class TestVol1 {
public static void main(String[] args) {
ShareDate1 shareDate = new ShareDate1();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
shareDate.add();
}
}).start();
}
while (Thread.activeCount() > 2){
// main线程和GC线程
Thread.yield(); //让这个放弃cpu权限到就绪状态
}
System.out.println(shareDate.num.get());
}
}
当然只是调用了API是不足以应付面试的,还需要一些他的原理,通过点进源码我们发现,里面用到了unsafe类,和CAS思想 。
cas:比较并交换 有点像乐观锁,GIT的提交版本号。
atmic这个类里面的vlaue值也是加了volatile,所以就是利用volatile特性保证禁止重排和可见,比较并交换保证原子,如何保证的呢,利用atmic的unsafe类,这个类里面几乎都是native方法,就是用C语言写的方法。
上面那个静态代码块就是通过unsafe类得到了这个类的内存地址,然后通过一条cpu级别的原语来取出这个地址的这个值是不是期望值,如果是就改掉。
就是说取值改值在cpu的指令完成的,而这个指令是原子的不能被中断。所以不会造成数据不一致。CAS也有缺点,利用循环,如果循环时间太长的话也会浪费cpu资源。第二个是只能保证一个共享变量的原子性。第三个可能会引起aba问题。aba问题其实就是一个版本问题。
import java.util.concurrent.atomic.AtomicInteger;
/**
* 比较并交换
*/
public class CAS {
public static void main(String[] args) {
AtomicInteger integer = new AtomicInteger(5);
//验证cas方法是先比较然后交换所以第二个失败了
System.out.println(integer.compareAndSet(5, 100));
System.out.println(integer.compareAndSet(5, 300));
}
}
得说一下原子更新引用。
import java.util.concurrent.atomic.AtomicReference;
/**
* 原子引用更新
*/
class User{
private Integer uid;
private String uName;
public User() {
}
public User(Integer uid, String uName) {
this.uid = uid;
this.uName = uName;
}
@Override
public String toString() {
return "User{" +
"uid=" + uid +
", uName='" + uName + '\'' +
'}';
}
public Integer getUid() {
return uid;
}
public void setUid(Integer uid) {
this.uid = uid;
}
public String getuName() {
return uName;
}
public void setuName(String uName) {
this.uName = uName;
}
}
public class CAS {
public static void main(String[] args) {
User dd = new User(1, "dd");
User ff = new User(2, "ff");
AtomicReference<User> reference = new AtomicReference<>();
reference.set(dd);
System.out.println(reference.compareAndSet(dd,ff));
System.out.println(reference.compareAndSet(ff,dd));
System.out.println(reference.compareAndSet(dd,ff));
}
}
如果我们有一个自定义的类,现在想让他也实现原子级别的更新,我们需要使用带时间戳的原子引用类
public class CAS {
public static void main(String[] args) {
User dd = new User(1, "dd");
User ff = new User(2, "ff");
AtomicStampedReference<User> reference = new AtomicStampedReference<>(dd, 0);
System.out.println(reference.compareAndSet(dd, ff, 0, 1));
System.out.println(reference.compareAndSet(ff, dd, 1, 0));
System.out.println(reference.compareAndSet(dd, ff, 0, 1));
}
}
禁止指令重排
计算机在执行程序时,为了提高性能,编译器和处理器会常常对指令做重排,一般分为以下三种。
1,单线程环境里面确保程序最终执行结果和代码执行顺序的结果一致。
2,处理器在进行重排序时必须考虑指令之间的数据依赖性。
3,多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致是无法确定的,结果也无法预测。
加了volatile就是有什么store store 屏障,load load屏障
但是JMM是要保证上述三个特性的,所以如果不用syn的话只加voltile还需要有其他东西保证原子性。
小综合,高级版本的单例,双端检索加volatile版本
public class Single {
public static void main(String[] args) {
Apple appleInstance = Apple.getAppleInstance();
Apple appleInstance1 = Apple.getAppleInstance();
System.out.println(appleInstance==appleInstance1);
System.out.println(appleInstance.hashCode());
System.out.println(appleInstance1.hashCode());
}
}
class Apple{
private volatile static Apple appleInstance;
private Apple(){
}
public static Apple getAppleInstance(){
if (appleInstance==null){
synchronized (Apple.class){
if (appleInstance==null){
appleInstance = new Apple();
}
return appleInstance;
}
}
return appleInstance;
}
}