彻底理解synchronized关键字

老子叫甜甜 提交于 2020-03-11 12:57:35


先看现象,再做总结。
业务场景,模拟很多人在抢票。

public class SynchronizredDemo {

    static int tickets = 1000;

    public void saleTickets() {
        int i = 1;
        while (i > 0) {
            i--;
            tickets--;
            System.out.println(Thread.currentThread().getName() + "----" + tickets);
        }
    }

    public static void main(String[] args) {
        final SynchronizredDemo demoA = new SynchronizredDemo();
        final long awaitTime = 2 * 10000;
        ExecutorService executorService = new ThreadPoolExecutor(1000, 1000,
                60L, TimeUnit.SECONDS,
                new ArrayBlockingQueue<Runnable>(10));

        for (int j = 0; j < 1000; j++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    demoA.saleTickets();
                }
            });
        }
        try {
            // 告诉线程池,如果所有任务执行完毕则关闭线程池
            executorService.shutdown();
            // 判断线程池是否在限定时间内,或者线程池内线程全部结束
            if (!executorService.awaitTermination(awaitTime, TimeUnit.MILLISECONDS)) {
                // 超时的时候向线程池中所有的线程发出中断(interrupted)。\
                executorService.shutdownNow();
            }
        } catch (InterruptedException e) {
            System.out.println("awaitTermination interrupted: " + e);
        }
        System.out.println("总共剩余" + tickets);
    }
}

将上面代码拷贝到你的IDEA中,运行它,你会发现启动1000个线程抢1000张票,最后剩余往往不是0,会大于0.因为会存在线程的工作内存里面的值不一定是最新的主内存的值。此时你可以在方法saleTickets上加上synchronized关键字,然后运行很多次,你会发现最后剩余票数一定是0。

public synchronized void saleTickets() {
    int i = 1;
    while (i > 0) {
        i--;
        tickets--;
        System.out.println(Thread.currentThread().getName() + "----" + tickets);
    }
}

这种加在方法上的锁 大家都叫方法锁。synchronized再底层是什么东西,可以javap反编译这段代码,但是我一直坚持一个理论就是多去试错API,对其举一反三,然后解决业务问题。而不是扎进底层出不来了。跑题了,言归正传,下面就一步一步怎么去举一反三一个api从而思考的更多。

下面改用每次new 一个线程的方式去抢票 而不是用线程池,因为我想控制每一个的单独的线程来执行额外的方法,来验证一些其他维度上面的思考正确与否。

public class SynchronizredDemo {

    static int tickets = 1000;

    public  void saleTickets() {
        int i = 4;
        while (i > 0) {
            i--;
            tickets--;
            System.out.println(Thread.currentThread().getName() + "----" + tickets);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        final SynchronizredDemo demoA = new SynchronizredDemo();
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                demoA.saleTickets();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            public void run() {
                demoA.saleTickets();
            }
        });
        Thread thread3 = new Thread(new Runnable() {
            public void run() {
                demoA.saleTickets();
            }
        });
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

这次我改为每次抢4张票 启动三个线程.会输出下面情况,如果输出还是按照顺序执行的情况,请多运行几次。

Thread-2----998
Thread-2----996
Thread-1----997
Thread-1----994
Thread-0----998
Thread-0----992
Thread-0----991
Thread-0----990
Thread-1----993
Thread-2----995
Thread-1----989
Thread-2----988

各个线程会存在乱序的情况,如果加上synchronized关键字,不管你执行多少次 都是正确的。

public  void saleTickets() {
    synchronized(this){
        int i = 4;
        while (i > 0) {
            i--;
            tickets--;
            System.out.println(Thread.currentThread().getName() + "----" + tickets);
        }
    }
}

大家可能会发现我这种加锁方式和上面线程池中的代码加锁方式不一样,这种synchronized(this)称为对象锁,获取的是当前对象实例。方法锁其实也是获取的是当前对象实例作为锁,两者唯一区别是粒度不一样而已,一个是锁住当前方法,一个是可以只锁做当前方法的部分代码块。那么重点来了,如果上面代码中再有一个方法,锁与不锁该方法,在线程中调用此方法会有什么结果呢。

public void noSync() {
    System.out.println(Thread.currentThread().getName() + "----" + tickets + "----NoSync");
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
Thread thread3 = new Thread(new Runnable() {
    public void run() {
        demoA.noSync();
        demoA.saleTickets();
    }
});

这里我说下结论 这两种情况你们自己试一下。

没有加synchronized关键字的方法当然不影响,不参与锁竞争,你们其他线程竞争的死去活来和我没关系,我还是能照常执行。

加synchronized方法都必须获得调用该方法的类实例的”锁“方能执行,否则所属线程阻塞,每个类实例对应一把锁。

方法一旦执行,就会独占该锁,一直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,从而重新进入可执行状态。

这种机制确保了同一时刻对于每一个类的实例,其所有声明为synchronized的成员函数中之多只有一个处于可执行状态,从而有效避免了类成员变量的访问冲突。

那么对于上面的例子就是一旦在noSync方法加上synchronized关键字以后,注意我故意在里面加了一个延时5秒钟,那么一旦某个线程执行了这个方法,且处于休眠当中以后,锁得不到释放,那么其他线程想执行其他含有synchronized关键字的方法也执行不了,也得乖乖等待noSync方法释放锁。当然上面说的都是一个实例的情况下,要是多个实例呢?那么就要讨论一下类锁。

public class SynchronizredDemo {

    static int tickets = 1000;

    public synchronized void saleTickets() {
        int i = 4;
        while (i > 0) {
            i--;
            tickets--;
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "----" + tickets);
        }
    }

    public void noSync() {
        System.out.println(Thread.currentThread().getName() + "----" + tickets + "----NoSync");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        final SynchronizredDemo demoA = new SynchronizredDemo();
        final SynchronizredDemo demoB = new SynchronizredDemo();
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                demoA.saleTickets();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            public void run() {
                demoB.saleTickets();
            }
        });
        Thread thread3 = new Thread(new Runnable() {
            public void run() {
                //demoA.noSync();
                demoA.saleTickets();
            }
        });
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

Thread-0----998
Thread-1----998
Thread-1----996
Thread-0----995
Thread-0----994
Thread-1----993
Thread-0----992
Thread-1----991
Thread-2----991
Thread-2----990
Thread-2----989
Thread-2----988
多个实例可以看到即使方法加锁,也是乱序执行的。

加上类锁以后

public class SynchronizredDemo {

    static int tickets = 1000;

    public   void saleTickets() {
        synchronized(SynchronizredDemo.class){
            int i = 4;
            while (i > 0) {
                i--;
                tickets--;
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "----" + tickets);
            }
        }
    }

    public void noSync() {
        System.out.println(Thread.currentThread().getName() + "----" + tickets + "----NoSync");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        final SynchronizredDemo demoA = new SynchronizredDemo();
        final SynchronizredDemo demoB = new SynchronizredDemo();
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                demoA.saleTickets();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            public void run() {
                demoB.saleTickets();
            }
        });
        Thread thread3 = new Thread(new Runnable() {
            public void run() {
                //demoA.noSync();
                demoA.saleTickets();
            }
        });
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

Thread-0----999
Thread-0----998
Thread-0----997
Thread-0----996
Thread-1----995
Thread-1----994
Thread-1----993
Thread-1----992
Thread-2----991
Thread-2----990
Thread-2----989
Thread-2----988

可以看到按照顺序执行的

类锁除了上面那种写法也可以下面这种

public static synchronized void saleTickets() {
    int i = 4;
    while (i > 0) {
        i--;
        tickets--;
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "----" + tickets);
    }
}

效果是一样的

总结

类锁(synchronized修饰静态的方法或者代码块)

由于一个class不论被实例化多少次,其中的静态方法和静态变量在内存中都只有一份。所以,一旦一个静态的方法被声明为synchronized。此类所有的实例对象在调用此方法,共用同一把锁,我们称之为类锁。

前方高能:重点:要考的哦
对象锁是用来控制实例方法之间的同步,而类锁是用来控制静态方法(或者静态变量互斥体)之间的同步的。
类锁只是一个概念上的东西,并不是真实存在的,他只是用来帮助我们理解锁定实例方法和静态方法的区别的。
java类可能会有很多对象,但是只有一个Class(字节码)对象,也就是说类的不同实例之间共享该类的Class对象。Class对象其实也仅仅是1个java对象,只不过有点特殊而已。
由于每个java对象都有1个互斥锁,而类的静态方法是需要Class对象。所以所谓的类锁,只不过是Class对象的锁而已。
获取类的Class对象的方法有好几种,最简单的是[类名.class]的方式。(百度:获取字节码的三种方式)

写在后面

请问是不断的探究关键字底层是怎么实现的好,还是知道上面这种用法好,个人感觉很多人都是去背各种面试题却没有真正的去用代码进行各种试错。我觉得这种试错的能力是非常重要的,尤其是我们这种应用层开发来说,对一个api的真正理解程度非常重要。

 

 

 

 

 

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!