Android多线程之线程安全详解

1 线程和进程的区别

首先一点,进程是包含线程的。就是一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。而一个运行的软件是可以包含多个进程的。线程是码顺序执⾏行行下来,执⾏行行完毕就结束的一条线。

线程和进程的具体区别如下:

  • 进程是资源分配的最小单位,线程是程序执行的最小单位。

  • 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。

  • 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。

  • 但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

2 多线程的使用

首先,我们看一下我们平时是如何使用多线程的。

Thread thread = new Thread() {
    @Override
    public void run() {
        System.out.println("Thread started!");
    }
};
thread.start();
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("Runnable started");
    }
};
Thread thread = new Thread(runnable);
thread.start();
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("Runnable started!");
    }
};
Executor executor = Executors.newCachedThreadPool();
executor.execute(runnable);

以上是几种最常见的多线程使用方式。

3 synchronized的使用

synchronized修饰方法和synchronized代码块

为了保证线程安全,我们经常会用到synchronized修饰方法。

synchronized可以用来修饰方法,也可以用作代码块。

private synchronized void setName(String name) {
    this.name = name;
}
private synchronized void setAge(int age) {
    synchronized (this) {
        this.age = age;
    }
}

synchronized其实是通过Monitor来保证内部资源的互斥访问。比如在内存中有这么一个方法A,B线程和C线程都可以访问A,如果A方法加了synchronized,则进入方法之前都要先访问它的Monitor,如果A线程已经在访问C方法,那么B线程访问C方法时,发现C方法已经有人在访问了,Monitor就会阻隔其它方法进入。

而我们在对方法加入synchronized加入Monitor,其实是对整个对象进行监控,如果一个类中有两个方法都加入了Monitor,那么,当第一个方法中有线程在访问时,无论是第一个方法,还是第二个方法,其它线程都不能进去。

那么,如果让同一类中的不同方法,既保证线程安全,又不会让它们相互干扰呢。就要用到synchronized代码块,如下所示,这样Monitor的对象就不是整个对象,而是类中的具体某个对象。有几个方法需要用到代码块,就创建几个这样的Monitor对象。

private Object monitor = new Object();
private synchronized void setAge(int age) {
    synchronized (monitor) {
        this.age = age;
    }
}

4 synchronized的作用

第一,加入了这个关键字,能够保证内部资源的互斥访问,在同一时间内,由相同的Monitor监视的代码,只能有一个线程访问,就像上文说的那样。

除了保证互斥访问外,还有另一个作用,也是非常重要的,那就是保证监视资源的数据同步,即在获取到Monitor后,会先将内存中的共享数据复制到自己的缓存中。而在释放Monitor的时候,会先将缓存的数据复制到内存中。这样保证了内存中的数据永远是最新的,获取到的数据不会因为线程不同步的原因出错。

5 volatile关键字

volatile也是用来解决线程同步问题,但是它是比较轻量级的,而且即使用了也不能像synchronized那样保证多线程下数据不会出错。一是它只对基本类型的赋值有效。比如你有个User对象,你对User对象的操作是具有原子性的,但是你对User内部的属性,比如name,age的操作就不具备。

另外,它也不能保证复合操作的原子性,比如i++,是由多个原子操作组成,volatile只能保证他们操作的i是同一块内存,但依然可能出现数据错误的情况。

6 Lock的使用

Lock用起来会更加麻烦一点,因为它不仅要使用lock来加锁,还要使用unlock来解锁。但是它也有它的好处。

ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
Lock readLock = lock.readLock();
Lock writeLock = lock.writeLock();
private int i = 0;

private void count() {
    writeLock.lock();
    try {
        i++;
    } finally {
        writeLock.unlock();
    }
}

private void print() {
    readLock.lock();
    try {
        System.out.print(i);
    } finally {
        readLock.unlock();
    }
}

如上所诉,我们可以使用Lock的实现类readLock和writeLock。有一个线程已经占用了读锁,即使已经有了A线程访问,它仍然能被其它线程访问读锁的方法,但是写锁的方法会等到读锁的方法释放后才能访问。反过来,如果一个线程已经占用了写锁,那无论其它线程是要访问读锁方法还是写锁方法,都需要等待写锁的释放。

简单的说,就是读锁之后可以访问读锁,但是需要等到读锁释放才能访问写锁。写锁之后,读锁和写锁都不能访问,都需要等待写锁释放。

其实也很容易理解,因为我们读取操作并不会改变内存中的共享资源,即使多个线程同时读取,也不会改变它们。但是写的操作是会改变共享资源的,所以进行写操作时候,其它线程不能对该资源进行操作。

你可能感兴趣的:(Android多线程之线程安全详解)