synchronized锁住什么?锁的范围?

synchronized关键字简单地讲,可以使用这一简单的关键字来保证线程安全。但是我们首先得知道synchronized到底锁住的是什么?锁住的范围又是什么?

这里测试实验仅用两个类:
Test.java类,里面只有一个函数,在线程中调用的函数。
Main.java类,整个实验的测试入口

Tips

  • 这里的代码有试探性实验的意味,所以正确与否需要查看对应的代码分析

版本1:synchronized锁住函数

这里使用最简单的synchronized 锁住整个函数,也就是类似如下的使用方式:

public synchronized void test(){
    // 代码段
}

下面是Test.java

public class Test {

    public synchronized void test(int i) {

        System.out.println("第"+i+"个函数块开始运行...");

        //System.out.println("第"+i+"try-catch代码块开始..");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //System.out.println("第"+i+"try-catch代码块结束..");

        System.out.println("第"+i+"函数块结束运行...");

    }
}

Main.java:

public class Main {

    public static void main(String[] args) {

        for (int i = 0; i < 3; i++) {

            final int k = i;

            new Thread(()->{
                new Test().test(k+1);
            }).start();

        }
    }
}

先看我电脑运行后的结果(各函数的顺序不一定是一样的,但是两个部分是一致的:三个开始,三个结束):

1个函数块开始运行...2个函数块开始运行...3个函数块开始运行...2函数块结束运行...3函数块结束运行...1函数块结束运行...

Main.java中,我们知道,接连创建三个线程,三个线程中运行三个不同Test对象的函数。通过结果,看到,由于线程的Thread.sleep(3000)睡眠3秒。CPU调度到其它线程运行了,所以才出现三个函数同时运行。

我们想要的效果是:一个线程进入synchronized函数,另一个线程想进入,必须等到正在占有锁的线程运行完。
那么这里的synchronized显然不是符合我们想要的效果。

我们先不做解释,再看下面的代码修改。下文会进行解释。

版本2:synchronized (this)锁

// Test类,修改成synchronized(this)锁代码块
public class Test {

    public  void test(int i) {

        synchronized(this) {

            System.out.println("第" + i + "个函数块开始运行...");

            //System.out.println("第"+i+"try-catch代码块开始..");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //System.out.println("第"+i+"try-catch代码块结束..");

            System.out.println("第" + i + "函数块结束运行...");
            
        }
    }
}
// Main.java不做任何修改
public class Main {

    public static void main(String[] args) {

        for (int i = 0; i < 3; i++) {

            final int k = i;

            new Thread(()->{
                new Test().test(k+1);
            }).start();

        }
    }
}

测试运行,发现与版本1实现的效果没有什么差别:

2个函数块开始运行...3个函数块开始运行...1个函数块开始运行...3函数块结束运行...1函数块结束运行...2函数块结束运行...

接着看下面,如何解决。

版本3:synchronized锁使用指南

我们发现,对Test类的修改,没有任何效果。
如下进行改进:
重点查看Main.java的改变

// 版本1的Test类,synchronized锁住函数
public class Test {

    public synchronized void test(int i) {

        System.out.println("第" + i + "个函数块开始运行...");

        //System.out.println("第"+i+"try-catch代码块开始..");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //System.out.println("第"+i+"try-catch代码块结束..");

        System.out.println("第" + i + "函数块结束运行...");

    }

}

// Main.java 
public class Main {

    public static void main(String[] args) {

        final Test testObject = new Test();    // 重点在这里哈

        for (int i = 0; i < 3; i++) {

            final int k = i;

            new Thread(()->{
                testObject.test(k+1);
            }).start();

        }
    }
}

我们看到,这一次我们在三个线程中执行的是同一个对象的带锁方法。
我的电脑得到的结果如下(顺序不一定一定是如下所示的,但是结果总是会有如下的结构:同一个线程函数运行的开始与结束成对):

1个函数块开始运行...1函数块结束运行...3个函数块开始运行...3函数块结束运行...2个函数块开始运行...2函数块结束运行...

我们看到如上的结果,都是必须在一个函数运行结束之后才能开始新线程函数的运行。

小结:synchronized函数作用范围

从上面我们可以看到,带有synchronized锁的函数,它锁的是某个对象的特定函数

也就是说,synchronized函数不会对一个类的不同对象起作用。

通过下面的例子我们知道,它确实是锁的特定对象特定函数

// Test.java
public class Test {

    public synchronized void test(int i) {


            System.out.println("第" + i + "个函数块开始运行...");


            // System.out.println("第"+i+"try-catch代码块开始..");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // System.out.println("第"+i+"try-catch代码块结束..");

            System.out.println("第" + i + "函数块结束运行...");


    }

    public void test1(){
        System.out.println("Test1");
    }

}
// Main.java
public class Main {

    public static void main(String[] args) {


        final Test testObject = new Test();

        for (int i = 0; i < 3; i++) {

            final int k = i;

            new Thread(()->{
                if((k+1)%2 == 0){
                    testObject.test(k+1);
                } else {
                    testObject.test1();
                }
                //
            }).start();

        }
    }
}

我电脑的运行结果如下,据此我们知道synchronized函数锁,它锁的并不是整个对象,而是此对象的此synchronized函数

Test1
第2个函数块开始运行...
Test1
第2函数块结束运行...

下面的试验也很重要:
先上代码:

// Test, 注意:这里有两个synchronized方法
public class Test {

    public synchronized void test(int i) {

        System.out.println("第" + i + "个函数块开始运行...");

        // System.out.println("第"+i+"try-catch代码块开始..");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //System.out.println("第"+i+"try-catch代码块结束..");

        System.out.println("第" + i + "函数块结束运行...");
    }

    public synchronized void test1(int i){
        System.out.println("第" + i + "个test1开始...");
        // ...
        System.out.println("第" + i + "个test1开始...");
    }

}
// Main,使用同一个对象,但是线程执行的函数有不同的地方
// 这里for循环开10个线程
public class Main {

    public static void main(String[] args) {

        final Test testObj = new Test();

        for (int i = 0; i < 10; i++) {

            final int k = i;

            new Thread(()->{
                if(((k+1)%2)==0) {
                    testObj.test(k + 1);
                } else {
                    testObj.test1(k+1);
                }
            }).start();

        }
    }
}

先看结果:

1个test1开始...1个test1开始...3个test1开始...3个test1开始...2个函数块开始运行...2函数块结束运行...6个函数块开始运行...6函数块结束运行...10个函数块开始运行...10函数块结束运行...9个test1开始...9个test1开始...8个函数块开始运行...8函数块结束运行...7个test1开始...7个test1开始...5个test1开始...5个test1开始...4个函数块开始运行...4函数块结束运行...

开十个线程,奇数号线程执行test1,偶数号线程执行test。通过结果我们能得出如下结论:多个线程执行同一对象的synchronized方法;无论这些方法是否是同一个,都只能有一个线程能执行该对象的同步方法。

通过对并发的理解,我们知道,平常我们定义一个如下所示的最简单的类:一个成员变量,加上与之配套的getter,setter。注意,这个类并不是线程安全的, 因为 过期数据问题:如果一个线程正在调用setter, 而另一个线程此时正在调用getter,它可能就看不到更新的数据了。

// NotThreadSafe
public class MutableInteger {
    private int value;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}

通过上面的分析,我们知道经过如下的修改,可以得到一个线程安全的类:

// ThreadSafe
public class MutableInteger {
    private int value;
    
    // 注意,添加关键字synchronized
    public synchronized int getValue() {
        return value;
    }
    
    // 注意,添加关键字synchronized
    public synchronized void setValue(int value) {
        this.value = value;
    }
}

版本4:synchronized(this)锁

再回到下面的代码:

// Test.java
public class Test {

    public  void test(int i) {

        synchronized(this) {
            System.out.println("第" + i + "个函数块开始运行...");

            //System.out.println("第"+i+"try-catch代码块开始..");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //System.out.println("第"+i+"try-catch代码块结束..");

            System.out.println("第" + i + "函数块结束运行...");
        }
    }

}
// Main.java
public class Main {

    public static void main(String[] args) {


        final Test testObject = new Test();

        for (int i = 0; i < 3; i++) {

            final int k = i;

            new Thread(()->{
                testObject.test(k+1);
            }).start();

        }
    }
}

通过之前的研究,我们很容易知道上面的结果符合我们的预期。

我们将Test.java改为如下所示:,看结果又什么不同(注意代码注释的变化):

// Test.java 修改
public class Test {

    public  void test(int i) {

        System.out.println("第" + i + "个函数块开始运行...");

        synchronized(this) {
            System.out.println("第"+i+"try-catch代码块开始..");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("第"+i+"try-catch代码块结束..");
        }

        System.out.println("第" + i + "函数块结束运行...");
    }
}

这里特意让synchronized(this) {}代码块括起来的为函数中间的一部分。
我得到的结果如下:

1个函数块开始运行...1try-catch代码块开始..2个函数块开始运行...3个函数块开始运行...1try-catch代码块结束..1函数块结束运行...3try-catch代码块开始..3try-catch代码块结束..3函数块结束运行...2try-catch代码块开始..2try-catch代码块结束..2函数块结束运行...

得到的结果很乱,但是我们要从中得到的信息就是:synchronized(this)锁住的为对象的某个代码块。也就是说,不在代码块内的代码,对象在多线程环境下可以任意运行,但只能有一个线程能拥有执行synchronized(this){}代码块的锁。

版本5:synchronized(Class)锁

通过前面的例子,我们知道synchronized(this){}锁住的为特定对象的特定代码块。

现在我们介绍下面的方法,锁住特定类的特定代码段。

注意Test类的注释//, Main.java的改动

Test类

public class Test {

    public  void test(int i) {

        //System.out.println("第" + i + "个函数块开始运行...");

        synchronized(Test.class) {
            System.out.println("第"+i+"try-catch代码块开始..");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("第"+i+"try-catch代码块结束..");
        }

        //System.out.println("第" + i + "函数块结束运行...");

    }
}

Main类:

public class Main {

    public static void main(String[] args) {

        for (int i = 0; i < 3; i++) {

            final int k = i;

            new Thread(()->{
                new Test().test(k+1);
            }).start();

        }
    }
}

运行的结果可能如下:

1try-catch代码块开始..1try-catch代码块结束..3try-catch代码块开始..3try-catch代码块结束..2try-catch代码块开始..2try-catch代码块结束..

分析:

看到Main.java的一段核心代码:

new Thread(()->{
    new Test().test(k+1);
}).start();

可以看到,每个线程运行的都是不同的Test对象。但是却能够达到我们的对锁的预期。
看到Test类的核心操作:

synchronized(Test.class) {
    // 代码段,省略
}

synchronized(Test.class)锁的是 整个Test类代码段 。两个关键词:
整个Test类代码段

也就是说,线程中只要是Test类的对象,只能有一个线程的Test对象能够访问synchronized(Test.class){}代码段

我们去掉Test类的注释代码段:

public class Test {

    public  void test(int i) {

        System.out.println("第" + i + "个函数块开始运行...");

        synchronized(Test.class) {
            System.out.println("第"+i+"try-catch代码块开始..");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("第"+i+"try-catch代码块结束..");
        }

        System.out.println("第" + i + "函数块结束运行...");

    }
}

运行后得到的结果可能如下:

1个函数块开始运行...1try-catch代码块开始..2个函数块开始运行...3个函数块开始运行...1try-catch代码块结束..3try-catch代码块开始..1函数块结束运行...3try-catch代码块结束..3函数块结束运行...2try-catch代码块开始..2try-catch代码块结束..2函数块结束运行...

运行结果很乱,但是我们能够提取到关键的信息:
synchronized(Test.class) {}代码块之外的代码,不同线程的Test对象可以任意执行;但是synchronized(Test.class) {}代码块之内的代码一次只能由一个线程进入并执行完全。

我们过滤一下信息,可以看到如下的效果:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX第1个函数块开始运行...1try-catch代码块开始..
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX第2个函数块开始运行...
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX第3个函数块开始运行...1try-catch代码块结束..3try-catch代码块开始..
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX第1函数块结束运行...3try-catch代码块结束..
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX第3函数块结束运行...2try-catch代码块开始..2try-catch代码块结束..
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX第2函数块结束运行...

现在结果是否很清晰了呢?

参考

synchronized锁住的是代码还是对象

你可能感兴趣的:(并发编程,并发编程,Java并发编程,synchronized)