-
浅谈volatile ,synchronized,CAS,锁升级
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,导致我们每次只能一个人去上厕所,但如果想我们尽快都上完厕所,就只能增加上厕所的速度了。笑