美文网首页Android技术栈
燃烧吧!我的并发之魂--synchronized

燃烧吧!我的并发之魂--synchronized

作者: e4e52c116681 | 来源:发表于2018-12-29 15:18 被阅读4次

    零、前言

    经历了两个月的沉淀,感觉整体能力有所提升,最近除了年终总结也没有什么打算了
    高并发这块一致是我的心病,在这年尾,抽刀,奋力一击吧
    虽然会用线程,但是总感觉有很多地方让我挺烦心,比如并发和那两个关键字
    曾经三次想要突破掉多线程,但都失败了,只好暂时离开,现在的我感觉应该可以了
    本文按照慕课网免费课程敲的,同时也加入了我大量的思考和绘图,希望对你有所帮助


    一、多线程的简单回顾

    1.入门级

    下面WhatIsWrong实现Runnable,并提供一个静态实例对象计时器i
    run方法让i自加10W次,下面的结果是多少?

    public class WhatIsWrong implements Runnable {
        static WhatIsWrong instance = new WhatIsWrong();
        static int i = 0;
        public static void main(String[] args) {
            Thread thread_1 = new Thread(instance);
            Thread thread_2 = new Thread(instance);
            thread_1.start();
            thread_2.start();
            System.out.println(i);
        }
        @Override
        public void run() {
            for (int j = 0; j < 100000; j++) {
                i++;
            }
        }
    }
    

    答案是0,简单讲一下:main中代码顺序执行虽然线程1,2都开启了,
    但是程序还是顺序执行的,会立刻走System.out.println(i);,实际看来run方法要慢一些

    简单分析.png
    2.如何让打印在两个线程完成后才调用

    两个方法:1)让主线程先睡一会、2)使用线程对象的join方法
    总之就是推迟System.out.println(i);的执行时间

    2.1:让主线程先睡一会

    这个方法很容易想到,但睡多久不好把握,一般小测试1s应该够了

    线程休眠.png
    public class WhatIsWrong implements Runnable {
        static WhatIsWrong instance = new WhatIsWrong();
        static int i = 0;
    
        public static void main(String[] args) {
            Thread thread_1 = new Thread(instance);
            Thread thread_2 = new Thread(instance);
            thread_1.start();
            thread_2.start();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i);
        }
    
        @Override
        public void run() {
            for (int j = 0; j < 100000; j++) {
                i++;
            }
        }
    }
    

    2.2.join方法

    正规的还是用join吧,他会让该线程先行,使以System.out.println(i);被推后

    线程join.png
    public class WhatIsWrong implements Runnable {
        static WhatIsWrong instance = new WhatIsWrong();
        static int i = 0;
    
        public static void main(String[] args) {
            Thread thread_1 = new Thread(instance);
            Thread thread_2 = new Thread(instance);
            thread_1.start();
            thread_2.start();
            try {
                thread_1.join();
                thread_2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i);
        }
    
        @Override
        public void run() {
            for (int j = 0; j < 100000; j++) {
                i++;
            }
        }
    }
    
    

    3.结果呢?
    3.1下面是十次结果
    137355  、 114412 、115381 、128482 、151021 、
    109093 、  128610 、128144 、122390 、123746
    

    3.2从中能看出什么?

    1).每次执行结果都不一样
    2).它们都大于100000且小于200000


    3.3为什么

    理论上两个线程,每个线程加100000次,一共应该200000才对
    想象一下这个场景:

    有两个神枪手对着靶子打(必中),每把枪有100000颗软子弹
    靶子有感应器和计数器,当软子弹接触靶子那一刻,计数器加1  
    当两个子弹同时接触时,感应器有无法及时反应,只会被记录一次,即计数器只+1
    然后两人疯狂扫射,最后看靶子上计数器最终的数值  
    
    可想而知最后计数器上的数应该是小于200000的,所以代码中也类似
    两个线程便是神枪手,run的时候开始同时疯狂扫射,i便是靶子
    

    3.4:i++发生了什么?

    1)内存中读取i的值,2)i=i+1,3)将结果写回内存
    i=9时,若线程2已经在第三步了,但还没写入内存。这时线程1进入,读出i的值仍是9,
    从而导致此次结束两个结果都是10,这就是为什么达不到200000的原因
    这就相当于两个神枪手同时开枪,靶子未及时反应而导致两颗同弹

    i++发生了什么.png

    不同步会出现什么状况.png

    4.怎么解决呢?

    先看问题出在哪,是两个人同时开枪对一个靶子
    一个人是不能在同一时刻发出两法子弹的,so,方法1:
    准备两个靶子,各自统计(像每个足球运动员一个足球一样,10000个人怎么办,然并卵)
    方法2:不允许两个人同时开枪,这便是synchronized
    神枪手1在扫射时,神射手2的枪自动锁死,如果100条线程也是类似,某一刻只能一人开枪

    public class WhatIsWrong implements Runnable {
        static WhatIsWrong instance = new WhatIsWrong();
        static int i = 0;
    
        public static void main(String[] args) {
            Thread thread_1 = new Thread(instance);
            Thread thread_2 = new Thread(instance);
            thread_1.start();
            thread_2.start();
    
            try {
                thread_1.join();
                thread_2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i);//200000
        }
    
        @Override
        public synchronized void run() {//只需轻轻加一个synchronized即可
            for (int j = 0; j < 100000; j++) {
                i++;
            }
        }
    }
    

    二、同步锁

    几种锁.png
    0.测试代码(此时还未同步)

    先看一下干了什么事:线程创建不说了,run方法中:
    打印信息-->当前线程睡三秒-->打印运行结束(如下图)
    根据时间线可以看出来打印结果(可以看出两个人一起睡了,这还得了...)

    非同步分析.png
    public class SynObj_Block implements Runnable {
        static SynObj_Block instance = new SynObj_Block();
    
        public static void main(String[] args) {
            Thread thread_1 = new Thread(instance);
            Thread thread_2 = new Thread(instance);
            thread_1.start();
            thread_2.start();
            try {
                thread_1.join();
                thread_2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("All Finished");
        }
    
        @Override
        public void run() {
            System.out.println("对象锁,代码块形式--name:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("运行结束,name:" + Thread.currentThread().getName());
        }
    }
    

    打印结果:

    对象锁,代码块形式--name:Thread-1
    对象锁,代码块形式--name:Thread-0
    运行结束,name:Thread-0
    运行结束,name:Thread-1
    All Finished
    

    1.对象锁之同步代码块锁

    上面说两个线程一起睡了,线程1先睡,线程2进来也睡了,能忍吗?不能忍!
    快把哥的四十米大刀,不对,是大锁拿来,在我睡觉前先把门锁上
    线程1进来睡,然后把门锁上,线程2就进不来,只能等线程1把锁打开

    同步代码块.png
    1.1:同步代码块的添加

    其他代码不变,就不贴了

    @Override
    public void run() {
        synchronized (this) {
            System.out.println("对象锁,代码块形式--name:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("运行结束,name:" + Thread.currentThread().getName());
        }
    }
    

    1.2:运行结果

    可见线程1睡完,线程2才能进来睡

    对象锁,代码块形式--name:Thread-0
    运行结束,name:Thread-0
    对象锁,代码块形式--name:Thread-1
    运行结束,name:Thread-1
    All Finished
    

    1.3:锁对象

    等等,这this是什么鬼?--有点基础的都知道是当前类对象

    System.out.println(this);// top.toly.并发.SynObj_Block@77c89a74
    
    同步代码块synchronized()接收一个对象,该对象可任意指定:
    Object lock = new Object();
    synchronized (lock) {//TODO}  
    新建一个对象也可以
    

    1.4:多把锁

    也会你会说:既然随便一个对象都可以当做锁对象,Java自己给内置个呗
    还传个参数,累不累人。等等,存在即合理,且看下面...
    想一下如果一个房间两张床,你上来把门锁了,岂不是不合理?
    那该怎么办?两扇门,两把不同的锁呗(就像两个人合租一间大房子一样)
    你可以根据图中时间线好好想想(画个图也不是那么容易的...且看且珍惜)

    两把锁.png
    /**
     * 作者:张风捷特烈
     * 时间:2018/12/28 0028:19:16
     * 邮箱:1981462002@qq.com
     * 说明:对象锁--代码块锁
     */
    public class SynObj_Block implements Runnable {
        static SynObj_Block instance = new SynObj_Block();
        Object lock1 = new Object();//第一把锁
        Object lock2 = new Object();//第二把锁
        public static void main(String[] args) {
            Thread thread_1 = new Thread(instance);
            Thread thread_2 = new Thread(instance);
            thread_1.start();
            thread_2.start();
    
    
            try {
                thread_1.join();
                thread_2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("All Finished");
        }
    
        @Override
        public void run() {
            synchronized (lock1) {
                System.out.println("lock1开始--name:" + Thread.currentThread().getName());
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("lock1结束,name:" + Thread.currentThread().getName());
            }
            synchronized (lock2) {
                System.out.println("lock2开始,代码块形式--name:" + Thread.currentThread().getName());
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("lock2结束,name:" + Thread.currentThread().getName());
            }
        }
    }
    
    对象锁lock1,代码块形式--name:Thread-0
    lock1睡醒了,name:Thread-0
    对象锁lock1,代码块形式--name:Thread-1
    对象锁lock2,代码块形式--name:Thread-0
    lock1睡醒了,name:Thread-1
    lock2睡醒了,name:Thread-0
    对象锁lock2,代码块形式--name:Thread-1
    lock2睡醒了,name:Thread-1
    All Finished
    

    有什么好处?两人合租房有什么好处,多把锁就有什么好处。
    可看出既完成任务,又减少了2秒,这也就两个线程而已
    如果百万级的线程数,哪怕微小的效率提升都是有价值的


    2.对象锁之普通方法锁

    正如1.4所想:我就是想简单的加个锁,每次同步代码块还有传个对象,挺烦的
    所以有一个叫方法锁,什么对象每个类都有?答案:this,方法锁的对象默认是this

    2.1:使用
    /**
     * 作者:张风捷特烈
     * 时间:2018/12/28 0028:19:16
     * 邮箱:1981462002@qq.com
     * 说明:对象锁--普通方法锁
     */
    public class SynObj_Method implements Runnable {
        static SynObj_Method instance = new SynObj_Method();
    
        public static void main(String[] args) {
            Thread thread_1 = new Thread(instance);
            Thread thread_2 = new Thread(instance);
            thread_1.start();
            thread_2.start();
            try {
                thread_1.join();
                thread_2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("All Finished");
        }
    
        @Override
        public void run() {
            sleep3ms();
        }
    
        public synchronized void sleep3ms() {
            System.out.println("方法锁测试--name:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("结束,name:" + Thread.currentThread().getName());
        }
    }
    

    2.2:打印结果

    和同步代码块一致

    方法锁测试--name:Thread-0
    结束,name:Thread-0
    方法锁测试--name:Thread-1
    结束,name:Thread-1
    All Finished
    

    2.3:如何证明方法锁的锁对象是this

    你说this就this?何以见得?

    @Override
    public void run() {
        sleep3ms();
        synchronized (this){
            System.out.println("测试开始--name:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("测试结束,name:" + Thread.currentThread().getName());
        }
    }
    
    public synchronized void sleep3ms() {
        System.out.println("方法锁测试--name:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("结束,name:" + Thread.currentThread().getName());
    }
    
    方法锁开始--name:Thread-0
    方法锁结束,name:Thread-0
    同步代码块测试开始--name:Thread-0
    同步代码块测试结束,name:Thread-0
    方法锁开始--name:Thread-1
    方法锁结束,name:Thread-1
    同步代码块测试开始--name:Thread-1
    同步代码块测试结束,name:Thread-1
    All Finished
    

    加上this同步代码块后:可见开始与结束两两配对
    说明方法锁和同步代码块的this锁是一把锁,也就是只有一扇门,必须一个一个睡


    2.4:反证
    @Override
    public void run() {
        sleep3ms();
        synchronized (""){
            System.out.println("测试开始--name:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("测试结束,name:" + Thread.currentThread().getName());
        }
    }
    
    public synchronized void sleep3ms() {
        System.out.println("方法锁测试--name:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("结束,name:" + Thread.currentThread().getName());
    }
    
    方法锁开始--name:Thread-0
    方法锁结束,name:Thread-0
    方法锁开始--name:Thread-1
    同步代码块测试开始--name:Thread-0
    方法锁结束,name:Thread-1
    同步代码块测试结束,name:Thread-0
    同步代码块测试开始--name:Thread-1
    同步代码块测试结束,name:Thread-1
    All Finished
    

    如果锁不是this,这里简单点用"",可见Thread-0的结束后
    Thread-1的方法锁开始和Thread-0的同步代码块测试开始是同时打印出来的
    说明有两扇门,那两把锁不是同一把,也反向表明,非this会产生两把锁
    综上正反两面,我们可以感受到方法锁的锁对象是this


    3.类锁之静态方法锁(static方法+synchronized)

    说是类锁,实质上是使用了Class对象当做锁,非要较真的话,你可以把他看作对象锁
    Class对象有什么特点:一个类可以有多个对象,但仅有一个Class对象
    这就可以导致:类锁只能在同一时刻被一个对象拥有


    3.1.static方法+synchronized

    普通方法+synchronized但是两个不同的Runnable对象线程

    public class Syn_Static_Method implements Runnable {
        static Syn_Static_Method instance1 = new Syn_Static_Method();
        static Syn_Static_Method instance2 = new Syn_Static_Method();
        public static void main(String[] args) {
            Thread thread_1 = new Thread(instance1);
            Thread thread_2 = new Thread(instance2);
            thread_1.start();
            thread_2.start();
            try {
                thread_1.join();
                thread_2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("All Finished");
        }
        @Override
        public void run() {
            sleep3ms();
        }
        public synchronized void sleep3ms() {
            System.out.println("静态方法锁开始--name:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("静态方法锁开始,name:" + Thread.currentThread().getName());
        }
    }
    

    好吧,用脚趾头想想也知道互不影响
    这相当于两个人有两个家,各自进各自的家睡觉天经地义
    你加synchronized锁你家的门管我什么事,所以synchronized这时并没用处

    静态方法锁开始--name:Thread-1
    静态方法锁开始--name:Thread-0
    静态方法锁开始,name:Thread-0
    静态方法锁开始,name:Thread-1
    All Finished
    

    我们都知道static关键字修饰的方法、变量,是可以令于类名(Class对象)的

    也就是不需要对象便可以运行,由static修饰的方法是不能用this对象的
    这就是为什么加一个static,锁就不同了的原因,至于锁是什么,除了它的老大还有人选吗?

    /**
     * 作者:张风捷特烈
     * 时间:2018/12/28 0028:19:16
     * 邮箱:1981462002@qq.com
     * 说明:对象锁--静态方法锁
     */
    public class Syn_Static_Method implements Runnable {
        //同上...略
        public static synchronized void sleep3ms() {//我就轻轻加个static
           //同上...略
        }
    }
    
    静态方法锁开始--name:Thread-0
    静态方法锁开始,name:Thread-0
    静态方法锁开始--name:Thread-1
    静态方法锁开始,name:Thread-1
    All Finished
    

    符合预期:这样就将一个类给锁起来了,只要是这个类的对象
    都会生效,这也是它的优势,也是static的本意:静态,具有全局控制力


    4.类锁之Class对象锁

    相当于把static+synchronized拆出来

    /**
     * 作者:张风捷特烈
     * 时间:2018/12/28 0028:19:16
     * 邮箱:1981462002@qq.com
     * 说明:对象锁--class锁
     */
    public class Syn_Class implements Runnable {
        static Syn_Class instance1 = new Syn_Class();
        static Syn_Class instance2 = new Syn_Class();
    
        public static void main(String[] args) {
            Thread thread_1 = new Thread(instance1);
            Thread thread_2 = new Thread(instance2);
            thread_1.start();
            thread_2.start();
            try {
                thread_1.join();
                thread_2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("All Finished");
        }
    
        @Override
        public void run() {
            sleep3ms();
        }
    
        public void sleep3ms() {
            synchronized (Syn_Class.class) {
                System.out.println("class锁开始--name:" + Thread.currentThread().getName());
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("class锁开始,name:" + Thread.currentThread().getName());
            }
        }
    }
    

    class锁开始--name:Thread-0
    class锁开始,name:Thread-0
    class锁开始--name:Thread-1
    class锁开始,name:Thread-1
    All Finished
    

    5.现在回头来看

    synchronized:同步的

    官宣:
    同步方法支持一种简单的策略来[防止线程干扰]和[内存一致性错误]:
    如果一个对象变量对多个线程可见,则对它的所有读写都是通过同步方法完成的
    
    民宣:
    保证同一时刻最多只一个线程执行该段代码,来保证并发安全
    

    三、多线程访问方法的一些情况

    感觉有点...麻烦

    1.两个线程访问一个对象的普通同步方法
    2.两个线程访问两个对象的普通同步方法
    3.两个线程访问静态同步方法
    4.两个线程分别访问普通同步方法和非同步方法
    5.两个线程分别访问一个对象的不同普通同步方法
    6.两个线程分别访问静态同步和非静态同步方法
    方法抛出异常后,会释放锁
    

    1.两个线程访问一个对象的普通同步方法

    二-->2中的例子:线程1,2访问一个对象instance的同步方法:sleep3ms
    同一个对象,需要等待锁的释放,才能进入普通同步方法


    2.两个线程访问两个对象的普通同步方法

    二-->3-->3.1中第一个小例子(用脚趾头想的那个)
    同一类的两个不同对象的普通同步方法,对于两个线程而言,同步是无用的


    3.两个线程访问静态同步方法

    二-->3-->3.1第二个小例子,轻轻加了个static
    由于静态同步方法的锁是class,锁对该类的所有对象都有效


    4.两个线程分别访问普通同步方法和非同步方法

    线程2的方法没加锁(非同步方法),就进来睡了呗,也没什么特别的
    注意两头几乎同时执行,测试了几次,两头的先后顺序不定

    两个线程分别访问普通同步方法和非同步方法.png
    /**
     * 作者:张风捷特烈
     * 时间:2018/12/29 0029:11:31
     * 邮箱:1981462002@qq.com
     * 说明:两个线程分别访问普通同步方法和非同步方法
     */
    public class SynOrNot implements Runnable {
        static SynOrNot instance = new SynOrNot();
    
        public static void main(String[] args) {
            Thread thread_1 = new Thread(instance);
            Thread thread_2 = new Thread(instance);
            thread_1.start();
            thread_2.start();
            try {
                thread_1.join();
                thread_2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("All Finished");
        }
    
        @Override
        public void run() {
            if (Thread.currentThread().getName().equals("Thread-0")) {
                sleep3msSync();
            } else {
                sleep3msCommon();
            }
        }
    
        public void sleep3msCommon() {
            System.out.println("非同步方法开始--name:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("非同步方法结束,name:" + Thread.currentThread().getName());
        }
    
        public synchronized void sleep3msSync() {
            System.out.println("同步方法开始--name:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("同步方法结束,name:" + Thread.currentThread().getName());
        }
    }
    
    同步方法开始--name:Thread-0
    非同步方法开始--name:Thread-1
    同步方法结束,name:Thread-0
    非同步方法结束,name:Thread-1
    All Finished
    

    5.两个线程分别访问一个对象的不同普通同步方法

    由于普通同步方法是this锁,所以对不同普通同步方法锁是一致的,都生效

    两个线程分别访问一个对象的不同普通同步方法.png
    /**
     * 作者:张风捷特烈
     * 时间:2018/12/29 0029:11:31
     * 邮箱:1981462002@qq.com
     * 说明:两个线程分别访问一个对象的不同普通同步方法
     */
    public class SynOfTwo implements Runnable {
        static SynOfTwo instance = new SynOfTwo();
        public static void main(String[] args) {
            Thread thread_1 = new Thread(instance);
            Thread thread_2 = new Thread(instance);
            thread_1.start();
            thread_2.start();
            try {
                thread_1.join();
                thread_2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("All Finished");
        }
    
        @Override
        public void run() {
            if (Thread.currentThread().getName().equals("Thread-0")) {
                sleep3msSync1();
            } else {
                sleep3msSync2();
            }
        }
    
        public synchronized void sleep3msSync2() {
            System.out.println("sleep3msSync2方法开始--name:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("sleep3msSync2结束,name:" + Thread.currentThread().getName());
        }
    
        public synchronized void sleep3msSync1() {
            System.out.println("sleep3msSync1开始--name:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("sleep3msSync1结束,name:" + Thread.currentThread().getName());
        }
    }
    
    sleep3msSync1开始--name:Thread-0
    sleep3msSync1结束,name:Thread-0
    sleep3msSync2方法开始--name:Thread-1
    sleep3msSync2结束,name:Thread-1
    All Finished
    

    6.两个线程分别访问静态同步和普通同步方法

    不测试都知道:一个是class锁,一个是this锁,锁不同,不生效
    在第5个的基础上加上static关键字,其余不变,结果不出所料

    两个线程分别访问静态同步和普通同步方法.png
    public static synchronized void sleep3msSync2() {
    
    sleep3msSync1开始--name:Thread-0
    sleep3msSync2方法开始--name:Thread-1
    sleep3msSync2结束,name:Thread-1
    sleep3msSync1结束,name:Thread-0
    All Finished
    

    7.抛出异常后,释放锁

    可以看出线程1抛异常后,线程2是可以正常运行的(说明线程1的锁已经被释放)
    就像线程1在睡觉,睡着睡着仙逝了,房东(JVM)会把它抬走,把锁给下一个人,继续睡...
    在第5个的代码上稍微修改:int a=1/0;//异常

    线程1出现异常.png
    public synchronized void sleep3msSync1() {
        System.out.println("sleep3msSync1开始--name:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
            int a=1/0;//异常
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sleep3msSync1结束,name:" + Thread.currentThread().getName());
    }
    
    sleep3msSync1开始--name:Thread-0
    Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
    sleep3msSync2方法开始--name:Thread-1
        at top.toly.并发.SynOfError.sleep3msSync1(SynOfError.java:54)
        at top.toly.并发.SynOfError.run(SynOfError.java:33)
        at java.base/java.lang.Thread.run(Thread.java:844)
    sleep3msSync2结束,name:Thread-1
    All Finished
    

    一把锁只能由一个线程获取,没拿到锁的线程必须等待
    不同的锁之间互不影响(相当于进不同的门,互不干扰,无需等待)
    无论正常执行还是抛出异常,都会释放锁


    8、synchronized的性质
    可重入:同一线程外层函数获取锁之后,内层函数可以直接再次获取该锁
    好处:避免死锁,提高封装性
    粒度:线程范围  
    即synchronized修饰的同步方法内部`并非只能`调用同步方法
    
    不可中断:比如我线程1要小睡个十万年,那线程2就要在门等上十万年(想走都不行)。
    

    四、Java内存模型(JMM--Java Memory Model)

    1.Java内存模型的概念

    描述Java程序中各变量(线程共享变量)的访问规则,
    即在JVM中将变量存储到内存和从内存中读取变量的底层细节

    1.所有的变量都存储在主内存中,
    2.每条线程都有自己独立的工作内存。其保存该线程用到的变量副本(主内存变量拷贝)。
    
    规定:
    [1]线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。
    [2]线程无法直接访问非己方工作内存中的变量,线程间变量值的传递需要间接通过主内存。
    
    JMM--java内存模型.png
    2.如何:线程1的修改被线程2看到

    1.工作内存1操作共享变量a后刷新到主内存
    2.然后线程2从主内存中读取共享变量a值并拷贝到自己的工作内存

    共享变量可见性.png
    3、synchronized实现可见性

    锁定的线程1所做的任何修改都要在释放锁之前从工作内存刷新到主内存
    线程2拿到锁时从主内存中拷贝需要的变量到自己的工作内存(从而实现共享变量的可见)


    4、缺陷:
    效率低:
    锁的释放情况少(只能自动释放,或异常)
    不能中断等待锁的线程
    
    不灵活:
    加锁和释放锁的时机单一,每个锁只有单一的条件
    无法知道释放成功获取锁
    

    5.注意点
    锁对象不能为空,作用域不宜过大,避免死锁
    |---锁对象的信息是放在对象头中,所以不能为空
    |---作用域过大,导致串行执行的代码变多,效率下降
    
    Lock还是synchronized
    |---尽量使用并发包里的原子类
    |---synchronized能完成的尽量不去Lock
    |---确实需要中断等待、灵活开解锁或Condition可以使用Lock锁
    
    多线程访问同步方法的几种情况
    
    死锁简单演示
    /**
     * 作者:张风捷特烈
     * 时间:2018/12/29 0029:11:31
     * 邮箱:1981462002@qq.com
     * 说明:死锁简单演示
     */
    public class SynKill implements Runnable {
        static SynKill instance1 = new SynKill();
        static SynKill instance2 = new SynKill();
        public static void main(String[] args) {
            Thread thread_1 = new Thread(instance1);
            Thread thread_2 = new Thread(instance1);
            thread_1.start();
            thread_2.start();
            try {
                thread_1.join();
                thread_2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("All Finished");
        }
        @Override
        public void run() {
            sleep3msSync1();
            sleep3msSync2();
        }
    
        public void sleep3msSync2() {
            synchronized (instance1) {
                System.out.println("sleep3msSync2方法开始--name:" + Thread.currentThread().getName());
                synchronized (instance2) {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("sleep3msSync2结束,name:" + Thread.currentThread().getName());
           }
        }
    
        public static synchronized void sleep3msSync1() {
            synchronized (instance2) {
                System.out.println("sleep3msSync1方法开始--name:" + Thread.currentThread().getName());
                synchronized (instance1) {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("sleep3msSync1结束,name:" + Thread.currentThread().getName());
            }
        }
    }
    
    死锁.png

    六、synchronized原理简述

    1.定义一个类,其中用一个同步方法
    public class Decode {
    
        private Object obj = new Object();
    
        public void say(Thread thread) {
            synchronized (obj){
    
            }
        }
    
    }
    

    2.反编译(含同步方法的类):
    I:\Java\Base\Thinking\src\top\toly\并发>javac -encoding utf-8 Decode.java
    
    I:\Java\Base\Thinking\src\top\toly\并发>javap -verbose Decode.class
    Classfile /I:/Java/Base/Thinking/src/top/toly/并发/Decode.class
      Last modified 2018年12月29日; size 465 bytes
      MD5 checksum 732654b709aafd523b08c943dcb1f235
      Compiled from "Decode.java"
    public class top.toly.并发.Decode
      minor version: 0
      major version: 54
      flags: (0x0021) ACC_PUBLIC, ACC_SUPER
      this_class: #4                          // top/toly/并发/Decode
      super_class: #2                         // java/lang/Object
      interfaces: 0, fields: 1, methods: 2, attributes: 1
    Constant pool:
       #1 = Methodref          #2.#18         // java/lang/Object."<init>":()V
       #2 = Class              #19            // java/lang/Object
       #3 = Fieldref           #4.#20         // top/toly/并发/Decode.obj:Ljava/lang/Object;
       #4 = Class              #21            // top/toly/并发/Decode
       #5 = Utf8               obj
       #6 = Utf8               Ljava/lang/Object;
       #7 = Utf8               <init>
       #8 = Utf8               ()V
       #9 = Utf8               Code
      #10 = Utf8               LineNumberTable
      #11 = Utf8               say
      #12 = Utf8               (Ljava/lang/Thread;)V
      #13 = Utf8               StackMapTable
      #14 = Class              #22            // java/lang/Thread
      #15 = Class              #23            // java/lang/Throwable
      #16 = Utf8               SourceFile
      #17 = Utf8               Decode.java
      #18 = NameAndType        #7:#8          // "<init>":()V
      #19 = Utf8               java/lang/Object
      #20 = NameAndType        #5:#6          // obj:Ljava/lang/Object;
      #21 = Utf8               top/toly/并发/Decode
      #22 = Utf8               java/lang/Thread
      #23 = Utf8               java/lang/Throwable
    {
      public top.toly.并发.Decode();
        descriptor: ()V
        flags: (0x0001) ACC_PUBLIC
        Code:
          stack=3, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: aload_0
             5: new           #2                  // class java/lang/Object
             8: dup
             9: invokespecial #1                  // Method java/lang/Object."<init>":()V
            12: putfield      #3                  // Field obj:Ljava/lang/Object;
            15: return
          LineNumberTable:
            line 9: 0
            line 11: 4
    
      public void say(java.lang.Thread);
        descriptor: (Ljava/lang/Thread;)V
        flags: (0x0001) ACC_PUBLIC
        Code:
          stack=2, locals=4, args_size=2
             0: aload_0
             1: getfield      #3                  // Field obj:Ljava/lang/Object;
             4: dup
             5: astore_2
             6: monitorenter  <---------------monitorenter
             7: aload_2
             8: monitorexit   <---------------monitorexit
             9: goto          17
            12: astore_3
            13: aload_2
            14: monitorexit  <---------------monitorexit
            15: aload_3
            16: athrow
            17: return
          Exception table:
             from    to  target type
                 7     9    12   any
                12    15    12   any
          LineNumberTable:
            line 14: 0
            line 16: 7
            line 17: 17
          StackMapTable: number_of_entries = 2
            frame_type = 255 /* full_frame */
              offset_delta = 12
              locals = [ class top/toly/并发/Decode, class java/lang/Thread, class java/lang/Object ]
              stack = [ class java/lang/Throwable ]
            frame_type = 250 /* chop */
              offset_delta = 4
    }
    SourceFile: "Decode.java"
    
    

    3.如果将同步代码块去掉,再反编译
    I:\Java\Base\Thinking\src\top\toly\并发>javap -verbose Decode.class
    Classfile /I:/Java/Base/Thinking/src/top/toly/并发/Decode.class
      Last modified 2018年12月29日; size 331 bytes
      MD5 checksum 7963d00f1f781bc47a9700c548692617
      Compiled from "Decode.java"
    public class top.toly.并发.Decode
      minor version: 0
      major version: 54
      flags: (0x0021) ACC_PUBLIC, ACC_SUPER
      this_class: #4                          // top/toly/并发/Decode
      super_class: #2                         // java/lang/Object
      interfaces: 0, fields: 1, methods: 2, attributes: 1
    Constant pool:
       #1 = Methodref          #2.#15         // java/lang/Object."<init>":()V
       #2 = Class              #16            // java/lang/Object
       #3 = Fieldref           #4.#17         // top/toly/并发/Decode.obj:Ljava/lang/Object;
       #4 = Class              #18            // top/toly/并发/Decode
       #5 = Utf8               obj
       #6 = Utf8               Ljava/lang/Object;
       #7 = Utf8               <init>
       #8 = Utf8               ()V
       #9 = Utf8               Code
      #10 = Utf8               LineNumberTable
      #11 = Utf8               say
      #12 = Utf8               (Ljava/lang/Thread;)V
      #13 = Utf8               SourceFile
      #14 = Utf8               Decode.java
      #15 = NameAndType        #7:#8          // "<init>":()V
      #16 = Utf8               java/lang/Object
      #17 = NameAndType        #5:#6          // obj:Ljava/lang/Object;
      #18 = Utf8               top/toly/并发/Decode
    {
      public top.toly.并发.Decode();
        descriptor: ()V
        flags: (0x0001) ACC_PUBLIC
        Code:
          stack=3, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: aload_0
             5: new           #2                  // class java/lang/Object
             8: dup
             9: invokespecial #1                  // Method java/lang/Object."<init>":()V
            12: putfield      #3                  // Field obj:Ljava/lang/Object;
            15: return
          LineNumberTable:
            line 9: 0
            line 11: 4
    
      public void say(java.lang.Thread);
        descriptor: (Ljava/lang/Thread;)V
        flags: (0x0001) ACC_PUBLIC
        Code:
          stack=0, locals=2, args_size=2
             0: return
          LineNumberTable:
            line 14: 0
    }
    SourceFile: "Decode.java"
    
    

    两次的对比可以看出:obj对象上的东西有点不一样
    加了synchronized代码块的,obj对象头会有monitorentermonitorexit
    注意是加锁时使用的对象obj的对象头


    4.monitorentermonitorexit
    monitorenter次数为0时:若线程1进入,monitorenter次数+1,线程1成为该Monitor的所有者
    若此时线程2进入,由于Monitor的所有者非线程2,线程2只能等待,直到monitorenter次数为0
    
    若线程1进入同步方法后,又调用了一次其他方法,则monitorenter次数+1,方法退出时-1(可重入)
    当monitorenter次数为0,说明:线程1的该同步方法执行完毕,将工作内存刷新到主内存,并释放锁  
    这时monitorenter次数为0,线程2允许进入,monitorenter次数+1,线程2成为该Monitor的所有者
    

    更深的东西以后慢慢来吧,先了解个线程同步的大概,并发的内功也不是一朝一夕能成的


    后记:捷文规范

    1.本文成长记录及勘误表
    项目源码 日期 备注
    V0.1 2018-12-29 燃烧吧!我的并发之魂--synchronized
    2.更多关于我
    笔名 QQ 微信 爱好
    张风捷特烈 1981462002 zdl1994328 语言
    我的github 我的简书 我的掘金 个人网站
    3.声明

    1----本文由张风捷特烈原创,转载请注明
    2----欢迎广大编程爱好者共同交流
    3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正
    4----看到这里,我在此感谢你的喜欢与支持


    icon_wx_200.png

    相关文章

      网友评论

        本文标题:燃烧吧!我的并发之魂--synchronized

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