java 基础:多线程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Gdeer/article/details/88832972

一、什么是线程

要说线程,先说进程,就像手机上的 App,每个 App 都有一个进程,它的所有操作都在这个进程里进行。

线程是进程里处理任务的单位。通常一个进程有一个主线程,多个子线程。如一个浏览器里,界面的显示就是它的主线程,当我们开始下载一个东西,如果在主线程下载,用户就不得不等到下载完成后再进行别的操作,这时就要将下载放在子线程中进行。

所以子线程,就是用来处理耗时任务的

1.1 线程的执行原理

一个进程同时只能运行一个线程,多线程操作其实是一种模拟并行执行的方式。
在这里插入图片描述

二、线程的状态

线程的一生包括生、活、死,共有6种状态,生、死一种,活分为四种。

  • New (新创建)
  • Runnable(可运行)
  • Blocked(被阻塞)
  • Waiting(等待)
  • Timed waiting(计时等待)
  • Terminated(被终止)

Blocked、Waiting、Timed waiting,也可以统称为阻塞状态,线程不享有 CPU 时间。

2.1 新创建线程

在这里插入图片描述
最常见的

Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
    }
});

2.2 可运行线程

thread.start()

调用 start 方法后,线程处于可运行状态。一个可运行线程可能在运行也可能没在运行,这取决于操作系统给线程提供运行的时间。

扫描二维码关注公众号,回复: 5679680 查看本文章

2.3 被阻塞线程和等待线程

在这里插入图片描述

  • 当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态。当所有其他线程释放该锁,并且线程调度器允许本线程持有它时,该线程变成非阻塞状态。

  • 当一个线程已经获得某个锁,但需要某种条件才能继续运行时,它会放弃这个锁,并进入等待状态。当收到通知时,它会继续判断该条件是否满足,从而决定进入可运行状态还是继续处于等待状态。

    • Object.wait()
    • Thread.join()
      a.join() 将线程 a 插入调用这句代码的线程里,当 a 执行完之后再执行原有的线程。
      实现原理:
      a 活着就调 a.wait() 造成原线程等待,a 死了会调 a.notify(),激活原线程。
    • 等待 java.util.concurrent 库中的 Lock 或 Condition 时
  • 当一个线程的等待状态有时间限制时,它处于计时等待状态。

    • Thread.sleep()
    • Object.wait()
    • Thread.join()
    • Lock.tryLock()
    • Condition.await()

2.4 被终止的线程

线程的终止:

  • run 方法执行完,正常死亡
  • 因为一个未捕获异常,意外死亡

没有可以强制线程终止的方法。但 interrupt 方法可以用来请求终止线程。

2.4.1 终结运行中的线程

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("work");
            }
        }
    });
    thread.start();

    Thread.sleep(2000);
    thread.interrupt();
    System.out.println("stop");
}

output:
...
work
work
work
stop

该线程将会在 2s 后停止。

2.4.2 终结阻塞状态下的线程

如果线程的 run 里有 sleep,情况会不一样,interrupt 和 sleep 将会冲突,清除各自的状态,抛出异常。
在这里插入图片描述

先调用了 sleep,再调用 interrupt(如上图注释所述,需在别的线程操作),会抛出 InterruptedException 异常,sleep 状态将被中断,同时 interrupt 标记位也没有置为 true。

Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().isInterrupted());
                e.printStackTrace();
            }
        }
    }
});
thread.start();
thread.interrupt();

output:
false
java.lang.InterruptedException: sleep interrupted

先调用了 interrupt,再调用 sleep,会抛出 InterruptedException 异常,interrupt 状态将被复位,同时 sleep 状态也无法进入。

Thread.currentThread().interrupt();
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    e.printStackTrace();
}
System.out.println(Thread.currentThread().isInterrupted());

output:
java.lang.InterruptedException: sleep interrupted
false

三、线程的属性

3.1 线程优先级

默认情况下,一个线程可以继承它父线程的优先级。当线程调度器有机会选择新线程时,它首先选择具有较高优先级的线程。

  • t.setPriority()

    设置一个线程的优先级。

  • Thread.yield()

    导致当前执行线程处于让步状态。让线程调度器去执行其他高优先级或同优先级的线程。

3.2 守护线程

守护线程的唯一用途是为其他线程提供服务。当只剩下守护线程时,虚拟机就退出了。

  • t.setDaemon()

    设置线程为守护线程或用户线程,这一方法必选在线程启动前调用。

3.3 未捕获异常处理器

线程中的未捕获异常会被传递到一个未捕获异常处理器中。该处理器必须实现 Thread.UnCaughtExceptionHandler 接口。
在这里插入图片描述

未捕获异常的分发:

public final void dispatchUncaughtException(Throwable e) {
    Thread.UncaughtExceptionHandler initialUeh =
            Thread.getUncaughtExceptionPreHandler();
    if (initialUeh != null) {
        try {
            initialUeh.uncaughtException(this, e);
        } catch (RuntimeException | Error ignored) {
            // Throwables thrown by the initial handler are ignored
        }
    }
    getUncaughtExceptionHandler().uncaughtException(this, e);
}

概述(详细请查看源码):

先交给 uncaughtExceptionPreHandler(Android 特有)预处理,再交给 uncaughtExceptionHandler 处理,uncaughtExceptionHandler 为空,则交给 group(ThreadGroup)处理。

ThreadGroup 中的处理:

public void uncaughtException(Thread t, Throwable e) {
    if (parent != null) {
        parent.uncaughtException(t, e);
    } else {
        Thread.UncaughtExceptionHandler ueh =
            Thread.getDefaultUncaughtExceptionHandler();
        if (ueh != null) {
            ueh.uncaughtException(t, e);
        } else if (!(e instanceof ThreadDeath)) {
            System.err.print("Exception in thread \""
                             + t.getName() + "\" ");
            e.printStackTrace(System.err);
        }
    }
}

Android 中就可以通过 UnCaughtExceptionHandler 来做一些异常的上报工作。

四、同步

有两种机制防止代码块受到并发访问的干扰。synchronized 关键字和 ReentranLock 类。

4.1 ReentrantLock

myLock.lock();
try {
    ...
} finally {
    myLock.unlock();
}

ReentrantLock,顾名思义,它是可重入的,即线程可以重复获取已经持有的锁。锁保持一个持有计数来跟踪对 lock 方法的嵌套调用。线程在每一次 lock 时都要调用 unlock 来释放锁。

  • rentrantLock.lock()

    获取一个锁;如果锁同时被另一个线程拥有则发生阻塞。

  • rentrantLock.unLock()

    释放这个锁。

4.2 条件对象

很多时候,一个方法的执行是需要一定条件的:

if (someCondition) {
    ...
}

当加了锁以后:

myLock.lock();
try {
    if (someCondition) {
        ...
    }
} finally {
    myLock.unlock()
}

由于不满足条件,只能放弃锁。但是放弃之后可能又会调到这里,那还是不满足条件,所以这时就产生了条件对象。条件对象有三个方法:await、signalAll、signal。

  • await()

    使当前线程阻塞在当前条件对象上,进入等待状态。

  • signalAll()

    通知所有阻塞在当前条件对象的线程解除等待。

  • signal()

    通知任意一个阻塞在当前条件对象的线程解除等待。

我们加上条件对象的等待看看:

myLock.lock();
try {
    if (!someCondition) {
        condition.await();
    }
    ...
} finally {
    myLock.unlock();
}

但这样也会有问题,当解除等待,回到线程执行位置的时候,不一定 someCondition 是满足的,即使其他线程发送 signalAll 信号的时候满足,在线程的调度中间也可能发生其他事情导致不满足。所有还要加上循环:

Condition condition = myLock.newCondition();
...
myLock.lock();
try {
    while (!someCondition) {
        condition.await();
    }
    ...
} finally {
    myLock.unlock();
}

4.3 synchronized 关键字

先回顾一下锁和条件:

  • 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。
  • 锁可以管理试图进入被保护代码段的线程。
  • 所可以拥有一个或多个相关的条件对象。
  • 每个条件对象管理那些已经进入被保护代码段但还不能运行的线程。

Lock 和 Condition 提供了线程同步机制,但略显复杂。Java 提供了一个更简便的方式,synchronized 关键字。

Java 中每一个对象都有一个内部锁。如果一个方法用 synchronized 关键字声明,那么对象的锁将保护整个方法。

public synchronized void method() {
    ...
}

等价于

public void method() {
    this.intrinsicLock.lock();
    try {
        ...
    } finally {
        this.intrinsicLock.unlock();
    }
}

内部对象锁只有一个条件对象。wait 方法添加一个线程到等待集中,notifyAll/notify 方法解除线程的等待状态。换句话说,调用 wait、notifyAll 方法等价于

intrinsicCondition.await();
intrinsicCondition.signalAll();

静态方法也可以声明为 synchronized,如果调用这种方法,该方法获得相关的类对象的内部锁。例如,Bank 类有一个静态同步方法,则当它调用时,Bank.class 对象会被锁住。

内部锁和条件的局限:

  • 不能中断一个正在试图获得锁的线程。
  • 试图获得锁时不能设定超时。
  • 每个锁仅有一个条件,可能不够。

Lock、sychronized 方法的选择:

  • 最好都不用,实在要用,优先选择 concurrent 包内的机制,如阻塞队列。
  • 其次用 sychronized。
  • 只有 sychronized 不满足需求时才使用 Lock。

4.3.1 同步阻塞

synchronized 除了可以用在普通方法、静态方法上外,还可以用于代码块,又称为同步阻塞、同步控制块、客户端锁定。

synchronized (obj) {
    ...
}

这样,线程就获得了 obj 的锁,来完成同步的工作。

注意,使用这样的方式来完成对 obj 对象的原子操作是不靠谱的,因为无法保证 obj 所有可能修改值的方法都是同步的。

4.4 Volatile 域

现代的处理器和编译器,会使得同步的问题出现的概率很高:

  • 多处理器的计算机能够暂时在寄存器或本地内存缓冲区保存内存中的值。结果是,运行在不同处理器上的线程可能在同一个内存位置取到不同的值。
  • 编译器可以改变指令执行的顺序以使得吞吐量最大化。在多线程时,中间值就有可能被另一个线程改变。

使用 sychronized 可以解决以上两个问题,但似乎有点开销过大。volatile 关键字为实例域的同步访问提供了一种免锁机制。如果一个域被声明为 volatile,那么编译器和虚拟机就知道它可能被另一个线程并发更新。然后他们就会:

  • 域的更改同步写入主存,保证各个线程同步
  • 禁止指令重排序,避免因此产生的同步问题。

这解决了上面两个问题,但它仍不能提供原子性。因为即使没有指令重排序,各个线程之间的并发更新仍会对结果产生影响。

4.5 final 变量

与 valatile 关键字相似,final 也可以通过禁止指令重排序来提供变量的可见性。

  • 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
  • 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

如声明一个

final A a = new A();

其他线程会在构造函数构造之后才看到这个 a 变量。

如果不加 final,就不能保证其他线程看到的是 a 更新后的值,而有可能先看到 null,然后 a 才被赋值。

4.6 线程局部变量

ThreadLocal 可以给线程提供私有变量。

详见:Java 基础:ThreadLocal 解析

4.7 锁测试与超时

当调用 lock 方法来获取一个锁时,很可能发生阻塞。所有应该更谨慎地申请锁。

  • tryLock()

    尝试获得锁,如果成功获得锁,如果不成功,不会阻塞,线程可以去做其他事情。

  • tryLock(long time, TimeUnit unit)

    给 tryLock 加上超时时间。

  • lockInterruptibly()

    相当于一个无限超时的 tryLock() 方法。

lock() 方法调用时不会中断。如果一个线程在等待获得一个锁时被中断,中断线程在获得锁之前一直处于阻塞状态。

tryLock(time)、lockInterruptibly 可以解决这个问题,如果调用它们,线程在等待状态下被中断,会抛出 InterruptedException 异常。这是一个很有用的特性,因为允许程序打破死锁。

五、阻塞队列

对于许多线程问题,可以通过使用一个或多个队列以优雅且安全的方式将其形式化。生产者线程向队列插入元素,消费者线程则取出它们。

阻塞队列常用的有 8 个方法,分为 3 类:

  • put、take

    添加、移出,满或空时会阻塞。

  • add、remove、element

    添加、移出、访问,满或空时会抛异常。

  • offer、poll、peek

    添加、移出、访问,满或空时返回错误提示(null、false)。

concurrent 包中提供了阻塞队列的实现,LinkedBlockingQueueLinkedBlockingDequeArrayBlockingQueuePriorityBlockingQueueDelayQueueLinkedTransferQueue 等。

使用阻塞队列来控制一组线程,程序在一个目录及它的所有子目录下搜索所有文件,打印出包含指定关键字的行。

六、线程安全的集合

6.1 Concurrent 的 Map、Set 和 Queue

java.Util.concurrent 包提供了映射表、有序集和队列的高效实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet 和 ConcurrentLinkedQueue。

ConcurrentHashMap 是 HashMap 的线程安全实现。

ConcurrentSkipListMap 是 TreeMap 的线程安全实现。

ConcurrentSkipListSet 是 TreeSet 的线程安全实现。

它们的 api 调用都与自己的 非线程安全实现一样。

6.2 CopyOnWriteArrayList

CopyOnWriteArrayList 和 CopyOnWriteArraySet 是线程安全的集合,其中所有的修改线程对底层数据进行复制。如果在迭代器生成后数组被修改了,迭代器仍引用旧数组,即使它们已经不一样了。

顾名思义,它们在写的时候复制,写在复制的数组上。

6.2.1 ArrayList 的不安全

创建一个 ArrayList,启动两个线程,都给 list 中添加元素。

final List<String> list = new ArrayList<>();
new Thread(new Runnable() {
    @Override
    public void run() {
        for (int j = 0; j < 10000; j++) {
            list.add("");
        }
        System.out.println("thread 0: " + list.size());
    }
}).start();

new Thread(new Runnable() {
    @Override
    public void run() {
        for (int j = 0; j < 10000; j++) {
            list.add("");
        }
        System.out.println("thread 1: " + list.size());
    }
}).start();

运行结果:

Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 6246
	at java.util.ArrayList.add(ArrayList.java:459)
	at com.gdeer.gdtesthub.threads.MutiThreadTest$1.run(MutiThreadTest.java:19)
	at java.lang.Thread.run(Thread.java:745)
thread 1: 14338

可以发现,发生了 ArrayIndexOutOfBoundsException。这是因为 list 的 add 操作可以分为三步:

  • 增大容量至size+1
  • size++
  • 给第size个item赋值

这三步在多线程执行时,就可能出现异常,如下图:
在这里插入图片描述

6.2.2 CopyOnWriteArrayList 的安全

CopyOnWriteArrayList 给写操作加了锁,从而避免了这种情况的发生。

6.2.3 CopyOnWriteArrayList 的不安全

CopyOnWriteArrayList 只给写加了锁,读没有加锁,这样保证了读取的速度,但仍有不安全的隐患。

向 CopyOnWriteArrayList 里放入 10000 个测试数据,启动两个线程,一个不断地删除元素,一个不断地读取容器中最后一个数据。

final List<String> list = new CopyOnWriteArrayList<>();
for (int j = 0; j < 10000; j++) {
    list.add("");
}
new Thread(new Runnable() {
    @Override
    public void run() {
        for (int j = 0; j < 10000; j++) {
            list.remove(0);
        }
        System.out.println("thread 0: " + list.size());
    }
}).start();

new Thread(new Runnable() {
    @Override
    public void run() {
        while (true) {
            try {
                String s = list.get(list.size() - 1);
            } catch (Exception e) {
                e.printStackTrace();
                break;
            }
        }
    }
}).start();

运行结果:

java.lang.ArrayIndexOutOfBoundsException: 19938
	at java.util.concurrent.CopyOnWriteArrayList.get(CopyOnWriteArrayList.java:387)
	at java.util.concurrent.CopyOnWriteArrayList.get(CopyOnWriteArrayList.java:396)
	at com.gdeer.gdtesthub.threads.MutiThreadTest$2.run(MutiThreadTest.java:33)
	at java.lang.Thread.run(Thread.java:745)
thread 0: 10000

可以发现,即使使用了 CopyOnWriteArrayList,也发生了ArrayIndexOutOfBoundsException。

即读写发生在不同线程中时,就有可能能产生问题。

七、Callable 和 Future

Runnable 是一段可执行的任务。

Callable 相当于一个有返回值的 Runnable。

Future 是一个对 Runnable 或者 Callable 任务的执行进行取消、查询、获取结果的工具。

FutureTask 是一个可以将 Callable 转为 Runnable 和 Future 的工具。

7.1 用法

7.1.1 new Thread 用法

// runnable
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        val = 2;
        System.out.println("runnable 计算完毕:" + val);
    }
};
new Thread(runnable).start();

// callable
Callable<Integer> callable = new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        Thread.sleep(1000);
        return 2;
    }
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
val = futureTask.get();
System.out.println("callable 计算完毕:" + val);

callable 执行比 runnable 多了两步,一步通过 FutureTask 将其转为 runnable,一步通过 futureTask.get() 获得其返回值。

7.1.1 执行器用法:

// runnable
ExecutorService executor = Executors.newCachedThreadPool();
executor.execute(runnable);

// callable
ExecutorService executor1 = Executors.newCachedThreadPool();
Future<Integer> future = executor1.submit(callable);
val = future.get();
System.out.println("callable 计算完毕:" + val);

这里 callable 执行比 runnable 多了一步,通过 executor1.submit().get() 获得其返回值。

猜你喜欢

转载自blog.csdn.net/Gdeer/article/details/88832972