1.1 同步( Synchronous )和异步(Asynch onous)

同步和异步通常用来形容一次方法调用 同步方法调用 始,调用者必须等到方法调用返 后,才能继续后续的行为 异步方法调用更像 个消息传递, 一旦开始 方法调用就会立即返回,调用者就可以继续后续的操作。

1.2 并发 Conc rrency )和并行 Parallelism)

井发偏重于多个任务交替执行,而多个任务之间有可能还是串行的,而并行是真正意义上的“同时执行”。

1.3 临界区

临界区用来表示一种公共资源或者说共享数据 ,可以被多个线程使用 是每一次,只能有 个线程使用它, 界区资源被 占用, 线程要想使用这个资源就必须等待。

1.4 阻塞( Blocking )和非阻塞( Non-Blocking)

阻塞和非阻 通常用 容多线程间的相互影响。 比如 个线程占用 |伍 界区资源,那么其他所有需要这个资源的线程就必须在这个临界区中等待 等待会导致线程挂起,这种情况就是阻塞。
非阻 意思与之相反 它强调没有 个线程可 以妨碍其 线程执行,所有的线程都会尝试不断前向执行 。

1.5 死锁( Deadlock )、饥饿( Starvation )和活锁( Livelock)

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
饥饿是指某一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行。
活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程。

1.6 线程与进程的区别

区别 进程 线程
开销 作为资源分配的单位 调度和执行的单位
所处环境 在操作系统中能同时运行多个任务(程序) 在同一应用程序中有多个顺序流同时执行
分配内存 系统在运行的时候会为每个进程分配不同的内存区域 除了CPU之外,不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程组只能共享资源
包含关系 没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个线程,则执行过程不是一条线的,而是
多条线(线程)共同完成的
线程是进程的一部分,所以线程有的时候被称为是轻权进程或者轻量级进程。

2.线程的创建

2.1 继承Java.lang.Thread类,并覆盖run() 方法

  1. public class ExtendsThreadTest extends Thread {
  2. @Override
  3. public void run() {
  4. for (int i = 0; i < 5; i++) {
  5. System.out.println(getName() + " " + i);
  6. }
  7. }
  8. public static void main(String[] args) {
  9. new ExtendsThreadTest().start();
  10. new ExtendsThreadTest().start();
  11. }
  12. }

输出:

  1. Thread-0 0
  2. Thread-1 0
  3. Thread-0 1
  4. Thread-1 1
  5. Thread-1 2
  6. Thread-1 3
  7. Thread-1 4
  8. Thread-0 2
  9. Thread-0 3
  10. Thread-0 4

Lambad方式:

  1. new Thread(() -> {
  2. Arrays.asList(1, 2, 3, 4, 5).stream().forEach(i -> System.out.println(currentThread().getName() + " " + i));
  3. }).start();

输出:

  1. Thread-0 1
  2. Thread-0 2
  3. Thread-0 3
  4. Thread-0 4
  5. Thread-0 5

优缺点:
优势:编写简单
劣势:无法继承其它父类

2.2 实现Java.lang.Runnable接口,并实现run() 方法

  1. public class RunnableThreadTest implements Runnable {
  2. @Override
  3. public void run() {
  4. for (int i = 0; i < 5; i++) {
  5. System.out.println(Thread.currentThread().getName() + " " + i);
  6. }
  7. }
  8. public static void main(String[] args) {
  9. new Thread(new RunnableThreadTest(), "线程1").start();
  10. new Thread(new RunnableThreadTest(), "线程2").start();
  11. }
  12. }

输出:

  1. 线程2 0
  2. 线程2 1
  3. 线程2 2
  4. 线程2 3
  5. 线程2 4
  6. 线程1 0
  7. 线程1 1
  8. 线程1 2
  9. 线程1 3
  10. 线程1 4

Lambad方式:

  1. public class RunnableThreadTest {
  2. public static void staticRun() {
  3. for (int i = 0; i < 5; i++) {
  4. System.out.println(Thread.currentThread().getName() + " " + i);
  5. }
  6. }
  7. public static void main(String[] args) {
  8. new Thread(RunnableThreadTest::staticRun).start();
  9. new Thread(RunnableThreadTest::staticRun).start();
  10. }
  11. }

输出:

  1. Thread-1 0
  2. Thread-0 0
  3. Thread-0 1
  4. Thread-0 2
  5. Thread-0 3
  6. Thread-0 4
  7. Thread-1 1
  8. Thread-1 2
  9. Thread-1 3
  10. Thread-1 4

优缺点:
优势:可以继承其它类,多线程可共享同一个Runnable对象
劣势:编程方式稍微复杂,如果需要访问当前线程,需要调用Thread.currentThread()方法

2.3 实现Callable接口

  1. public class CallableThreadTest implements Callable {
  2. @Override
  3. public Object call() throws Exception {
  4. int i = 0;
  5. for (; i < 5; i++) {
  6. System.out.println(Thread.currentThread().getName() + "i: " + i);
  7. }
  8. return i;
  9. }
  10. public static void main(String[] args) throws ExecutionException, InterruptedException {
  11. Callable callable = new CallableThreadTest();
  12. FutureTask future = new FutureTask(callable);
  13. for (int i = 0; i < 5; i++) {
  14. System.out.println(Thread.currentThread().getName() + "i: " + i);
  15. new Thread(future, "有返参的线程;").start();
  16. }
  17. System.out.println("线程返参:" + future.get());
  18. }
  19. }

输出:

  1. maini: 0
  2. maini: 1
  3. maini: 2
  4. maini: 3
  5. maini: 4
  6. 有返参的线程;i: 0
  7. 有返参的线程;i: 1
  8. 有返参的线程;i: 2
  9. 有返参的线程;i: 3
  10. 有返参的线程;i: 4
  11. 线程返参:5

Lambad方式:

  1. public class CallableThreadLambdaTest {
  2. public static void main(String[] args) throws ExecutionException, InterruptedException {
  3. FutureTask ft = new FutureTask(() -> {
  4. // Arrays.asList(1, 2, 3, 4, 5).stream().forEach(i -> System.out.println(Thread.currentThread().getName() + ";i:" + i));
  5. int i = 0;
  6. for (; i < 5; i++) {
  7. System.out.println(Thread.currentThread().getName() + "i: " + i);
  8. }
  9. return i;
  10. });
  11. // Arrays.asList(1, 2, 3, 4, 5).stream().forEach(i -> new Thread(ft, "Lambad方式用Callable创建线程;"+i+";").start());
  12. for (int i = 0; i < 3; i++) {
  13. new Thread(ft, "Lambad方式用Callable创建线程;" + i + ";").start();
  14. }
  15. System.out.println(ft.get());
  16. }
  17. }

优缺点:
与实行Runnable相比, Callable功能更强大些• 方法不同
可以有返回值,支持泛型的返回值
可以抛出异常
需要借助FutureTask,比如获取返回结果

3 线程的生命周期

image.png

新生状态:
• 用new关键字建立一个线程对象后,该线程对象就处于新生状态。
• 处于新生状态的线程有自己的内存空间,通过调用start进入就绪状态
• 就绪状态:
• 处于就绪状态线程具备了运行条件,但还没分配到CPU,处于线程就绪队列,等待系统为其分配CPU
• 当系统选定一个等待执行的线程后,它就会从就绪状态进入执行状态,该动作称之为“cpu调度”。
• 运行状态:
• 在运行状态的线程执行自己的run方法中代码,直到等待某资源而阻塞或完成任务而死亡。
• 如果在给定的时间片内没有执行结束,就会被系统给换下来回到等待执行状态。
• 阻塞状态:
• 处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进
入阻塞状态。
• 在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入
就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。
• 死亡状态:
• 死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有三个。一个是正常运行的线程完成了它的全部工作;另一个是线
程被强制性地终止,如通过执行stop方法来终止一个线程[不推荐使用】,三是线程抛出未捕获的异常

4 线程的优先级

线程的优先级用数字表示,范围从1到10• Thread.MIN_PRIORITY = 1
• Thread.MAX_PRIORITY = 10
• Thread.NORM_PRIORITY = 5
• 使用下述方法获得或设置线程对象的优先级。
• int getPriority();
• void setPriority(int newPriority);
• 注意:优先级低只是意味着获得调度的概率低。并不是绝对先调用优先级高后调用
优先级低的线程。

5 控制线程方法

• join ()
• 阻塞指定线程等到另一个线程完成以后再继续执行
• sleep ()
• 使线程停止运行一段时间,将处于阻塞状态
• 如果调用了sleep方法之后,没有其他等待执行的线程,这个时候当前线程不会马上恢复执行!
• yield ()
• 让当前正在执行线程暂停,不是阻塞线程,而是将线程转入就绪状态
• 如果调用了yield方法之后,没有其他等待执行的线程,这个时候当前线程就会马上恢复执行!
• setDaemon()
• 可以将指定的线程设置成后台线程
• 创建后台线程的线程结束时,后台线程也随之消亡
• 只能在线程启动之前把它设为后台线程
• interrupt()
• 并没有直接中断线程,而是需要被中断线程自己处理
• stop()
• 结束线程,不推荐使用

5.1 interrupt、interrupted和isInterrupted的区别

interrupt() 向当前调用者线程发出中断信号
isinterrupted() 查看当前中断信号是true还是false
interrupted() 是静态方法,查看当前中断信号是true还是false并且清除中断信号,顾名思义interrupted为已经处理中断信号。
注:interrupt()方法发出的中断信号只能被wait() sleep() join()这三个方法捕捉到并产生中断。
示例:

  1. public class InterruptTest {
  2. public static void main(String[] args) throws InterruptedException {
  3. Thread t = new Thread(new InterruptThreadTest());
  4. t.start();
  5. Thread.sleep(1000);
  6. System.out.println("InterruptThreadTest 开始中断...");
  7. t.interrupt();
  8. //t.isInterrupted();//isInterrupted不能被InterruptThreadTest线程捕获
  9. System.out.println("main结束");
  10. }
  11. }
  12. class InterruptThreadTest implements Runnable {
  13. @Override
  14. public void run() {
  15. System.out.println("InterruptThreadTest start...");
  16. try {
  17. Thread.sleep(2000);
  18. } catch (InterruptedException e) {
  19. System.out.println("捕获到中断...");
  20. Thread t = Thread.currentThread();
  21. t.interrupt();
  22. System.out.println("是否中断isInterrupted:" + t.isInterrupted());
  23. System.out.println("是否中断isInterrupted2:" + t.isInterrupted());
  24. System.out.println("查看中断标识,并清理中断标识;interrupted:" + t.interrupted());
  25. System.out.println("是否中断isInterrupted:" + t.isInterrupted());
  26. }
  27. System.out.println("InterruptThreadTest stop...");
  28. }
  29. }

输出:

  1. InterruptThreadTest start...
  2. InterruptThreadTest 开始中断...
  3. main结束
  4. 捕获到中断...
  5. 是否中断isInterruptedtrue
  6. 是否中断isInterrupted2true
  7. 查看中断标识,并清理中断标识;interruptedtrue
  8. 是否中断isInterruptedfalse
  9. InterruptThreadTest stop...

6 线程同步

背景:当多个线程访问同一个数据时,容易出现线程安全问题。需要让线程同步,保证数据安全
线程同步:当两个或两个以上线程访问同一资源时,需要某种方式来确保资源在某一时刻只被一个线程
线程同步的实现方案

6.1 同步代码块:

  1. synchronized (obj){ }

6.2 同步方法:

  1. private synchronized void makeWithdrawal(int amt) {}

6.3 Lock锁:

JDK1.5后新增功能,与采用synchronized相比,lock可提供多种锁方案,更灵活
java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语
言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁
定语义。
• ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义, 但是添加了类似锁投票、
定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。
• 注意:如果同步代码有异常,要将unlock()写入finally语句块
• Lock和synchronized的区别
• 1.Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁
• 2.Lock只有代码块锁,synchronized有代码块锁和方法锁
• 3.使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

7 为什么 wait 方法在Object类里面,而不是Thread类

1 两个线程之间的通信机制,例如:生产者消费者模型;
2 每个对象都可以上锁
3 在Java中,所有对象都有一个监视器,HotSpot虚拟机的对象头(Object Header)分为两部分,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄(Generational GC Age)等。这部分数据的长度在32位和64位的Java虚拟机中分别会占用32个或64个比特,官方称它为“Mark Word”。另外一部分用于存储指向方法区对象类型数据的指针。
在 Java 设计中,线程不能被指定,它总是运行当前代码的线程。但是,我们可以指定监视器(这是我们称之为等待的对象)。这是一个很好的设计,因为如果我们可以让任何其他线程在所需的监视器上等待,这将导致“入侵”,影响线程执行顺序,导致在设计并发程序时会遇到困难。请记住,在 Java 中,所有在另一个线程的执行中造成入侵的操作都被弃用了(例如 Thread.stop 方法)。