并发编程

75 阅读6分钟

线程基础

一个Android应用在创建的时候会开启一个线程,我们叫它主线程或者UI线程。如果我们想要访问网络或者数据库等耗时的操作时,都会开启子线程去处理。如果在主线程执行耗时操作则会被阻塞,android也会抛出异常,因此多线程在Android开发中占据着十分重要的地位。

进程与线程

  • 进程:进程就是程序的实体,是受操作系统管理的基本运行单元。
  • 线程:一个进程包含许多子任务,这些子任务就是线程,是操作系统调度的最小单元,也叫作轻量级进程。
  • 使用多线程的原因:
    • 使用多线程可以减少程序的响应时间
    • 使用多线程能简化程序的结构

线程的状态

  • New:新创建状态
  • Runnable:可运行状态
  • Blocked:阻塞状态
  • Waiting:等待状态
  • Timed waiting:超时等待状态
  • Terminated:终止状态

1.PNG

创建线程

1.继承Thread类,重写run()方法

public class TestThread extends Thread{
    //重写run方法
    public void run(){
        System.out.println("Hello World");
    }
    public static void main(String[] args){
        //创建Thread子类的实例,即创建了线程对象
        Thread mThread = new TestThread();
        //调用线程对象的start方法来启动线程
        mThread.start();
    }
}

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

public class TestRunnable implements Runnable {
    public void run(){
        System.out.println("Hello World");
    }
    public static void main(String[] args){
        //创建thread子类的实例,并用实现runnable接口的对象作为参数实例化该对象
        TestRunnable mTestRunnable = new TestRunnable();
        Thread mThread = new Thread(mTestRunnable);
        mThread.start();
    }
}

3.实现Callable接口,重写call()方法

public class TestCallable{
    public static class MyTestCallable implements Callable{
        public String call() throws Exception{
            return "Hello World";
        }
    }
    public static void main(String[] args){
        MyTestCallable mTestCallable = new MyTestCallable();
        ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
        //运行callable后返回一个future对象,表示异步计算的结果,提供了检查计算是否完成的方法
        Future mFuture = mExecutorService.submit(mTestCallable);
        try{
            //等待线程结束并返回结果
            System.out.println(mFuture.get());
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

在这3种方式中,一般推荐用实现Runnable接口的方式,其原因是,一个类应该在其需要加强或者修改时才会被继承。因此如果没有必要重写Thread类的其他方法,那么在这种情况下最好用实现Runnable接口的方式。

理解中断

当线程的run方法执行完毕,或者在方法中出现没有捕获的异常时,线程将终止。

interrupted(): 用来请求中断线程。当一个线程调用 interrupt 方法时,线程的中断标识位将被置位(中断标识位为true)

下面用中断来终止线程:

public class StopThread {
    public static void main(String[] args) throws InterruptedException{
        MoonRunner runnable = new MoonRunner();
        Thread thread = new Thread(runnable,"MoonThread");
        thread.start();
        //调用sleep方法使mainThread睡眠10ms
        TimeUnit.MILLISECONDS.sleep(10);
        thread.interrupt();
    }
    public static class MoonRunner implements Runnable{
        private long i;
        @Override
        public void run(){
            while(!Thread.currentThread().isInterrupted()){
                i++;
                System.out.println("i="+i);
            }
            System.out.println("stop");
        }
    }
}

同步

在多线程应用中,两个或者两个以上的线程需要共享对同一个数据的存取。如果两个线程存取相同的对象,并且每一个线程都调用了修改该对象的方法,这种情况通常被称为竞争条件。为了消除这种竞争,我们可以在一个线程获得该对象时,就给它一把锁,等它完成任务再把锁给另外的线程。

重入锁与条件对象

重入锁(ReentranLock):支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。

用 ReentrantLock保护代码块的结构如下 :

Lock mLock = new ReentrantLock();
mLock.lock();
try{
...
}
finally{
    mLock.unLock();
}

这一结构确保任何时刻只有一个线程进入临界区,临界区就是在同一时刻只能有一个任务访问的代码区。

下面我们写一个支付宝的例子:

public class Alipay {
    private double[] accounts;
    private Lock alipaylock;

    public Alipay(int n,double money){
        accounts = new double[n];
        for(int i=0;i<accounts.length;i++){
            accounts[i] = money;
        }
    }


    public void transfer(int from,int to,double amount) throws InterruptedException{
        alipaylock.lock();
        try{
            while(accounts[from]<amount){
                //wait
            }
        } finally{
            alipaylock.unlock();
        }
    }
}

结果我们发现当转账方余额不足,如果再有其它线程给该转账方转足够的钱,本应该可以转账成功。但是这个线程已经获取了锁,它具有排他性,别的线程无法获取锁来进行存款操作,因此我们需要引入条件对象。

条件对象:使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。

一个锁对象拥有多个相关的条件对象,可以用newCondition方法获得一个条件对象,我们得到条件对象后调用await方法,当前线程就被阻塞了并放弃了锁。修改后如下:

public class Alipay {
    private double[] accounts;
    private Lock alipaylock;
    private Condition condition;
    //创建一个名为lock的Object类,以便使用Object的锁
    private Object lock = new Object();

    public Alipay(int n,double money){
        accounts = new double[n];
        for(int i=0;i<accounts.length;i++){
            accounts[i] = money;
        }
        alipaylock = new ReentrantLock();
        //得到条件对象
        condition = alipaylock.newCondition();
    }

    public void transfer(int from,int to,double amount) throws InterruptedException{
        alipaylock.lock();
        try{
            while(accounts[from]<amount){
                System.out.println("余额不足!");
                condition.await();
            }
            accounts[from] -= amount;
            accounts[to] += amount;
            condition.signalAll();
            System.out.println("转账成功\n"+from+"->"+to+":"+amount);
            System.out.println(from+":"+accounts[from]+"\t"+to+":"+accounts[to]);
        } finally{
            alipaylock.unlock();
        }
    }

一旦一个线程调用 await 方法,它就会进入该条件的等待集并处于阻塞状态,直到另一个线程调用了同一个条件的signalAll方法时为止。

当调用signalAll方法时并不是立即激活一个等待线程,它仅仅解除了等待线程的阻塞,以便这些线程能够在当前线程退出同步方法后,通过竞争实现对对象的访问。

同步方法

  • Java中的每一个对象都有一个内部锁。如果一个方法用 synchronized 关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。
public synchronized void method(){
    ...
}

上例改写后为:

public synchronized void transfer(int from,int to,int amount) throws InterruptedException {
        while(accounts[from]<amount){
            wait();
        }
        //转账的操作
        accounts[from] = accounts[to] - amount;
        accounts[to] = accounts[to] + amount;
        notifyAll();
}

内部对象锁只有一个相关条件,wait方法将一个线程添加到等待集中,notifyAll或者notify方法解除等待线程的阻塞状态。

同步代码块

每一个Java对象都有一个锁,线程可以调用同步方法来获得锁,同步代码块也可以获得锁。

synchronized(obj){
​
}

如上例也可以这样写:

public void transfer(int from,int to,int amount){
    synchronized(lock){
        //转账操作
        accounts[from] -= amount;
        accounts[to] += amount;
    }
}

volatitle

1. java内存模型

2. 原子性、可见性和有序性

  • 原子性

对基本数据类型变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行完毕,要么就不执行。

  • 可见性

可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。

  • 有序性

线程按顺序执行同步代码。

3. volatile关键字

volatile保证可见性和有序性,但不保证原子性。

4.正确使用volatile

使用volatile必须具备以下两个条件:

(1)对变量的写操作不会依赖于当前值。

(2)该变量没有包含在具有其他变量的不变式中。

示例:包含了一个不等式:下界总是小于或等于上界

public class NumberRange{
    private volatile int lower,upper;
    public NumberRange(int lower,int upper){
        this.lower = lower;
        this.upper = upper;
    }
​
    public int getLower(){
        return lower;
    }
    public int getUpper(){
        return upper;
    }
    public void setLower(int value){
        if(value>upper)
            throw new IllegalArgumentException();
        lower = value;
    }
    public void setUpper(int value) {
        if (value < lower)
            throw new IllegalArgumentException();
        upper = value;
    }
​
    public static void main(String[] args){
        NumberRange num = new NumberRange(0,5);
        Thread thread1 = new Thread(){
            @Override
            public void run() {
                num.setLower(4);
            }
        };
        Thread thread2 = new Thread(){
            @Override
            public void run() {
                num.setUpper(3);
            }
        };
        thread1.start();
        thread2.start();
        System.out.println("lower:"+num.getLower()+"\t"+"upper:"+num.getUpper());
    }
}

这种方式将lower和upper字段定义为volatile类型不能够充分实现类的线程安全。如果当两个线程在同一时间使用不一致的值执行setLower和setUpper的话,则会使范围处于不一致的状态。

volatile的两种使用场景

(1)状态标志

volatile boolean shutdownRequested;
public void shutdown(){
    shutdownRequested = true;
    System.out.println("stop");
}
public void doWork(){
    while(!shutdownRequested){
        System.out.println("running..."+(++count));
    }
}

(2)双重检查模式(DCL)

public class Singleton {
    private volatile static Singleton instance = null;
    //双重检查模式
    public Singleton getInstance(){
        if(instance == null){
            synchronized (this){
                if(instance==null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}