Android_原理篇_线程的控制和原理2(并发)

版权声明:本文为博主菜菜龙原创文章,转载请注明地址http://blog.csdn.net/jambestwick。

前言:上次讲解讲解了下线程的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.availabledPer mits()==0时,也就不允许线程访问了;
一旦semaphore.release(),也就是释放锁时,也就是semaphore.availabledPermits()>0时,又有线程可以进行访问了,是不是分方便。大笑


本文就写到此
附上源码地址:http://github.com/jambestwick/SemaphoreTest,觉得简单还不错,记得关注哦,谢谢大笑



















发布了19 篇原创文章 · 获赞 19 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/baidu_30084597/article/details/79253761
今日推荐