并发编程总结之synchronized细节问题
brucewu01
8年前
<p>摘要:本节主要介绍了并发编程下怎么避免数据脏读和什么是synchronized的可重入锁,synchronized的可重入锁的几种使用场景下,是线程安全的。</p> <h2><strong>脏读</strong></h2> <h3><strong>什么是脏读</strong></h3> <p>对于对象的同步和异步方法,我们在设计程序,一定要考虑问题的整体性,不然会出现数据不一致的错误,最经典的错误就是脏读(DirtyRead)。</p> <h3><strong>示例Code</strong></h3> <pre> <code class="language-java">/** * 业务整体需要使用完整的synchronized,保持业务的原子性。 * * @authorxujin * */ publicclassDirtyRead{ privateString username ="xujin"; privateString password ="123"; publicsynchronizedvoidsetValue(String username, String password){ this.username = username; try{ Thread.sleep(2000); } catch(InterruptedException e) { e.printStackTrace(); } this.password = password; System.out.println("setValue最终结果:username = "+ username +" , password = "+ password); } //①这里getValue没有加synchronized修饰 publicvoidgetValue(){ System.out.println("getValue方法得到:username = "+this.username +" , password = "+this.password); } publicstaticvoidmain(String[] args)throwsException{ finalDirtyRead dr =newDirtyRead(); Thread t1 = newThread(newRunnable() { @Override publicvoidrun(){ dr.setValue("张三","456"); } }); t1.start(); Thread.sleep(1000); dr.getValue(); } } </code></pre> <p>上面的Code中,getValue没有加synchronized修饰,打印结果如下,出现脏读</p> <pre> <code class="language-java">getValue方法得到:username = 张三 , password = 123 setValue最终结果:username = 张三 , password = 456 </code></pre> <p>只需在getValue加synchronized修饰,如下:</p> <pre> <code class="language-java">publicsynchronizedvoidgetValue(){ System.out.println("getValue方法得到:username = "+this.username +" , password = "+this.password); } </code></pre> <p>运行结果如下,没有造成数据脏读</p> <pre> <code class="language-java">setValue最终结果:username = 张三 , password = 456 getValue方法得到:username = 张三 , password = 456 </code></pre> <h3><strong>小结</strong></h3> <p>在我们对对象中的一个方法加锁的时候,需要考虑业务的或程序的整体性,也就是为程序中的set和get方法同时加锁synchronized同步关键字,保证业务的(service层)的原子性,不然会出现数据错误,脏读。</p> <h2><strong>synchronized的重入</strong></h2> <h3><strong>什么是synchronized的重入锁</strong></h3> <ol> <li>synchronized,它拥有强制原子性的内置锁机制,是一个重入锁,所以在使用synchronized时,当一个线程请求得到一个对象锁后再次请求此对象锁,可以再次得到该对象锁,就是说在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以拿到锁。</li> <li>当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞.</li> </ol> <p>简单的说:关键字synchronized具有 锁重入 的功能,也就是在使用 synchronized时 , 当一个线程 得到一个 对象锁 的 锁后 , 再次请求此对象时 可以 再次 得到该 对象对应的锁 。</p> <h3><strong>嵌套调用关系synchronized的重入</strong></h3> <p>嵌套调用关系synchronized的重入也是线程安全的,下面是method1,method2,method3都被synchronized修饰,调用关系method1–>method2–>method3,也是线程安全的。</p> <pre> <code class="language-java">/** * synchronized的重入 * * @authorxujin * */ publicclassSyncReenTrant{ publicsynchronizedvoidmethod1(){ System.out.println("method1.."); method2(); } publicsynchronizedvoidmethod2(){ System.out.println("method2.."); method3(); } publicsynchronizedvoidmethod3(){ System.out.println("method3.."); } publicstaticvoidmain(String[] args){ finalSyncReenTrant sd =newSyncReenTrant(); Thread t1 = newThread(newRunnable() { @Override publicvoidrun(){ sd.method1(); } }); t1.start(); } </code></pre> <p>运行结果如下:</p> <pre> <code class="language-java">method1.. method2.. method3.. </code></pre> <h3><strong>继承关系的synchronized的重入</strong></h3> <p>简单 Code1:</p> <pre> <code class="language-java">public class Son extends Father { public synchronized void doSomething() { System.out.println("child.doSomething()"); // 调用自己类中其他的synchronized方法 doAnotherThing(); } private synchronized void doAnotherThing() { // 调用父类的synchronized方法 super.doSomething(); System.out.println("child.doAnotherThing()"); } public static void main(String[] args) { Son child = new Son(); child.doSomething(); } } class Father { public synchronized void doSomething() { System.out.println("father.doSomething()"); } } </code></pre> <p>运行结果:</p> <pre> <code class="language-java">child.doSomething() father.doSomething() child.doAnotherThing() </code></pre> <ol> <li>这里的对象锁只有一个,就是child对象的锁,当执行child.doSomething时,该线程获得child对象的锁,在doSomething方法内执行doAnotherThing时再次请求child对象的锁,因为synchronized是重入锁,所以可以得到该锁,继续在doAnotherThing里执行父类的doSomething方法时第三次请求child对象的锁,同理可得到,如果不是重入锁的话,那这后面这两次请求锁将会被一直阻塞,从而导致死锁。</li> <li>所以在Java内部,同一线程在调用自己类中其他synchronized方法/块或调用父类的synchronized方法/块都不会阻碍该线程的执行,就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入。因为java线程是基于“每线程(per-thread)”,而不是基于“每调用(per-invocation)”的(java中线程获得对象锁的操作是以每线程为粒度的,per-invocation互斥体获得对象锁的操作是以每调用作为粒度的)</li> </ol> <p>我们再来看看重入锁是怎么实现可重入性的,其实现方法是为每个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁。</p> <pre> <code class="language-java">publicclassSyncExtends{ // 父类 staticclassFather{ publicinti =10; publicsynchronizedvoidoperationSup(){ try{ i--; System.out.println("Father print i = "+ i); Thread.sleep(100); } catch(InterruptedException e) { e.printStackTrace(); } } } // 子类继承父类 staticclassSonextendsFather{ publicsynchronizedvoidoperationSub(){ try{ while(i >0) { i--; System.out.println("Son print i = "+ i); Thread.sleep(100); this.operationSup(); } } catch(InterruptedException e) { e.printStackTrace(); } } } publicstaticvoidmain(String[] args){ Thread t1 = newThread(newRunnable() { @Override publicvoidrun(){ Son sub = newSon(); sub.operationSub(); } }); t1.start(); } } </code></pre> <p>运行结果如下:</p> <pre> <code class="language-java">Son print i = 9 Father print i = 8 Son print i = 7 Father print i = 6 Son print i = 5 Father print i = 4 Son print i = 3 Father print i = 2 Son print i = 1 Father print i = 0 </code></pre> <p>参考文章:</p> <p><a href="/misc/goto?guid=4959726719893332765" rel="nofollow,noindex">http://blog.csdn.net/aigoogle/article/details/29893667</a></p> <p> </p> <p> </p> <p>来自:http://blog.xujin.org/2016/11/26/bf/bf-synchronized/</p> <p> </p>