一、初识线程
1.什么是线程
进程是系统分配资源的最小单位,线程是系统调度的最小单位,一个进程内的线程之间是可以共享资源的,每个进程至少有一个线程存在,即主线程
2.创建线程的方式
2.1创建线程-方法1-继承Thread类
可以通过继承Thread来创建一个线程类,该方法的好处是this代表的就是当前线程,不需要通过Thread.current.Thread来获取当前线程的引用
class MyThread extends Thread{
@Override
public void run(){
System.out.println("这里是线程运行的代码");
}
}
MyThread t = new MyThread();
t.start();//线程开始运行
2.2创建线程-方法2-实现Runnable接口
通过实现Runnable接口,并且调用Thread的构造方法时将Runnable对象作为target参数传入来创建对象,该方法的好处是可以规避类的单继承的限制,但需要通过Thread.currentThread()来获取当前线程的引用
class MyRunnable implements Runnable{
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+"这里是线程运行的代码");
}
}
Thread t = new Thread(new MyRunnable());
t.start();//线程开始运行
2.3创建线程-其他变形(了解)
//使用匿名内部类创建Thread子类对象
Thread t1 = new Thread(){
@Override
public void run(){
System.out.println("使用匿名内部类创建Thread子类对象");
}
}
//使用匿名内部类创建Runnable子类对象
Thread t2 = new Thread(new Runnable(){
@Override
public void run(){
System.out.println("使用匿名内部类创建Runnable子类对象");
}
});
//使用lambda表达式创建Runnable子类对象
Thread t3 = new Thread(()->System.out.println("使用匿名内部类创建Thread子类对象"));
Thread t4 = new Thread(()->{
System.out.println("使用匿名内部类创建Thread子类对象");
二、Thread类
1.什么是Thread类
Thread类是JVM用来管理线程的一个类,换句话说,每个线程都有一个唯一的Thread对象与之关联
每个执行流,也需要有一个对象来描述,类似于下图所示,而Thread类的对象就是用来描述一个线程执行流的,JVM会将这些Thread对象组织起来,用于线程调度,线程管理
2.Thread的常见构造方法
方法 | 说明 |
---|---|
Thread | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target,String name) | 使用Runnable对象创建线程对象,并命名 |
Thread(ThreadGroup group,Runnable target) | 线程可以被用来分组管理,分好的组即使线程组 |
3.Thread的常见属性
属性 | 获取方法 |
---|---|
ID | long getID() |
名称 | String getName() |
状态 | Thread.state getState() |
优先级 | int getPriority() |
是否后台线程(守护线程) | boolaean isDaemon() |
是否存活 | boolean isAlive() |
是否被中断 | boolean isInterruppted() |
中断这个线程 | void interrupt () |
等待这个线程死亡 | void join() |
等待这个线程最多mills ms | void join(long millis) |
如果这个线程使用单独的Runnable运行对象构造,则调用该Runnable对象的run方法,否则不执行任何操作并返回 | void run() |
将此线程标记为daemon线程或用户线程 | |
设置线程名 | void setName(String name) |
设置优先级 | void setPriority(int newPriority() |
导致线程开始执行,Java虚拟机调用此线程的run方法 | void start() |
- ID是线程的唯一标识,不同线程不会重复
- 名称是各种调试工具用到
- 状态表示线程当前所处的一个情况
- 优先级高的线程理论上来说更容易被调度到
- 关于后台线程,需要记住一个点:JVM会在一个进程的所有非后台线程结束后,才会结束运行
- 是否存活,即简单的理解,为run方法是否运行结束了
三、多线程的效率问题
最高效率和系统资源+线程数+单个线程执行的任务量都有关系
1.使用多线程提高效率需要考虑的因素
- 所有线程执行是并发+并行
- 线程创建、销毁是比较耗时
- 线程的调度由系统决定(线程越多,系统调度越频繁,线程就绪态转变为运行态也是有性能及时间消耗)
- 单个线程运行的任务量
2.run方法 vs start方法
- run方法是线程运行的时候执行的代码块,线程启动是通过start方法启动
- run方法直接调用,不会启动线程,只是在当前main线程中调用run方法
- start方法启动这个线程
3.java进程的退出
- 至少有一个线程是非守护线程,没有被销毁,进程就不会退出
- 非守护线程一般可以称为工作线程,守护线程可以称为后台线程
t.setDaemon(true)
设置守护线程
注意事项:
优先级更高的线程,更有可能会先执行,但不是一定,知识几率更大
Runnable是就绪态和运行态的并集
四、多线程
1.线程的让步
Thread.yield;//将当前线程由运行态转变为就绪态
2.线程的等待
t.join();
t.join(2000);
3.休眠当前线程
sleep.sleep(8000);
4.线程的中断
不是真实的直接中断,具体是否要中断,由线程自己决定
boolean isInterrupted();//测试这个线程是否被中断
void interrupt();//中断这个线程
static boolean interrupted();//测试这个线程是否中断
- 线程调用wait()/join()/sleep()阻塞时,如果把当前线程给中断,会直接抛一个异常,而线程运行状态时,需要自行判断中断标志位,处理中断操作,阻塞状态时,通过捕获及处理异常来处理中断线程的逻辑
- 抛出异常后,线程中断标志位会重置
- 线程的真实的中断方法:过期方法stop()
- 线程启动以后,中断标志位为false
- 在线程运行态中,处理线程中断,需要自行通过判断中断标志位,来进行中断的处理逻辑(Thread.isInterrupted()/Thread.interrupted()),通过这两种方法来判断
- 线程因调用wait()/join()/sleep()处于阻塞状态时,将线程中断,会直接抛出Interrupted Exception异常
- 抛出异常后,重置线程的中断标志位为true
- 也可以使用自定义的中断标志位
- 自定义的标志位能满足线程处于运行态的中断操作,但不能满足线程处于阻塞状态时的中断操作
5.通信-对象的等待集wait set
- wait()的作用是让当前线程进入等待作用,同时,wait()也会让当前对象释放它所持有的的锁,“直到其他线程调用此对象的notify()方法或notifyAll()方法”,当前线程被唤醒(进入“就绪状态”)
- notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程,notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程
- wait(long timeout)让当前线程处于(阻塞)状态,直到其他线程调用此对象的额notify()方法或者notifyAll()方法,或者超过指定的时间量,当前线程被唤醒(进入“就绪状态”)
5.1wait方法()
wait()方法就是使线程停止运行
- 方法wait()的作用是使当前执行代码的线程进行等待,wait()方法是Object类的方法,该方法是用来将当前线程置入“预执行队列”中,并且在wait()所在的代码处停止执行,直到接到通知或被中断为止
- wait()方法只能在同步方法中或同步块中调用,如果调用wait()时,没有持有适当的锁,会抛出异常
- wait()方法执行后,当前线程释放锁,线程与其他线程竞争重新获取锁
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("等待中...");
object.wait();
System.out.println("等待已过...");
}
System.out.println("main方法结束...");
}
这样在执行到object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到了另外一个方法唤醒的方法notify()
5.2notify()方法
notify方法就是使停止的线程继续运行。
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。如果有多个线程等待,则有线程规划器随机挑选出一个呈wait状态的线程。
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出
同步代码块之后才会释放对象锁。
5.3 notifyAll()方法
notifyAll方法可以一次唤醒所有的等待线程
注意:唤醒线程不能过早,如果在还没有线程在等待中时,过早的唤醒线程,这个时候就会出现先唤醒,在等待的效果了。这样就没有必要在去运行wait方法了。
5.4 wait 和 sleep 的对比(面试题)
其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间。用生活中的例子说的话就是婚礼时会吃糖,和家里自己吃糖之间有差别。说白了放弃线程执行只是 wait 的一小段现象。
当然为了面试的目的,我们还是总结下: - wait 之前需要请求锁,而wait执行时会先释放锁,等被唤醒时再重新请求锁。这个锁是 wait 对像上的 monitor lock
- sleep 是无视锁的存在的,即之前请求的锁不会释放,没有锁也不会请求。
- wait 是 Object 的方法
- sleep 是 Thread 的静态方法
五、线程安全问题
1.线程不安全的操作
private static final int NUM = 20;
private static final int COUNT = 1000;
//同时启动20个线程,每个线程对同一个变量操作,循环1000次,循环++操作
private static int SUM;//int 数据类型,值处于-128-127,在常量池中,超出范围,处于堆
public static void main(String[] args){
for(int i = 0;i < NUM;i++){
new Thread(new Runnable(){
@Override
public void run(){
for(int j = 0;j < COUNT;j++){
SUM++;
}
}
}).start();
while(Thread.activeCount()>1){
System.out.println();
}
}
//打印结果每次不同且不是200000
2.线程的安全问题
a. 原子性:
- List item不具有原子性,在代码行之间插入了并发/并行执行的其他代码
- 造成的结果:业务逻辑处理出现问题
- 特殊的原子性代码:(分解执行存在编译为class文件时,也可能存在CPU执行指令)
- n++,n–,++n,–n都不是原子性,要分解成三条指令,从内存读取变量到CPU,修改变量,写回内存
- Object对象的new操作:Object obj = new Object();分解为三条指令:分配对象的内存,初始化对象,将对象赋值给变量
b.可见性:
new Thread(new Runnable(){
@Override
public void run(){
for(int j = 0;j < COUNT;j++){
SUM++;
}
}
}).start();
- 从主内存中将SUM变量复制到线程的工作内存
- 在工作内存中修改变量(+1操作)
- 将SUM变量从线程的工作内存写回主内存
为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题
造成线程不安全,共享变量发生了修改的丢失
c:重排序
线程内代码是JVM,CPU都进行重排序,给我们的感觉就是线程内的代码是有序的,是因为重排序优化方案会保证线程内代码执行的依赖关系- 线程内看自己代码运行,都是有序的,但是看其他线程代码运行,都是无序的
- 如果都是私有变量,最终结果是正确的,如果是共享变量,最终结果是错误的
五、解决线程不安全的操作
1.synchronized关键字
当线程释放锁时,JVM会把该线程对应的工作内存中的共享变量刷新到主内存中,当线程获取锁时,JVM会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
1.1具体使用
a:静态方法上:对当前类对象进行加锁
b:实例方法上:对this对象加锁
c:代码块:Synchronized(对象){ 。。。}
2.进入synchronized代码行,需要获取对象锁
- 获取成功:往下执行代码
- 获取失败:阻塞在synchronized代码行
3.退出synchronized代码块,或synchronized方法
- 退回对象锁
- 通知JVM及系统,其他线程可以来竞争这把锁
4.注意事项:
- synchronized用的锁是存在java对象头里的
- synchronized同步块对同一条线程来说是可重入的,不会出现把自己锁死的问题
- 同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入
- 对一个共享变量的修改操作,大家都在同步修改,就要加锁了
- synchronized(this),对当前对象加锁Rubbable加锁:线程不安全
- synchronized(…class)对当前类对象加锁:线程安全
5.关注点:
- 对哪个对象加锁—一个对象只有一把锁
- 只有同一个对象,才会有同步互斥的作用(多线程线程安全的三大特性都能满足)
- 对于synchronized内的代码来说,在同一个时间点,只有一个线程在运行(没有并发、并行)
- 运行的线程越多,性能下降越快,归还对象锁的时候,线程不停的在被唤醒阻塞状态切换
- 同步代码执行时间越短,性能下降也比较快
synchronized(safeThread.class){
...}//对当前类对象加锁
等同于public static synchronized void increment(){
...}
synchronized(this){
...}//对this对象进行加锁
等同于public synchronized void increment2(){
...}
6.执行过程图解
注意:唤醒是JVM和系统级别,程序还是阻塞的
7.多线程操作需要考虑的地方
- 安全
- 效率
在保证安全的前提条件下,尽可能的提高效率:
线程执行时间比较长,考虑多线程(线程的创建、销毁的时间消耗)
如果不能保证安全,所有代码都没有意义—先安全,再效率
8.明确锁的是什么
锁的Synchronized对象
public class SynchronizedDemo{
public synchronized void method(){
}
public static void main(String[] args){
SynchronizedDemo demo = new SynchronizedDemo();
demo.method();//进入方法会锁demo指向对象中的锁,出方法会释放demo指向的对象中的锁
}
}
锁的SynchronizedDemo类的对象
public class SynchronizedDemo{
public synchronized static void method(){
}
public static void main(String[] args){
nethod();//进入方法会锁SynchronizedDemo.class对象中的锁,出方法会释放SynchronizedDemo.class指向的对象中的锁
}
明确锁的对象
public class SynchronizedDemo{
public void method(){
//进入代码块会锁this指向对象中的锁,出代码块会释放this指向对象中的锁
synchronized(this){
}
}
public static void main(String[] args){
SynchronizedDemo demo = new Synchronized();
demo.methon();
}
}
2.volatile关键字
volatile修饰的共享变量,可以保证可见性,部分保证顺序性
class ThreadDemo{
private volatile int n;
}
2.1说明
- volatile不能保证原子性,所以不能满足n++,n–操作的线程安全
- volatile对变量进行赋值操作时,需要视常量(不能依赖变量)
2.2注意点
- volatile保证可见性,保证有序性,不能保证原子性
- volatile修饰的变量,进行赋值不能依赖变量(常量赋值可以保证线程安全)
- 使用场景:volatile可以结合线程加锁的一些手段,提高线程效率,只是变量的读取、常量赋值可以不加锁,而是使用volatile,提高效率
六、多线程案例
1.单例模式
1.1饿汉模式
class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
1.2懒汉模式–单线程版
class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
1.3 懒汉模式-多线程版-性能低
class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
1.4 懒汉模式-多线程版-二次判断-性能高
class Singleton {
private static volatile Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
2.线程池
线程池最大的好处就是减少每次启动、销毁线程的损耗
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class ThreadPool {
private static class Worker extends Thread {
}
private BlockingQueue<Runnable> jobQueue;
private int nCurrentThreads;
private int nThreads;
private Worker[] workers;
public ThreadPool(int nThreads, int nCachedJobs) {
this.jobQueue = new ArrayBlockingQueue<>(nCachedJobs);
this.nCurrentThreads = 0;
this.nThreads = nThreads;
this.workers = new Worker[nThreads];
}
public void execute(Runnable command) throws InterruptedException {
if (nCurrentThreads < nThreads) {
Worker worker = new Worker();
workers[nCurrentThreads++] = worker;
worker.start();
} else {
jobQueue.put(command);
}
}
}
}
七、总结
1.保证线程安全的思路
- 使用没有共享资源的模型
- 适用共享资源只读,不写的模型
- 不需要写共享资源的模型
- 使用不可变对象
- 直面线程安全(重点)
- 保证原子性
- 保证顺序性
- 保证可见性
2.对比线程和进程
2.1 线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
11.2 进程与线程的区别
- 进程是系统进行资源分配和调度的一个独立单位,线程是程序执行的最小单位。
- 进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈。
- 由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信。
- 线程的创建、切换及终止效率更高。