Android多线程总结

 锁原理

synchronized的缺点

经常使用synchronized应该都知道它是有一些不好的地方的:

  1. 无法判断锁状态,不知当前线程是否锁住了,从java层上来讲是无法知道线程是否锁住了该对象。
  2. 不可中断,如果线程一直获取不到锁,那么就会一直等待,直到占用锁的线程把锁释放。
  3. synchronized不是公平锁,新来的线程与等待的线程都有同样机会获得锁。其实大多数情况下,实际开发中都是要公平锁的。
  4. 因为是关键字的方式实现获取锁和释放锁,导致中间过程不可控。
  5. 当升级为重量级锁后会造成用户空间切换到内核空间,资源会耗费较大。

并发三大方法

  1. synchronized关键字,原理是monitor机制
  2. Atomic包,AtomicInteger,AtomicBoolean,AtomicLong等,原理是应用Volatile关键字和CAS理论实现并发
  3. Locks包,原理是应用CAS理论实现并发

Atomic

单例模式用Volatile修饰

正常我们写一个单例模式应该会这样写:

public class SingleInstance {
    private static SingleInstance instance;

    private SingleInstance() {

    }

    public static SingleInstance getInstance() {
        if (instance == null) {
            synchronized (SingleInstance.class) {
                if (instance == null) {
                    instance = new SingleInstance();
                }
            }
        }

        return instance;
    }
}

这就是DCL懒汉式的写法,为什么要用两个if判断instance 是否为空呢?用一个if加上synchronized不行吗?比如写成这样:

public static SingleInstance getInstance(){
        if(instance == null){
            synchronized(SingleInstance.class){
                instance = new SingleInstance();
            }
        }
        return instance;
    }

这样写的话,线程1拿到锁对象然后执行instance = new SingleInstance()完后继续往下走,而此时要释放锁,线程2这时已经执行到拿到synchronized(SingleInstance.class)这一行了,在等待锁对象的释放,然后线程1此时执行完instance = new SingleInstance()就释放锁,最后走return instance,紧接着线程2拿到锁后又执行一次instance = new SingleInstance(),这就等于又创建多一个SingleInstance()对象,不符合单例模式了。

因此要在线程2拿到锁后再加多一个判断,判断instance是否为空,为空的才去执行instance = new SingleInstance(),因此此时线程1已经返回了instance对象,所以线程2此时拿到锁后判断instance不为空,就不再执行,释放锁,也就不用再抢占锁资源了。这就是DCL懒汉式单例模式。不过它其实也有有可能会造成运行时异常的。

接下来先把该代码的getInstance()方法编译成dex文件指令,然后输出到一个txt文件里,看看getInstance()方法的dex指令是怎样的:

已经将一些无关紧要的指令省略掉了,如图所示就是getInstance()方法在dex文件里的指令,源码中ourInstance = new UserManger()对应的指令是000b行、000d行和0010三行指令,也就是说平时我们所写的构造对象代码,其实虚拟机执行的就是这三行指令:new-instance,invoke-direct,sput-object,只有经过这三个指令执行后,一个对象才能构造成功,然后被使用。 

而这就有个问题,就是虚拟机在加载指令的时候,它是并发(多线程)执行的,这就会发生指令重排序,也就是在加载这三条指令时,不一定是按照顺序1-2-3来执行,有可能是1-3-2。

那么如果当new-instance,invoke-direct,sput-object这三条指令执行的顺序变成1-3-2执行,就代表当线程1执行完new-instance和sput-object指令后,该对象是被构造出来,而且并有变量引用指向,但此时是没有实例数据的,所以当线程2执行代码到 **if(instance == null)** 这一行的时候,认为该对象不为null,从而直接返回,那么它再使用该对象时它的实例数据都没有,肯定就会空指针异常了(虽然概率很低,但也不能忽视)。

所以为了避免发生这种问题的风险,就可以使用关键字volatile:

public class SingleInstance {

    private volatile static SingleInstance instance;

    private SingleInstance() {

    }

    public static SingleInstance getInstance() {
        if (instance == null) {
            synchronized (SingleInstance.class) {
                if (instance == null) {
                    instance = new SingleInstance();
                }
            }
        }

        return instance;
    }
}

用关键字volatile去修饰instance变量,这样SingleInstance()对象对于线程1和线程2来说是可见的,他们互相监听该对象,线程2监听到该对象还没被完全构造完,就会等线程1执行完第2步指令invoke-direct后才会去使用该对象。

所以volatile的作用能让对象公开化,这样线程A修改该对象后,会同步更新到另一个线程上。这是因为每个线程里有独立的高速缓冲区,一核代表一个线程,每个线程先在自己的缓冲区里把数据给CPU操作,如果别的线程要互相通信操作对方的数据,那只有线程1把自己缓冲区的数据复制到内存里,然后线程1再去内存里对内存这份数据进行读取:

而volatile的原理就是把该对象放到主内存里特意划分的内存屏障空间里,这块空间里的数据都被每个线程进行监听和实时更新:

修改volatile变量时会将修改后的值刷新到主内存中(里面的内存屏障区)去,因此此时其他线程它们自己的缓冲区里的变量值失效,因此当它们这些线程去读取该变量值时候就会重新去读取主内存中(里面的内存屏障区)刚刚更新的值。

但这种可见性也只是限定于原子性操作,就单纯的取值和赋值,像i++这种不是原子性操作的计算,用volatile修饰也没用,这点是要注意的,i++是复合操作,分三步的:

  1. 读取 i 的值
  2. 对 i 进行加1
  3. 将 i 的值写回到内存

中间是多了一步加1,所以volatile无法保证原子性,用代码实验一下:

public class LockRunnable implements Runnable{

    private volatile static int a = 0;

    private Object lockObject = new Object();

    @Override
    public void run() {
        for (int i = 0; i < 100000; i++) {
            add();
        }
    }

    private void add() {
        a++;
    }

    public static void main(String[] args) {
        LockRunnable lockRunnable1 = new LockRunnable();
        LockRunnable lockRunnable2 = new LockRunnable();
        Thread thread1 = new Thread(lockRunnable1);
        Thread thread2 = new Thread(lockRunnable1);
        thread1.start();
        thread2.start();
        try {
            thread2.join();
            thread1.join();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(a);
    }
}

a变量用volatile去修饰,尽管这样,最终结果并不是我们预期的200000:

 那么要让i++能并发起来不会有问题,而且又不会像synchronized那样有变成重量锁的风险,还有另一种方式,就是使用CAS。

CAS 

CAS,Compare and Swap,比较并替换。CAS有三个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

当更新一个变量的值为新值的时候,只有当变量的预期值A和内存地址V中的实际值相同时,才会将内存地址V对应的值修改为新值B,下面来看使用了CAS这个例子:

线程1要更新值之前,线程2抢先一步将内存地址V中的变量值更新为2,然后线程1此时提交更新前会检查比较它自己的预期值A(1)和内存地址V中此时的实际值(已经被线程2更新为2)(因为CAS会使用volatile修饰变量所以能实时监听变量值),如果相同才会去更新刚刚自己的要更新的B值(2),很明显此时是不相同,所以不会进行修改,也就是修改失败。

当线程1下一次要修改值时,这次没其他线程抢先一步修改并更新,发现V中变量值跟A值是相同的,所以更新成功为2:

这就是CAS机制了,相信不难理解。

当然CAS也是有弊端的,那就是线程1是不知道线程2到底把这个变量值修改了多少次,假设线程2把值1修改成2后又修改成1,再更新,那线程1是不知道到底线程2有没有修改过这个变量值的,这就是ABA问题,可以使用AtomicStampedReference类解决,当修改过值后,因为该变量被AtomicStampedReference修饰,会记录一个时间戳的版本号,每次修改都会产生不同的版本号,从而识别出是否有修改过了。

public class Person {

    private int age;

    public static void main(String[] args) {
        Person person = new Person();
        person.age = 18;
        AtomicReference<Person> atomicReference  = new AtomicReference<>();
        atomicReference.set(person);
    }

}

ReentrantLock

它能让我们自己来控制锁,可以说是完美解决了synchronized不可控的缺点了:

public class LockRunnable implements Runnable{

    private volatile static int a = 0;

    private Object lockObject = new Object();

    public static ReentrantLock lock = new ReentrantLock(true);

    @Override
    public void run() {
        for (int i = 0; i < 100000; i++) {
            lock.lock();
            try {
                i++;
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    }
}

我们不用synchronized修饰,使用ReentrantLock锁,构造的时候传true参数表示公平锁,然后调用lock.lock()表示锁住以下要同步的代码块,然后lock.unlock()表示释放锁。

ReentrantLock的原理其实是,使用LockSupport.park来休眠和LockSupport.unpark取消休眠,然后还使用了CAS来同步多线程竞争锁对象的情况,还有等待队列(双向链表实现)来确定公平性等,如果我们自己来仿照ReentrantLock来自定义锁,可以按照这样的思路:

public class MyReentrantLock {

    private AtomicBoolean isLock = new AtomicBoolean();
    public Queue<Thread> waitThreads = new ConcurrentLinkedDeque<>();

    public void lock() {
        Thread currentThread = Thread.currentThread();
        waitThreads.add(currentThread);

        while (currentThread != waitThreads.peek()
                || !isLock.compareAndSet(false, true)) {

            LockSupport.park(this);//当前线程休眠,处于waiting

        }
        waitThreads.remove();
    }

    public void unlock() {
        isLock.set(false);
        LockSupport.unpark(waitThreads.peek());
    }
}

首先是定义了AtomicBoolean 类型的变量确保可见性和原子性,然后后面用它来调用compareAndSet()方法,这个方法就是使用了CAS,第一个参数表示旧的预期值A,第二个参数则表示要修改的新值B,这样isLock 变量不仅拥有了Atomic的优点,还使用CAS确保了线程同步,我们外部调用该锁也很方便:

@Override
    public void run() {
        for (int i = 0; i < 100000; i++) {
            myReentrantLock.lock();
            try {
                i++;
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                myReentrantLock.unlock();
            }
        }
    }

跟ReentrantLock的调用一样。

线程池

说到并发怎么能不讲线程池呢,当一个线程start()之后,是在run方法里通过wait()、sleep()等方法来改变状态,而线程池是改不了线程的状态的,线程池的作用是用来管理这些线程何时开启start,还没开启的线程怎么排队,优先级是怎样的,线程池就是做这些工作。

下面来自定义一个线程池,首先定义一个接口,接口方法实现两个方法,加入Runnable对象和删除:

public interface IThreadPool {
    void add(Runnable runnable);
    void remove();
}

然后定义这个线程池:

public class ThreadPoolImpl implements IThreadPool{

    private static int WORK_NUM = 2;//线程池内的工作线程数为2

    private static PriorityBlockingQueue<Runnable> blockingQueue = new PriorityBlockingQueue<>();//线程队列

    @Override
    public void add(Runnable runnable) {

    }

    @Override
    public void remove() {

    }
}

首先实现IThreadPool接口,然后定义了一个固定线程(也就是该线程池里一直工作运行的线程)数量为2,还定义了一个权限队列(具有优先级PriorityBlockingQueue)blockingQueue 。接下来定义这个工作线程:

class ThreadPoolWorkerThread extends Thread{

        private boolean isRunning = true;

        @Override
        public void run() {
            while (isRunning) {
                synchronized (ThreadPoolWorkerThread.class) {
                    try {
                        Runnable runnable = blockingQueue.take();
                        //取出线程后执行它的run逻辑
                        runnable.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

线程池里的工作线程是为了从队列里取出线程,然后进行它们各自的run方法,而这个过程是需要同步,所以使用了synchronized锁(根据之前分析的锁机制可以根据实际情况来优化)。那什么时候来开启这些工作线程,当然是外部在构造线程池的时候开启这些工作线程,所以ThreadPoolImpl 的构造方法:

public ThreadPoolImpl() {
        for (int i = 0; i < WORK_NUM; i++) {
            ThreadPoolWorkerThread thread = new ThreadPoolWorkerThread();
            thread.start();
        }
}

当外部Activity构造ThreadPoolImpl对象的时候就构造出线程池内部两个工作线程然后让它们启动起来,别忘了线程池的添加线程方法:

 @Override
    public void add(Runnable runnable) {
        //这里可以根据实际要求设置一个添加数据的限制

        blockingQueue.add(runnable);
    }

正如注释所说那样,如果实际需要线程池的队列数量有限制,可以在此之前判断一下然后才进行添加。基本上整个线程池的创建思路就是这些,另外一些优化和补充的功能和细节则要根据实际开发需求来定,最后外部Activity要使用该线程池则如下:

public class MainActivity extends AppCompatActivity {


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

        IThreadPool threadPool =  new ThreadPoolImpl();
        for (int i = 0; i < 10; i++) {
            threadPool.add(new Task("线程" + i));
        }
    }

    static class Task implements Runnable{
        private String name;

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

        @Override
        public void run() {

        }
    }
}

写在最后

代码不难,相信大家都能理解,在构造线程池的时候,就已经让它里面的工作线程启动,然后不断轮询队列里的线程,而队列里的线程则通过外部调用者来添加进去,删除线程的方法也是外部去操纵线程池的remove方法。

另外要说明一下,虽然在工作线程里不断轮询去从队列里取线程的时候会有阻塞(加了锁),但因为工作线程本身也是子线程,所以不会阻塞到主线程(UI线程)了。

  • 26
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值