1.volatile
俩个功能:1.保证线程可见性 2.禁止指令重排序
前言
1.1保证线程可见性
假设A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道,使用volatile关键字,会让所有线程都会读到变量的修改值
在下面的代码中,running是存在于堆内存的t对象中,当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去读取堆内存,这样当主线程修改running的值之后,t1线程感知不到,所以start会先运行一段时间才end,而在running前面加上volatile后,将会强制所有线程都去堆内存中读取running的值,所以很快会到end。但同时,volatile并不能保证多个线程共同修改running变量时所带来的不一致问题
//没有volatile,start会过一段时间才停下来,因为线程t1不会每次都去读取堆内存 private static volatile boolean running = true; private static void m() { System.out.println("m start"); while (running) { System.out.println("hello"); } System.out.println("m end!"); } public static void main(String[] args) throws IOException { new Thread(T01_HelloVolatile::m, "t1").start(); SleepHelper.sleepSeconds(1); running = false; System.in.read(); }
1.2.禁止指令重排序
//volatile虽然达到了按需初始化的目的,但也带来了线性不安全问题 //通过synchronized解决,但也带来了效率低下 public static M getInstance(){ if(INSTANCE == null){ //双重检查 synchronized(M.class){ try(){ }catch{} //如果只有volatile,别的线程也会new,加上synchronized只有等当前线程已经初始化成功别的线程才会访问,此时已经new过了就不会在new了 INSTANCE = new M(); } } }
2.synchronized
简介:用于给某个对象上锁,保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,
是Java中解决并发问题的一种最常用最简单的方法 ,他可以确保线程互斥的访问同步代码
2.1synchronized的目的
举栗子:
public class Test { volatile int count = 0;//解决方法:synchronized void m()。保证每个线程结束后另一个线程再拿到count的值,此时可以加到10w void m() { for (int i = 0; i < 10000; i++) count++; } public static void main(String[] args) { Test t = new Test(); List<Thread> threads = new ArrayList<Thread>(); //10个线程,每个线程调用m方法 for (int i = 0; i < 10; i++) { threads.add(new Thread(t::m, "thread-" + i)); } threads.forEach((o) -> o.start()); threads.forEach((o) -> { try { o.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println(t.count); } }
==========
结果:45854
在上面的例子中,m方法将count加到10000,我们创建了十个线程,每个线程调用一次m方法,正常来说我们的结果应该是10w,那为什么结果会是45854呢?
我们需要先了解count++发生了什么可以看到count++需要三个步骤,如果线程1的count加到1,线程2拿到的是1,线程3拿到的也是1,线程2返回2,线程3返回2,实际结果应该为3,所以丢失了1次更新。
导致最后的结果少加了很多次。解决办法就是在m方法前面加锁,保证一个线程结束后第二个线程才可以执行。也可以采用AtomicInteger 类,
因为AtomicInteger类是原子性操作的
//结果是10w AtomicInteger count = new AtomicInteger(0); //不需要在加synchronized /* synchronized */void m() { for (int i = 0; i < 10000; i++) //if count1.get() < 1000 count.incrementAndGet(); //count1++ }
注意点:
1.程序当中如果出现异常,默认情况锁被释放,需要catch,不然容易被别的程序乱入,导致数据不一致
2.锁定对象的时候不能用String常量,Integer,Long,
3.锁的是对象,不是代码
4.无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
5.每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码
6.同步代码块中的语句越少越好,因为上锁后性能会变低
7.锁定某对象o,如果o的属性发生改变,不影响锁的使用,但是如果o变成另外一个对象,则锁定的对象发生改变应该避免将锁定对象的引用变成另外的对象
2.2synchronized的优化
我们已经知道使用synchronized虽然可以保证线程的安全行,但同时也变得效率低下,每次只能通过一个线程,既然每次只能通过一个,这种形式不能改变的话,那么我们能不能让每次通过的速度变快一点了。打个比方,只有一个厕所,我们一群人在排队等着,由于使用了synchronized,导致我们每次只能一个人去上厕所,但如果想我们尽快都上完厕所,就只能增加上厕所的速度了。笑??。这种优化方式同样可以引申到锁优化上,缩短获取锁的时间。
synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,
每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS
3.CAS简介
3.1CAS的机制
CAS 操作包含三个操作数 —— 老值(V)、预期原值(A)和新值(B)。
简而言之就是V=A的情况,B给A。V!=A的情况,返回V。
V=A的情况:表明该值没有被其他线程更改过
V!=A的情况:表明该值已经被其他线程改过了则老值A不是最新版本的值了,所以不能将新值B赋给V,
当多个线程使用CAS操作一个变量时,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程。
3.2 CAS的问题
最典型的是ABA问题。那什么是ABA问题呢?
因为CAS会检查老值有没有变化,这里存在这样一个有意思的问题,比如一个int类型的老值A变为了成B,然后再变成A,刚好在做CAS时检查发现老值并没有变化依然为A,但是实际上的确发生了变化。打个比方,你的女朋友跟别人睡了一夜,第二天回来还是你原来的女朋友吗?人还是那个人,但可能心已经变了。同样的道理,这里就是A变为了成B,然后再变成A,懂了吧。笑??。
解决方案如果是int类型当然无所谓,但如果是引用类型需要加版本号version,修改完后版本号+1即可,版本号在哪里加呢,在对象头里面。
3.3 对象头
对象头里包括(64位机器占96位):运行时元数据(Mark Word)(占64位)、类型指针(Klass Point)(占32位)
运行时元数据里又包括:哈希值(HashCode)、GC分代年龄、锁状态标志
4.锁的升级
我们已经知道采用synchronized的话虽然会保证线程的安全性,但同样的带来了极大的性能低下,其实在JDK1.6之前,synchronized就是一种重量级锁
4.1无锁
无锁是指没有对资源进行锁定,所有线程都可以访问并修改同一资源,但同时只有一个线程可以修改成功。如果有多个线程同时修改同一值,必定会有一个线程修改成功,其他失败的线程会不断尝试直到修改成功
4.2偏向锁
锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
简言之就是一个线程获取锁后,会在对象头记录这个线程的ID,下次来不需要再进行CAS操作就可以直接获得锁。
4.3轻量级锁(自旋锁)
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。
简言之就是锁是偏向锁的时候,被别的线程所访问,锁会升级成轻量级锁,别的线程会通过自旋的形式尝试获取锁
4.4重量级锁
长时间的自旋是非常消耗资源的,一个线程持有锁,别的线程就只能忙等,但忙等是由限度的(循环里面进行的,默认进行10次),某个达到最大自旋的线程,轻量级锁就会升级为重量级锁,别的线程一旦遇到重量级锁,就不会忙等,直接会将自己挂起,等待被唤醒。
画一个图可能更直观一点
总结:
volatile:1.保证线程可见性 2.禁止指令重排序
synchronized:给对象上锁,保证线程同步
CAS:三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。
锁的四种状态:无锁,偏向锁,轻量级锁,重量级锁
以上就是关于volatile ,synchronized,CAS,锁升级的介绍,这篇文章估计还有很多需要修改的地方,我也还有很多不清楚的地方,欢迎大家指出,我们一起进步。