一、前言
读书笔记系列主要记录自己看的书籍中的知识点,算是一个归纳整理吧。《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()方法可以将该组中所有正在运行的线程批量停止。