美文网首页
类锁 、对象锁探究

类锁 、对象锁探究

作者: karlsu | 来源:发表于2017-02-26 21:07 被阅读1503次

    熟练掌握多线程编程是程序猿的基本技能之一,很多朋友在平时的工作中,也许用惯了开源库,虽然知道自己写的代码是支持多线程的,却不懂多线程实现的原理。作者差不多也是这种状态,每次遇到问题才去翻资料。今天恰巧又想到了多线程的一个问题,所以得空自己写个demo证实下。

    假如我们有一个工具类Utils,包含两个同步方法,如下:

        public synchronized void makeCall() {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println("makeCall");
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public synchronized void sendMail() {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println("sendMail");
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    

    在main函数开启两个线程分别调用上面两个方法

     public static void main(String[] agrs) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Utils utils = new Utils();
                    utils.makeCall();
                }
            }).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Utils utils = new Utils();
                    utils.sendMail();
                }
            }).start();
        }
    

    结果会是怎样的呢,会存在互斥的问题吗?看下面的结果

    makeCall
    sendMail
    .......
    makeCall
    sendMail
    
    Process finished with exit code 0
    

    从结果来看,并不存在互斥的情况,有些同学有疑问了,Utils的两个方法不是都加锁了吗,为什么没有同步呢?
    我们把调用方法的函数改一改,如下:

     public static void main(String[] agrs) {
           final Utils utils = new Utils();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    utils.makeCall();
                }
            }).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    utils.sendMail();
                }
            }).start();
        }
    

    看下打印结果:

    makeCall
    makeCall
    makeCall
    makeCall
    makeCall
    sendMail
    sendMail
    sendMail
    sendMail
    sendMail
    

    从结果来看,本次调用出现了互斥现象,原因很简单,第一个实验,两个方法都加锁了是没毛病的,问题在于Utils的每个方法的锁是当前对象,对于两个不同的对象,相当于两把不同的锁,当然不存在互斥了。实验二恰好使用的是同一个对象,也就是同一把锁,也就存在互斥行为了。

    继续实验三

    // 将makeCall改成static的
    public static synchronized void makeCall() {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println("makeCall");
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        public static void main(String[] agrs) {
           final Utils utils = new Utils();
            new Thread(new Runnable() {
                @Override
                public void run() {
                // 直接调用Utils的静态方法
                    Utils.makeCall();
                }
            }).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    utils.sendMail();
                }
            }).start();
        }
    

    打印结果如下:

    makeCall
    sendMail
    ........
    makeCall
    sendMail
    

    本次打印结果也不存在互斥现象,在改下代码

    public class Utils {
        //静态同步方法
        public static synchronized void makeCall() {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println("makeCall");
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        //静态同步方法
        public static synchronized void sendMail() {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println("sendMail");
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
     public static void main(String[] agrs) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Utils.makeCall();
                }
            }).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Utils.sendMail();
                }
            }).start();
        }
    
    

    打印结果如下:

    makeCall
    makeCall
    makeCall
    makeCall
    makeCall
    sendMail
    sendMail
    sendMail
    sendMail
    sendMail
    

    从输出来看,出现了互斥现象。实验三将makeCall改成静态同步方法,这把锁是类锁,而sendMail的锁依然是一个对象。在实验四,将两个方法都改成静态同步方法,使用的就是同一把锁了。

    上面的几个实验都是为了引出两个概念,类锁/对象锁。
    对象锁:JVM 在创建对象的时候,默认会给每个对象一把唯一的对象锁,一把钥匙
    类锁:每一个类都是一个对象,每个对象都拥有一个对象锁。

    总结:
    1.对象锁钥匙只能有一把才能互斥,才能保证共享变量的唯一性
    2.在静态方法上的锁,和实例方法上的锁,默认不是同样的,如果同步需要制定两把锁一样。
    3.关于同一个类的方法上的锁,来自于调用该方法的对象,如果调用该方法的对象是相同的,那么锁必然相同,否则就不相同。比如 new A().x() 和 new A().x(),对象不同,锁不同,如果A的单利的,就能互斥。
    4.静态方法加锁,能和所有其他静态方法加锁的进行互斥
    5.静态方法加锁,和xxx.class 锁效果一样,直接属于类的

    延伸一下:既然有了synchronized修饰方法的同步方式,为什么还需要synchronized修饰同步代码块的方式呢?
    当某个线程进入同步方法获得对象锁,那么其他线程访问这里对象的同步方法时,必须等待或者阻塞,这对高并发的系统是致命的。如果某个线程在同步方法里面发生了死循环,那么它就永远不会释放这个对象锁,那么其他线程就要永远的等待。当然同步方法和同步代码块都会有这样的缺陷,只要用了synchronized关键字就会有这样的风险和缺陷。既然避免不了这种缺陷,那么就应该将风险降到最低。这也是同步代码块在某种情况下要优于同步方法的方面。
    例如在某个类的方法里面:这个类里面声明了一个对象实例,

    Object obj=new Object ();
    

    在某个方法里面调用了这个实例的方法obj.program();但是调用这个方法需要进行同步,不能同时有多个线程同时执行调用这个方法。这时如果直接用synchronized修饰调用了obj.program();代码的方法,那么当某个线程进入了这个方法之后,这个对象其他同步方法都不能给其他线程访问了。假如这个方法需要执行的时间很长,那么其他线程会一直阻塞,影响到系统的性能。如果这时用synchronized来修饰代码块:

    synchronized(obj){
    obj.program();
    }
    

    那么这个方法加锁的对象是obj这个对象,跟执行这行代码的对象没有关系,当一个线程执行这个方法时,这对其他同步方法时没有影响的,因为他们持有的锁都完全不一样。

    相关文章

      网友评论

          本文标题:类锁 、对象锁探究

          本文链接:https://www.haomeiwen.com/subject/fglawttx.html