一、实现方式
线程的实现有两种方式,一是继承Thread类,二是实现Runnable接口。
二、状态
1.新建状态
创建了一个线程对象。
2.就绪状态
线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3.运行状态
就绪状态的线程获取了CPU,执行run()方法。
4.阻塞状态
阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
a.等待阻塞
运行的线程执行wait()方法,JVM会把该线程放入等待池中。
b.同步阻塞
运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
c.其他阻塞
运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5.死亡状态
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
三、run和start的区别
当调用start方法的时候,该线程就进入就绪状态。等待CPU进行调度执行,此时还没有真正执行线程。
当调用run方法的时候,是已经被CPU进行调度,执行线程的主要任务。
四、wait,join,sleep,yield, notify,notifyall,synchronized相关释义
1.wait
在其他线程调用对象的notify或notifyAll方法前,导致当前线程等待。线程会释放掉它所占有的“锁标志”,从而使别的线程有机会抢占该锁。
唤醒当前对象锁的等待线程使用notify或notifyAll方法,wait() 和notify()必须在synchronized函数或synchronized block中进行调用。
2.sleep
在指定时间内让当前正在执行的线程暂停执行,但不会释放“锁标志”。不推荐使用。sleep()使当前线程进入阻塞状态,在指定时间内不会执行。
3.yield
暂停当前正在执行的线程对象。yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。yield()只能使同优先级或更高优先级的线程有执行的机会。
4.join
等待该线程终止。等待调用join方法的线程结束,再继续执行。如:在main线程中执行t.join(),主要用于等待t线程运行结束,若无此句,main则会执行完毕,导致结果不可预测。(join方法会释放this对象的锁)
五、sleep() 与 wait()的区别
1.有关类
这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object 类。
2.有关锁
最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
3.有关资源
sleep不出让系统资源;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。
4.有关唤醒
sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。
5.有关使用
wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
6.有关异常
sleep需要捕获异常,而wait不需要
六、ThreadLocal
1.定义
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
2.适用场景和总结
- 每个线程需要有自己单独的实例
- 实例需要在多个方法中共享,但不希望被多线程共享
- ThreadLocal 并不解决线程间共享数据的问题
- ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
- 每个线程持有一个 Map 并维护了 ThreadLocal 对象与具体实例的映射,该 Map 由于只被持有它的线程访问,故不存在线程安全以及锁的问题
- ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题
- ThreadLocalMap 的 set 方法通过调用 replaceStaleEntry 方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏
- ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
七、线程安全
1.概念
-
原子性
一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。
-
可见性
当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到。
-
顺序性
程序执行的顺序按照代码的先后顺序执行。
2.如何解决多线程并发问题
-
如何保证原子性
a.锁和同步
- 常用的保证Java操作原子性的工具是锁和同步方法(或者同步代码块)。使用锁,可以保证同一时间只有一个线程能拿到锁,也就保证了同一时间只有一个线程能执行申请锁和释放锁之间的代码。
- 与锁类似的是同步方法或者同步代码块。使用非静态同步方法时,锁住的是当前实例;使用静态同步方法时,锁住的是该类的Class对象;使用静态代码块时,锁住的是synchronized关键字后面括号内的对象。
- 无论使用锁还是synchronized,本质都是一样,通过锁来实现资源的排它性,从而实际目标代码段同一时间只会被一个线程执行,进而保证了目标代码段的原子性。这是一种以牺牲性能为代价的方法。
b.CAS(compare and swap)
基础类型变量自增(i++)是一种常被误以为是原子操作而实际不是的操作。Java中提供了对应的原子操作类来实现该操作,并保证原子性,其本质是利用了CPU级别的CAS指令。由于是CPU级别的指令,其开销比需要操作系统参与的锁的开销小。AtomicInteger使用方法如下。
AtomicInteger atomicInteger = new AtomicInteger();
for(int b = 0; b < numThreads; b++) {
new Thread(() -> {
for(int a = 0; a < iteration; a++) {
atomicInteger.incrementAndGet();
}
}).start();
}
-
如何保证可见性
Java提供了volatile关键字来保证可见性。当使用volatile修饰某个变量时,它会保证对该变量的修改会立即被更新到内存中,并且将其它缓存中对该变量的缓存设置成无效,因此其它线程需要读取该值时必须从主内存中读取,从而得到最新的值。
-
如何保证顺序性
- Java中可通过volatile在一定程序上保证顺序性,另外还可以通过synchronized和锁来保证顺序性。
- synchronized和锁保证顺序性的原理和保证原子性一样,都是通过保证同一时间只会有一个线程执行目标代码段来实现的。
- 除了从应用层面保证目标代码段执行的顺序性外,JVM还通过被称为happens-before原则隐式地保证顺序性。两个操作的执行顺序只要可以通过happens-before推导出来,则JVM会保证其顺序性,反之JVM对其顺序性不作任何保证,可对其进行任意必要的重新排序以获取高效率。
happens-before原则(先行发生原则)
- 传递规则:如果操作1在操作2前面,而操作2在操作3前面,则操作1肯定会在操作3前发生。该规则说明了happens-before原则具有传递性
- 锁定规则:一个unlock操作肯定会在后面对同一个锁的lock操作前发生。这个很好理解,锁只有被释放了才会被再次获取
- volatile变量规则:对一个被volatile修饰的写操作先发生于后面对该变量的读操作
- 程序次序规则:一个线程内,按照代码顺序执行
- 线程启动规则:Thread对象的start()方法先发生于此线程的其它动作
- 线程终结原则:线程的终止检测后发生于线程中其它的所有操作
- 线程中断规则: 对线程interrupt()方法的调用先发生于对该中断异常的获取
- 对象终结规则:一个对象构造先于它的finalize发生
3.volatile适用场景
volatile适用于不需要保证原子性,但却需要保证可见性的场景。一种典型的使用场景是用它修饰用于停止线程的状态标记。如下所示
boolean isRunning = false;
public void start () {
new Thread( () -> {
while(isRunning) {
someOperation();
}
}).start();
}
public void stop () {
isRunning = false;
}
在这种实现方式下,即使其它线程通过调用stop()方法将isRunning设置为false,循环也不一定会立即结束。可以通过volatile关键字,保证while循环及时得到isRunning最新的状态从而及时停止循环,结束线程。
4.相关问题
问:平时项目中使用锁和synchronized比较多,而很少使用volatile,难道就没有保证可见性?
答:锁和synchronized即可以保证原子性,也可以保证可见性。都是通过保证同一时间只有一个线程执行目标代码段来实现的。
问:锁和synchronized为何能保证可见性?
答:根据JDK 7的Java doc中对concurrent
包的说明,一个线程的写结果保证对另外线程的读操作可见,只要该写操作可以由happen-before
原则推断出在读操作之前发生。
The results of a write by one thread are guaranteed to be visible to a read by another thread only if the write operation happens-before the read operation. The synchronized and volatile constructs, as well as the Thread.start() and Thread.join() methods, can form happens-before relationships.
问:既然锁和synchronized即可保证原子性也可保证可见性,为何还需要volatile?
答:synchronized和锁需要通过操作系统来仲裁谁获得锁,开销比较高,而volatile开销小很多。因此在只需要保证可见性的条件下,使用volatile的性能要比使用锁和synchronized高得多。
问:既然锁和synchronized可以保证原子性,为什么还需要AtomicInteger这种的类来保证原子操作?
答:锁和synchronized需要通过操作系统来仲裁谁获得锁,开销比较高,而AtomicInteger是通过CPU级的CAS操作来保证原子性,开销比较小。所以使用AtomicInteger的目的还是为了提高性能。
问:还有没有别的办法保证线程安全
答:有。尽可能避免引起非线程安全的条件——共享变量。如果能从设计上避免共享变量的使用,即可避免非线程安全的发生,也就无须通过锁或者synchronized以及volatile解决原子性、可见性和顺序性的问题。
问:synchronized即可修饰非静态方式,也可修饰静态方法,还可修饰代码块,有何区别
答:synchronized修饰非静态同步方法时,锁住的是当前实例;synchronized修饰静态同步方法时,锁住的是该类的Class对象;synchronized修饰静态代码块时,锁住的是synchronized关键字后面括号内的对象。