Android-并发基础一
1.线程启动的方式
启动线程的方式只有两种:
2.线程状态
java中线程有2种状态
3.死锁
死锁是指:两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
死锁产生的四个必要条件:
模拟一个死锁的场景如下:
private static Object mLock1 = new Object();//第一个锁
private static Object mLock2 = new Object();//第二个锁
/**
* 先拿到锁1 再申请锁2
* @throws InterruptedException
*/
private static void task1() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (mLock1){
System.out.println(threadName+" get lock 1");
Thread.sleep(100);
synchronized (mLock2){
System.out.println(threadName+" get lock 2");
}
}
}
/**
* 先申请锁2 再申请锁1
* @throws InterruptedException
*/
private static void task2() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (mLock2){
System.out.println(threadName+" get lock 2");
Thread.sleep(100);
synchronized (mLock1){
System.out.println(threadName+" get lock 1");
}
}
}
private static class TestThread extends Thread{
private String name;
public TestThread(String name) {
this.name = name;
}
@Override
public void run() {
Thread.currentThread().setName(name);
try {
task1();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread.currentThread().setName("main thread");
//子线程执行任务1
TestThread testThread = new TestThread("test thread");
testThread.start();
//主线程执行任务2
task2();
}
这样就产生了,死锁。
产生的影响是:
那么如何解决死锁呢?
4.活锁
两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放的过程。
解决办法:每个线程休眠随机数,错开拿锁的时间。
5.线程饥饿
由于线程的优先级比较低,一直无法获取到CPU分配的时间片进行执行,这个现象被称为线程饥饿。
6.ThreadLocal
ThreadLocal 和 Synchonized 都用于解决多线程并发访问问题。
可是ThreadLocal 与 synchronized 有本质的差别。synchronized 是利用锁的机制,使变量或代码块在某一时该仅仅能被一个线程访问。而 ThreadLocal 为每个线程都提供了变量的副本,使得每个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。
ThreadLocal的使用:
ThreadLocal 类接口很简单,只有 4 个方法:
public final static ThreadLocal RESOURCE = new ThreadLocal();RESOURCE代表一个能够存放String类型的ThreadLocal对象。此时不论什么一个线程能够并发访问这个变量,对它进行写入、读取操作,都是线程安全的。
持有关系是:
从下面的源码中我们可以看到:Thread持有ThreadLocalMap,这个map里边持有一个Entry对象数组,Entry的key就是ThreadLocal,value就是需要保存的数据。
public class Thread implements Runnable {
...
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
线程持有ThreadLocalMap成员变量
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
ThreadLocalMap持有Entry数组表table成员变量
Entry的key保存的就是ThreadLocal对象,使用弱引用进行存储的,value就是我们保存的Object数据。
7.CAS基本原理
CAS:Compare and swap,比较并交换,实现原子操作的一种方式。
CAS基本思路:每一个 CAS 操作过程都包含三个运算符:一个内存地址 V,一个期望的值 A 和一个新值 B,操作的时候如果这个地址上存放的值等于这个期望的值 A,则将地址上的值赋为新值 B,否则不做任何操作。
get获取变量的值(旧值)–》计算后得到新值–》Compare内存中的变量值跟旧值进行比较,如果相等–》将旧值更改为新值,如果Compare比较不相等,再重来一次,这就是CAS的执行思路。
源码中Atomic开头的类都是原子操作类:
AtomicInteger
AtomicIntegerArray
AtomicBoolean
AtomicLong
AtomicReference:原子更新引用类型,如果有多个变量需要进行原子操作,可以封装到一个对应里边使用原子引用类型。
AtomicStampedReference:利用版本戳的形式记录了每次改变以后的版本号,这样的话就不会存在ABA问题了,这就是 AtomicStampedReference 的解决方案。AtomicStampedReference 是使用pair 的int stamp 作为计数器使用,AtomicStampedReference 可以得到数据被修改过几次。
AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。这个类只关心是否被修改过,修改几次不管。
public AtomicMarkableReference(V initialRef, boolean initialMark) {
pair = Pair.of(initialRef, initialMark);
}
使用CAS存在的问题:
ABA问题:因为 CAS 需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是 A,变成了 B,又变成了A,那么使用CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。解决办法:使用AtomicStampedReference或者AtomicMarkableReference。
循环时间长开销大:自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。
只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。还有一个办法,就是把多个共享变量合并成一个共享变量来操作。
8.阻塞队列
队列:是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
阻塞队列:
并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序整体处理数据的速度。
为了解决这种生产消费能力不均衡的问题,便有了生产者和消费者模式。
生产者和消费者模式是通过一个容器来解决生产者和消费者的强耦合问题
生产者和消费者彼此之间不直接通信,而是通过阻塞队列来进行通信,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
阻塞队列里边的方法也不全是阻塞的,可以分为四类:
常见的阻塞队列有:
以上的阻塞队列都实现了 BlockingQueue 接口,也都是线程安全的。颜色加深的三个比较常用。