Android面试之线程同步的方法

一 什么是进程和线程?进程和线程的区别?

  1. 进程是资源分配的最小单元,线程是程序执行的最小单元(进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位)
  2. 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。
    而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
  3. 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
  4. 但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间

二 线程的生命周期

在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种状态。当线程start后,它不能一直"独占"着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。按自己的理解整理出一张线程生命周期图,参照生命周期图更直观的了解线程。下面着重分析这五种状态:

  1. 新建 New
  2. 就绪 Runnable
  3. 运行 Running
  4. 阻塞 Blocked
  5. 死亡 Dead

Android面试之线程同步的方法_第1张图片

2.1 新建(New)

public class XThread extends Thread{
    @Override
        public void run() {
            
        }
}
//新建就是new出对象
XThread thread = new XThread();

当程序使用new关键字创建了一个线程之后,该线程就处于一个新建状态(初始状态),此时它和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,并初始化了其成员变量值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。

2.2 就绪(Runnable)

当线程对象调用了Thread.start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有开始运行,它只是表示该线程可以运行了。从start()源码中看出,start后添加到了线程列表中,接着在native层添加到VM中,至于该线程何时开始运行,取决于JVM里线程调度器的调度(如果OS调度选中了,就会进入到运行状态)。

2.3 运行 (Running)

如果处于就绪状态的线程获得了CPU资源,就开始执行run方法的线程执行体,则该线程处于运行状态。run方法的那里呢?其实run也是在native线程中。源码如下:

status_t Thread::run(const char* name, int32_t priority, size_t stack)
{
    Mutex::Autolock _l(mLock);
    //保证只会启动一次
    if (mRunning) {
        return INVALID_OPERATION;
    }
    ...
    mRunning = true;
 
    bool res;
    
    if (mCanCallJava) {
        //还能调用Java代码的Native线程
        res = createThreadEtc(_threadLoop,
                this, name, priority, stack, &mThread);
    } else {
        //只能调用C/C++代码的Native线程
        res = androidCreateRawThreadEtc(_threadLoop,
                this, name, priority, stack, &mThread);
    }
 
    if (res == false) {
        ...//清理
        return UNKNOWN_ERROR;
    }
    return NO_ERROR;
}

mCanCallJava在Thread对象创建时,在构造函数中默认设置mCanCallJava=true.

  • 当mCanCallJava=true,则代表创建的是不仅能调用C/C++代码,还能能调用Java代码的Native线程
  • 当mCanCallJava=false,则代表创建的是只能调用C/C++代码的Native线程。

2.4 阻塞(Blocked)

阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况大概三种:

  1. 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
  2. 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
  3. 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)

线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。

线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。唤醒线程后,就转为就绪(Runnable)状态。

线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。

线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
线程I/O:线程执行某些IO操作,因为等待相关的资源而进入了阻塞状态。比如说监听system.in,但是尚且没有收到键盘的输入,则进入阻塞状态。

线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意性的,并在对实现做出决定时发生。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。

附:线程睡眠(sleep)和线程等待(wait)都会阻塞线程,它们的区别是什么?

sleep():
sleep 是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复,调用sleep 不会释放对象锁。由于没有释放对象锁,所以不能调用里面的同步方法。
sleep()使当前线程进入停滞状态(阻塞当前线程),让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;
sleep()是Thread类的Static(静态)的方法;因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象的机锁并木有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。
在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。
其次sleep()必须捕获异常;在sleep的过程中过程中有可能被其他对象调用它的interrupt(),产生InterruptedException异常,如果你的程序不捕获这个异常,线程就会异常终止,进入TERMINATED状态,如果你的程序捕获了这个异常,那么程序就会继续执行catch语句块(可能还有finally语句块)以及以后的代码。

wait():
wait()方法是Object类里的方法;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁);可以调用里面的同步方法,其他线程可以访问;
wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。
wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常。

一旦一个对象调用了wait方法,必须要采用notify()和notifyAll()方法唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了wait()后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了wait()方法的对象。

2.4 死亡 (Dead)

线程会以以下三种方式之一结束,结束后就处于死亡状态:

  • run()方法执行完成,线程正常结束。
  • 线程抛出一个未捕获的Exception或Error。
  • 直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用。

三 实现线程同步的方法

3.1 ThreadLocal

网上很多资料都把ThreadLocal定义为解决多线程同步问题的一个方案,但是ThreadLocal并不是为了解决多线程间共享变量的问题。举个例子:在一个电商系统中,用一个Long型变量表示某个商品的库存量,多个线程需要访问库存量进行销售,并减去销售数量,以更新库存量。在这个场景中,是不能使用ThreadLocal类的。
ThreadLocal适用的场景是,多个线程都需要使用一个变量,但这个变量的值不需要在各个线程间共享,各个线程都只使用自己的这个变量的值。

此外,我们使用ThreadLocal还能解决一个参数过多的问题。例如一个线程内的某个方法f1有10个参数,而f1调用f2时,f2又有10个参数,这么多的参数传递十分繁琐。那么,我们可以使用ThreadLocal来减少参数的传递,用ThreadLocal定义全局变量,各个线程需要参数时,去全局变量去取就可以了。

3.2 synchronized关键字

  1. synchronized修饰方法(同步方法)
  2. synchronized修饰代码块(同步代码块)

注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Blank blank = new Blank();

        MyRunnable myRunnable = new MyRunnable(blank);

        Thread thread1= new Thread(myRunnable,"线程1");
        thread1.start();

        Thread thread2= new Thread(myRunnable,"线程2");
        thread2.start();


    }


    class Blank{

        private int account = 100;

        public int getAccount() {

            return account;

        }


        /**
         * 存钱同步方法
         * */
        public synchronized void save1(int money) {

            account = account +  money;

        }

        /**
         * 存钱同步代码快
         * */
        public  void save2(int money) {
            synchronized (this){
                account = account +  money;
            }


        }

        /**
         * 存钱非同步方法
         * */

        public void save(int money) {

            account = account +  money;

        }



    }

    class MyRunnable implements Runnable {

        private Blank blank;

        public MyRunnable(Blank blank){
            this.blank = blank;
        }


        @Override
        public void run() {
            for (int i = 1 ; i < 10 ;i++){

                blank.save2(10);
                System.out.println(Thread.currentThread().getName() + ":" + i + "账户余额为:" + blank.getAccount());

            }
        }
    }



} 

3.3 可重入锁ReentrantLock

在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和块具有相同的基本行为和语义,并且扩展了其能力
ReentrantLock类的常用方法有:

什么是可重入锁?
当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。(自己请求自己的锁是可以的,不会阻塞)

原理:
重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。

使用:

ReentrantLock() : 创建一个ReentrantLock实例

lock() : 获得锁

unlock() : 释放锁

注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Blank blank = new Blank();

        MyRunnable myRunnable = new MyRunnable(blank);

        Thread thread1 = new Thread(myRunnable, "线程1");
        thread1.start();

        Thread thread2 = new Thread(myRunnable, "线程2");
        thread2.start();


    }


     class Blank {

        private int account = 100;
        private ReentrantLock lock = new ReentrantLock();

        public int getAccount() {

            return account;

        }

        public void save(int money) {
            lock.lock();
            try {
                account = account + money;
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    class MyRunnable implements Runnable {

        private Blank blank;

        public MyRunnable(Blank blank) {
            this.blank = blank;
        }


        @Override
        public void run() {
            for (int i = 1; i < 10; i++) {

                blank.save(10);
                System.out.println(Thread.currentThread().getName() + ":" + i + "账户余额为:" + blank.getAccount());

            }
        }
    }
}

Android面试之线程同步的方法_第2张图片

3.4 使用特殊域变量(volatile)实现线程同步

常用于保持内存可见性和防止指令重排序。
内存可见性(Memory Visibility)是指所有线程都能看到共享内存的最新状态。
volatile关键字通过“内存屏障”来防止指令被重排序。

   public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Blank blank = new Blank();

        MyRunnable myRunnable = new MyRunnable(blank);

        Thread thread1 = new Thread(myRunnable, "线程1");
        thread1.start();

        Thread thread2 = new Thread(myRunnable, "线程2");
        thread2.start();


    }


     class Blank {

         private volatile int account = 100;


         public int getAccount() {

             return account;

         }

         public void save(int money) {
             account = account + money;
         }
     }

    class MyRunnable implements Runnable {

        private Blank blank;

        public MyRunnable(Blank blank) {
            this.blank = blank;
        }


        @Override
        public void run() {
            for (int i = 1; i < 10; i++) {

                blank.save(10);
                System.out.println(Thread.currentThread().getName() + ":" + i + "账户余额为:" + blank.getAccount());

            }
        }
    }
}

Android面试之线程同步的方法_第3张图片

3.4 使用原子变量实现线程同步

需要使用线程同步的根本原因在于对普通变量的操作不是原子的。
什么是原子操作呢?
原子操作即是进行过程中不能被中断的操作,针对某个值的原子操作在被进行的过程中,CPU绝不会再去进行其他的针对该值的操作。为了实现这样的严谨性,原子操作仅会由一个独立的CPU指令代表和完成。原子操作是无锁的,常常直接通过CPU指令直接实现。

使用:
在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,
使用该类可以简化线程同步。小工具包,支持在单个变量上解除锁的线程安全编程。

Android面试之线程同步的方法_第4张图片
其中AtomicInteger(乐观锁)为例 :
Android面试之线程同步的方法_第5张图片

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Blank blank = new Blank();

        MyRunnable myRunnable = new MyRunnable(blank);

        Thread thread1 = new Thread(myRunnable, "线程1");
        thread1.start();

        Thread thread2 = new Thread(myRunnable, "线程2");
        thread2.start();


    }


     class Blank {


         private AtomicInteger account = new AtomicInteger(100);

         public int getAccount() {

             return account.get();

         }

         public void save(int money) {
             account.addAndGet(money);
         }
     }

    class MyRunnable implements Runnable {

        private Blank blank;

        public MyRunnable(Blank blank) {
            this.blank = blank;
        }


        @Override
        public void run() {
            for (int i = 1; i < 10; i++) {

                blank.save(10);
                System.out.println(Thread.currentThread().getName() + ":" + i + "账户余额为:" + blank.getAccount());

            }
        }
    }
}

Android面试之线程同步的方法_第6张图片

你可能感兴趣的:(android,面试,多线程)