欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android 线程的控制及原理 并发

程序员文章站 2022-04-22 10:59:18
前言:上次讲解讲解了下线程的Android的线程基础,后来觉得其实还有很多值得补充的地方,今天算是把线程这块完结,请大家批评指正。 本文主要包含关键字volitate,Semap...

前言:上次讲解讲解了下线程的Android的线程基础,后来觉得其实还有很多值得补充的地方,今天算是把线程这块完结,请大家批评指正。

本文主要包含关键字volitate,Semaphore,AtomicInteger,通过对他们的分析,让你了解到线程同步,并发等情况。

好了,闲话少说,我问先来回顾下线程。

一、原理

线程的原理大家也都很清楚,就是在一个进程中,执行单独的一段代码块,采用多线程的方式,可以实现我们程序的执行效率,节约我们的时间,同时也消耗相对较多的内存以及性能上的资源。

二、实际应用

实际中为什么会用到多线程,管理线程呢,有很多种场景,

1.在Android由于主线程不予许做耗时操作,所以需要选择子线程执行

2.当执行过大数据的传输以及读写时,多线程可以帮助我们缩短时间完成例如上传、下载等工作

3.在压力测试时,会用多线程看看线程例如运行、阻塞情况。

4.服务器编程、分压和监听器,也会用到多线程。

等等

所以说,多线程在实际操作中应用还是比较多的,掌握了多线程,也是对自己在开发设计上的提高。

三、主要内容

1.volitate java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。volatile关键字对一个实例的域的同步访问提供了一个免锁(lock-free)机制。如果把域声明为volatile,那么编译器和虚拟机就知道该域可能会被另一个线程并发更新。对象内需要同步的域值少,使用锁显得浪费和繁琐场景,这时使用volatile。一些并发容器(ConcurrentHashMap,etc)的实现内使用了volatile。利用jvm对volatile承诺的happen-before原则。

说了这么多有点懵圈,简单几点:

1)volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;

2)volatile仅能使用在变量级别;

3)volatile仅能实现变量的修改可见性,有序性,不能保证原子性(这个我们下面讲到);

4)volatile不会造成线程的阻塞;

5)volatile标记的变量不会被编译器优化(下面讲到)。

针对3、和5,我们细说

·可见性:在java中每个线程都有自己的线程栈,当一个线程执行需要数据时,会到主存中将需要的数据复制到自己的线程栈中,然后对线程栈中的副本进行操作,再操作完成后再将数据写回到主存中,volitate修饰的变量一旦改变,会立即写入主存,被其他的线程全部更新可以。

·有序性:即程序执行的顺序按照代码的先后顺序执行。我们写代码会有一个先后的顺序,但是那仅仅是我们看到的顺序,但是当编译器编译时会进行指令重排,于是代码的执行顺序有可能和我们想的不一样。例如:

int i = 0;             
boolean flag = false; //语句3
i = 1;                //语句1 
flag = true;          //语句2

语句1和语句2的执行顺序改变一下对程序的结果并没有什么影响,所以这时可能会改变这两条指令的顺序。那么语句2会不会在语句3之前执行呢,答案是不会呢,因为语句2用到了语句3声明的变量,这时编译器会限制语句的执行顺序来保证程序的正确性。

在单线程中,改变指令的顺序可能不会产生不良后果,但是在多线程中就不一定了。例如:

//线程1:
context = loadContext();   // 语句1
inited = true;             // 语句2

//线程2:while(!inited ){
  sleep()
}
doSomethingwithconfig(context);
由于语句1和语句2没有数据依赖性,所以编译器可能会将两条指令重新排序,如果先执行语句2,这时线程1被阻塞,然后线程2的while循环条件不满足,接着往下执行,但是由于context没有赋值,于是会产生错误,这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。

·编译器优化:

volatile int i=10; 
int j = i; 
... 
int k = i; 
volatile 告诉编译器i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,因而编译器生成的可执行码会重新从i的地址读取数据放在k中。
而优化做法是,由于编译器发现两次从i读数据的代码之间的代码没有对i进行过操作,它会自动把上次读的数据放在k中。而不是重新从i里面读。这样以来,如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的稳定访问,不会出错。

·着重讲下原子性:

//记录当前的产品数量
private static volatile int count =0;
public static void main(String []args){
    //生产线线程
    new Thread(new Producer()).start();
    //消费者线程
    new Thread(new Consumer()).start();
}

//生产者类
static class Producer implements Runnable{

    @Override
    public void run() {
        while (true){
            try {
                empty.acquire();//等待空位
                mutex.acquire();//等待读写锁
                count++;
                mutex.release();//释放读写锁
                full.release();//放置产品
                System.out.println("消费者=======还剩: "+count+"个产品, 操作线程还剩: "+mutex.availablePermits()+", 产品还剩: "+full.availablePermits()+", 空余位置还剩: "+empty.availablePermits());
                //随机休息一段时间,让生产者线程有机会抢占读写锁
                Thread.sleep(((int)Math.random())%10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

//消费者类
static class Consumer implements Runnable{

    @Override
    public void run() {
        while (true){
            try {
                full.acquire();//等待产品
                mutex.acquire();//等待读写锁
                count--;
                mutex.release();//释放读写锁
                empty.release();//释放空位
                System.out.println("消费者=======还剩: "+count+"个产品, 操作线程还剩: "+mutex.availablePermits()+", 产品还剩: "+full.availablePermits()+", 空余位置还剩: "+empty.availablePermits());
                //随机休息一段时间,让消费者线程有机会抢占读写锁
                Thread.sleep(((int)Math.random())%10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

日志:

消费者=======还剩: 3个产品, 操作线程还剩: 3, 产品还剩: 2, 空余位置还剩: 8
消费者=======还剩: 2个产品, 操作线程还剩: 3, 产品还剩: 1, 空余位置还剩: 9
消费者=======还剩: 1个产品, 操作线程还剩: 3, 产品还剩: 0, 空余位置还剩: 10
消费者=======还剩: 1个产品, 操作线程还剩: 3, 产品还剩: 0, 空余位置还剩: 10
消费者=======还剩: 2个产品, 操作线程还剩: 3, 产品还剩: 1, 空余位置还剩: 9
消费者=======还剩: 2个产品, 操作线程还剩: 3, 产品还剩: 1, 空余位置还剩: 9
消费者=======还剩: 2个产品, 操作线程还剩: 3, 产品还剩: 1, 空余位置还剩: 9
消费者=======还剩: 3个产品, 操作线程还剩: 3, 产品还剩: 2, 空余位置还剩: 8
执行一段时间的日志:

消费者=======还剩: -3个产品, 操作线程还剩: 3, 产品还剩: 2, 空余位置还剩: 8
消费者=======还剩: -2个产品, 操作线程还剩: 3, 产品还剩: 3, 空余位置还剩: 7
消费者=======还剩: -1个产品, 操作线程还剩: 3, 产品还剩: 4, 空余位置还剩: 6
消费者=======还剩: 0个产品, 操作线程还剩: 3, 产品还剩: 5, 空余位置还剩: 5
消费者=======还剩: -5个产品, 操作线程还剩: 3, 产品还剩: 0, 空余位置还剩: 10
消费者=======还剩: 0个产品, 操作线程还剩: 3, 产品还剩: 5, 空余位置还剩: 5
消费者=======还剩: -1个产品, 操作线程还剩: 3, 产品还剩: 4, 空余位置还剩: 6
消费者=======还剩: -2个产品, 操作线程还剩: 3, 产品还剩: 3, 空余位置还剩: 7
消费者=======还剩: 1个产品, 操作线程还剩: 3, 产品还剩: 6, 空余位置还剩: 4
消费者=======还剩: -2个产品, 操作线程还剩: 3, 产品还剩: 3, 空余位置还剩: 7
呀,怎么产品还是负数了????

这也就是volitate的问题所在,上面这段代码看似没有什么问题呀,怎么回事,要知道java中什么是原子性,其实简单理解就是不能再细分的操作,例如 int i =0;这句话是一个赋值语句,而count++呢?其实他并不是一个原子级操作,其实有这么几步骤

1> 从主存中读取当前的count值,

2>将count+1;

3>将步骤2的结果再写入给count;

好,接着往下说:

如果producer在有两个在并发生产的时候(a,b线程),当a执行到count++语句时,从主存中获取了count的当前值(N),而这是a线程受阻,b线程执行了producer并且完成了count++(N+1),而这时候由于a线程已经读取过了count的值,还是之前的N,所以当a,b都执行完成之后,只是对count加了一次,因此,数据就没法得到保证了。

有人会问就会奇怪:volitate变量不是刷新主存吗?是的没错,但是a已经读取了count,所以a线程还是没有增加的旧值。

注意:volitate在读取上面保持同步作用,但在写上面不保持同步。

好,既然volitate无法满足原子性的需求,怎么办呢,怎么实现高并发的操作并保持变量符合逻辑呢?

2.AtomicInteger类

private static AtomicInteger count = new AtomicInteger(0) ;
 public static void main(String []args){
        //生产线线程
        new Thread(new Producer()).start();
        //消费者线程
        new Thread(new Consumer()).start();
    }

    //生产者类
    static class Producer implements Runnable{

        @Override
        public void run() {
            while (true){
                try {
                    empty.acquire();//等待空位
                    mutex.acquire();//等待读写锁
                    count.getAndIncrement();
                    mutex.release();//释放读写锁
                    full.release();//放置产品
                    System.out.println("消费者=======还剩: "+count+"个产品, 操作线程还剩: "+mutex.availablePermits()+", 产品还剩: "+full.availablePermits()+", 空余位置还剩: "+empty.availablePermits());
                    //随机休息一段时间,让生产者线程有机会抢占读写锁
                    Thread.sleep(((int)Math.random())%10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //消费者类
    static class Consumer implements Runnable{

        @Override
        public void run() {
            while (true){
                try {
                    full.acquire();//等待产品
                    mutex.acquire();//等待读写锁
                    count.decrementAndGet();
                    mutex.release();//释放读写锁
                    empty.release();//释放空位
                    System.out.println("消费者=======还剩: "+count+"个产品, 操作线程还剩: "+mutex.availablePermits()+", 产品还剩: "+full.availablePermits()+", 空余位置还剩: "+empty.availablePermits());
                    //随机休息一段时间,让消费者线程有机会抢占读写锁
                    Thread.sleep(((int)Math.random())%10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
日志:

消费者=======还剩: 0个产品, 操作线程还剩: 3, 产品还剩: 0, 空余位置还剩: 10
消费者=======还剩: 0个产品, 操作线程还剩: 3, 产品还剩: 0, 空余位置还剩: 10
消费者=======还剩: 1个产品, 操作线程还剩: 3, 产品还剩: 1, 空余位置还剩: 9
消费者=======还剩: 1个产品, 操作线程还剩: 3, 产品还剩: 1, 空余位置还剩: 9
消费者=======还剩: 0个产品, 操作线程还剩: 3, 产品还剩: 0, 空余位置还剩: 9
消费者=======还剩: 0个产品, 操作线程还剩: 3, 产品还剩: 0, 空余位置还剩: 10
消费者=======还剩: 1个产品, 操作线程还剩: 3, 产品还剩: 1, 空余位置还剩: 9
运行1小时日志:

消费者=======还剩: 7个产品, 操作线程还剩: 3, 产品还剩: 7, 空余位置还剩: 3
消费者=======还剩: 8个产品, 操作线程还剩: 3, 产品还剩: 8, 空余位置还剩: 2
消费者=======还剩: 7个产品, 操作线程还剩: 3, 产品还剩: 7, 空余位置还剩: 3
消费者=======还剩: 7个产品, 操作线程还剩: 3, 产品还剩: 7, 空余位置还剩: 3
消费者=======还剩: 6个产品, 操作线程还剩: 3, 产品还剩: 6, 空余位置还剩: 4
消费者=======还剩: 5个产品, 操作线程还剩: 3, 产品还剩: 5, 空余位置还剩: 5
消费者=======还剩: 7个产品, 操作线程还剩: 3, 产品还剩: 7, 空余位置还剩: 3
消费者=======还剩: 4个产品, 操作线程还剩: 3, 产品还剩: 4, 空余位置还剩: 6
消费者=======还剩: 5个产品, 操作线程还剩: 3, 产品还剩: 5, 空余位置还剩: 5
消费者=======还剩: 4个产品, 操作线程还剩: 3, 产品还剩: 4, 空余位置还剩: 6
消费者=======还剩: 5个产品, 操作线程还剩: 3, 产品还剩: 5, 空余位置还剩: 5
消费者=======还剩: 4个产品, 操作线程还剩: 3, 产品还剩: 4, 空余位置还剩: 6
搞定。
总结:

1)AtomicInteger是在使用非阻塞算法实现并发控制,在一些高并发程序中非常适合,但并不能每一种场景都适合,不同场景要使用使用不同的数值类。

2)AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减。

好,接着我们说说Semaphore。

3.Semaphore类

Semaphore是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore可以用来构建一些对象池,资源池之类的,比如数据库连接池,我们也可以创建计数为1的Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。

它的主要处理流程是:

1、通过Semaphore的acquire()方法申请许可;

2、调用类成员变量sync的acquireSharedInterruptibly(1)方法处理,实际上是父类AbstractQueuedSynchronizer的acquireSharedInterruptibly()方法处理;

3、AbstractQueuedSynchronizer的acquireSharedInterruptibly()方法会先在当前线程未中断的情况下先调用tryAcquireShared()方法尝试获取许可,未获取到则调用doAcquireSharedInterruptibly()方法将当前线程加入等待队列。acquireSharedInterruptibly()

至于如何加入等待队列,还有等待队列的线程如何竞争获取许可,本文不做分析,需要理解AbstractQueuedSynchronizer源码

4、接下来竞争许可信号的tryAcquireShared()方法则分别由公平性FairSync和非公平性NonfairSync各自实现。

上面的例子也很清晰了,

当信号量都持有semaphore.acquire(),也就是semaphore.availabledPermits()==0时,也就不允许线程访问了;

一旦semaphore.release(),也就是释放锁时,也就是semaphore.availabledPermits()>0时,又有线程可以进行访问了,