Thread详解6:synchronized的使用(二)

1 脏读

脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是脏数据,依据脏数据所做的操作可能是不正确的。

我这里提脏读是因为我发现有的朋友因为怕出现脏读而给所有的getValue的操作加锁,但是,这并不总是正确的,因为加锁会降低效率。getValue,而不修改数据,一般是不加锁的。但在一些特殊的业务逻辑下,可能确实需要,这个遇到再说,我只是提一下。

2 synchronized锁重入

在讲锁重入之前,我们先来明确几个概念:

  1. 如果给static的成员变量或者方法加synchronized,那么这个锁是与这个类相关的。
  2. 如果给非static的成员变量或者方法加synchronized,那么这个锁是与对象实例相关的,也就是每个对象实例都有一把对应的锁。
  3. 同一个类中,给多个非static方法加 synchronized,它们关联的其实是同一把锁,也就是对象实例的锁。
  4. 一个线程要调用一个对象的synchronized的方法,就必须先获得该对象的锁,如果没有获取,则只能等待这个锁被其他线程释放。

好的,下面问题就来了,我们结合示例代码来看:

public class Son extends Father {

    public static void main(String[] args) {
        Son lw = new Son();
        lw.doSomething();
    }

    public synchronized void doSomething() {
        System.out.println(Thread.currentThread().getName() + ": Son->doSomething()");
        doAnotherThing(); // 调用自己类中其他的synchronized方法
        super.doSomething(); // 调用父类的synchronized方法
    }

    private synchronized void doAnotherThing() {
        System.out.println(Thread.currentThread().getName() + ": Son->doAnotherThing()");
    }
}

class Father {
    public synchronized void doSomething() {
        System.out.println(Thread.currentThread().getName() + ": Father->doSomething()");
    }
}

我们从main方法看起,lw是一个对象实例,lw.doSomething()调用了son的synchronized doSomething()方法,所以这个时候线程main需要获得对象lw的锁。结果son 的doSomething() 要调用 另一个synchronized的方法:doAnotherThing(),这个时候,main线程得再次获取lw对象的锁。如果synchronized 的锁是不可重入的,那么因为这个锁已经被占有了(被它自己),所以第二获得失败;而且根据这个代码的逻辑,这个程序陷入死锁了。我们来运行一下,看看结果:

main: Son->doSomething()
main: Son->doAnotherThing()
main: Father->doSomething()

由这个结果我们可以做出如下总结:

  1. synchronized的锁是可重入的。
  2. 锁重入是指:一个线程在获取了一个锁而且还持有的情况下,再次请求这个锁,它是可以再次获得这个锁的。
  3. (BTW)锁是否可以重入,是跟它的实现逻辑相关的。这里重进入的实现是通过为每个锁关联一个请求计数和一个占有它的线程。

3 synchronized(this)与synchronized方法

这一小节要讲的问题其实在博主的上一篇博文中就埋下了伏笔:【Thread详解5:synchronized的使用(一)(http://blog.csdn.net/cds86333774/article/details/50995063)】。

synchronized方法与synchronized(this)比较的弊端其实没有那么明显,为什么呢? 因为不过是更灵活一些。如果一个方法“很大”,比如要执行很多东西,耗时很长,如果你直接给这么一个“大的”方法加上synchronized修饰,那么程序的效率无疑很低。这个情况下,如果我把这个“大”方法再细分一下,一些资源(代码)是需要同步的,一些不需要,我们就可以在这个“大”方法的内部有选择地添加synchronized(this)代码块,从而起到一定的优化作用,然而在本质上还是一样的,都是给对象实例加锁。我们如果把一个“大”方法分成几个“小”方法,然后有选择地给这些“小”方法添加synchronized,再用一个方法把几个方法包装一下,其实和synchronized(this)一样嘛。所以我觉得一些书中和博客上说“synchronized(this) 比synchronized方法好”,其实是欠妥的。

下面,我用代码来证明一下自己的理论:

package medium;

public class PrintClass {
    /** * 大方法中调用小方法,小方法中有一个是synchronized方法 */
    public void print1() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "的【一般的代码】在打印");
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        printSynchronizedMethod();
    }

    /** * 一个大方法,其中有一段代码是同步代码块 */
    public void print2() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "的【一般的代码】在打印");
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 同步代码块
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                System.out.printf(" %s的【同步的*代码块*】在打印\n", Thread.currentThread().getName());
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /** * 同步方法 */
    synchronized public void printSynchronizedMethod() {
        for (int i = 0; i < 10; i++) {
            System.out.printf(" %s的【同步的*方法*】在打印\n", Thread.currentThread().getName());
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

首先,调用“大方法中调用一般小方法 + synchronized小方法”来编写的print1 并记录输出:

package medium;

public class Test3 extends Thread{
    private PrintClass pClass;
    private String name;

    public Test3(PrintClass pClass, String name) {
        this.pClass = pClass;
        this.name = name;
    }

    @Override
    public void run() {
        super.run();
        if(name.equals("syn-method")){
            pClass.print1();;
        }
        else {
            pClass.print2();;
        }
    }

    public static void main(String[] args) {
        PrintClass pClass = new PrintClass();
        Test3 thread1 = new Test3(pClass, "syn-method");
        thread1.start();
        Test3 thread2 = new Test3(pClass, "syn-method");
        thread2.start();
    }

}

输出为:

Thread-0的【一般的代码】在打印
Thread-1的【一般的代码】在打印
Thread-0的【一般的代码】在打印
Thread-1的【一般的代码】在打印
Thread-0的【一般的代码】在打印
Thread-1的【一般的代码】在打印
Thread-1的【一般的代码】在打印
Thread-0的【一般的代码】在打印
Thread-1的【一般的代码】在打印
Thread-0的【一般的代码】在打印
Thread-0的【一般的代码】在打印
Thread-1的【一般的代码】在打印
Thread-0的【一般的代码】在打印
Thread-1的【一般的代码】在打印
Thread-1的【一般的代码】在打印
Thread-0的【一般的代码】在打印
Thread-0的【一般的代码】在打印
Thread-1的【一般的代码】在打印
Thread-1的【一般的代码】在打印
Thread-0的【一般的代码】在打印
                  Thread-1的【同步的*方法*】在打印
                  Thread-1的【同步的*方法*】在打印
                  Thread-1的【同步的*方法*】在打印
                  Thread-1的【同步的*方法*】在打印
                  Thread-1的【同步的*方法*】在打印
                  Thread-1的【同步的*方法*】在打印
                  Thread-1的【同步的*方法*】在打印
                  Thread-1的【同步的*方法*】在打印
                  Thread-1的【同步的*方法*】在打印
                  Thread-1的【同步的*方法*】在打印
                  Thread-0的【同步的*方法*】在打印
                  Thread-0的【同步的*方法*】在打印
                  Thread-0的【同步的*方法*】在打印
                  Thread-0的【同步的*方法*】在打印
                  Thread-0的【同步的*方法*】在打印
                  Thread-0的【同步的*方法*】在打印
                  Thread-0的【同步的*方法*】在打印
                  Thread-0的【同步的*方法*】在打印
                  Thread-0的【同步的*方法*】在打印
                  Thread-0的【同步的*方法*】在打印

将main方法中的”syn-method”改为其他字符串,从而执行使用代码块编写的print2方法:

    public static void main(String[] args) {
        PrintClass pClass = new PrintClass();
        Test3 thread1 = new Test3(pClass, "XXX");
        thread1.start();
        Test3 thread2 = new Test3(pClass, "XXX");
        thread2.start();
    }

输出:

Thread-0的【一般的代码】在打印
Thread-1的【一般的代码】在打印
Thread-1的【一般的代码】在打印
Thread-0的【一般的代码】在打印
Thread-0的【一般的代码】在打印
Thread-1的【一般的代码】在打印
Thread-1的【一般的代码】在打印
Thread-0的【一般的代码】在打印
Thread-1的【一般的代码】在打印
Thread-0的【一般的代码】在打印
Thread-1的【一般的代码】在打印
Thread-0的【一般的代码】在打印
Thread-1的【一般的代码】在打印
Thread-0的【一般的代码】在打印
Thread-1的【一般的代码】在打印
Thread-0的【一般的代码】在打印
Thread-1的【一般的代码】在打印
Thread-0的【一般的代码】在打印
Thread-1的【一般的代码】在打印
Thread-0的【一般的代码】在打印
                  Thread-1的【同步的*代码块*】在打印
                  Thread-1的【同步的*代码块*】在打印
                  Thread-1的【同步的*代码块*】在打印
                  Thread-1的【同步的*代码块*】在打印
                  Thread-1的【同步的*代码块*】在打印
                  Thread-1的【同步的*代码块*】在打印
                  Thread-1的【同步的*代码块*】在打印
                  Thread-1的【同步的*代码块*】在打印
                  Thread-1的【同步的*代码块*】在打印
                  Thread-1的【同步的*代码块*】在打印
                  Thread-0的【同步的*代码块*】在打印
                  Thread-0的【同步的*代码块*】在打印
                  Thread-0的【同步的*代码块*】在打印
                  Thread-0的【同步的*代码块*】在打印
                  Thread-0的【同步的*代码块*】在打印
                  Thread-0的【同步的*代码块*】在打印
                  Thread-0的【同步的*代码块*】在打印
                  Thread-0的【同步的*代码块*】在打印
                  Thread-0的【同步的*代码块*】在打印
                  Thread-0的【同步的*代码块*】在打印

结论

输出一致,效果一样。

4 synchronized(this)的用法

当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

然而我更想强调的一点是,【当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。】

public class PrintClass {

    /** * 一般的代码 */
    public void printNormally(String s) {
        for (int i = 0; i < 10; i++) {
            System.out.println(s + "的【一般的代码】在打印");
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

    /** * 同步代码块 */
    public void printSynchronizedCode(String s) {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                System.out.printf(" %s的【同步的*代码块*】在打印\n", s);
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
package medium;

public class Test3 extends Thread{
    private PrintClass pClass;
    private String name;

    public Test3(PrintClass pClass, String name) {
        this.pClass = pClass;
        this.name = name;
    }

    @Override
    public void run() {
        super.run();
        if(name.equals("syn")){
            pClass.printSynchronizedCode(name);
        }
        else {
            pClass.printNormally(name);
        }
    }

    public static void main(String[] args) {
        PrintClass pClass = new PrintClass();
        Test3 synThread = new Test3(pClass, "syn");
        Test3 asynThread = new Test3(pClass, "asyn");
        synThread.start();
        asynThread.start();
    }

}

输出

                  syn的【同步】的代码块在打印
asyn的【非同步】的代码块在打印
                  syn的【同步】的代码块在打印
asyn的【非同步】的代码块在打印
                  syn的【同步】的代码块在打印
asyn的【非同步】的代码块在打印
                  syn的【同步】的代码块在打印
asyn的【非同步】的代码块在打印
                  syn的【同步】的代码块在打印
asyn的【非同步】的代码块在打印
                  syn的【同步】的代码块在打印
asyn的【非同步】的代码块在打印
                  syn的【同步】的代码块在打印
asyn的【非同步】的代码块在打印
                  syn的【同步】的代码块在打印
asyn的【非同步】的代码块在打印
                  syn的【同步】的代码块在打印
asyn的【非同步】的代码块在打印
                  syn的【同步】的代码块在打印
asyn的【非同步】的代码块在打印

可以明显看出,syn线程在调用pClass的同步代码块的时候,asyn线程依然可以调用pClass的非同步的代码。这一点很重要。

你可能感兴趣的:(同步代码块,同步方法,弊端,synchroniz)