signed

QiShunwang

“诚信为本、客户至上”

多线程之Synchroized解析

2021/4/26 17:09:05   来源:

前言


既然提到Android的多线程,那我们就先来回顾一下线程是怎么创建的先。Android常用的线程创建方式主要用两种(这两种线程的创建方式是我们在开发过程中比较常用到的,还有线程池以及Callable可以创建线程,只是用的会稍微少一些):

  • 继承Thread类,重写Thread的run()方法;
  • 实现Runnable接口,然后重写Runnable接口里面的run()方法;

两者的关系:
Thread类其实是实现了Runnable接口的,然后两者都需要重写run()方法,Runnable可以实现多个线程共享同个资源;

对于线程的讲解还是离不开卖票的存在,就打个比方,有两个黄牛,一个叫小T(Thread),另一个是小R(Runnable),有一天他们各自抢到了五张Jay Chou的演唱会门票,然后就在演唱会开始之前,在附近加价转让,小T有两名手下,便把这转票任务交给这两名手下,然后自己去忙别的事情,两名手下的转卖流程是这样子的(定义一个Thread的子类,然后重写run()方法):

public class TicketThread extends Thread {
	private int ticket = 5;
	private String name;

	public TicketThread(String name) {
		this.name = name;
	}

	public void run() {
		for (int i = 0; i < 5; i++) {
			if (ticket > 0) {
				Log.d("Tag-",name + "卖出一张票,编号为" + ticket--);
			}
		}
	}
}

然后票很快就被抢光了(实例化Thread的子类,调用start()方法启动线程)

TicketThread t1 = new TicketThread("手下T1");
TicketThread t2 = new TicketThread("手下T2");
t1.start();
t2.start();

在这里插入图片描述
小T的票已经卖完了,小R的手下则是这样完成任务的(创建一个实例,实现Runnable接口,然后重写对应的run方法):

    public class TicketRunnable implements Runnable {
        private int ticket = 5;
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                if (ticket > 0){
                    Log.d("Tag-", Thread.currentThread().getName() + "卖出一张票,编号为: " + ticket--);
                }
            }

        }
    }

结果也是一售而空(调用start启动线程)

        TicketRunnable runnable = new TicketRunnable();
        Thread r1 = new Thread(runnable, "手下R1");
        Thread r2 = new Thread(runnable, "手下R2");

        r1.start();
        r2.start();

在这里插入图片描述
然后这里就会出现一个问题,为什么同样是五张票,而小T却可以卖到双倍的价钱,原因竟是因为小T把门票复印了一份,然后再进行售卖(用Thread的方式创建并启动一个线程,相当于任务也是新建一份,也就是说,new了两个线程,两个线程间的数据是相互独立的,而用Runnable的实现方式则不会这样,因为都是用同一个Runnable创建的,所以他们的资源是共享的)

然后小R心里就很不舒服,于是就去举报小T,然后小T也因此被抓了,小R的生意就越来越好了,便扩大自己的业务范围。
在某天收到有人的举报,说小R卖假票,小R说不可能,自己是诚信经营,怎么可能卖假票,于是就对当天的售票业务进行排查,业务如下:

  public class Task{
        int tickets = 20;
        public void sellTickets(){
            if (tickets > 0){
                try {
                    //办理手续,让线程休眠1s
                    Thread.sleep(1000);
                    Log.d("Tag-", Thread.currentThread().getName() + "卖出一张票,编号为" + tickets--);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    public class TicketRunnable implements Runnable {
        Task task;
        public TicketRunnable(Task task) {
            this.task = task;
        }

        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                task.sellTickets();
            }
        }
    }

还是之前那两个手下去转让:

        Task task = new Task();
        TicketRunnable runnable = new TicketRunnable(task);
        Thread r1 = new Thread(runnable, "手下R1");
        Thread r2 = new Thread(runnable, "手下R2");
        r1.start();
        r2.start();

结果便发生了如下一幕,出现了一票两售的情况
在这里插入图片描述
调查发现是因为手下R2先答应卖给顾客A,然后顾客B刚好也找了手下R1要这张票,结果两个手下都收了钱,才发现票只有一张,无奈之下只能作假了(多个线程先后操作共享数据造成数据错误)。小R为了再次避免出现这种情况,找了自己的一个朋友过来帮忙监督,这人正是小S(synchronized),小S让小R把要出售的演唱会门票都交到他手里,让小S自己统一管理整个转让的业务,每次转让之前小S都会给R1,R2一张门票,只有等他们手上的那一张门票成功转让出去了,才可以再次来小S这里继续拿门票转让,这样问题也就解决了(换成Java来说,就是在Java中每个对象都会有一个内部锁,当使用synchronized修饰一个方法的时候,这个方法就会收到对象锁的保护,当多线程统一访问一个方法时,一次只能有一个线程进行访问对应的方法,当这个线程开始访问这个方法是,会持有这个锁,其他的线程没有对应的锁则不能访问对应的方法,只有当获得锁的线程执行完该方法并释放对象锁后,别的线程才可拿到锁进入该方法),从此小S加入之后,,问题也得到了解决,业务逻辑修改成如下:


    public class Task{
        int tickets = 10;

        public synchronized void sellTickets(){
            if (tickets > 0){
                try {
                    Thread.sleep(500);
                    Log.d("Tag-", Thread.currentThread().getName() + "卖出一张票,编号为" + tickets--);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

在这里插入图片描述

同个对象内多个同步方法
再来个比较重口味的比例哈·~,小R公司一层楼只有一个坑位,某日,小R来找小S吐槽,说他在蹲坑的时候,有人不顾他的感受直接打开了他的厕所门,结果四目相对,场面一度尴尬,让小S想想办法避免这种情况(流程如下):

    public class WCTask{
        public void wc1(){
            Log.d(TAG, "小R开始进入坑位 ");
            try {
                Thread.sleep(500);
                Log.d(TAG, "小R开始蹲坑 ");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Log.d(TAG, "小R离开坑位 ");
        }


        public void wc2(){
            Log.d(TAG, "小X开始进入坑位 ");
            try {
                Thread.sleep(500);
                Log.d(TAG, "小X开始蹲坑 ");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Log.d(TAG, "小X离开坑位 ");
        }
    }


    public class WCRunnable1 implements Runnable {

        WCTask wcTask;

        public WCRunnable1(WCTask wcTask) {
            this.wcTask = wcTask;
        }

        @Override
        public void run() {
            wcTask.wc1();
        }
    }


    public class WCRunnable2 implements Runnable {

        WCTask wcTask;

        public WCRunnable2(WCTask wcTask) {
            this.wcTask = wcTask;
        }

        @Override
        public void run() {
            wcTask.wc2();
        }
    }
        WCTask task = new WCTask();
        WCRunnable1 runnable1 = new WCRunnable1(task);
        WCRunnable2 runnable2 = new WCRunnable2(task);
        Thread thread1 = new Thread(runnable1);
        Thread thread2 = new Thread(runnable2);
        thread1.start();
        thread2.start();

在这里插入图片描述
结果发现小R刚进入坑位,小X酒跟着进来了,要是一男一女还好说,两男的话直接进入猎杀时刻,为了避免这种情况,小S制定了一套策略,必要要等上一个人上完厕所出来之后,下一个人下可以进去,修改后的业务如下:

    public class WCTask{
        public synchronized void wc1(){
            Log.d(TAG, "小R开始进入坑位 ");
            try {
                Thread.sleep(500);
                Log.d(TAG, "小R开始蹲坑 ");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Log.d(TAG, "小R离开坑位 ");
        }


        public synchronized void wc2(){
            Log.d(TAG, "小X开始进入坑位 ");
            try {
                Thread.sleep(500);
                Log.d(TAG, "小X开始蹲坑 ");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Log.d(TAG, "小X离开坑位 ");
        }
    }

在这里插入图片描述
这样上厕所才是正常的(当一个线程访问对象的某个synchronized同步方法时,其他线程对对象中所有其它synchronized同步方法的访问将被阻塞)
但是还有个问题,打个比方是男厕哈,当小R进去蹲坑的时候,这时候来了个男的小K想上小号,小K可以忽略坑位的存在,因为旁边有小号的地方可以给他放水,业务如下:

  public class WCTask{
        public synchronized void wc1(){
            Log.d(TAG, "小R开始进入坑位 ");
            try {
                Thread.sleep(500);
                Log.d(TAG, "小R开始蹲坑 ");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Log.d(TAG, "小R离开坑位 ");
        }


        public synchronized void wc2(){
            Log.d(TAG, "小X开始进入坑位 ");
            try {
                Thread.sleep(500);
                Log.d(TAG, "小X开始蹲坑 ");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Log.d(TAG, "小X离开坑位 ");
        }

        private String WC = "nc";
        public void wc3(){
            synchronized (WC){
                Log.e(TAG,"小K在小号的地方放水");
            }
        }
    }


    public class WCRunnable1 implements Runnable {

        WCTask wcTask;

        public WCRunnable1(WCTask wcTask) {
            this.wcTask = wcTask;
        }

        @Override
        public void run() {
            wcTask.wc1();
        }
    }


    public class WCRunnable2 implements Runnable {

        WCTask wcTask;

        public WCRunnable2(WCTask wcTask) {
            this.wcTask = wcTask;
        }

        @Override
        public void run() {
            wcTask.wc2();
        }
    }


    public class WCRunnable3 implements Runnable {

        WCTask wcTask;

        public WCRunnable3(WCTask wcTask) {
            this.wcTask = wcTask;
        }

        @Override
        public void run() {
            wcTask.wc3();
        }
    }

在这里插入图片描述
这也就说明了,小K可以直接上小号,而不用等小R蹲完坑之后在上小号了(synchronized (obj){}同步代码块和用synchronized声明方法的作用基本一致,都是对synchronized作用范围内的代码进行加锁保护,其区别在于synchronized同步代码块使用更加灵活、轻巧,synchronized (obj){}括号内的对象参数即为该代码块持有锁的对象。例如上述例子中,前面两个wc方法中的同步代码块持有锁的对象为WCTask的实例对象,而wc3方法中的同步代码块持有锁的对象则为nc。因为对象都有自己的对象锁,只能保护属于自己的同步代码块或同步方法,所以即使其他线程进入前两个方法的同步代码块中并获得相应对象的锁,也不会阻塞进入wc3方法的线程执行其中的同步代码)