Android多线程编程基础篇实例讲解

Android多线程编程基础篇实例讲解



  1. 两个概念

进程:操作系统进行资源分配和调度的基本单位,简言之,进程就是程序的实体
线程:操作系统调度的最小单位,一个进程可以包含多个线程


  1. 多线程编程的几个优点

1.减少程序的等待时间,可以把耗时的操作单独交给一个线程执行,不影响UI线程交互。
2.与进程相比,线程创建和切换开销更小,多线程在数据共享方面效率非常高。
3.计算机本身就具有执行多线程的能力,如果使用单线程,则会造成系统资源浪费。
4.简化程序结构,使程序便于理解和维护。


  1. 线程的状态(6种状态)
new :新创建状态,线程被创建还没有调用start方法,在线程运行之前还有一些工作
需要准备。

Runnable:可运行状态,调用start方法后,线程就处于Runnable状态,一个可运行的
线程可能正在运行,也可能没有运行,取决于系统调用的时间。

Blocked:阻塞状态。表示线程被锁阻塞,暂时不活动。

Waiting:等待状态。线程暂时不活动,并且不运行任何代码,这消耗最少的系统资源
直到线程调度器重新激活他。

Timed waiting:超时等待状态,和等待状态不同的是,他可以在指定的时间内
自动返回。

Terminated:终止状态,表示当前线程已执行完毕,导致线程终止的有两种情况:
第一种是run方法执行完毕正常退出;第二种就是因为一个没有捕获的异常而终止了
run方法,导致线程进入终止状态。

六种状态:创建,可运行状态,阻塞状态,等待,超时等待,终止。

线程创建以后,调用Thread的start方法,开始进入运行状态,当线程执行wait方法
以后,线程进入等待状态,进入等待状态的线程需要其他线程通知才能返回
运行状态,超时等待相当于在等待状态上加上了时间限制,如果超过时间限制就会
返回运行状态,当线程调用同步方法时,如果线程没有获得锁则进入阻塞状态
当阻塞状态的线程获得锁时,重新回到运行状态,当线程执行完毕或者遇到以外
异常终止时,都会进入终止状态。


  1. 创建线程(三种方式)

1.继承Thread重写方法

public class demoThread extends Thread {
    @Override
    public void run() {
        super.run();
        System.out.println("helloworld!");
    }
    public static  void main(String args[]){
        demoThread thread=new demoThread();
        thread.run();
    }
}

2.实现Runnable接口并实现该接口的run()方法

public class demoThread implements Runnable{

    public static  void main(String args[]){
        demoThread thread=new demoThread();
        Thread thread1=new Thread(thread);
        thread1.run();
    }
    @Override
    public void run() {
        System.out.println("helloworld");
    }
}

3.重写Callable接口,重写call()方法
Callable接口实际是属于Executor框架中的功能类,Callable接口与Runnable接口
类似,但是功能比runnable功能更加强大。
1.callable可以在任务接收后提供一个返回值,Runnable无法提供这个功能
2.callable中的call()方法可以抛出异常,而Runable无法抛出异常
3.运行callablee可以拿到一个Future对象,Future对象表示异步计算的结果,它提供了检查结果
是否完成的方法。由于线程属于异步计算模型,因此无法从别的线程中得到函数的
返回值,在这种情况下,可以使用Future来监视目标线程调用call()方法的情况
,但调用Future的get方法获取结果时,当前线程就会阻塞,直到call()方法返回结果

public class demoThread {
    public static class MyTestCallable implements Callable{
        @Override
        public Object call() throws Exception {
            return "helloworld";
        }
    }
    public static  void main(String args[]){
           MyTestCallable myTestCallable=new MyTestCallable();
        ExecutorService service=Executors.newSingleThreadExecutor();
       Future future= service.submit(myTestCallable);
       try {
           System.out.println(future.get());
       }catch (Exception e){
           e.printStackTrace();
       }
    }
}

一般推荐使用实现Runnable接口的方式,其原因是一个类应该在其需要加强或者修改 时才会被继承。




  1. 理解中断

当线程的run方法执行完毕,或者在方法中出现没有捕获的异常时,线程将终止
在java早期有一个stop方法,其他线程可以调用它终止线程,但是这个方法
现在已经不推荐使用了,interrupt方法可以用来请求中断线程,当一个线程调用
interrupt方法时,线程的中断标识位将被置位,可以调用Thread.currentThread
.isInterrupted()。还可以调用Thread.interrupt()来对中断标识位进行复位,但是如
果一个线程被阻塞就无法检测中断状态。如果一个线程处于阻塞状态,
线程在检查中断标识位 时如果发现中断标识位为true则会抛出异常,
并且在抛出异常之前将将线程 的中断标识为复位,即重新设置为false,需要注意的是
被中断的线程不一定会终止,中断线程是为了引起线程的注意,被中断的线程可以决定
如何去响应中断如果是比较重要的线程则不会理会中断,而大部分情况则是线程
会将中断作为一个终止的请求。

安全的终止线程的方法一

public class demoThread {
    public static void main(String args[]) throws InterruptedException{
            MoonRunner moonRunner=new MoonRunner();
            Thread thread=new Thread(moonRunner);
            thread.start();
            TimeUnit.MILLISECONDS.sleep(1000);//
            thread.interrupt();
    }
    public static class MoonRunner implements Runnable{
       private int i=0;
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()){
                i++;
                System.out.println("i="+i);
            }
            System.out.println("stop");
        }
    }
}

除了中断还可以通过boolean变量来控制是否需要终止线程。
上面两种是比较常用的安全终止线程的方法。



  1. 同步

在多线程应用中如果两个或者两个以上的线程需要共享同一个数据的存储
如果两个线程同时存储相同的对象,并且每一个线程都调用了修改该对象的
方法,这种情况通常被称为竞争条件,竞争条件最容易理解的例子如下:

车站卖火车票,每一个售票点是一个线程,资源就是所有的票,如果有两个
或者两个以上的售票点同时售出同一趟列车的同一个座位就会产生问题,
不使用同步无法保证其原子性,解决方法如下:当一个线程要使用火车票
这个资源时,我们就交给他一把锁,等他把事情做完以后再把锁给另外一个要用这个
资源的线程。

重入锁与条件对象

synchronized关键字自动提供了锁以及相关机制,大多数需要显示锁的情况使用
synchronized非常方便,但是我们引入重入锁和条件对象时,能够更好的理解
synchronized关键字,重入锁ReentrantLock是Java SE 5.0引入的,就是支持
重进入的锁,他表示该锁能够支持一个线程对资源的重复加锁.

Lock mlock=new ReentrantLock();
mlock.lock();
try{
....
}finally{
mlock.unlock();
}

这一结构确保任何时刻只有一个线程进入临界区,临界区就是在用一个时刻
只能有一个任务代码访问的代码区,一旦一个线程封锁了锁对象,其他任何线程
都无法进入lock语句,把解锁的操作放在finally中是是十分必要的,如果在临界区
发生了异常,锁是必须要释放的,否则其他线程就会永远被阻塞。

进入临界区以后,比方说一个人在付款时发现钱不够,则虽然这个线程获得锁可以操作
余额,但是不满足条件,这是我们可以使用一个条件对象来管理那些已经获得锁
但是不满足任务执行条件的线程。

下面通过一个支付宝转账的例子来说明重入锁。

public class Alipay {
    private static Alipay alipay;
    private double[] accounts;//账号列表
    private Lock alipayLock;//全局锁对象
    private Condition condition;//条件对象
    public Alipay(int n,double money){
        accounts=new double[n];
        alipayLock=new ReentrantLock();//重入锁
        condition=alipayLock.newCondition();//条件对象
        for(int i=0;i<accounts.length;i++){
            accounts[i]=money;
        }
    }
    public void transfer(int from ,int to,int money) throws InterruptedException{
        //转账
        alipayLock.lock();//获得锁
        System.out.println(from+"号转账获得锁");
        try{
            while (accounts[from]<money){
                //判断当前转出金额与余额大小
                System.out.println("进入阻塞状态并放弃锁");
                condition.await();//进入阻塞状态,放弃当前锁
            }
            //转账操作
            accounts[from]=accounts[from]-money;
            accounts[to]=accounts[to]+money;
            System.out.println("账户"+from+"余额为:"+accounts[from]);
            System.out.println("账户"+to+"余额为:"+accounts[to]);
            condition.signalAll();//唤醒被由于不满足转账条件的阻塞的线程
        }finally {
            alipayLock.unlock();
            System.out.println("释放锁");
        }
    }

    public static void main(String args[]) throws InterruptedException {
        alipay =new Alipay(5, 250);
        myThread mythread=new myThread();
        Thread thread=new Thread(mythread);
        thread.start();
        TimeUnit.MILLISECONDS.sleep(10);
        alipay.transfer(3, 2, 200);


    }
    static class myThread implements Runnable{

        @Override
        public void run() {
            try {

                System.out.println("2号给4号转账200");
                alipay.transfer(2, 4, 300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

打印的输出:

2号给4号转账200
2号转账获得锁
进入阻塞状态并放弃锁
3号转账获得锁
账户3余额为:50.0
账户2余额为:450.0
释放锁
账户2余额为:150.0
账户4余额为:550.0
释放锁

2号给4号转账获得锁以后由于不满足条件进入阻塞状态并放弃锁,3号给2号
转了200后,调用condition.signalAll()唤醒不满足条件被阻塞的线程重新去获得
执行。请注意这里子线程一定要在main线程之前开启,如果main线程进入阻塞 子线程也将无法开启

采用同步方法synchronized代码更加简洁,但原理和上面是一样的,不需要我们
去创建条件对象和锁对象,只需要输入条件即可,代码如下:

public class Alipay {
    private static Alipay alipay;
    private double[] accounts;//账号列表
    public Alipay(int n,double money){
        accounts=new double[n];
        for(int i=0;i<accounts.length;i++){
            accounts[i]=money;
        }
    }
    public synchronized void transfer(int from ,int to,int money) throws InterruptedException{
        //转账
        System.out.println(from+"号转账获得锁");
            while (accounts[from]<money) {
                //判断当前转出金额与余额大小
                System.out.println("进入阻塞状态并放弃锁");
                wait();//进入阻塞状态,放弃当前锁
            }
            //转账操作
            accounts[from]=accounts[from]-money;
            accounts[to]=accounts[to]+money;
            System.out.println("账户"+from+"余额为:"+accounts[from]);
            System.out.println("账户"+to+"余额为:"+accounts[to]);
            notifyAll();//唤醒被由于不满足转账条件的阻塞的线程
            System.out.println("释放锁");
        }


    public static void main(String args[]) throws InterruptedException {
        alipay =new Alipay(5, 250);
        myThread mythread=new myThread();
        Thread thread=new Thread(mythread);
        thread.start();
        TimeUnit.MILLISECONDS.sleep(10);
        alipay.transfer(3, 2, 200);


    }
    static class myThread implements Runnable{

        @Override
        public void run() {
            try {

                System.out.println("2号给4号转账200");
                alipay.transfer(2, 4, 300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

  1. volatile关键字简单介绍

如果声明一个域为volatile,那么编译器和虚拟机就知道该域是可能
被另外一个线程并发更新的。

两个线程之间如果要进行通信,必须要经历下面两个步骤:
1.线程A把线程A本地内存中更新过的共享变量刷新到主存中去
2.线程B去主存中读取线程A之前已更新过的共享变量

当一个共享变量被volatile修饰后,他就具备两个含义,
1.线程修改了该变量的值,变量的新值对其他线程是立即可见的,会强制
将修改的值写入主存
2.禁止指令使用重排序

简而言之,使用了volatile声明的域,在值更新时会强制写入主存,其他的线程
也能即刻的知道该值发生变化,其他的普通变量在线程中更新时,写入主存
的时间是不确定的。





下一篇深入讲解volatile关键字和阻塞队列的知识

发布了123 篇原创文章 · 获赞 74 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/weixin_43927892/article/details/101037889