一、初识线程
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都进行重排序,给我们的感觉就是线程内的代码是有序的,是因为重排序优化方案会保证线程内代码执行的依赖关系- 线程内看自己代码运行,都是有序的,但是看其他线程代码运行,都是无序的
- 如果都是私有变量,最终结果是正确的,如果是共享变量,最终结果是错误的
3.结合JMM看待java进程运行,多线程不安全问题
java类名
类名的进程,启动—>执行java.exe进程
- 初始化JVM参数
- 创建JVM虚拟机
- 启动后台线程
- 启动java级别main线程(执行java main方法)
五、解决线程不安全的操作
1.synchronized关键字
线程安全用的,同步关键字
当线程释放锁时,JVM会把该线程对应的工作内存中的共享变量刷新到主内存中,当线程获取锁时,JVM会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
作用:对一段代码加锁操作,让某段代码满足三个特性:原子性,可见性,有序性
原理:多个线程间同步互斥(一段代码在任意一个时间点,只有一个线程执行:加锁,释放锁)
加锁,释放锁:基于对象来进行加锁/释放锁,不是把代码锁了
1.1具体使用
a:静态方法上:对当前类对象进行加锁
b:实例方法上:对this对象加锁
c:代码块:Synchronized(对象){ 。。。}
注意事项:只有对同一个对象加锁,才会让线程产生同步互斥的作用
1.2.进入synchronized代码行,需要获取对象锁
- 获取成功:往下执行代码
- 获取失败:阻塞在synchronized代码行
synchronized这行代码,加锁,}结束的代码行,释放锁
1.3.退出synchronized代码块,或synchronized方法
- 退回对象锁
- 通知JVM及系统,其他线程可以来竞争这把锁
1.4.注意事项:
- synchronized用的锁是存在java对象头里的
- synchronized同步块对同一条线程来说是可重入的,不会出现把自己锁死的问题
- 同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入
- 对一个共享变量的修改操作,大家都在同步修改,就要加锁了
- synchronized(this),对当前对象加锁Rubbable加锁:线程不安全
- synchronized(…class)对当前类对象加锁:线程安全
1.5.关注点:
- 对哪个对象加锁—一个对象只有一把锁
- 只有同一个对象,才会有同步互斥的作用(多线程线程安全的三大特性都能满足)
- 对于synchronized内的代码来说,在同一个时间点,只有一个线程在运行(没有并发、并行)
- 运行的线程越多,性能下降越快,归还对象锁的时候,线程不停的在被唤醒阻塞状态切换
- 同步代码执行时间越短,性能下降也比较快
synchronized(safeThread.class){...}//对当前类对象加锁
等同于public static synchronized void increment(){...}
synchronized(this){...}//对this对象进行加锁
等同于public synchronized void increment2(){...}
1.6.执行过程图解
注意:唤醒是JVM和系统级别,程序还是阻塞的
1.7.多线程操作需要考虑的地方
- 安全
- 效率
在保证安全的前提条件下,尽可能的提高效率:
线程执行时间比较长,考虑多线程(线程的创建、销毁的时间消耗)
如果不能保证安全,所有代码都没有意义—先安全,再效率
1.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();
}
}
使用不同的对象加锁,没有同步互斥的作用
public static void increment(){
synchronized(new synchronizedTest()){
COUNT++;
}
}
2.volatile关键字
(1)保证可见性
(2)禁止指令重排序,建立内存屏障
(3)不保证原子性
class ThreadDemo{
private volatile int n;
}
2.1说明
-
分解后的指令,有volatile修饰的变量,这行指令禁止重排序
-
volatile不能保证原子性,所以不能满足n++,n–操作的线程安全
-
volatile对变量进行赋值操作时,需要视常量(不能依赖变量)
2.2注意点
- volatile保证可见性,保证有序性,不能保证原子性
- volatile修饰的变量,进行赋值不能依赖变量(常量赋值可以保证线程安全)
2.3使用场景
- 一般读写分离的操作,提高性能
(1)写操作不依赖共享变量,赋值是一个常量
(2)作用在读,写依赖其他手段(加锁)
六、多线程案例
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操作。
2.2 进程与线程的区别
- 进程是系统进行资源分配和调度的一个独立单位,线程是程序执行的最小单位。
- 进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈。
- 由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信。
- 线程的创建、切换及终止效率更高。
3.程序计数器
线程私有的,程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器
4.Java虚拟机栈
也是线程私有的,它的生命周期和线程相同,虚拟机栈描述的是Java方法执行 的线程内存模型,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧,由于存储局部变量表,操作数栈,动态连接,方法和出口等信息,每一个方法被调用,直至执行完毕的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程
5.本地方法栈
线程私有的,与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务
6.java堆
线程共享的,所有的对象以及数组都是在堆上分配
7.方法区
线程共享的,与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量(final修饰),静态变量(static修饰的),即时编译器编译后的代码缓存等数据
8.运行时常量池
线程共享的,运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
9.直接内存
本地内存中的,直接内存并不是虚拟机运行时数据区的一部分,也不是<java虚拟机规范>中定义的内存区域,但是这部分内存也被频繁的使用,而且也可能导致OutOfMemoryError异常出现.
IO操作会将数据从本地内存中复制到java进程内存,效率比较差