1. 多线程基本知识
1.1 多线程运行的原理
原理:CPU 在线程中做时间片的切换。
一个(多核中的一个) CPU 在在运行程序的过程中某个时刻点上,只能运行一个程序。而 CPU 可以在 多个程序之间进行高速的切换 (轮询制)。而切换频率和速度太快,导致人的肉眼看不到。
1.2 实现线程的两种方式
- 继承 Thread
- 声明实现Runnable接口
- 还可以实现Callable接口
1.3 线程的状态图解
- 新建状态(New):新创建了一个线程对象。
- 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的
start()
方法。该状态 的线程位于可运行线程池中,变得可运行,等待获取 CPU 的使用权。 - 运行状态(Running):就绪状态的线程获取了 CPU,执行程序代码。
- 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。 直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
- 等待阻塞:运行的线程执行
wait()
方法,JVM 会把该线程放入等待池中。(wait 会释 放持有的锁) - 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池中。
- 其他阻塞:运行的线程执行 sleep()或 join()方法,或者发出了 I/O 请求时,JVM 会把 该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕 时,线程重新转入就绪状态。(注意:sleep 是不会释放持有的锁)
- 等待阻塞:运行的线程执行
- 死亡状态(Dead):线程执行完了或者因异常退出了 run()方法,该线程结束生命周期。
1.4 几个重要的方法的区别:
sleep(timeout)
:当前线程进入阻塞状态,暂停执行一定时间,不会释放锁标记
join()
:join()方法会使当前线程等待调用join()
方法的线程结束后才能继续执行
yield()
:调用该方法的线程重回可执行状态,不会释放锁标记,可以理解为交出 CPU 时间片, 但是不一定有效果,因为有可能又被马上执行。该方法的真正作用是使具有相同或者更高优 先级的方法得到执行机会。
wait(timeout)
:wait 方法通常和notify()
/notifyAll()
搭配使用,当前线程暂停执行,会释放锁 标记。进入对象等待池。直到调用notify()
方法之后,线程被移动到锁标记等待池。只有锁 标记等待池的线程才能获得锁
1.5 Join的用法
联合线程:
线程的join方法表示一个线程等待另一个线程完成后才执行。有人也把这种方式称为联合线程,就是说把当前线程和当前线程所在的线程联合成一个线程。join方法被调用之后,线程对象处于阻塞状态。
适用于A线程需要等到B线程执行完毕,再拿B线程的结果再继续运行A线程.
说人话: A线程需要拿到B线程的执行结果,才能继续往下.
1 | class Join extends Thread{ |
2. Java同步关键词解释
2.1. synchronized
属于 JVM 级别加锁,底层实现是: 在编译过程中,在指令级别加入一些标识来实现的。
1. 锁对象注意点: 必须是锁的同一个对象
2. 锁获取和释放
- 锁的获取是由JVM决定的, 用户无法操作
- 锁的释放也是由JVM决定的
Synchronized
无法中断正在阻塞队列或者等待队列的线程。
3. 什么时候会释放
- 获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
- 线程执行发生异常,此时 JVM 会让线程自动释放锁。
4.格式
1 | // 加同步格式: |
5.线程执行互斥代码的过程
获得互斥锁
清空工作内存
从主内存拷贝变量的最新副本到工作内存
执行代码
将更新后的共享变量的值刷新到主内存
释放互斥锁
Lock -> 主内存 -> 工作内存 -> 主内存 -> unlock
2.2 Lock
手动获取或释放锁, 提供了比 synchronized 更多的功能
Lock 锁是 Java 代码级别来实现的,相对于 synchronized 在功能性上,有所加强,主要是,公平锁,轮 询锁,定时锁,可中断锁等,还增加了多路通知机制(Condition),可以用一个锁来管理多 个同步块。另外在使用的时候,必须手动的释放锁。Lock 锁的实现,主要是借助于队列同 步器(我们常常见到的 AQS)来实现。它包括一个 int 变量来表示状态;一个 FIFO 队列,来 存储获取资源的排队线程。
基本使用
1 | class X { |
1. lock 和 synchronized 的区别
- Lock 不是 Java 语言内置的,synchronized 是 Java 语言的关键字,因此是内置特性。Lock 是一个类,通过这个类可以实现同步访问;
- Lock 和 synchronized 有一点非常大的不同,采用 synchronized 不需要用户去手动释放锁, 当 synchronized 方法或者 synchronized 代码块执行完之后,系统会自动让线程释放对锁的占 用;而 Lock 则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现 象。
2. Lock 接口中方法的使用
ReentrantLock 类
ReentrantLock 是唯一实现了 Lock 接口的类,并且 ReentrantLock 提供了更多的方法,ReentrantLock,意思是“可重入锁”。
lock()、tryLock()、tryLock(long time, TimeUnit unit)、lockInterruptibly()
是用来获取锁的。
unLock()
方法是用来释放锁的。
四个获取锁方法的区别
lock()
,阻塞方法,该方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被 其他线程获取,则进行等待。由于在前面讲到如果采用 Lock,必须主动去释放锁,并且在 发生异常时,不会自动释放锁。因此一般来说,使用 Lock 必须在 try{}catch{}块中进行,并 且将释放锁的操作放在 finally 块中进行,以保证锁一定被被释放,防止死锁的发生。tryLock()
,非阻塞方法,该方法是有返回值的,它表示用来尝试获取锁,如果获取成功, 则返回 true,如果获取失败(即锁已被其他线程获取),则返回 false,也就说这个方法无论 如何都会立即返回。在拿不到锁时不会一直在那等待。tryLock(long time, TimeUnit unit)
,阻塞方法,阻塞给定时长,该方法和 tryLock()方法是 类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还 拿不到锁,就返回 false。如果一开始拿到锁或者在等待期间内拿到了锁,则返回 true。lockInterruptibly()
这个方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待 获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通 过 lock.lockInterruptibly()想获取某个锁时,假若此时线程 A 获取到了锁,而线程 B 只有在等 待,那么对线程 B 调用threadB.interrupt()
方法能够中断线程 B 的等待过程。
2.3 Lock 与 synchronized 的选择
- Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现;
- synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生; 而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因 此使用 Lock 时需要在 finally 块中释放锁;
- Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等 待的线程会一直等待下去,不能够响应中断;
- 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
- Lock 可以提高多个线程进行读操作的效率。
2.4 读写锁
- 线程进入读锁的前提条件: 没有其他线程的写锁, 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个
- 线程进入写锁的前提条件: 没有其他线程的读锁, 没有其他线程的写锁
ReentrantReadWriteLock 与 ReentrantLock 都是单独的实现,彼此之间没有继承或实现的关系。
ReadWriteLock 类
1 | // API |
ReentrantReadWriteLock 类
ReentrantReadWriteLock 里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和 writeLock()用来获取读锁和写锁。
注意:不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写 锁,则申请写锁的线程会一直等待释放读锁。
如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程 会一直等待释放写锁。
2.5 死锁
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象, 若无外力作用,它们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起 无法完成任务。
死锁的发生必须满足以下四个条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规 定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁
2.6 Volatile 特殊域变量
多线程编程,我们要解决的问题集中在三个方面:
- 原子性。最简单的例子就是,i++,在多线程环境下,最终的结果是不确定的,为什么?就 是因为这么一个++操作,被编译为计算机底层指令后,是多个指令来完成的。那么遇到并发 的情况,就会导致彼此“覆盖”的情况。
- 可见性。通俗解释就是,在 A 线程对一个变量做了修改,在 B 线程中,能正确的读取到 修改后的结果。究其原理,是 cpu 不是直接和系统内存通信,而是把变量读取到 L1,L2 等 内部的缓存中,也叫作私有的数据工作栈。修改也是在内部缓存中,但是何时同步到系统内 存是不能确定的,有了这个时间差,在并发的时候,就可能会导致,读到的值,不是最新值。
- 指令重排。这里只说指令重排序,虚拟机在把代码编译为指令后执行,出于优化的目的, 在保证结果不变的情况下,可能会调整指令的执行顺序。
valotile,能满足上述的可见性和有序性。但是无法保证原子性。
可见性,是在修改后,强制把对变量的修改同步到系统内存。而其他 cpu 在读取自己的内部 缓存中的值的时候,发现是 valotile 修饰的,会把内部缓存中的值,置为无效,然后从系统 内存读取。
有序性,是通过内存屏障来实现的。所谓的内存屏障,可以理解为,在某些指令中,插入屏 障指令,用以确保,在向屏障指令后面继续执行的时候,其前面的所有指令已经执行完毕。
3. Java多线程中常见的面试题
1. sleep(),wait(),join(),yield()
四个方法的区别
总结:
1):sleep(),Thread 类中的方法,表示当前线程进入阻塞状态,不释放锁
2):wait(),Object 类中的方法,表示线程进入等待状态,释放锁,所以一般能调用这个方 法的都是同步代码块,或者获取了锁的线程代码,通常和 notify()和 notifyAll()方法结合使用
3):join(),Thread 类中的方法,假如在 a 线程中调用 b 线程对象的 join()方法,表示当前 a 线程阻塞,直到 b 线程运行结束
4):yield(),Thread 类中的方法,表示线程回可执行状态。跟 sleep 方法一样,也不交出锁, 只不过不带时间参数,是指交出 cpu
2. Thread
和 Runnable
的区别
总结:
实现 Runnable 接口比继承 Thread 类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免 java 中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
4):线程池只能放入实现 Runable 或 callable 类线程,不能直接放入继承 Thread 的类
…