synchronized同步锁小记

个类里面有两个synchronized锁的方法,现在在多线程里面调用这两个方法,他们的执行顺序是怎么样的?相信很多人在面试的时候都遇到过这个问题。
源码如下:

public class ExtendsAndIpmlement {
    public synchronized void first(int time){
        System.out.println("1个打印时间:" + System.currentTimeMillis() + "      " + time);
    }
    public synchronized void second(int time){
        System.out.println("2个打印时间:" + System.currentTimeMillis() + "      " + time);
    }
}

调用方式一:直接new线程for循环50次:

public class ExtendsClass {
    static final ExtendsAndIpmlement andIpmlement = new ExtendsAndIpmlement();
    public static void main(String[] args) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 50; j++) {
                        andIpmlement.first(1);
                    }
                }
            }).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 50; j++) {
                        andIpmlement.second(1);
                    }
                }
            }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int j = 0; j < 50; j++) {
                    andIpmlement.second(2);
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int j = 0; j < 50; j++) {
                    andIpmlement.first(2);
                }
            }
        }).start();
    }
}

执行顺序:first—1、second—1、second—2、first—1、second—2、second—1、first—2

可以看出,执行顺序是毫无规律的,也就是说,方法里面的synchronized锁没有达到我们原本以为的效果。几乎每一个方法在执行过程中都被打断,然后被插入别的代码执行,这根我们预期中的加锁后应该是一杆子撸到底执行完后别的才能接着执行的结果不一样。

其实这个结果是正常的,完全是满足synchronized锁功能预期的,为什么?我们先来看看synchronized锁是加在那里的?是加在方法上面的,那么它对应的作用是什么?那就是线程在争取到CPU的执行权限后,在代码执行期间,一直持有对加synchronized锁的方法的占有权限,别的线程无法侵占夺取权限,直到执行完成后才对加synchronized锁的方法占用权限的释放。这个时候别的权限才有机会获取对该方法的执行权限。但这并不影响别的线程获取到其他的方法的调用权限并获得CPU的调度执行。所以就出现了不同方法不同传值状态的交叉执行的情况。

当synchronized锁住一个对象后,别的线程如果也想拿到这个对象的锁,就必须等待这个线程执行完成释放锁,才能再次给对象加锁,这样才达到线程同步的目的。即使两个不同的代码段,都要锁同一个对象,那么这两个代码段也不能在多线程环境下同时运行。

因此上面的执行结果显得比较凌乱的原因也就找到了。

那现在我们再来看看在现场里面通过加类锁的方式调用方法的执行结果:

public class ExtendsClass {
    static final ExtendsAndIpmlement andIpmlement = new ExtendsAndIpmlement();
    public static void main(String[] args) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (ExtendsAndIpmlement.class){
                        for (int j = 0; j < 50; j++) {
                            andIpmlement.first(1);
                        }
                    }
                }
            }).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (ExtendsAndIpmlement.class){
                        for (int j = 0; j < 50; j++) {
                            andIpmlement.second(1);
                        }
                    }
                }
            }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (ExtendsAndIpmlement.class){
                    for (int j = 0; j < 50; j++) {
                        andIpmlement.second(2);
                    }
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (ExtendsAndIpmlement.class){
                    for (int j = 0; j < 50; j++) {
                        andIpmlement.first(2);
                    }
                }
            }
        }).start();
    }
}

执行结果如下:first—1、second—1、first—2、second—2

在这里我们可以清楚地看到,执行顺序不是按照我们开启线程的调用顺序执行的,但是每一个线程调用方法的执行过程是完整连续的,执行过程中并没有被打断,插入执行别的代码片段。至于原因,我们结合synchronized锁关键字不难理解到。

但是,这里敲黑板了,这里看起来是按照我们调用的顺序执行的,但是如果我们多执行几次上面的代码,就会发现执行顺序是在变化的,也就是不是每次都是按照我们调用的顺序来执行的。

但是这里有个有趣的现象:如果我们给ExtendsAndIpmlement.java里面的first和second方法分别加上static关键字,然后在运行,则会发现,无论我们运行多少次,无论我们循环调用多少回,系统都是按照我们调用的顺序执行的,实现了顺序调用的多线程下的同步安全。

现在我们再来看看通过加对象锁的方式调用方法的执行结果:

public class ExtendsClass {
    static final ExtendsAndIpmlement andIpmlement = new ExtendsAndIpmlement();
    public static void main(String[] args) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (andIpmlement){
                        for (int j = 0; j < 50; j++) {
                            andIpmlement.first(1);
                        }
                    }
                }
            }).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (andIpmlement){
                        for (int j = 0; j < 50; j++) {
                            andIpmlement.second(1);
                        }
                    }
                }
            }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (andIpmlement){
                    for (int j = 0; j < 50; j++) {
                        andIpmlement.second(2);
                    }
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (andIpmlement){
                    for (int j = 0; j < 50; j++) {
                        andIpmlement.first(2);
                    }
                }
            }
        }).start();
    }
}

执行结果如下:first—1、second—1、second—2、first—2

执行的顺序是按照我们开启线程的调用顺序来执行的,而且每一个线程调用方法的执行过程是完整连续的,执行过程中并没有被打断,插入执行别的代码片段。

我们可以得到这样的结论:类锁和对象锁是可以保证多并发条件下的安全性的,但是不能保证按照我们调用的顺序来执行。

所以我们在用synchronized关键字的时候,能缩小代码段的范围就尽量缩小,能在代码段上加同步就不要再整个方法上加同步。这叫减小锁的粒度,使代码更大程度的并发。原因是基于以上的思想,锁的代码段太长了,别的线程是不是要等很久。

因此我们可以得出结论:单单只是在方法加synchronized锁关键字,并不一定能实现多并发条件下的线程安全的需求。我们得根据具体的需求来考虑如何加锁。

原创文章 118 获赞 149 访问量 9万+

猜你喜欢

转载自blog.csdn.net/haoyuegongzi/article/details/105822074