文章目录
- 线程基本概念
- 线程的创建方式
- 线程调度-------常用的方法
- 线程的生命周期和状态
- 并发编程的根本原因
- Java内存模型(JMM)
- 多线程核心的根本问题
- volatile关键字
- 保障原子性
- synchronized和ReentrantLock的区别
线程基本概念
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序就是一个进程从创建,运行到消亡的过程。
在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
- 程序:静态的代码
并发:一个CPU同时执行多个任务,指两个或多个事件在同一个时间段内发生。比如:秒杀、多个人做同一件事。
并发编程:例如:买票,抢购,秒杀等场景,有大量的请求访问同一个资源。
会出现线程安全的问题,所以需要通过编程来控制解决让多个线程依次访问资源,称为并发编程。
并行:多个CPU同时执行多个任务,指两个或多个时间在同一时刻发生(同时发生)。比如:多个人同时做不同的事。
进程:运行中的程序,被加载到内存中,是操作系统分配内存的基本单位,每个进程都有自己独立的一块内存空间,一个进程可以有多个线程(至少有一个)
线程:线程是程序处理的基本最小单位,是cpu执行的单元,是CPU调度和分派的基本单位,它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
线程的创建方式
- 继承Thread类
继承Thread类,重写run() 方法,在run() 方法中写入线程要执行的程序,并实例化自定义的线程子类,也就是创建线程对象,调用start() 方法启动线程
特点:编写简单,可直接操作线程,适用于单继承
public class MyThread extends Thread { @Override public void run() { // 线程执行逻辑 } } // 创建并启动线程 MyThread myThread = new MyThread(); myThread.start();
- 实现Runnable接口
实现Runnable接口,重写run() 方法,在run() 方法中写入线程要执行的程序,在创建 Thread 对象时将 Runnable 子类实例作为参数传递并调用 start 方法启动线程。
特点:避免单继承局限性,便于共享资源
public class MyRunnable implements Runnable { @Override public void run() { // 线程执行逻辑 } } // 创建并启动线程 MyRunnable myRunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); thread.start();
线程调度-------常用的方法
Thread thread = new Thread(td,"自定义线程");//创建线程的名称 thread.start();//启动线程 thread.join();//这个线程运行完了之后运行其他的 thread.getName();//获取线程名称 thread.setName();//设置线程的名称 thread.setPriority(1);//设置线程优先级(1-10) thread.getPriority();//得到线程优先级 thread.sleep(200);//以指定的毫秒数暂停执行 thread.currentThread();//返回对当前正在执行的线程对象的引用 thread.yield();//让步 thread.isAlive();//判断线程是否活着 setDaemon(bool);//设置线程是否为守护线程。 object.notify();//唤醒等待的线程 object.wait(); /* wait() 必须在同步代码块中使用 必须是使用同步锁对象调用wait() wait()调用后,锁会释放 必须要通过其他线程来唤醒 */
线程的生命周期和状态
Java线程在循行的生命周期中的指定时刻只可能处于下面6种不同状态的其中一个状态:
- new:初始状态,线程被创建出来但是没有调用
start()
- runnable:运行状态,线程被调用了
start()
等待运行的状态- blocked:阻塞状态,需要等待锁释放
- waiting:等待状态,表示该线程需要等待其它线程做出一些特定动作(通知或中断)
- time_waiting:等待超时状态,可以在指定的时间后自行返回而不是像waiting那样一直等待
- terminated:终止状态,表示该线程已经运行完毕
- 守护线程
并发编程的根本原因
并发编程的根本原因在于计算机处理器的性能增长已经不能再利用频率提升这样的传统方法来提高计算机性能。因此,提高计算机的性能的唯一方法就是增加计算机的处理器核心数和使用并行计算的方式来实现
单核CPU,线程是串行执行,操作系统中有一个叫任务调度器的组件,将CPU的时间片分给不同的线程使用,由于CPU在线程间的切换非常快,人类感觉是同时运行的。
总结为一句话就是:微观串行,宏观并行,一般会将这种线程轮流使用 CPU的做法称为并发,concurrent。
多核 CPU下,每个核(core)都可以调度运行线程,这时候线程可以是并 行的。
Java内存模型(JMM)
Java内存模型(Java Memory Model,JMM)是一套规范,定义了多线程的程序在执行时,内存中的各个变量、对象以及执行顺序等行为。
Java内存模型的主要目的是解决在并发编程中,由于线程之间共享同一变量或对象可能导致不正确的计算结果的问题,例如线程安全问题、可见性问题等。
Java内存模型规定了所有变量都存储在主内存中,每个线程都有自己的工作内存,线程在执行时从主内存中读取变量值到工作内存中,执行结束之后将结果写会主内存,不能直接读写主内存中的变量。为了保证不同线程之间的内存可见性,Java内存模型定义了一些同步机制,比如volatile关键字、synchronized关键字、Lock等,这些机制可以防止多线程并发访问同一变量或者对象时出现的竞争问题和数据一致性问题。
总之,Java内存模型是Java并发编程中的重要概念,它规定了共享变量的可见性、易失性和有序性,帮助程序员编写正确、高效的多线程程序。
多线程核心的根本问题
不可见性:
多个线程分别同时对共享数据操作,彼此之间不可见,操作完成后写回主内存,有可能出现问题。一个线程对共享变量的修改,不能被另外一个线程立刻看到,如今的多核处理器,每个CPU内核都有自己的缓存,而缓存仅仅可以被自己所在的处理器内核可见,CPU缓存与内存的数据不容易保持一致。
无序性:
无序性是指程序的执行顺序不能被预测,即多线程程序中线程的执行顺序不确定,可能出现多个线程交叉执行的情况。这个特性可以提高程序的并发性能,但也为编写并发程序带来了额外的复杂性。
在Java中,出现无序性的主要原因是线程之间的工作内存和主内存的数据不一致。Java内存模型规定,每个线程都有自己的工作内存,线程在执行时需要将共享变量从主内存中读取到工作内存中,数据修改后再将数据写回到主内存中。但是,可能存在线程读取了主内存中的旧值,或者其他线程对共享变量的修改对该线程不可见的情况,导致出现无序性问题。
为了解决无序性问题,Java提供了volatile关键字,保证变量对所有线程的可见性。使用volatile关键字修饰的共享变量的值在每次被线程访问时,都强制从主内存中读取。这意味着每次访问的值都是最新的,能够避免出现无序性问题。
另外,Java 1.5引入了java.util.concurrent包中提供的原子变量类(如AtomicInteger,AtomicLong等)可以使用其提供的CAS操作,完成保证操作的原子性和可见性的操作,它们是线程安全的,可以用来替代那些非原子性的操作。这些类不但保证了原子性,也支持非阻塞算法,避免了使用synchronized关键字导致的线程阻塞和等待。
最后还有一个解决无序性问题的方法是使用Java.util.concurrent包下的Lock接口,它提供了比synchronized关键字更细粒度的锁机制。Lock接口提供了多种锁类型,如可重入锁、公平锁、读写锁等,可以根据需要灵活地使用相应的锁类型。
综上所述,Java提供了多种方法来解决无序性问题,比如使用volatile,原子变量类,Lock接口等。在编写多线程程序时,需要根据具体情况选择合适的方式,保证程序的正确性和高并发性能。
非原子性:
原子性是指一个操作是不可分割的,要么全部执行成功,要么全部不执行,中间不会出现被其他线程干扰的情况。在多线程编程中,如果涉及到多个线程并发访问同一资源时,并且这些操作需要保证原子性,否则会出现数据不一致的情况。
原子性是拒绝多线程交叉操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作
总结:
缓存(工作内存)导致可见性问题,编译(指令重排)优化带来有序性问题,线程切换带来原子性问题。缓存、线程、编译优化的目的和我们写并发程序的目的是相同的,都是提高程序安全性和性能
volatile关键字
当一个线程执行过程中,提取的数据都是从内存中提取的,当其他线程修改后,当前线程获取到的错误的数据。使用
volatile
关键字就可以解决这些问题,volatile
关键字可以用于声明一个变量,它可以确保变量在每次使用时从内存中读取,而在写入时也会立即刷新到内存中。这就保证了可见性和禁止指令重排序优化。
volatile底层实现原理
volatile 修饰的变量在操作前,添加内存屏障,不让它的指令干扰
内存屏障是一种CPU指令,用于限制指令重排序和缓存到内存之间的顺序。内存屏障对程序的执行和优化都有显著影响,可以保证多线程下的程序正确性和一致性。比如,一条内存屏障指令可以禁止编译器和处理器将其后面的指令移到内存屏障指令之前。
volatile 修饰的变量添加内存屏障之外,还要通过缓存一致性协议(MESI)将数据写回到主内存,其他工作内存嗅探后,把自己工作内存数据过期,重新从主内存读取最新的数据
保障原子性
1. 通过加锁的方式,让线程互斥执行来保证一次只有一个线程对共享资源访问
2. 在Java中还提供一些原子类(JUC),在低并发情况下使用,是一种无锁实现,
在JUC(java.util.concurrent)中,你可以见到java.util.concurrent.atomic和java.util.concurrent.locks。
加锁是一种阻塞式方式实现,原子变量是非阻塞式方式实现原子类的原子性是通过volatile+CAS实现原子操作的
AtomicInteger类中的value是有volatile关键字修饰的,这就保证了value的内存可见性
低并发情况使用AtomicInteger
3. 采用自旋思想:
采用CAS机制(Compare-And-Swap)比较并交换
概述:将第一次获取的值存入工作内存作为期望值,对数据进行修改,然后写入主内存,写之前判断与主内存中的值是否一致,一致直接写,不一致更新为新的值。
特点:
- 不加锁,所有的线程都可以对共享数据操作
- 适合低并发使用
- 由于不加锁,其他线程不需要阻塞,效率高
缺点:大并发时,不停自旋判断,导致CPU占用率高
ABA问题:
某个线程将内存值由A改为了B,再由B改为了A。当另外一个线程使用预期值进行判断时,与内存值相同,当前线程的CAS操作无法判断这个值是否发生过变化。
解决ABA问题可以设置版本号,每次操作改变版本号,可以进行判断
synchronized和ReentrantLock的区别
synchronized
和ReentrantLock
都可以用来实现 Java 中的线程同步。它们的作用类似,但是在用法和特性上还是有一些区别的。
synchronized
是 Java 内置的关键字,可以修饰代码块和方法,自动获取锁、释放锁,可以避免因为锁的释放问题导致的死锁;而ReentrantLock
是Java类,只能对某段代码进行修饰,需要手动进行锁的获取和释放。
ReentrantLock
的灵活性更高,比如支持可重入锁、支持公平锁和非公平锁、支持多个条件变量等,而synchronized
则相对简化,更加方便快捷。多个线程争抢
synchronized
的锁时,其中一个线程拿到锁后,其他线程进入锁池等待,直到持有锁的线程释放锁,其他等待线程才能继续竞争锁。而ReentrantLock
可以灵活地控制锁的公平性和非公平性,以及等待的顺序。
synchronized
在底层是依赖于 JVM 实现的,而ReentrantLock
是使用java.util.concurrent
包提供的一种基于接口的可重入锁,这种可重入锁的性能比较优秀,适用于高并发场景。综上所述,
ReentrantLock
更加灵活,支持更多的特性和操作,适用于复杂的场景;而synchronized
更加简化,使用方便,适用于一些简单的场景。