1 /** 2 * synchronized 修饰实例⽅法 3 */ 4 public synchronized void increase() { 5 i++; 6 }
1 public class Demo21 { 2 public static int i; 3 private synchronized static void method() { 4 System.out.println("我是类锁的第⼀种形式:static形式,我叫" + Thread.currentThread().getName()); 5 try { 6 Thread.sleep(1000); 7 for (int j = 0; j < 100000; j++) { 8 i++; 9 } 10 } catch (InterruptedException e) { 11 e.printStackTrace(); 12 } 13 System.out.println(Thread.currentThread().getName() + "1=" + i); 14 } 15 }
1 private void method2() { 2 synchronized (Demo21.class) { 3 // synchronized (this) { 4 System.out.println("我是类锁的第⼀种形式:static形式,我叫" + 5 Thread.currentThread().getName()); 6 try { 7 Thread.sleep(1000); 8 for (int j = 0; j < 100000; j++) { 9 i++; 10 } 11 } catch (InterruptedException e) { 12 e.printStackTrace(); 13 } 14 System.out.println(Thread.currentThread().getName() + "1=" + i); 15 } 16 }synchronized 关键字的底层原理
⾸先切换到类的对应⽬录执⾏ javac XXX.java 命令⽣成编译后的.class ⽂件,然后执⾏ javap -c -s -v -l XXX.class。
synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执⾏ monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于 每个Java对象的对象头中,synchronized 锁便是通过这种⽅式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。 当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执⾏monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外⼀个线程释放为⽌。
⾸先切换到类的对应⽬录执⾏ javac XXX.java 命令⽣成编译后的 .class ⽂件,然后执⾏ javap -c -s -v -l XXX.class。
synchronized 修饰的⽅法并没有 monitorenter 指令和 monitorexit 指令,取得代之的却是ACC_SYNCHRONIZED 标识,该标识指明了该⽅法是⼀个同步⽅法,JVM 通过该ACC_SYNCHRONIZED 访问标志来辨别⼀个⽅法是否声明为同步⽅法,从⽽执⾏相应的同
步调⽤。 synchronized JDK1.6之后的性能优化 锁主要存在四中状态,依次是:⽆锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈⽽逐渐升级。注意锁可以升级不可降级,这种策略是为了提⾼获得锁和释放锁的效率。引⼊偏向锁的⽬的和引⼊轻量级锁的⽬的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使⽤操作系统互斥量产⽣的性能消耗。但是不同是:轻量级锁在⽆竞争的情况下使⽤CAS 操作去代替使⽤互斥量。⽽偏向锁在⽆竞争的情况下会把整个同步都消除掉。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁,竞争成果后为轻量级锁如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层⾯挂起,还会进⾏⼀项称为⾃旋锁的优化⼿段。
互斥同步对性能最⼤的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转⼊内核态中完成(⽤户态转换到内核态会耗费时间)。
⼀般线程持有锁的时间都不是太⻓,所以仅仅为了这⼀点时间去挂起线程/恢复线程是得不偿失的。 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后⾯来的请求获取锁的线程等待⼀会⽽不被挂起呢?看看持有锁的线程是否很快就会释放锁”。为了让⼀个线程等待,我们只需要让线程执⾏⼀个忙循环(⾃旋),这项技术就叫做⾃旋。而自适应自旋就是自旋的次数不是固定的,是自适应的。锁消除理解起来很简单,它指的就是虚拟机即使编译器在运⾏时,如果检测到那些共享数据不可能存在竞争,那么就执⾏锁消除。锁消除可以节省毫⽆意义的请求锁的时间。
原则上,我们在编写代码的时候,总是推荐将同步块的作⽤范围限制得尽量⼩,——直在共享数据的实际作⽤域才进⾏同步,这样是为了使得需要同步的操作数量尽可能变⼩,如果存在锁竞争,那等待线程也能尽快拿到锁。
⼤部分情况下,上⾯的原则都是没有问题的,但是如果⼀系列的连续操作都对同⼀个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。 Synchronized 和 ReenTrantLock 的对⽐volatile与可⻅性 可⻅性是指当多个线程访问同⼀个变量时,⼀个线程修改了这个变量的值,其他线程能够⽴即看得到修改的值。 Java内存模型规定了所有的变量都存储在主内存中,每条线程还有⾃⼰的⼯作内存,线程的⼯作内存中保存了该线程中是⽤到的变量的主内存副本拷⻉,线程对变量的所有操作都必须在⼯作内存中进⾏,⽽不能直接读写主内存。不同的线程之间也⽆法直接访问对⽅⼯作内存中的变量,线程间变量的传递均需要⾃⼰的⼯作内存和主存之间进⾏数据同步进⾏。所以,就可能出现线程1改了某个变量的值,但是线程2不可⻅的情况。 Java中的volatile关键字提供了⼀个功能,那就是被其修饰的变量在被修改后可以⽴即同步到主内存,被其修饰的变量在每次是⽤之前都从主内存刷新。因此,可以使⽤volatile来保证多线程操作时变量的可⻅性。
volatile与内存屏障 我们都知道,为了性能优化,JMM(java内存模型)在不改变正确语义的前提下,会允许编译器和处理器对指令序列进⾏重排序,那如果想阻⽌重排序要怎么办了?答案是可以添加内存屏障。JMM内存屏障分为四类: StoreStore屏障:禁⽌上⾯的普通写和下⾯的volatile写重排序; StoreLoad屏障:防⽌上⾯的volatile写与下⾯可能有的volatile读/写重排序; LoadLoad屏障:禁⽌下⾯所有的普通读操作和上⾯的volatile读重排序; LoadStore屏障:禁⽌下⾯所有的普通写操作和上⾯的volatile读重排序;
为了实现volatile内存语义时,编译器在⽣成字节码时,会在指令序列中插⼊内存屏障来禁⽌特定类型的处理器重排序 1. 在每个volatile写操作的前⾯插⼊⼀个StoreStore屏障; 2. 在每个volatile写操作的后⾯插⼊⼀个StoreLoad屏障; 3. 在每个volatile读操作的后⾯插⼊⼀个LoadLoad屏障; 4. 在每个volatile读操作的后⾯插⼊⼀个LoadStore屏障。 需要注意的是:volatile写是在前⾯和后⾯分别插⼊内存屏障,⽽volatile读操作是在后⾯插⼊两个内存屏障
不可变对象
让并发编程变得更简单 说到并发编程,可能很多朋友都会觉得最苦恼的事情就是如何处理共享资源的互斥访问,可能稍不留神,就会导致代码上线后出现莫名其妙的问题,⼤多数情况下,对于资源互斥访问的场景,都是采⽤加锁的⽅式来实现对资源的串⾏访问, 来保证并发安全,如synchronize关键字,Lock锁等。但是这种⽅案最⼤的⼀个难点在于: 在进⾏加锁和解锁时需要⾮常地慎重。如果加锁或者解锁时机稍有⼀点偏差,就可能会引发重⼤问题,然⽽这个问题Java编译器⽆法发现。既然采⽤串⾏⽅式来访问共享资源这么容易出现问题,那么有没有其他办法来解决呢? 事实上,引起线程安全问题的根本原因在于:多个线程需要同时访问同⼀个共享资源。 假如没有共享资源,那么多线程安全问题就⾃然解决了,Java中提供的ThreadLocal机制就是采取的这种思想。 然⽽⼤多数时候,线程间是需要使⽤共享资源互通信息的,如果共享资源在创建之后就完全不再变更,如同⼀个常量,⽽多个线程间并发读取该共享资源是不会存在线上安全问题的,因为所有线程⽆论何时读取该共享资源,总是能获取到⼀致的、完整的资源状态。 不可变对象就是这样⼀种在创建之后就不再变更的对象,这种特性使得它们天⽣⽀持线程安全,让并发编程变得更简单。 很多时候⼀些很严重的bug是由于⼀个很⼩的副作⽤引起的,并且由于副作⽤通常不容易被察觉,所以很难在编写代码以及代码review过程中发现,并且即使发现了也可能会花费很⼤的精⼒才能定位出来。 如何创建不可变对象
final
线程不安全类 | 线程安全类 |
StringBuilder | StringBuffer |
SimpleDateFormat | JodaTime |
ArrayList | CopyOnWriteArrayList |
HashSet,TreeSet | CopyOnWriteArraySet,ConcurrentSkipListSet |
HashMap,TreeMap | ConcurrentHashMap,ConcurrentSkipListMap |
。。。 | 。。。 |
实现原理:CopyOnWriterArrayList 允许并发的读,读操作是⽆锁的,性能较⾼。写操作的话,⽐如向容器增加⼀个元素,则⾸先将当前容器复制⼀份,然后在新副本上执⾏写操作,结束之后再将原容器的引⽤指向新容器。
优点:读操作性能很⾼,因为⽆需任何同步措施,⽐较适⽤于读多写少的并发场景。Java 的 list 在遍历时,若中途有其他线程对容器进⾏修改,则会抛出ConcurrentModificationException 异常。⽽CopyOnWriteArrayList由于其“读写分离”的思想,遍历和修改操作分别作⽤在不同的 list容器,所以迭代的时候不会抛出 ConcurrentModificationExecption 异常了。 缺点: 缺点也很明显,⼀是内存占⽤问题,毕竟每次执⾏写操作都要将原容器拷⻉⼀份,数据量⼤时,对内存压⼒较⼤,甚⾄可能引起频繁GC,⼆是⽆法保证实时性,Vector 对读写操作均加锁同步,可以保证容器的读写强⼀致性,CopyOnWriteArrayList由于其实现策略的原因,写和读分别作⽤于不容容器上,在写的过程中,读是不会发⽣阻塞的,未切换索引置新容器时,是读不到刚写⼊的数据的。 总结:读旧的,写新的,写完指针指向新的,旧的没有被引用就会被GC