一、前言
读书笔记系列主要记录自己看的书籍中的知识点,算是一个归纳整理吧。《Java多线程编程核心技术》这本书主要讲解了 Java多线程相关的知识。全书分为7章,下面将记录个人认为每章中重要的知识点。
二、Java多线程的基础
1、进程和线程
进程是资源分配的最小单位,线程是CPU调度的最小单位。直观点理解:对于操作系统来说,一个任务就是一个进程,比如打开一个浏览器就是启动一个浏览器进程, 打开两个记事本就启动了两个记事本进程。有些进程不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件 事,就需要同时运行多个“子任务”,把进程内的这些“子任务”称为线程(Thread)。每个进程至少要做一件事,所以进程里至少要有一个线程。线程是最小的执行 单元,而进程由至少一个线程组成。如何调度进程和线程,完全由操作系统决定。
特别注意:多进程和多线程的程序涉及到同步、数据共享的问题。进程之间共享信息可通过TCP/IP协议,线程间共享信息可通过共用内存。线程不能够独立执行, 必须依存在应用程序中,由应用程序提供多个线程执行控制。进程有独立的地址空间,相互不影响,线程没有自己独立的地址空间(地址空间都是按进程分配的, 但在地址空间里有专属于线程的线程栈,地址空间是系统给进程分配的虚拟内存,线程栈是线程自己独有的)。进程的切换比线程的切换开销大。每个进程对应一个 JVM实例,多个线程共享JVM里的堆。单核CPU执行多任务,是操作系统轮流让各个任务轮流执行,由于CPU的执行速度实在是太快了,感觉就像所有任务都在同时 执行一样。真正的并行执行多任务只能在多核CPU上实现。
注意:Java采用单线程编程模型,JVM创建主线程,主线程可以创建子线程。
2、Java多线程的几种实现方式
(1)继承Thread类,重写run()方法;
(2)实现Runnable接口,重写run()方法;
(3)通过Callable和FutureTask创建线程;
(4)通过线程池创建线程。
3、sleep()方法
使当前执行的线程休眠(暂时停止执行)指定的毫秒数,线程不会失去对监视器的所有权。休眠时间结束后,进入就绪状态,和其他线程一起竞争CPU的执行 时间。注意:wait()是Object里的方法,wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。调用wait方法的线程,不会自己唤醒, 需要线程调用notify / notifyAll方法唤醒等待池中的所有线程,才会进入就绪队列中等待系统分配资源。sleep方法会自动唤醒,如果时间不到, 想要唤醒,可以使用interrupt方法强行打断。
4、停止线程
(1)使用退出标志,使线程正常退出,当run()执行完后,线程终止;
(2)stop()方法强制执行,废弃了,不要用这个方式;
(3)interrupt()方法,该方法是在当前线程打了个停止标记,并不会真的停止线程。
注意:sleep()状态下停止某一个线程,会进入catch语句,并且清除停止状态值,使之变成false。
/**
* Causes the currently executing thread to sleep (temporarily cease
* execution) for the specified number of milliseconds, subject to
* the precision and accuracy of system timers and schedulers. The thread
* does not lose ownership of any monitors.
*
* @param millis
* the length of time to sleep in milliseconds
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public static native void sleep(long millis) throws InterruptedException;
5、interrupted()、isInterrupted()
(1)interrupted()方法,测试当前线程是否已经是中断状态,执行后具有将状态标志清除为false的功能; (2)isInterrupted()方法,测试线程Thread对象是否已经是中断状态,但不清除状态标志。
6、yield()
yield()方法的作用是放弃当前的CPU资源,将它让给其他的任务去占用CPU执行时间。但是放弃的时间不确定,有可能刚刚放弃,马上又获得了CPU时间片。 yield让当前线程由“运行状态”进入到“就绪状态”。
7、优先级
操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU资源较多。线程的优先级分为1~10 10个等级,1最低,10最高。注意:优先级和执行顺序具有 不确定性和随机性。
8、守护线程
Java线程分两种:用户线程、守护线程。守护线程是一种特殊的线程,当进程中不存在非守护线程了,那么守护线程自动销毁。典型的守护线程就是垃圾回收线程。
三、多线程中对并发访问的控制
1、synchronized关键字
(1)synchronized关键字取得的锁是对象锁(对象锁锁住的是,同样由synchronized修饰的方法或代码段),而不是把一段代码或者方法、函数当作锁。 哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁Lock,那么其他线程就只能呈现等待状态,前提是多个线程访问的 是同一个对象。如果多个线程访问多个对象,那么JVM会创建多个锁。
(2)synchronized关键字拥有锁重入的功能,在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次获得该对象的锁的。在一个 synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。当存在父子类继承关系时,子类是完全可以通过“可重入锁” 调用父类的同步方法的(注意:同步不可以继承,子类方法中也需要加上synchronized)。当一个线程执行的代码出现异常时,其所持有的锁会自动释放。
注意:只有共享资源的读写访问才需要同步化,如果不是共享资源,那么就没有同步的必要。
注意:A线程先持有object对象的Lock锁,那么B线程可以以异步方式调用object对象中的非synchronized类型的方法;A线程先持有object对象的Lock锁, B线程如果这时也调用object对象的synchronized类型的方法则需等待,也就是同步。
(3)synchronized关键字声明方法的话在某些情况下是有弊端的,比如A线程调用同步方法执行一个长时间的任务,那么B线程就需要等待很长时间。这个时候可以使用 同步代码块。当两个并发线程访问同一个对象中的synchronized(this)同步代码块时,一段时间内只能有一个线程被执行,另一个线程需要等待当前线程执行完 这个代码块后才可以执行该代码块,但是另一个线程可以访问该对象中的非synchronized(this)同步代码块。
注意:当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对同一个object中所有其他synchronized(this)同步代码块的访问将被 阻塞。即synchronized使用的对象监视器是一个。synchronized、synchronized(this)都是锁定当前对象的。
(4)Java还支持将“任意对象”作为“对象监视器”来实现同步,这个“任意对象”大多数是实例变量及方法的参数,使用格式为synchronized(非this对象), 锁非this对象具有一定的优点:如果在一个类中有很多个synchronized方法,这时虽然能实现同步,但是会受到阻塞,影响效率;如果使用同步代码块锁 非this对象,则synchronized(非this)代码块中的程序与同步方法是异步的,不与其他锁this同步方法争抢this锁,则可以大大提高运行效率。
注意:当多个线程同时执行synchronized(x){}同步代码块时呈同步效果;当其他线程执行x对象中synchronized同步方法时呈同步效果;当其他线程执行 x对象方法里面的synchronized(this)代码块时也呈现同步效果。
(5)synchronized关键字加到static静态方法上是给Class类上锁,而synchronized关键字加到非static静态方法上是给对象上锁。(一个是Class锁,一个是 对象锁,是会产生异步的)。
Class锁可以对类的所有对象实例起作用,也就是说如果作用在两个实例,那么静态的同步方法还是同步运行。synchronized(类.class)同步代码块的作用和 synchronized static方法的作用是一样的。
锁对象的改变:在将任何数据类型作为同步锁时,需要注意的是,是否有多个线程同时持有锁对象,如果同时持有相同的锁对象,那么这些线程之间就是同步的, 如果分别获得锁对象,这些线程之间就是异步的。
注意:只要对象不变,即使对象的属性改变,结果还是同步的。
2、volatile关键字
volatile关键字的主要作用是使实例变量在多个线程间可见。volatile关键字,强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值。 但是volatile关键字不支持原子性。
(1)synchronized和volatile比较
volatile是线程同步的轻量级实现,synchronized是重量级;volatile只能修饰变量,synchronized可以修饰方法以及代码块。
多线程访问volatile不会发生阻塞,而synchronized会发生阻塞。
volatile能保证数据的可见性,但不能保证原子性,而synchronized可以保证原子性,也可以间接保证可见性,它会将私有内存和公有内存中的数据做同步。
volatile解决的是变量在多个线程之间的可见性,而synchronized解决的是多个线程之间访问资源的同步性。
四、线程间的通信、交互
1、等待、通知机制(wait/notify机制)
在调用wait()前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait方法。执行wait()后,当前线程释放锁。方法notify() 也要在同步方法或同步块中调用,即在调用前,线程必须获得该对象的对象级别锁。在执行notify()方法后,当前线程不会马上释放该对象锁,呈 wait状态的线程也不能马上获得该对象锁,要等到执行notify()方法的线程将程序执行完,即退出synchronized代码块后,当前线程才会释放锁。 总结:wait使线程停止运行,notify使停止的线程继续运行。
wait()方法可以使调用该方法的线程释放共享资源的锁,然后从运行状态退出,进入等待队列,直到被再次唤醒。notify()方法可以随机唤醒等待队列 中等待同一共享资源的“一个”线程,并使该线程退出等待队列,进入可运行状态。notifyAll()使所有等待队列中等待同一共享资源的“全部”线程 从等待状态退出,进入可运行状态。
wait(long)方法的功能是等待一段时间内是否有线程对锁进行唤醒,如果超过这个时间就自动唤醒。
2、生产者、消费者模式
原理基于wait/notify。特别注意:一些大厂面试会问这个,手写。
3、join()方法
join()的作用是等待线程对象销毁。使用场景举例:主线程创建并启动子线程,子线程运行时间长的话,主线程线运行完,如果主线程想要获取子线程 的运行结果,也就是主线程想要等子线程运行完再结束,那么就可以用join()。
方法join的作用是使所属的线程对象x正常执行run()方法中的任务,而使当前线程z进行无限期的阻塞,等待线程x销毁后再继续执行线程z后面的代码。
join()在内部使用wait()方法进行等待,而synchronized关键字使用的是“对象监视器”原理做同步。
4、ThreadLocal
ThreadLocal解决的是变量在不同线程间的隔离性,也就是不同线程拥有自己的值。(可参考:一文带你搞定ThreadLocal原理与使用)。
五、Lock的使用
1、ReentrantLock类
调用ReentrantLock对象的lock()方法获得锁,调用unlock()方法释放锁。
一个Lock对象里面可以创建多个Condition(即监视器对象)实例,线程对象可以注册在指定的Condition中,从而可以选择性的进行线程通知, 调度上更加灵活。在使用notify()/notifyAll()方法进行通知时,被通知的线程由JVM随机选择,但使用ReentrantLock结合Condition可以 实现“选择性通知”。
Object中的wait()相当于Condition里的await()。Object里的notify()相当于Condition里的signal()。
2、公平锁、非公平锁
锁Lock分为公平锁、非公平锁。公平锁表示线程获得锁的顺序是按照线程加锁的顺序来分配的,先来先得。非公平锁是一种获得锁的抢占机制,随机 获得锁。
3、ReentrantReadWriteLock
类ReentrantLock具有完全互斥排他的效果,即同一时刻只有一个线程在执行ReentrantLock.lock()方法后面的业务。这样保证了实例变量的线程 安全性,但是效率低。可以使用读写锁。
读写锁中,读相关操作的锁,称为共享锁;写操作相关的锁,称为排它锁。即读锁之间不互斥,写锁与读锁互斥,写锁与写锁互斥。
五、定时器
这一章都是讲Timer类的。对定时任务感兴趣的可以去研究研究分布式定时任务,实际项目中,一般还是用分布式定时任务多一些。
六、单例模式与多线程
单例设计模式,在实际应用中比较常见。但是结合多线程使用时候,还是需要有很多需要注意的地方。
1、饿汉模式/立即加载
立即加载(饿汉模式)就是使用类的时候已经将对象创建完毕。
public class MyObject {
private static MyObject myObject = new MyObject();
private MyObject() {
}
public static MyObject getInstance() {
// 缺点:不能有其他实例变量,因为该方法没做同步,可能出现非线程安全问题
return myObject;
}
}
2、懒汉模式/延迟加载
延迟加载就是在调用get()方法时实例才被创建。
public class MyObject {
private static MyObject myObject;
private MyObject() {
}
public static MyObject getInstance() {
// 没做同步,不安全
if (myObject == null) {
myObject = new MyObject();
}
return myObject;
}
}
3、双锁检查机制(存在反射攻击问题、序列化问题)
public class MyObject {
private volatile static MyObject myObject;
private MyObject() {
}
public static MyObject getInstance() {
if (myObject == null) {
synchronized (MyObject.class) {
if (myObject == null) {
myObject = new MyObject();
}
}
}
return myObject;
}
}
4、使用静态内置类(存在序列化问题)
public class MyObject {
private static class MyObjectHelper {
private static MyObject myObject = new MyObject();
}
private MyObject() {
}
public static MyObject getInstance() {
return MyObjectHelper.myObject;
}
}
5、使用static代码块
静态代码块中的代码在使用类的时候就已经执行了,可以利用该特性来实现单例设计模式。
public class MyObject {
private static MyObject instance = null;
private MyObject() {
}
static {
instance = new MyObject();
}
public static MyObject getInstance() {
return instance;
}
}
6、使用枚举enum (最佳,推荐这种)
枚举enum和静态代码块的特性相似,在使用枚举类时,构造方法会被自动调用,也可以利用该特性实现单例设计模式。
public enum Singleton {
INSTANCE;
public Singleton getInstance() {
return INSTANCE;
}
}
七、拾漏增补
1、线程的状态
public enum State {
// 至今尚未启动的线程
New,
// 正在JVM中执行的线程
RUNNABLE,
// 受阻塞并等待某个监视器锁的线程
BLOCKED,
// 无限期的等待另一个线程来执行某一特定操作的线程
WAITING,
// 等待另一个线程来执行,取决于指定等待时间的操作的线程
TIMED_WAITING,
// 已退出的线程
TERMINATED;
}
New状态是线程实例化后还未执行start()方法时的状态,Runnable包含Ready和Running,yield就是将线程从Running置为Ready。
wait、join—>WAITING,sleep(time)、wait(time)、join(time)—>TIMED_WAITING
2、线程组 (ThreadGroup)
可以把线程归属到线程组中,线程组中可以有线程对象,也可以有线程组,组里还可以有线程。线程组的作用是批量管理线程或者线程组对象。
线程组有自动归属特性,如果实例化一个ThreadGroup线程组x时,没有指定x所属的线程组,那么x线程组自动归属到当前线程对象所属的线程组里。
JVM的根线程组是system,system没有父线程组。(我们常用的main开始方法,它所在的线程组是main,线程组main的父线程组是system)。
ThreadGroup.interrupt()方法可以将该组中所有正在运行的线程批量停止。