Java多线程中的概念
多线程涉及到很多概念,如果能把这些概念理解清楚,多线程程序其实也没什么难的 。这篇文章算是我学多线程的笔记吧
进程(process)和线程(thread)
在操作系统中,两个比较容易混淆的概念是进程(process)和线程(thread)。操作系统中的进程是一个计算机程序的运行实例。
计算机程序中包含了需要执行的指令,而进程则表示正在执行的指令。对同一个计算机程序可以创建多个进程。这些进程的运行状态各不相同。进程一般作为资源的组织单位。进程有自己独立的地址空间,包含程序内容和数据。不同进程的地址空间是互相隔离的。进程拥有各种资源和状态信息,包括打开的文件、子进程和信号处理器等。线程表示的是程序的执行流程,是CPU调度执行的基本单位。线程有自己的程序计数器、寄存器、堆栈和帧等。同一进程中的线程共用相同的地址空间,同时共享进程所拥有的内存和其他资源。
区别
可见性
什么是可见性
共享内存与操作
volatile 易变的,不稳定的
volatile功能 : 具有同步处理和对 long型和double的原子操作
某线程对volatile字段的写操作的结果对其他线程立即可见,volatile字段的写入处理并不会被缓存。
关键词volatile用来对共享变量的访问进行同步。
对一个volatile变量的上一次写入操作和下一次读取操作之间存在“在之前发生”的顺序。也就是说,上一次写入操作的结果对下一次读取操作是肯定可见的。
在写入volatile变量值之后,CPU缓存中的内容会被写回主存;
在读取volatile变量时,CPU缓存中的对应内容被置为失效状态,重新从主存中进行读取。将变量声明为volatile相当于为单个变量的读取和写入添加了同步操作。但是volatile在使用时不需要利用锁机制,因此性能要优于synchronized关键词。关键词volatile的主要作用是确保对一个变量的修改被正确地传播到其他线程中。
最常使用volatile变量的一个场景是把作为循环结束的判断条件的变量声明为volatile。
原子操作
在Java中原子操作:
- 对于非long型和double型的域的读取和写入操作是原子操作。
- 对象引用的读取和写入操作也是原子操作。
在使用long型和double型的共享变量时,需要把变量声明为volatile,以保证读取和写入操作的完整性。
synchronized
使用synchronized关键词主要用来实现线程之间的互斥,即同一时刻只有一个线程允许执行特定的代码。通过互斥的方式来保证多个线程访问共享变量时的正确性。
在使用synchronized时有一个错误倾向,那就是被synchronized所保护的代码过多,比如一个方法中只有少数几行代码访问共享变量,却把整个方法声明为synchronized。这么做虽然不会对程序的正确性造成影响,但是会影响程序的性能。正确的做法是把方法中需要同步的代码用synchronized代码块包围即可。
监视器
每个synchronized关键字在使用时都有一哥监视器对象先对应。在一个线程允许执行方法或代码块之前,需要先获取对应的监视器对象上的锁。
synchronized非静态方法
实例方法使用的是当前对象实例所关联的监视器对象。
synchronized静态方法
静态方法对应的监视器对象是所在类对应的Class类的对象所关联的监视器对像。
synchronized 代码块
synchronized代码块对应的监视器是synchronized代码块声明中的对象所关联的监视器对象。
线程的互斥处理
同步处理
非公平锁
当一个线程A持有锁,而线程B、C处于阻塞状态(或等待)状态,若线程A释放锁,JVM将从线程B、C中随机选择一个线程持有锁并使其获得执行权,这叫非公平锁(因为没有按先后顺序)。
公平锁
当一个线程A持有锁,而线程B、C处于阻塞状态(或等待)状态,若线程A释放锁,若JVM选择了等待时间最长的一个线程持有锁,则为公平锁。需要注意的是,即使是公平锁,JVM也无法准确做到’公平’。在程序中不能以此作为精确的计算。
自旋锁
乐观锁
final字段与线程安全
Object类的wait、notify和notifyAll方法
由于wait方法的成功调用需要当前线程持有监视器对象上的锁,因此wait方法的调用需要放在使用synchronized关键词声明的方法或代码块中。当执行wait方法时,当前线程已经进入了synchronized关键词所声明的互斥块中,已经持有所需的锁。在synchronized方法或代码块中使用的监视器对象必须是wait方法调用的接收者所关联的监视器对象。
wait方法的作用是使当前线程进入等待状态,对应的notify和notifyAll方法用来通知线程离开等待状态。调用一个对象的notify方法会从该对象关联的等待集合中选择一个线程来唤醒。被唤醒的线程可以和其他线程竞争运行的机会。与notify方法相对应的notifyAll方法会唤醒对象关联的等待集合中的所有线程。而notify方法所唤醒
的线程的选择由虚拟机实现来决定,不能保证一个对象所关联的等待集合中的线程按照所期望的顺序被唤醒。很可能一个线程被唤醒之后,发现它所要求的条件并没有满足,而重新进入等待状态,而真正需要被唤醒的线程却仍然处于等待集合中。因此,当等待集合中可能包含多个线程时,一般使用notifyAll方法。不过notifyAll
方法会导致线程在没有必要的情况下被唤醒,之后又马上进入等待状态,因此会造成一定的性能影响,不过可以保证程序的正确性。与wait方法相同,notify和notifyAll方法在调用时都要求当前线程拥有方法调用接收者所关联的监视器对象上的锁。当线程被唤醒之后,由于在调用wait方法时已经释放了之前所持有的监视器对象上的锁,线程需要重新竞争锁来获得继续运行wait方法调用完成之后的代码的机会。通常要把wait方法的调用包含在一个循环中。循环的条件是线程可以继续执行需要满足的逻辑条件。如果线程继续执行的逻辑条件不满足,那么线程应该再次调用wait方法来重新进入等待状态。
synchronized(obj){
while(/*逻辑条件不满足*/)
{
obj.wait();
}
//条件满足
}
Thread类
线程状态
在一个Thread类的对象被创建出来之后,它可能处于不同的状态中。进行与线程相关的不同操作可能导致该Thread类的对象所处的状态发生变化。不同的线程状态由枚举类型Thread.State来表示,可以通过Thread类的getState方法来得到。Thread.State只表示虚拟机中线程的状态,并不表示对应的操作系统上的线程的状态。Thread.State中包含的线程状态有以下几种。
- NEW:线程刚被创建出来。一个新创建的Thread类的对象处于此状态中。
- RUNNABLE:线程处于可运行的状态。
该线程有可能正在运行,也有可能在等待其他操作系统中的资源 - BLOCKED:线程在等待一个监视器对象上的锁
时,处于此状态。当一个线程尝试执行声明为synchronized的方法或代码块,又无法获取对应的锁时,处于BLOCKED状态。 - WAITING:调用某些方法会使当前线程进入等待状态。这个等待没有超时时间。处于这个状态的线程等待其他线程执行
特定的操作来使当前线程退出等待状态。 - TIMED_WAITING:该状态类似于WAITING,但是增加了指定的超时时间。当超时时间过去,如果线程等待的条件仍然没
有发生,那么线程也会退出等待状态。 - TERMINATED:线程的运行已经终止。
线程在同一时刻只能处于上述六种状态中的一种。了解线程的状态可以为调试提供帮助。
线程中断
线程中断是线程之间的一种通信方式。
- 通过一个线程对应的Thread类的对象的interrupt()方法可以向该线程发出中断请求。
- 通过Thread类的isInterrupted()方法可以查询此标记来判断是否有中断请求发生。
- Object类的wait方法及Thread类的join方法和sleep方法都会抛出受检异常java.lang.InterruptedException。
在调用这3个方法及其重载形式时,必须捕获InterruptedException异常并进行处理。当线程由于调用这3个方法而进入等待状态时,通过interrupt方法中断该线程会导致该线程离开等待状态。对于wait方法调用来说,线程需要在重新获取到监视器对象上的锁之后才能抛出nterruptedException异常,并执行对InterruptedException异常的处理逻辑。Thread类中还有一个与线程中断相关的方法interrupted。该方法不但可以判断当前线程是否被中断,还可以清除线程内部的中断标记。如果调用interrupted方法的返回值为true,说明该线程曾经被中断过。在interrupted方法调用完成之后,
线程内部的中断标记已经被清空。
线程中断的一个典型应用场景是实现可取消的任务。
有些线程在执行任务时会使用一个无限循环来重复执行。这时需要一种方式来结束线程的运行。
一种做法是使用之前介绍的volatile变量作为结束标记;另外一种做法是向线程发出中断请求。
线程等待、睡眠和让步
Thread类的join方法提供了一种简单的同步方式,允许当前线程等待另外一个线程运行结束。
如果线程A通过调用线程B的join方法等待线程B运行结束,那么在线程B中对共享变量所做的修改
对于线程A是肯定可见的。一般的做法是,在线程A中创建并启动线程B之后,线程A执行另外的一
些操作,接着调用join方法等待线程B完成。线程B和线程A通过修改共享变量的方式来进行交互。
public void useJoin() {
Thread thread = new Thread() {
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thread.start();// 启动线程 //执行其他操作
thread.getState();
try {
thread.join();// 等待线程运行结束
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Thread类的静态方法sleep可以让当前线程进入睡眠状态一段时间。在睡眠状态下,线程的代码执
行会暂停,但是线程不会释放所持有的锁。因此不要把对sleep方法的调用放在synchronized方法
或代码块中,否则会造成其他等待获取锁的线程长时间处于等待状态。 如果当前线程因为某些原
因无法继续执行,那么可以使用yield方法来尝试让出所占用的CPU资源,让其他线程获得运行的机会。
调用yield方法对操作系统上的调度器来说是一个信号,但是调度器不一定会立即进行线程切换。
调用yield方法可以使线程切换更加频繁,从而让某些与多线程相关的错误更容易暴露出来。在实际开
发中调用yield方法可以作为进行测试的一个辅助手段。
死锁问题指的是两个线程分别持有另外一个线程所需要获取的锁,同时又希望获取另外一个线程已经持
有的锁。每个线程都由于等待对方释放锁而处于等待状态,因此无法释放自己持有的锁来让对方运行。
这样造成的结果是两个线程都无法运行。
优先级倒置问题指的是线程的优先级由于锁机制而无法成功应用。有可能因为低优先级的线程持有锁的
时间过长,导致高优先级的线程长时间处于等待状态。
线程运行时间
一个线程的运行分为三个部分:T1为线程的启动时间,T2为线程的运行时间,T3为线程的销毁时间。
线程池
如果一个线程不能被重复使用,每次创建一个线程都要经历启动、运行、销毁三个部分,肯定会增加系统的响应时间。
T2时间无法避免,只能通过优化代码来实现降低运行时间。T1和T2都可以通过线程池(Thread Pool)来缩减时间,比如容器(或系统)启动时,创建足够的线程,当容器(或系统)需要时,直接从线程池中获得线程,运算结果,再把线程返回到线程池中。
非阻塞方式
在程序中,对共享变量的使用一般遵循一定的模式,即由读取、修改和写入三步组成。在读取步骤中读取共享变量的当前值,在修改步骤中根据变量的当前值进行某些修改操作,最后在写入步骤中把修改之后的结果作为共享变量的新值写入。
在实现线程安全的计数器时,AtomicInteger和AtomicLong类是最佳的选择。
阻塞队列
阻塞队列和其他队列的区别,插入数据时,如果数据已满会一直等待直到可以插入数据为止,设定初始容量后,将不会自动扩容。
我是IT小王,如果喜欢我的文章,可以扫码关注我