多线程
文章目录
线程的模型
进程是系统分配资源的最小单位,线程是系统调度的最小单位
多线程:一个进程中不只一个线程
线程的优点
1.更好的利用CPU资源,多线程可在主线程执行任务同时执行其任务,不需要等待
2.同一进程的各线程之间可以共享该进程的所有资源
3.创建线程代价比较小,而系统创建进程要为该进程分配资源
4.与进程之间切换相比,线程之间的切换需要OS做的工作要少得多
5.线程占有的资源比较少
6.计算密集型应用,可将计算分解到多个线程实现
7.I/O密集型应用,可将I/O操作重叠,线程可以同时等待不同的I/O操作
线程的操作
线程的创建
-
继承Thread类
private static class MyThread extends Thread { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(i); } } } Thread a = new MyThread(); a.start(); Thread c = new Thread(new MyThread()); c.start();
-
实现Runnable接口
private static class MyRunnable implements Runnable { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(i); } } } Thread b = new Thread(new MyRunnable()); b.start();
-
其他变形
public static void UseAnonymous() { //使用匿名类创建子类对象 // == 直接创建线程对象 Thread a = new Thread() { @Override public void run() { //线程要执行的内容 } }; //使用匿名类创建 Runnable 子类对象 // == 先创建目标对象,再创建线程对象 Thread b = new Thread(new Runnable() { @Override public void run() { //线程要执行的内容 } }); //使用lambda表达式创建 Runnable 子类对象 Thread c = new Thread(() -> { //线程要执行的内容 }); }
Thread类
-
构造方法
-
属性
Thread t = Thread.currentThread(); //获取当前线程 System.out.println(t.getId()); //线程的唯一标识 System.out.println(t.getName()); //线程的名称 System.out.println(t.getState()); //线程的状态 System.out.println(t.getPriority()); //线程的优先级 System.out.println(t.isDaemon()); //是否为后台进程 //JVM会在所有非后台线程结束后,结束运行 System.out.println(t.isAlive()); //是否存活,只有 NEW/TERMINATED 返回false System.out.println(t.isInterrupted()); //是否被中断
-
常见方法
-
run()
- 提供线程一个指令清单
-
start()
- 启动线程,把线程放到就绪队列,使其拥有被调度的资格
-
通知终止:interrupt()
-
中断线程的另一种方式:共享标记,当子线程中有sleep时,无法实时响应
-
A线程通知B线程终止 配图
-
B线程正在sleep/join/wait
- 通知是以InterruptedException给出
- 状态位仍为false
-
B线程清醒(没有以上行为)
- t 通过 t.isInterrupted() / Thread.interrupted()判断
- t.isInterrupted() 状态位不变、指t这个线程、用于第三方线程查看B状态
- / Thread.interrupted() 执行后将状态位改为false、指本线程、用于B自己查看
-
-
-
等待停止:join()
- 主线程阻塞在这里,等待该线程的结束
-
sleep()
- 休眠当前线程
-
Thread.currentThread()
-
Thread.yield()
- 主动放弃CPU,但保留争抢CPU的资格
-
线程的状态
状态转移
线程安全
概念
- 多线程情况下代码运行的情况是100%符合预期的,就是线程安全的
线程不安全
-
出现线程不安全的原因
- 线程被CPU调度具有不确定性
- 线程被CPU调度下来具有不确定性
-
条件
-
多线程之间有共享资源
-
哪些是共享的
- 变量的类型(基本/引用)不能决定该变量是否是线程共享的
-
-
且对共享资源有修改操作
-
-
出现线程不安全的情况
-
automic原子性被破坏
-
什么是原子性
- 是指在CPU在执行一个操作时不可以在中途暂停然后再调度,不能被中断操作,要不执行完成,要不就不执行
-
为什么会出现问题
-
一条Java语句中不一定是原子的,也不一定只是一条指令
- i++/i–:a.读取i的值 b.修改i的值 c.把i的值写回共享区域
- Object o = new Object():a.分配空间 b.构造方法初始化 c.赋值给变量
- 原子操作:变量 = 常量 (long/double除外)
-
如果不保证原子性,当线程正对着变量操作,中间其他线程进来打断操作,就会造成错误的结果
-
-
如何解决问题
-
synchronized
- 保证方法和代码块内的操作是原子性的
-
volatile
- 保证 long/double类型变量 = 常量 是原子性的
-
-
-
visible内存的可见性没有遵守
-
什么是内存可见性
- 当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
-
为什么会出现问题
- 高速缓存带来的问题:执行的中间值只保存在高速缓存中,内存中没有及时变化。如果有多个CPU的话,每个CPU有自己的高速缓存,而CPU共享数据是用内存的,某CPU当前计算的结果,其他CPU是看不到的
- JMM规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,不同工作内存间是不可见的,工作内存中保存了线程需要的变量,不同线程间变量的传递需要经过主内存才可以;
- 1.把数据从主内存中load到工作内存中 2.修改工作内存中的数据 3.在合适的时候,把数据save回主内存
- 普通的共享变量则无法保证可见性,因为不同线程在各自工作内存中更改变量值后,再重新写回主内存的时间是不确定的,再当其他线程读取主内存值时,也可能是原来的旧值;
-
如何解决问题
-
变量被修改后立即同步到工作内存中
- 保证主内存的数据最新
-
同时把其他线程中的工作内存的该变量存为过期。目的使其他线程使用该变量时,必须从主内存中重新加载
- 保证其他线程使用的数据最新
-
-
-
reorder代码重排序产生副作用
-
什么是代码重排序
- 指令的最终执行顺序可能不是代码的书写顺序。要求,不能因为代码重排序出现问题
-
为什么代码重排序
- 提升代码的执行效率,程序的代码,往往不是最优解
-
谁在代码重排序
- 1.编译期间 编译器 2.运行期间 JVM 3.运行期间 CPU指令
-
为什么会出现问题
- 多线程情况下会有问题
-
如何解决问题
- happen-befor
- 某些机制限制了重排序的自由度
-
-
如何保证线程安全
-
1.减少资源的共享。只要没有资源共享,线程就是安全的
-
2.若必须共享,尽量不修改共享资源或者共享不可变对象。只要没有修改共享资源,线程就是安全的
-
3.若必须对共享资源进行修改,通过某些机制保证
-
synchronized
-
语法
public class Synchronized { synchronized void 普通方法() { //同步代码块 } void 普通方法2() { synchronized (this){ //同步代码块 } } synchronized static void 静态方法() { //同步代码块 } void 静态方法2() { synchronized (Synchronized.class){ //同步代码块 } } void 其他方法() { Object o = new Object(); synchronized (o) { } } }
-
锁
- 每个对象都有一把锁,对象是堆上内存区域的抽象
-
过程
-
线程AB共同争抢一把锁
- A抢到锁,则B抢锁失败,B必须让出CPU且没有资格抢锁,B:Runnable->Blocked,就绪队列->阻塞队列
- Aunlock,把当时因为抢这把锁失败的线程释放,如B:Blocked->Runnable(Ready),阻塞队列->就绪队列
-
-
判断是抢的哪一把锁
-
互斥
- 都在抢锁且抢的是同一把锁
-
从开始抢锁到抢到锁可能会很久
-
作用
-
原子性
- 保证lock到unlock的这段时间不会被其他互斥线程中断
-
适度的保证可见性
- sync请求锁成功的时候,强制把当前的工作内存清掉,从主内存中重新读取
- sync释放锁的时候,强制把当前锁的持有工作内存刷新到主内存中
-
适度的影响重排序
-
-
-
volatile
-
语法
public class Volatile { volatile int a; static volatile int b; }
-
作用
- 修饰变量,保证long/double 变量 = 常数 具有原子性
- 修饰变量,保证可见性
- volatile Person p = new Person() 不允许重排序
-
-
等待唤醒机制
-
语法
-
public static void main(String[] args) throws InterruptedException { Object o = new Object(); o.wait(); o.notify(); o.notifyAll(); }
-
必须配合synchronized使用
-
-
wait()
-
作用
-
当A线程调用 o.wait()后
- A会放弃CPU,并且失去争抢CPU的资格
- Runable->Waiting;就绪队列->o指向对象的等待集(wait-set)
- 等待被线程唤醒
-
-
执行流程
- 更改线程状态
- 把调用的线程放到o的等待集上
- 会把o这把锁释放掉
- 当前线程放弃CPU
- 当前线程重新拥有CPU
- 再次请求o这把锁,继续向下执行
- 请求成功后,wait调用结束
-
-
notify()/notifyAll()
-
作用
-
当B线程调用 o.notify()后
- B没有什么变化
- o指向的等待集上的任意一个线程,Waiting->Runnable; 等待集->就绪队列
-
o.notifyAll()
- 唤醒等待集上的所有线程
-
-
注意点
- 只会唤醒当前时间片正在休眠的线程
-