一 volatile
1,不用volatile关键字
首先,让我们了解一下JAVA的内存模型。
从图中可以看出:
①每个线程都有一个自己的本地内存空间--线程栈空间。线程执行时,先把变量从主内存读取到线程自己的本地内存空间,然后再对该变量进行操作
②对该变量操作完后,在某个时间再把变量刷新回主内存
会发生的问题:可能造成一个线程在主内存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
例:
public class RunThread extends Thread { private boolean isRunning = true; public boolean isRunning() { return isRunning; } public void setRunning(boolean isRunning) { this.isRunning = isRunning; } @Override public void run() { System.out.println("进入到run方法中了"); while (isRunning == true) { } System.out.println("线程执行完成了"); } } class Run { public static void main(String[] args) { try { RunThread thread = new RunThread(); thread.start(); Thread.sleep(1000); thread.setRunning(false); } catch (InterruptedException e) { e.printStackTrace(); } } }
测试结果:线程始终没有结束,一直堵塞在while循环里面,而main线程已经改变了isRunnig变量,说明RunThread线程读取不到 isRunnig 变量的变化。
2.用了volatile关键字
继续使用上述的测试代码,给 isRunnig变量前面增加 volatile 关键字,发现测试结果正常。
这里涉及到 volatile 关键字保证可见性的特性。
使用了volatile关键字的变量,会使处理器引发两件事情:
A 将当前处理器缓存的数据写回系统内存
B 这个写回内存的操作会导致其他处理器里缓存了该内存地址的数据无效。
简而言之:
没有用volatile 之前是这样的
加了volatile之后是这样的,可以保证被volatile修饰的变量的每次改变,都会被其它线程看到,即可见性。
3.volatile关键字的其它特性
volatile关键保证线程的有序性:为了提高执行效率,java中的编译器和处理器可以对指令进行重新排序,重新排序会影响多线程并发的正确性,有序性就是要保证不进行重新排序。
例:
** * 一个简单的展示Happen-Before的例子. * 这里有两个共享变量:a和flag,初始值分别为0和false.在ThreadA中先给a=1,然后flag=true. * 如果按照有序的话,那么在ThreadB中如果if(flag)成功的话,则应该a=1,而a=a*1之后a仍然为1,下方的if(a==0)应该永远不会为真,永远不会打印. * 但实际情况是:在试验100次的情况下会出现0次或几次的打印结果,而试验1000次结果更明显,有十几次打印. */ public class SimpleHappenBefore { /** 这是一个验证结果的变量 */ private static int a=0; /** 这是一个标志位 */ private static boolean flag=false; public static void main(String[] args) throws InterruptedException { //由于多线程情况下未必会试出重排序的结论,所以多试一些次 for(int i=0;i<1000;i++){ ThreadA threadA=new ThreadA(); ThreadB threadB=new ThreadB(); threadA.start(); threadB.start(); //这里等待线程结束后,重置共享变量,以使验证结果的工作变得简单些. threadA.join(); threadB.join(); a=0; flag=false; } } static class ThreadA extends Thread{ @Override public void run(){ a=1; flag=true; } } static class ThreadB extends Thread{ @Override public void run(){ if(flag){ a=a*1; } if(a==0){ System.out.println("ha,a==0"); } } } }
测试结果:在试验100次的情况下会出现0次或几次的打印结果,而试验1000次结果更明显,有十几次打印.并且在1000次的情况下,即使对两个字段添加了volatile,也会出现打印结果,猜想可能是由于B线程优先于A线程运行。
4.volatile关键字不保证原子性。
原子性:是指线程的多个操作是一个整体,不能被分割,要么就不执行,要么就全部执行完,中间不能被打断。
比如,变量的自增操作 i++,分三个步骤:
①从内存中读取出变量 i 的值
②将 i 的值加1
③将 加1 后的值写回内存
这说明 i++ 并不是一个原子操作。因为,它分成了三步,有可能当某个线程执行到了第②时被中断了,那么就意味着只执行了其中的两个步骤,没有全部执行。
例:
class MyThread extends Thread { public volatile static int count; private static void addCount() { for (int i = 0; i < 100; i++) { count++; } System.out.println("count=" + count); } @Override public void run() { addCount(); } } public class RunThread { public static void main(String[] args) { MyThread[] mythreadArray = new MyThread[100]; for (int i = 0; i < 100; i++) { mythreadArray[i] = new MyThread(); } for (int i = 0; i < 100; i++) { mythreadArray[i].start(); } } }
期望的正确的结果应该是 100*100=10000,但是,实际上count并没有达到10000
原因是:volatile修饰的变量并不保证对它的操作(自增)具有原子性。(对于自增操作,可以使用JAVA的原子类AutoicInteger类保证原子自增)
比如,假设 i 自增到 5,线程A从主内存中读取i,值为5,将它存储到自己的线程空间中,执行加1操作,值为6。此时,CPU切换到线程B执行,从主从内存中读取变量i的值。 由于线程A还没有来得及将加1后的结果写回到主内存,线程B就已经从主内存中读取了i,因此,线程B读到的变量 i 值还是5
相当于线程B读取的是已经过时的数据了,从而导致线程不安全性。
此处可能会有疑惑?这不是跟可见性违背了吗?
楼主的理解是:在线程A把值为6这个结果更新到主内存中时,被打断暂停,cpu执行线程B,而线程B从主内存中取出的还是5,导致最终结果小于10000.
二 synchronized
1.三种使用方式
A 修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
B 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。
C 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
synchronized 关键字底层原理属于 JVM 层面
1. synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
2.synchronized 修饰的方法取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
在 Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销
2.synchronized方法的缺点
使用synchronized关键字声明方法有些时候是有很大的弊端的,比如我们有两个线程一个线程A调用同步方法后获得锁,那么另一个线程B获取锁就需要等待A执行完,但是如果说A执行的是一个很费时间的任务的话这样就会很耗时。
通过synchronized同步语句块解决这个问题
3.结论:
1. 其他线程执行对象中synchronized同步方法和synchronized(this)代码块时呈现同步效果(都是获取当前对象的锁);
2. 如果两个线程使用了同一个“对象监视器”,运行结果同步,否则不同步( synchronized (object) {……….} );
3.synchronized关键字加到static静态方法和synchronized(xxx.class)代码块上都是是给Class类上锁,而synchronized关键字加到非static静态方法上是给对象上锁。
4.尽量不要使用synchronized(string),而使用synchronized(object),因为String可能引用的是常量池同一对象,例 String s1 = "a"; String s2="a";
三 volatile 与 synchronized 对比
1 volatile轻量级,只能修饰变量。synchronized重量级,还可修饰方法。所以volatile的性能要好。
2 volatile只能保证数据的可见性,不保证数据的原子性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞。
synchronize不仅保证可见性,而且还保证原子性,多个线程争抢synchronized锁对象时,会出现阻塞。
3.volatile关键字用于解决变量在多个线程之间的可见性,而ynchronized关键字解决的是多个线程之间访问资源的同步性。