java多线程synchronized volatile解析
先简单说说原子性:具有原子性的操作被称为原子操作。原子操作在操作完毕之前不会线程调度器中断。即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。在Java中,对除了long和double之外的基本类型的简单操作都具有原子性。简单操作就是赋值或者return。比如”a = 1;“和 “return a;”这样的操作都具有原子性。但是在Java中,类似”a += b”这样的操作不具有原子性,不是同步的就会出现难以预料的结果。
在我们平常的编程过程中,经常会遇到线程安全与不全这类的词语,相信刚开始很多新手碰到这个都会很头疼。现拿万恶的火车票举个例子吧。如杭州到武汉还剩10张车票,有4个售票点正在售杭州到武汉的票,若所写程序是线程不安全的,则当a售票点售出票1时,b售票点也正在售票,由于a售票点售出后还未及时更新到主内存中,因此b售票点可能同样售出的是票1,这就造成了两个人买到一张座位的情况,这就是线程不安全的。若线程安全,则当a售票点在售票1时,只有等a售票点将票1售出,并更新了主内存数据后,其余售票点才能继续卖票,并且数据中票1已经卖掉了,不会再重复操作。此时即利用了synchronized锁的机制。
- synchronized是一个方法或块的修饰符,synchronized分为方法同步和块同步。
1、synchronized 实例方法
1 //加在方法上 2 public synchronized void function() { 3 //somecode 4 }
该同步是同步在拥有该方法的对象上的,同一时间只有一个线程能访问到该方法(前提是在一个实例上,若有几个实例,则可以同时进行)。
2、synchronized 静态方法
1 public static synchronized void function(){ 2 //somecode 3 }
由于静态方法不属于对象,而是属于类,会将这个方法所在类的class对象上锁。
3、synchronized 块
1 //同步块 2 public void method1(){ 3 synchronized(this){ 4 //somecode 5 } 6 } 7 8 public void method2(Object o){ 9 synchronized(o){ 10 //somecode 11 } 12 }
锁定的是调用这个方法的对象(method1)或传入的对象(method2),当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(对象)来充当锁。
- volatile是一个变量修饰符
一个volatile类型的变量不允许线程从主内存中将变量的值拷贝到自己的存储空间。因此,一个声明为volatile类型的变量将在所有的线程中同步的获得数据,不论你在任何线程中更改了变量,其他的线程将立即得到同样的结果。volatile变量具有synchronized的可见性特性,但是不具备原子特性。这就是说线程能够自动发现volatile变量的最新值。volatile变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。
正确使用volatile修饰符:a、对变量的写操作不依赖于当前值。b、该变量没有包含在具有其他变量的不变式中。
=========================================关于两者的区别==============================
1.volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
2.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
3.volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
4.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
5.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
一些例子:
synchronized
1 public class SyncTest { 2 3 public static void main(String[] args) { 4 Outputter outputter = new Outputter(); 5 MyThread myThread1 = new MyThread(outputter, "zhangsan"); 6 MyThread myThread2 = new MyThread(outputter, "lisi"); 7 Thread threada = new Thread(myThread1); 8 Thread threadb = new Thread(myThread2); 9 threada.start(); 10 threadb.start(); 11 } 12 13 } 14 15 class MyThread implements Runnable { 16 Outputter outputter; 17 String name; 18 19 public MyThread(Outputter outputter, String name) { 20 this.outputter = outputter; 21 this.name = name; 22 } 23 24 @Override 25 public void run() { 26 outputter.output(name); 27 } 28 } 29 30 /** 31 * 当没有锁定时,将输出"zhlangisisan"等不确定的值。 32 * 33 * @author lcm 34 * 35 */ 36 class Outputter { 37 public void output(String name) { 38 // 为了保证对name的输出不是一个原子操作,这里逐个输出name的每个字符 39 for (int i = 0; i < name.length(); i++) { 40 System.out.print(name.charAt(i)); 41 } 42 } 43 } 44 45 /** 46 * 若想得到zhangsanlisi或者lisizhangsan,就需要用到synchronized。 47 * 48 * @author lcm 49 * 50 */ 51 class Outputter { 52 public synchronized void output(String name) { 53 // 为了保证对name的输出不是一个原子操作,这里逐个输出name的每个字符 54 for (int i = 0; i < name.length(); i++) { 55 System.out.print(name.charAt(i)); 56 } 57 } 58 } 59 60 class Outputter { 61 public void output(String name) { 62 // 为了保证对name的输出不是一个原子操作,这里逐个输出name的每个字符 63 synchronized (this) { //代码块也可以传入一个对象,但需保证是同一个对象 64 for (int i = 0; i < name.length(); i++) { 65 System.out.print(name.charAt(i)); 66 } 67 } 68 } 69 } 70 71 /** 72 * 若修饰静态静态代码块,则是对整个class对象加锁 73 * 74 * @author lcm 75 * 76 */ 77 class Outputter { 78 public synchronized static void output(String name) { 79 // 为了保证对name的输出不是一个原子操作,这里逐个输出name的每个字符 80 for (int i = 0; i < name.length(); i++) { 81 System.out.print(name.charAt(i)); 82 } 83 } 84 }
volatile 无法保证对变量操作的原子性
1 public class VolatileTest { 2 private volatile int inc = 0; 3 4 public static void main(String[] args) { 5 final VolatileTest volatileTest = new VolatileTest(); 6 for (int i = 0; i < 1000; i++) { 7 new Thread() { 8 @Override 9 public void run() { 10 try { 11 Thread.sleep(1); 12 volatileTest.inc(); 13 } catch (Exception e) { 14 e.printStackTrace(); 15 } 16 } 17 }.start(); 18 } 19 20 while (Thread.activeCount() > 1){ 21 // 保证前面的线程都执行完 22 Thread.yield(); 23 } 24 System.out.println(volatileTest.inc); 25 } 26 27 private void inc() { 28 inc++; 29 } 30 }
运行结果并不是1000,volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。
线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值是10,然后进行加1操作,并把11写入工作内存,最后写入主存。然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。那么两个线程分别进行了一次自增操作后,inc只增加了1。
1 //线程1 2 boolean stop = false; 3 while(!stop){ 4 doSomething(); 5 } 6 7 //线程2 8 stop = true;
上述代码当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
若加上volatile,会强制将修改的值立即写入主存,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。