CH1 使用线程

多线程创建方式

  • 通过继承Thread类创建线程类
  • 通过实现Runnable接口创建线程类
  • 通过callable和Future接口创建线程
    • Callable接口提供了一个call()方法可以作为线程执行体,但call()方法比run()方法功能更强大,call()方法的功能的强大体现在:call()方法可以有返回值,call()方法可以声明抛出异常;

实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以理解为任务是通过线程驱动从而执行的。

其实,Thread 类实际上也是继承了 Runnable 接口, 那也就意味着, Thread构造函数不仅可以传入Runnable 接口的对象,也可以传入一个 Thread 类的对象。

run()和start()有什么区别?

run()方法被称为线程执行体,它的方法体代表了线程需要完成的任务,而start()方法用来启动线程。
调用start()方法启动线程时,系统会把该run()方法当成线程执行体来处理。但如果直接调用线程对象的run()方法,则run()方法立即就会被执行,而且在run()方法返回之前其他线程无法并发执行。也就是说,如果直接调用线程对象的run()方法,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体。

注意: 执行 start() 方法的顺序不代表线程启动的顺序。

介绍一下线程的生命周期

在线程的生命周期中,它要经过新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、就绪之间切换。
当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。
当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。
如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态,如果计算机只有一个CPU,那么在任何时刻只有一个线程处于运行状态。当然,在一个多处理器的机器上,将会有多个线程并行执行;当线程数大于处理器数时,依然会存在多个线程在同一个CPU上轮换的现象。
当一个线程开始运行后,它不可能一直处于运行状态,线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。对于采用抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务。当该时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。当发生如下情况时,线程将会进入阻塞状态:

  • 线程调用sleep()方法主动放弃所占用的处理器资源。
  • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
  • 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
  • 线程在等待某个通知(notify)。
  • 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法。

针对上面几种情况,当发生如下特定的情况时可以解除上面的阻塞,,:

  • 调用sleep()方法的线程经过了指定时间。
  • 线程调用的阻塞式IO方法已经返回。
  • 线程成功地获得了试图取得的同步监视器。
  • 线程正在等待某个通知时,其他线程发出了一个通知。
  • 处于挂起状态的线程被调用了resume()恢复方法。

线程会以如下三种方式结束,结束后就处于死亡状态:

  • run()或call()方法执行完成,线程正常结束。
  • 线程抛出一个未捕获的Exception或Error。
  • 直接调用该线程的stop()方法来结束该线程,该方法容易导致死锁,通常不推荐使用。

image.png

如何实现线程同步?

  1. 同步方法即有synchronized关键字修饰的方法,由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。需要注意, synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
  2. 同步代码块即有synchronized关键字修饰的语句块,被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。需值得注意的是,同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
  3. ReentrantLock Java 5新增了一个java.util.concurrent包来支持同步,其中ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。需要注意的是,ReentrantLock还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,因此不推荐使用。
  4. volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值。需要注意的是,volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。
  5. 原子变量在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。例如AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换Integer。可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。

线程是否可以重复启动

只能对新建状态的线程使用start()方法,否则会引发IllegalThreadStateException异常。

Java多线程之间的同步方式

java中线程通信主要有以下3种方式:

  1. wait()、notify()、notifyAll()

wait()、notify()、notifyAll()用来实现线程之间的通信,这三个方法都不是Thread类中所声明的方法,而是Object类中声明的方法。原因是每个对象都拥有锁,所以让当前线程等待某个对象的锁,当然应该通过这个对象来操作。并且因为当前线程可能会等待多个线程的锁,如果通过线程来操作,就非常复杂了。另外,这三个方法都是本地方法,并且被final修饰,无法被重写,并且只有采用synchronized实现线程同步时才能使用这三个方法。

  • wait()方法可以让当前线程释放对象锁并进入阻塞状态、
  • notify()方法用于唤醒一个正在等待相应对象锁的线程,使他进入就绪队列
  • notifyAll()方法用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列。

join()

在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。
对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先于 b 线程的输出。

wait()和sleep()的方法区别

  • wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法,sleep() 是让当前线程休眠;
  • wait() 会释放锁,sleep() 不会。

线程安全问题

方法中的变量是不存在非线程安全问题的,永远都是线程安全的,这是方法内部的变量是私有的特性所决定的。

互斥同步

synchronized

可以用来同步一个代码块,如果一个线程进入了一个同步代码块,其他线程就必须等待。
用来同步一个方法也一样,只可以作用于一个对象。

Synchronized取得的锁都是对象锁。而不是把一段代码或者方法当做锁,也就是说,哪个线程先执行带有synchronized关键字的方法,哪个线程就持有该方法所属对象的锁Lock。

存在父子类继承关系的时候,子类可以通过“可重入锁”调用父类的同步方法。
同步不能继承,所以还是要在子类方法中添加synchronized关键字。

ReentrantLock

ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。

synchronized和ReentrantLock比较

  1. 两者都是可重入锁

可重入锁:重入锁,也叫做递归锁,可重入锁指的是在一个线程中可以多次获取同一把锁,比如:一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁, 两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

  1. 锁的实现

synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。

  1. 性能

新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。

  1. 等待可中断

当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock 可中断,而 synchronized 不行。

  1. 公平锁

公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。

  1. 锁绑定多个条件

一个 ReentrantLock 可以同时绑定多个 Condition 对象。
使用选择
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。

synchronized 和 Lock 有什么区别?

  • synchronized 可以给类. 方法. 代码块加锁;而 lock 只能给代码块加锁。
  • synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
  • 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

ThreadLocal是什么

ThreadLocal,即线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。

锁机制

重量级锁

在JDK6之前,synchronized 一直被称为重量级锁。

自旋锁

JDK6之后默认开启,它不会将处于等待状态的线程挂起,而是通过无限循环的方式,不断检测是否能够获取锁。由于单个线程占用锁的时间非常短,所以说循环次数不会太多,可能很快就能够拿到锁并运行,这就是自旋锁。
当然,等待时间非常短的情况下,自旋锁的表现会很好,但是如果等待时间太长,由于循环是需要处理器继续运算的,所以这样只会浪费处理器资源,因此自旋锁的等待时间是有限制的,默认情况下为10次,如果失败,会进而采用重量级锁机制。

轻量级锁

从JDK6开始,为了减少获得锁和释放锁带来的性能消耗,引入了轻量级锁。
轻量级锁的目标是,在无竞争的情况下,减少重量级锁产生的性能消耗,它并不是为了代替重量级锁,实际上就是赌一手同一时间只有一个线程在占用资源),它不像是重量级锁那样,需要向操作系统申请互斥量。