指的是不能被线程调度机制中断的操作,它会在上下文切换之前执行完毕。由于read,load,store,write,use,assign都能够保证原子性,故对一个基本类型变量的访问和赋值可以看作原子操作。对于synchronized块之间的操作也具有原子性。
x = 1; // 具有原子性 y = x; // 2个指令,use了x的值,再assign到y x++; // 4个指令,use了x的值,生成常数1,x加1,再assign到x指的是当一个线程修改了共享变量值,其他线程能够立即得知这个修改。
普通变量的修改首先发生在本线程的工作内存中,这会导致各个工作内存的不一致性。当一个线程结束后会将各自的工作内存同步回主内存,另一个线程读取这个变量时会从主内存中读取它的新值。volatile变量也是同样的过程,只是它修改后立即同步回主内存,并通知其他工作内存中的此变量失效。如果其他线程需要使用此变量时,只能从主内存中重新读取它的新值。这就保证了多线程下的变量可见性。synchronized同步块也具有可见性。这是由于对一个变量unlock之前,必须先将它同步回主内存中。final修饰的变量也具有可见性。当一个final变量被初始化后(构造器完成之前没有将this引用传递出去),此变量在其他线程中可见。指的是机器会对指令进行重排序来达到运行时的优化。这就导致了代码书写上的先后顺序不能在执行时得到保证,但是在单线程内看,程序执行的结果和按照串行执行的结果保持一致。而使用多线程时执行结果就不能保证了。Java可以有两种方法保证线程间的有序性
volatile可以防止指令随意的重排序,它的作用相当于一个内存屏障,也就是重排序时不能将内存屏障之后的指令排在它之前。synchronized同步块可以保证有序,是因为同一时刻只允许一个线程对某个变量进行lock操作。因此多个线程只能有序的进入同一个同步块。volatile是最轻量级的同步机制,但是它只保证了被修饰变量的可见性和有序性,而不能保证原子性,从而不能解决很多并发同步问题。
由于volatile能够保证可见性和有序性,唯一不能保证原子性,因此如果一个操作本身具有原子性,那么使用volatile修饰后就可以保证并发的同步性。应用场合有两个
变量的赋值不依赖于它的当前值或别的变量的当前值,即直接使用assign指令而没有使用use指令,具有原子性保证只有一个线程对变量进行修改,而别的线程只进行读取,读取值不一定是最新的,但修改不会出错可以解决所有并发问题,但是容易造成滥用而导致并发性能不高。可以作为方法的修饰符,表示要进入方法时需要获取本对象的锁,也可以使用synchronized(object){...}对代码块加锁,表示要进入代码块时需要获取object的锁。 一旦有一个线程获取了锁而进入了同步块中,所有的其他线程都会进入等锁池而阻塞。这有时会导致不必要的阻塞时间。同时,Java线程是映射到操作系统原生线程上的,阻塞和唤醒都需要从用户态转换到内核态,要花费较多处理器时间。而volatile变量的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
ReentrantLock可以显式的创建,锁定和释放,与synchronized的内建锁复杂但是更灵活,尤其是进入同步块后如果抛出异常,可以进行清理工作。另外还可以实现一些高级功能,包括等待可中断(线程可以放弃等待而做其他事情),公平锁(按照申请锁的时间获取锁),多条件绑定(通过调用newCondition()方法添加多个Condition)。
Lock lock = new ReentrantLock(); lock.lock(); try { ... } finally { lock.unlock(); } ReentrantLock lock = new ReentrantLock(); boolean captured = lock.tryLock(); try { ... } finally { if (captured) lock.unlock(); }加锁属于阻塞同步,即无论共享数据是否真的出现竞争都会加锁,这是悲观的并发策略。而利用硬件指令还可以实现非阻塞同步,这是一种基于冲突检测的乐观并发策略。它可以先操作,如果没有其他线程争用共享数据则操作成功,而如果产生了冲突,则不断重试直到操作成功。这涉及一条处理器指令compare-and-swap(CAS),它具有原子性,表示变量符合旧值时才会用新值更新变量,否则不更新,最后都返回旧值。 例如Java8中AtomicInteger类的incrementAndGet()方法,会用到sun/misc/Unsafe类中的getAndAddInt()方法,其中的compareAndSwapInt()就是CAS操作。下述代码的意义是对于实例var1在偏移var2处的旧值为var5,如果即将要赋值的时候发现获取的值不符合var5(CAS指令操作),说明此时有其他线程已经修改了这个变量,于是继续获取新的var5,直到赋值前获取的值符合var5,则用var5+var4更新var5。
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }阻塞线程比较耗时,是因为挂起和恢复线程都需要转换内核态,而锁定共享数据往往只持续很短的时间。因此有时只需要让线程执行一个忙循环(自旋)等待,但是不放弃处理器执行,就可以获取锁。前提是等待时间不能太长,自适应自旋锁可以调整自旋的时间。
如果代码上要求同步,但是经过逃逸分析发现不可能存在共享数据竞争,因而可以将锁进行消除。有些代码的同步可能不是人为加入的,而是源码自带的。
如果一系列加锁和解锁是对同一个对象连续进行的,就可以将同步范围扩大到整个序列的外部,这样就可以进行一次加锁和解锁了。
认为大部分锁在同步时间内是不存在竞争的。通过利用对象头信息Mark Word和CAS操作判断对象是否加锁,如果没有,则直接进入同步块执行,这个判断过程就是轻量级锁;否则说明的这个加锁的对象有竞争,轻量级锁就要膨胀为重量级锁,也就是使用互斥量的一般锁。 解锁也要使用CAS操作,将原来的Mark Word的记录替换回来,如果替换成功说明在此期间没有线程尝试过获取该锁,从而解锁完成;如果失败,则一定有线程尝试过获取该锁,所以在解锁完成后还要唤醒等锁的线程。
在轻量级锁的基础上,如果没有竞争,线程将CAS操作也取消,且这个偏向锁会偏向第一个获得它的线程。如果执行过程中该锁没有被其他线程获取,则持有偏向锁的线程永远不用同步。但是一旦有线程尝试获取该锁时,偏向模式被撤销,将锁对象恢复为未锁定或者轻量级锁。