龙空技术网

张小飞的Java之路——第二十三章——多线程——同步

小艾编程 12

前言:

当前你们对“java中的同步方法”大体比较着重,大家都想要学习一些“java中的同步方法”的相关资讯。那么小编同时在网摘上汇集了一些关于“java中的同步方法””的相关文章,希望朋友们能喜欢,我们一起来学习一下吧!

写在前面:

视频是什么东西,有看文档精彩吗?

视频是什么东西,有看文档速度快吗?

视频是什么东西,有看文档效率高吗?

1. 数据安全问题

诸小亮:多线程的使用虽然可以帮助我们提高效率,但是也可能造成问题

张小飞:是吗?什么样的问题?

诸小亮:数据安全问题

1. 前提

诸小亮:你还记得之前我们说了CPU切换线程的情况吗?

张小飞:当然了,CPU是不断的切换线程执行的,当CPU从 A 线程切换到 B 线程时,A 线程就会暂停执行

诸小亮:没错,那你看看下面这段代码,CPU可能让线程暂停到哪一行?

public void run() {    System.out.println(name + "。。。。。");    System.out.println(name + "。。。。。");    System.out.println(name + "。。。。。");}

张小飞:上面 3 句代码,这里的每一句代码都有可能被CPU的调度打断,导致当前线程暂停执行

诸小亮:你说的不错,都有可能,具体看 CPU 心情

2. 多线程操作数据

诸小亮:接下来,我们就来看一下——多线程中可能出现的数据安全问题

张小飞:到底什么时候会产生问题呢?

诸小亮:当多个线程同时操作同一个数据时候,可能产生数据安全问题

class Hero implements Runnable{    //1. 定义一个静态变量,多个线程同时操作它    public static int num = 10;    @Override    public void run() {        while (true){// while中用true,这是死循环,谨慎使用,这里是为了演示效果            //2. run方法中,对num--,当num<=0时,跳出循环            if(num > 0){                //sleep(5),让当前线程休眠5毫秒,此时CPU会执行其他线程                try { Thread.sleep(5); } catch (InterruptedException e) {}                num--;                System.out.println(Thread.currentThread().getName() + "***********" + num);            }else{                break;            }        }    }}public class ThreadDemo {    public static void main(String[] args) {        Hero hero = new Hero();        Thread yase = new Thread(hero, "yase");        Thread daji = new Thread(hero, "daji");        //3. 开启两个线程操作num        yase.start();        daji.start();    }}

结果:

诸小亮:仔细看上面的代码,当 num>0 时,才会执行输出语句,但是却输出了负数

张小飞:是啊,为什么会出现负数?

诸小亮:我们来看一下代码的执行过程:

2个线程刚开始正常执行,都会执行num--。。。。。当 num=1 时,假如 'yase'先进入 先进入 if ,休眠5毫秒,这时 CPU 切换到线程‘daji’进入 if 语句    ‘yase’休眠结束,执行num--,接着‘daji’睡醒后也执行num--,最终num=-1

张小飞:原来是这样,如果不让这两个线程休眠,是不是可以避免这种情况?

诸小亮:即使没有 sleep 语句,也可能输出负数,只不过概率太低而已

张小飞:额。。。,我明白了,线程可能暂停到任意一句代码

诸小亮:是的,所以记住,一个线程在操作数据时,其他线程也参与运算,最终可能造成数据错误

张小飞:那么如何解决这种问题呢?

诸小亮:保证操作数据的代码在某一时间段内,只被一个线程执行,执行期间,其他线程不能参与

2. 线程同步

张小飞:怎么才能保证‘在某一时间段内,只被一个线程执行’呢?

诸小亮:那就要用到——线程同步

线程同步:就是让线程一个接一个的排队执行同步:是一个接一个的意思

张小飞:排队执行,有意思,怎么才能让线程排队?

诸小亮:Java中提供 synchronized 关键字用来实现同步,可以解决多线程时的数据安全问题

张小飞:原来这样的关键字,赶快告诉我怎么用啊

1. synchronized 代码块

诸小亮:synchronized 的使用非常简单,其格式:

synchronized(锁对象){	需要同步代码块}

张小飞:‘锁对象’是什么东西?

诸小亮:可以理解为钥匙、通行证,只有线程拿到通行证后,才能执行 { } 中的代码

1. 演示

诸小亮:我们使用 synchronized 修改一下 run 方法:

public static Object obj = new Object();@Overridepublic void run() {    while (true){        //使用同步代码块,把 obj 作为锁对象        synchronized (obj){            if(num > 0){                //sleep(5),让当前线程休眠5毫秒,此时CPU会执行其他线程                try { Thread.sleep(5); } catch (InterruptedException e) {}                num--;                System.out.println(Thread.currentThread().getName() + "***********" + num);            }else{                break;            }        }    }}

结果:

张小飞:可以具体解释一下吗?

诸小亮:当然可以

当线程执行到 synchronized (obj) 时,会获取尝试 obj 对象synchronized 保证多线程下,只有一个线程能获取到obj对象,而其他线程会阻塞(暂停)当多个线程同时执行到 synchronized (obj) 这句代码时,只有一个线程能够拿到obj,其他线程暂停当持有 obj 的线程从 synchronized 退出后,会释放 obj 对象,然后其他线程再次争夺 obj

诸小亮:这样就保证了 synchronized 中的代码在某一时刻只能被一个线程执行

张小飞:原来如此

2. 锁对象

张小飞:不过,synchronized 的锁对象都可以是什么?

诸小亮:好问题!在 synchronized 中,锁对象可以是任意对象,只要是引用类型的对象都可以作为锁对象

张小飞:那么如何选择合适的锁对象呢?

诸小亮:这就要根据实际情况去选择了,在之后我们会介绍两种常用的锁对象

张小飞:好的

3. 好处和弊端

诸小亮:刚才演示了 synchronized 的最用,你能总结一下它的好处和弊端吗?

张小飞:当然可以了

好处:解决多线程数据安全问题弊端:同步代码块同时只能被一个线程执行,降低效率

诸小亮:嗯嗯,还不错

4. synchronized的注意事项

诸小亮:使用 synchronized 需要注意,必须在多线程场景下使用

线程数>1,就是多线程

张小飞:明白

诸小亮:另外,多线程争抢的锁对象只能有一个,下图中的做法要坚决抵制

诸小亮:上图中的做法是会被开除的

张小飞:这是自然,确保锁对象的唯一性是确保 synchronized 正确性的前提

诸小亮:哟,没想到你总结的还挺到位

张小飞:那是自然,也不看是跟谁学习的

2. 同步方法

诸小亮:下面,我们来说说——同步方法

张小飞:是把 synchronized 放到方法上吗?

诸小亮:是的,把 synchronized 放到方法上,那么这个方法就是同步方法

1. 演示

诸小亮:同步方法,限制同一时刻只有一个线程可以执行这个方法,比如:

class Hero implements Runnable{    public static int num = 10;    public static Object obj = new Object();    @Override    public void run() {        //run方法中调用同步方法        this.show();    }    //同步方法    public synchronized void show(){        while (true){            if(num > 0){                try { Thread.sleep(5); } catch (InterruptedException e) {}                num--;                System.out.println(Thread.currentThread().getName() + "***********" + num);            }else{                break;            }        }    }}public class ThreadDemo {    public static void main(String[] args) {        Hero hero = new Hero();        Thread yase = new Thread(hero, "yase");        Thread daji = new Thread(hero, "daji");        yase.start();        daji.start();    }}

结果:

张小飞:上面的输出表示,只有 yase 线程进入了 show 方法吧

诸小亮:不错,yase线程进入show方法后,操作 num,然后退出方法,接着 daji 线程进入,发现num已经为0

所以直接退出

张小飞:不是说使用 synchronized,都需要锁对象吗,同步方法的锁对象是???

2. 同步方法的锁对象

诸小亮:同步代码块需要一个锁对象,但是同步方法并不需要,因为:同步方法的锁是this(当前对象)

张小飞:当前对象?

诸小亮:是的,下面的代码可以证明

class Hero implements Runnable{    public static Object obj = new Object();    @Override    public void run(){        //1. 使用 this 作为锁对象        synchronized (this){            try { Thread.sleep(2000); } catch (InterruptedException e) {}            System.out.println(Thread.currentThread().getName() + "。。。。。。。"+this);        }    }	//2. 同步方法的锁是 this    public synchronized void show(){        System.out.println(Thread.currentThread().getName() + "。。。。。。。"+this);    }}public class ThreadDemo {    public static void main(String[] args) throws Exception {        Hero hero = new Hero();        //3. 启动 ‘亚瑟’ 线程,自动执行run方法        new Thread(hero,"亚瑟").start();        //4. 主线程休眠100毫秒后,执行show这个同步方法        Thread.sleep(100);        hero.show();    }}

诸小亮:上面代码中,run方法中的 this 就是 hero对象,这个知道吧

张小飞:这时自然

诸小亮:好的,那我们运行一下,它的结果是

张小飞:看结果,应该是 ‘亚瑟’线程执行结束后,在执行的 main 线程中的 show 方法

诸小亮:不错,其具体过程是

亚瑟线程先执行,走到 Thread.sleep(2000) 时,休眠2秒,但这时它已经持有 this (也就是hero对象)然后main线程休眠100毫秒后,执行show方法尝试获取this,无法获取到,于是等待1.9秒所以最终结果。。。。。

张小飞:明白了

3. 静态同步方法

张小飞:synchronized 可以放到静态方法上吗

诸小亮:可以的,这时就是静态同步方法

张小飞:那么,静态同步方法的锁对象是???

诸小亮:锁对象是:Hero.class,也就是方法所在类的 class 对象

张小飞:什么是 class 对象?

诸小亮:Hero.class 也是对象,称为 Class 或字节码对象,是 JVM 把 class 文件加载到内存后创建一个对象

张小飞:这么说,每一个类都有自己的 class 对象了?

诸小亮:是的,我们修改上面的代码

class Hero implements Runnable{    public static Object obj = new Object();    @Override    public void run(){        //1. 锁对象换成了Hero.class,也就是Hero字节码对象        synchronized (Hero.class){            try { Thread.sleep(2000); } catch (InterruptedException e) {}            System.out.println(Thread.currentThread().getName() + "。。。。。。。"+Hero.class);        }    }	//2. show方法改成static    public static synchronized void show(){        System.out.println(Thread.currentThread().getName() + "。。。。。。。"+Hero.class);    }}public class ThreadDemo {    public static void main(String[] args) throws Exception {        Hero hero = new Hero();        new Thread(hero,"亚瑟").start();        Thread.sleep(100);        Hero.show();    }}

结果:

4. 死锁

诸小亮:在使用 synchronized 时,要特别注意一种情况——死锁

张小飞:死锁是什么意思?

诸小亮:其实就是两个线程互相影响,A 线程等 B 线程释放锁,同时 B 线程也在等 A 线程释放锁,比如:

张小飞:能写个代码具体看看吗?

诸小亮:代码我都准备好了,死锁——通常发生在同步嵌套

同步嵌套:synchronized 中 还有 synchronized

示例:

class Hero implements Runnable{    //1. 搞两个锁对象 A、B    public static Object A = new Object();    public static Object B = new Object();        public boolean bool;    Hero(boolean bool){        this.bool = bool;    }    @Override    public void run(){        //2. 当 bool 是 true         if(bool){            synchronized (A){//先获取 A 锁,然后睡一会儿,再获取 B 锁                System.out.println(Thread.currentThread().getName() + "------拿到A锁,准备获取B锁。。。。。。。");                try { Thread.sleep(200); } catch (InterruptedException e) {}                synchronized (B){                    System.out.println(Thread.currentThread().getName() + "。。。。。。。");                }            }        }else{//3. 当 bool 是 false             synchronized (B){ //先获取B锁,睡一会儿,再获取A锁                System.out.println(Thread.currentThread().getName() + "------拿到B锁,准备获取A锁。。。。。。。");                try { Thread.sleep(200); } catch (InterruptedException e) {}                synchronized (A){                    System.out.println(Thread.currentThread().getName() + "。。。。。。。");                }            }        }    }}public class ThreadDemo {    public static void main(String[] args) throws Exception {        new Thread(new Hero(true), "yase").start();        new Thread(new Hero(false), "daji").start();    }}

诸小亮:你自己研究一下这个代码吧

张小飞:好的

五分钟后。

张小飞:结果是这样子的

诸小亮:那你能详细解释一下原因吗?

张小飞:我试试吧,其本质是——死锁导致程序无法继续运行,也一直不结束,其过程:

假设 yase 线程先执行,在获取的 A 锁后,睡觉daji 线程接着执行,在获取 B 锁后,睡觉yase 线程睡醒后,尝试获取 B 锁,但是已经被 daji 线程拿到了,于是阻塞daji 线程睡醒后,尝试获取 A 锁,但是已经被 yase 线程拿到了,也阻塞yase 线程需要获取 B 锁后,执行完代码,再能释放 A 锁daji 线程需要获取 A 锁后,执行完代码,再能释放 B 锁最终两个线程都阻塞,程序卡死

诸小亮:嗯嗯,分析的还不错,一定要记住:

死锁是开发中的禁忌,绝对禁止出现,所以开发中尽量避免同步代码块嵌套使用

标签: #java中的同步方法