java并发编程——四 互斥机制(锁) 原理及对比
来源:互联网 发布:阿里云系统盘多大合适 编辑:程序博客网 时间:2024/06/05 21:06
多线程共享资源,比如一个对象的内存,怎样保证多个线程不会同时访问(读取或写入)这个对象,这就是并发最大的难题,因此产生了 互斥机制(锁)。
synchronized
When should you synchronize? Apply Brian’s Rule of Synchronization:
If you are writing a variable that might next be read by another
thread, or reading a variable that might have last been written by
another thread, you must use synchronization, and further, both the
reader and the writer must synchronize using the same monitor lock.
作用:
可见性:
获取锁后,该线程本地存储失效,临界区(就是获得锁后释放锁之前 的代码区)从主存获取数据,并在释放锁后刷入主存。
互斥:
保证临界区代码线程间互斥。
synchronized实现同步的基础:
java中每个对象都可以作为锁
具体表现(代码实例):
public class myTest { public static synchronized void inc() throws IOException { System.out.println("when call incBlocked(),this method watis myTest.class lock "); } public static synchronized void incBlocked() throws Exception { System.in.read();// Blocked } public synchronized void inc2Blocked() throws IOException { System.in.read();// Blocked } public synchronized void inc2() throws IOException { System.out.println(" inc2() enter"); }}
- 对于普通synchronized方法,锁是当前实例对象’.
如果是不同对象,则每个线程有自己的对象的锁,线程间彼此互不干预。
public static void main(String[] args) throws InterruptedException { Thread t3 = new Thread(new Runnable() { @Override public void run() { myTest t = new myTest(); try { t.inc2Blocked(); } catch (Exception e) { e.printStackTrace(); } } }); t3.start(); TimeUnit.SECONDS.sleep(1); Thread t4 = new Thread(new Runnable() { @Override public void run() { myTest t = new myTest(); try { t.inc2(); } catch (Exception e) { e.printStackTrace(); } } }); t4.start(); System.out.println("main Thread end"); }//t3线程锁住的只是t3线程中的对象myTest t = new myTest();//t4线程中的锁,锁住的是t4线程中的新对象,不会因为t3中的阻塞(myTest对象锁)而导致t4等待
- 对于静态同步方法,锁是当前类的Class对象
多个线程中只有一个静态方法可以执行,因为锁住了同一个类的 Class对象
Thread t = new Thread(new Runnable() { @Override public void run() { try { myTest.incBlocked(); } catch (Exception e) { e.printStackTrace(); } } }); t.start(); TimeUnit.SECONDS.sleep(1); // 静态锁,锁住了myTest.class,当调用 myTest.inc()时,等待myTest.class Thread t2 = new Thread(new Runnable() { @Override public void run() { // myTest t = new myTest(); try { myTest.inc(); } catch (Exception e) { e.printStackTrace(); } } }); t2.start();
- 静态同步方法与非静态synchronized方法相互之间不影响(即不存在锁的强占,因为一个是myTest.class锁,另一个是当前对象的锁)
public static void main(String[] args) throws InterruptedException { Thread t = new Thread(new Runnable() { @Override public void run() { try { myTest.incBlocked();//forever blocked } catch (Exception e) { e.printStackTrace(); } } }); t.start(); TimeUnit.SECONDS.sleep(1); Thread t4 = new Thread(new Runnable() { @Override public void run() { myTest t = new myTest(); try { t.inc2();//get t lock and enter } catch (Exception e) { e.printStackTrace(); } } }); t4.start();
- 对于synchronized(obj)要看清楚什么对象已经用于锁定(synchronized后面括号的内容)。在同一个对象上进行同步的线程将彼此阻塞,在不同对象上锁定的线程将永远不会彼此阻塞。
synchronized锁原理
持有Monitor对象,通过进入、退出这个Monitor对象来实现锁机制,使用 monitorenter指令 与 moniterexit指令
以synchronized代码块为例,代码编译后,monitorenter指令插入到synchronized代码块开始的位置,moniterexit指令插入到方法结束或异常结束的地方.JVM保证monitorenter指令 与 moniterexit指令 一一对应,任何对象都有一个monitor与之关联,当且一个monitor对象被持有时,处于锁状态。当一个对象锁被持有,执行monitorenter指令,另一个线程试图获取这个monitor的所有权,直到前一个线程执行完monitorexit后,才允许获取!
public class Synchronized { public static void main(String[] args) { synchronized (Synchronized.class) { } m(); } public static synchronized void m() { }}
javap -v Synchronized.class:
synchronized 块是通过插入monitorenter,monitorexit完成同步的;而synchronized方法是使用ACC_SYNCHRONIZED标志位。其实本质上都是,通过获取每个对象的monitor对象,实现互斥性可见性!
............public static void main(java.lang.String[]); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: ldc #1 // class com/four/Synchronized 2: dup 3: monitorenter 4: monitorexit 5: invokestatic #16 // Method m:()V 8: return LineNumberTable: line 5: 0 line 8: 5 line 9: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String; public static synchronized void m(); flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED Code: stack=0, locals=0, args_size=0 0: return LineNumberTable: line 12: 0 LocalVariableTable: Start Length Slot Name Signature } ............
synchronized用的锁存在于对象头中,每个对象的对象头部都会存放hashcode,分代年龄,锁标记。略
一个任务可以多次获得锁,比如在一个线程中调用一个对象的 synchronized标记的方法,在这个方法中调用第二个synchronized标记的方法,然后在第二个synchronized方法中调用第三个synchronized方法。一个线程每次进入一个synchronized方法中JVM都会跟踪加锁的次数,每次+1,当该这个方法执行完毕,JVM计数-1;当JVM计数为0时,锁完全被释放,其他线程可以访问该变量。
在使用并发时将对象的field设为private 很重要!尤其是使用static变量(evil static variable)
show you the code :
public class BeanOne { static Date d = new Date();}
public class Test { private synchronized void setD(BeanOne one) { one.d = null; Thread.yield(); one.d = new Date(); } public static void main(String[] args) { final BeanOne one = new BeanOne(); final Test t = new Test(); ExecutorService ex = Executors.newCachedThreadPool(); for (int i = 0; i < 1000; i++) { ex.execute(new Runnable() { @Override public void run() { t.setD(one); } }); ex.execute(new Runnable() { @Override public void run() { t.setD(one); } }); ex.execute(new Runnable() { @Override public void run() { System.out.println(one.d); } }); } }}
output:Thu Feb 25 00:11:26 CST 2016nullnullnullnull
显示锁
注意:return 语句放在try中,以避免过早的释放锁;JDOC推荐lock.lock后跟try{}finally{}
参考上篇文中的例子,我们用显示锁实现互斥机制:
private Lock lock = new ReentrantLock(); public int next() { lock.lock(); try { ++currentEvenValue; // Danger point here! Thread.yield(); // 加快线程切换 ++currentEvenValue; return currentEvenValue; } finally { lock.unlock(); } }
使用 Lock lock =new ReentrantLock()的问题是代码不够优雅,增加代码量;我们一般都是使用synchronized实现互斥机制。但是1.当代码中抛出异常时,显示锁的finally里可以进行资源清理工作。2.ReentrantLock还给我们更细粒度的控制力
public class AttemptLocking { private Lock lock = new ReentrantLock(); private void untimed() { // lock.lock(); boolean captured = lock.tryLock(); try { System.out.println("tryLock() " + captured); } finally { if (captured) { lock.unlock(); } } } private void timed() { boolean captured = false; try { captured = lock.tryLock(2, TimeUnit.SECONDS); } catch (Exception e) { throw new RuntimeException(e); } try { System.out.println("lock.tryLock(2, TimeUnit.SECONDS) " + captured); } finally { if (captured) { lock.unlock(); } } } public static void main(String[] args) { final AttemptLocking attemptLocking = new AttemptLocking(); attemptLocking.untimed(); attemptLocking.timed(); new Thread() { { setDaemon(true); } public void run() { attemptLocking.lock.tryLock(); System.out.println("acquired"); } }.start(); Thread.yield(); attemptLocking.untimed(); attemptLocking.timed(); }}
Atomic&Volatie
什么是原子性(Atomic):不会被线程调度机制中断的操作,一旦操作开始,就会在线程上下文切换之前完成操作.
原子性应用于除了long\double之外其他的基本数据类型,因为long\double 是64bit ,JVM对于64bit会当作两个32bit的操作来执行,那么在这两个执行直接可能会发生上下文切换。
当我们给 long\double 加上 volatile,可以保证原子性操作,仅限于读、写操作,比如long l=0;l++,++操作就是典型的非原子性操作,因为“++”操作其实是一个读操作与一个写操作的组合操作!
volatile
保证共享变量的“可见性”(一个线程修改这个共享变量时,另一个线程可以读取到修改的值),某些情况下使用恰当的话,比synchronized性能更好,因为它不会竞争锁,就不会引起上下文切换。
原理解析
用volatile修饰后的变量,转化为汇编语言后,会多出“lock add1…. ”的指令,该指令会引发两件事情:
1.将当前处理器缓存行的数据写入系统主存
2.写回主存操作使其他cpu中缓存了该内存的数据失效(详见下文,volatile内存语义)
为了提高处理速度,cpu先将系统内存的数据放到内部缓存(L1,L2,L3…)中,然后再进行操作,下次会存在缓存命中(cache hit).如果变量声明了volatile,写操作时,JVM会向cpu发送一条lock前缀命令,会将该数据直接写入内存中,并使其他处理器缓存该变量的内存地址失效,保证缓存的一致性。
多个线程去访问一个非volatile域,并且不用synchronized,其中一个任务修改了这个域,很可能这时只是把这个“修改”放入了处理器缓存中,而非主内存。其他线程,可能并不会读到这个域的修改值(读操作发生在主内存!)。所以可以使用volatile去保证每次修改,都会把最新的值刷入主内存(或者你也可以使用synchronized去给每个访问这个域的方法加锁,synchronized也可以保证修改刷到主内存)!
当一个volatile域依赖于它之前的值(如++i 这种递增),或者它依赖于其他变量,volatile就无法工作了。 建议使用 synchronized而非 volatile.请看下边的例子:
慎重依赖基本类型的“原子性”
class CircularSet { private int[] array; private int len; private int index = 0; public CircularSet(int size) { array = new int[size]; len = size; for (int i = 0; i < size; i++) { array[i] = -1; } } public synchronized void add(int i) { array[index] = i; index = ++index % len; } public synchronized boolean contains(int val) { for (int i = 0; i < len; i++) { if (array[i] == val) { return Boolean.TRUE; } } return Boolean.FALSE; }}public class SerialNumberChecker { private static final int SIZE = 10; private static CircularSet serials = new CircularSet(1000); private static ExecutorService exec = Executors.newCachedThreadPool(); static class SerialChecker implements Runnable { @Override public void run() { while (Boolean.TRUE) { int serial = SerialNumberGenerator.nextSerialNumber(); if (serials.contains(serial)) { System.out.println("Duplicate:" + serial); System.exit(0); } serials.add(serial); } } } public static void main(String[] args) { // 是个线程同时对SerialNumberGenerator的域serialNumber进行读写操作; // 如果serialNumber++是原子性的,程序不会中断 for (int i = 0; i < SIZE; i++) { exec.execute(new SerialChecker()); } }}
public class SerialNumberGenerator { // 使用volatile保证 serialNumber的可见性(值改变变后刷入主内存) private static volatile int serialNumber = 0; public static int nextSerialNumber() { // serialNumber++你认为是原子性吗? return serialNumber++; }}//Outp:Duplicate:3954
以上例子证明了,volatile 基本变量 自增时,并无法保证原子性!所以,
1.一般情况下使用synchronized 而非 volatile. 2. ++操作是非原子性的,典型的读写组合操作
public class Atomicity {int i;void f1() { i++; }void f2() { i += 3; }} /* Output: (Sample)...//字节码:void f1();Code:0: aload_01: dup2: getfield #2; //Field i:I 首先,get5: iconst_16: iadd7: putfield #2; //Field i:I 经过几个步骤,最后put10: returnvoid f2();Code:0: aload_01: dup2: getfield #2; //Field i:I 首先,get5: iconst_36: iadd7: putfield #2; //Field i:I 经过几个步骤,最后put10: return*///:~
原子性
cpu角度的原子性实现:
总线锁定
当一个线程在cpu1中执行i++操作时,会锁定系统内存与各个cpu之间的通信——总线,保证cup1执行i++操作时其他任务不会改变主存中i的值。但是在这个锁定期间其他任何指令都不会执行.缓存锁定
当一个线程在cpu1中执行i++操作时,不会锁定系统内存与各个cpu之间的通信,会锁定这个数据缓存数据的内存地址,只允许cpu1的i++操作写入主存,阻止其他任务(cpu2 :i++)改变这个数据(缓存一致性),同时使其他cpu缓存失效
java原子性实现
- 使用CAS循环
伪代码:
for (;;) { currentValue = getValue();// 获取当前数据,位操作原子性 boolean success = compareAndSet(currentValue, currentValue++);// 如果currentValue未改变(currentValue==getValue()),那么用currentValue++(新值)替换currentValue if (success) { break; } }
Volatile内存语义
public class VolatileTest { volatile long i = 0; private long get() { return i; } private void set(long i) { this.i = i; } private void addOne() { i++; }}等同于:class VolatileTest2 { long i = 0; private synchronized long get() {//原子性 return i; } private synchronized void set(long i) {//原子性 this.i = i; } private void addOne() {// i++并没有加方法锁,该操作不是原子性的 int tempI = get(); tempI += 1L; set(tempI); }}
总结:volatile
原子性:volatile变量,读写操作原子性,但对于++i这种复合操作并非原子性
可见性:volatile变量的读操作总是可以看到最后一次写操作的更新数据。
volatile写操作内存语义:把该线程的本地存储刷入主存(与释放锁的内存语义相同)
volatile读操作内存语义:把该线程对应的本地存储置为无效,从主存中读取。(与获取锁的内存语义相同)
synchornized 与 volatile 的比较
- synchornized与volatile共同点:
保证数据的可见性(读取主存); - synchornized缺点:
1 synchornized 会引发锁竞争,导致上下文切换,影响性能,volatile不会.
2 synchronized 因为锁竞争,有引发死锁、饿死等多线程问题,volatile不会. - volatile缺点:
1 volatile保证可见性但不保证原子性(如i++),synchronized保证可见性同时保证原子性
2 仅限于在变量级别使用,而synchronized用法更广泛
http://www.javaperformancetuning.com/news/qotm051.shtml
Lock(显式锁)与synchronized(隐式锁) 的对比
( tryLock()) 非阻塞的获取锁,也可设置等待时间
synchronized悲观锁的并发策略,获得独占锁,所谓独占锁就是一个线程进入synchronized后获得锁,其他线程如果也想获得这个锁只有进入(wait())阻塞状态,阻塞状态会引发上下文切换,当较多线程竞争,会产生频繁上下文切换。
Lock实现乐观锁的策略,使用CAS算法, 不会产生线程的阻塞(lock.lockInterruptibly())与synchronized不同,获取到锁的线程如果中断,捕获interrupted异常,同时释放锁
当代码中抛出异常时,显示锁的finally里可以进行资源清理工作。
- Lock还给我们更细粒度的控制力
http://www.it610.com/article/2267333.htm
JDK原子类
原子操作:不可中断的一个或一组操作
Atomiclnteger, AtomicLong, AtomicReference
还是建议使用 synchronized 或Lock,上述原子类使用,详见JDK Document
待续………
- java并发编程——四 互斥机制(锁) 原理及对比
- Java并发编程-互斥(四)
- Java并发编程(四)《锁原理》
- Java并发编程(4)-互斥
- Java并发机制及锁的实现原理
- Java并发机制及锁的实现原理
- Java并发机制及锁的实现原理
- 《Java并发编程的艺术》笔记二——Java并发机制的底层实现原理.md
- Java并发编程系列之一:并发机制的底层原理
- Java并发编程系列之一:并发机制的底层原理
- 【并发编程】Java中断机制——协作式中断含义及应用
- Java并发编程——线程安全及解决机制简介
- 【java并发】传统线程互斥技术—synchronized
- Java并发编程之同步互斥问题
- Java并发编程之同步互斥问题
- Java并发编程之线程互斥笔记
- Java高并发编程:定时器、互斥、同步通信技术
- 《Java并发编程的艺术》笔记三——锁的升级与对比.md
- uva 10313 Pay the Price
- BZOJ2310 parkii (插头DP)
- 第十二章编程练习(6)
- redis集群搭建示例
- mongodb 3.2 实战(一)非关系型数据库设计,如何进行mongo的数据库设计?
- java并发编程——四 互斥机制(锁) 原理及对比
- EasyDarwin开发出类似于美拍、秒拍的短视频拍摄SDK:EasyVideoRecorder
- Connector of Dynamics CRM and AX: Leads, Opportunity, and Sales quotations
- 如何在maven的pom.xml中添加本地jar包
- leetcode-1-two sum
- canvas小知识
- Ngnix配置问题,图片的security.limit_extensions问题
- Java对存储过程的调用方法
- debian8.3安装为知笔记的两种方式,ppa源和编译安装,