四、线程生命周期(状态)
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State
这个枚举中给出了六种线程状态:
线程状态 | 导致状态发生条件 |
NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread只有线程对象,没有线程特征。 |
Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。调用了t.start()方法 :就绪(经典教法) |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
Timed Waiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
4.1、新生状态
用new关键字建立一个线程后,该对象就处于新生状态,处于新生状态的多线程有自己的内存空间,通过调用start()方法进行就绪状态。
4.2、就绪状态
处于就绪状态的线程具备了运行的条件,但是还没有分配到CPU,处于线程就绪队列,等待系统为其分配CPU,当系统选定一个等待执行的线程后,它就会从就绪状态进入执行状态,该动作称为"CPU调度",等待状态还有一个名字也叫作就绪状态。
4.3、运行状态
在运行状态的线程执行自己的run方法中的代码,直到因为等待某资源而阻塞或者完成任务而死亡,如果在给定的时间内没有执行结束,就会被系统换下来回到等待执行的状态。
4.4、阻塞状态
处于运行状态的线程在某种情况下,比如说执行了sleep(睡眠)方法,或者是等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态
在阻塞状态的的线程不会马上进入就绪队列,只有当引起阻塞状态的原因消除时,如睡眠时间已到或者等待的I/O设备空闲下来,线程便进入了就绪状态,重新进入到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续执行。冻结状态、静止状态都是阻塞状态
4.5、死亡状态
死亡状态是线程生命周期中最后的一个状态。
引起线程死亡的原因有三种:
- 正常运行的线程执行完了他的全部工作。
- 线程被强制性地终止(stop方法)。
- 线程抛出了未捕获的异常。
五、线程安全问题
5.1、问题引入
在多线程环境下,什么时候会出现数据错乱的问题?
多个线程并发访问共享资源,并对共享资源进行破坏性操作(增删改)的时候,一定会出现数据错乱的问题
如何解决
在多线程环境下,如果对共享资源进行破坏性操作的时候,需要同步操作。
5.2、同步操作
如果希望一系列操作(在代码中可以认为是很多句语句),要么都执行,要么都不执行,我们把这种操作叫做原子性操作,原则性操作可以认为是业务上不可分割的单元。
Java实现原子性操作的过程叫做同步操作,常见的有两种方式实现同步:
- 同步代码块
- 同步方法
- Lock锁
5.3、同步代码块
把原子性操作放到一个代码块中,就是同步代码块,使用关键字synchronized
synchronized (mutex) {//mutex 称为同步锁,也叫互斥锁。 // 原子性操作 }
改造之前的火车卖票的代码
package day16_thread.classing.thicks; /** * @author Xiao_Lin * @date 2020/12/20 14:15 */ public class MyRun implements Runnable { private int count = 500; @Override public void run() { for (int i=0;i<1000;i++){ synchronized (this){ if (count>20){ count--; System.out.println(Thread.currentThread().getName()+"卖了一张票。还剩下"+count+"张票"); } } } } }
package day16_thread.classing.thicks; /** * @author Xiao_Lin * @date 2020/12/20 14:17 */ public class TestRun { public static void main(String[] args) { MyRun myRun = new MyRun(); Thread t1 = new Thread(myRun,"窗口A"); Thread t2 = new Thread(myRun,"窗口B"); Thread t3 = new Thread(myRun,"窗口C"); Thread t4 = new Thread(myRun,"窗口D"); t1.start(); t2.start(); t3.start(); t4.start(); } }
原则上,锁对象建议使用共享资源,但是遵循以下两个点:、
- 在实例方法中建议使用
this
作为锁对象,此时this
正好是共享资源。 - 在静态方法中建议使用
类名.calss
字节码作为锁对象。
5.3.1、同步监视器
synchronized(obj){}
中的 obj 称为同步监视器,同步代码块中的同步监视器可以是任何对象,但是推荐使用共享资源作为同步监视器,且同步监视器不能是基本数据类型,同时也不推荐使用包装类型(会有自动拆箱和装箱)
5.3.2、总结
- 如果需要实现原子性操作,必须对共享资源加锁。
- 如果线程运行时,发现不是加锁的那个线程,那么此时会导致该线程阻塞,进入阻塞状态。
- 如果是需要对共享资源进行破坏性操作的时候,推荐使用实现
Runnable
接口会比较方便。
5.4、同步方法
当原子性操作代码很长且需要重复调用的时候,可以考虑将同步代码块中的代码抽取出来变成同步方法。
修饰符 synchronized 返回值类型 方法名称(){ //原子性操作 }
同步方法中无需指定同步监视器,因为同步方法的监视器就是this,也就是对象本身。
5.5、Lock锁
java.util.concurrent.locks.Lock
机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大。
Lock锁也称同步锁,加锁与释放锁方法化了,他是显示的,需要我们手动加,方法如下:
public void lock()
:加同步锁。public void unlock()
:释放同步锁。
public class Ticket implements Runnable{ private int ticket = 100; //创建锁对象 Lock lock = new ReentrantLock(); /* * 执行卖票操作 */ @Override public void run() { //每个窗口卖票的操作 //窗口 永远开启 while(true){ lock.lock();//上锁操作 if(ticket>0){//有票 可以卖 //出票操作 //使用sleep模拟一下出票时间 try { Thread.sleep(50); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } //获取当前线程对象的名字 String name = Thread.currentThread().getName(); System.out.println(name+"正在卖:"+ticket--); } lock.unlock();//解锁操作 } } }
5.6、线程通信
多个线程由于处在同一个进程,所以互相通信是比较容易的。
线程通信的核心方法:
public void wait()
: 让当前线程进入到等待状态 此方法必须锁对象调用。public void notify()
: 唤醒当前锁对象上等待状态的某个线程 此方法必须锁对象调用。public void notifyAll()
: 唤醒当前锁对象上等待状态的全部线程 此方法必须锁对象调用。
线程通信的经典模型:生产者与消费者问题。
- 生产者负责生成商品,消费者负责消费商品。
- 生产不能过剩,消费不能没有。
模拟一个案例:小明和小红有一个共同账户(共享资源),他们有3个爸爸(亲爸,岳父,干爹)给他们存钱。小明和小红去取钱,如果有钱就取出,然后等待自己,唤醒他们3个爸爸们来存钱。他们的爸爸们来存钱,如果发现有钱就不存,没钱就存钱,然后等待自己,唤醒孩子们来取钱。做整存整取:10000元。
package com; // 账户对象 public class Account { private String cardId ; private double money ; // 余额。 public Account() { } public Account(String cardId, double money) { this.cardId = cardId; this.money = money; } // 亲爸,干爹,岳父 public synchronized void saveMoney(double money) { try{ // 1.知道是谁来存钱 String name = Thread.currentThread().getName(); // 2.判断余额是否足够 if(this.money > 0){ // 5.等待自己,唤醒别人! this.notifyAll(); this.wait(); }else{ // 3.钱没有,存钱 this.money += money; System.out.println(name+"来存钱,存入了"+money+"剩余:"+this.money); // 4.等待自己,唤醒别人! this.notifyAll(); this.wait(); } }catch (Exception e){ e.printStackTrace(); } } // 小明 小红 public synchronized void drawMoney(double money) { try{ // 1.知道是谁来取钱 String name = Thread.currentThread().getName(); // 2.判断余额是否足够 if(this.money > 0){ // 3.账户有钱,有钱可以取 this.money -= money; System.out.println(name+"来取钱"+money+"取钱后剩余:"+this.money); // 4.没钱,先唤醒别人,等待自己,。 this.notifyAll(); this.wait(); }else{ // 5.余额不足,没钱,先唤醒别人,等待自己,。 this.notifyAll(); this.wait(); } }catch (Exception e){ e.printStackTrace(); } } public String getCardId() { return cardId; } public void setCardId(String cardId) { this.cardId = cardId; } public double getMoney() { return money; } public void setMoney(double money) { this.money = money; } }
package com; /** 取钱的线程类 */ public class DrawThread extends Thread { private Account acc ; // 定义了一个账户类型的成员变量接收取款的账户对象! public DrawThread(Account acc , String name){ super(name); // 为当前线程对象取名字 this.acc = acc ; } @Override public void run() { while(true){ try { Thread.sleep(4000); acc.drawMoney(10000); } catch (Exception e) { e.printStackTrace(); } } } }
package com; /** 存钱的线程类 */ public class SaveThread extends Thread { private Account acc ; // 定义了一个账户类型的成员变量接收取款的账户对象! public SaveThread(Account acc , String name){ super(name); // 为当前线程对象取名字 this.acc = acc ; } @Override public void run() { while(true){ try { Thread.sleep(4000); acc.saveMoney(10000); } catch (Exception e) { e.printStackTrace(); } } } }
package com; /** 目标:线程通信(了解原理,代码几乎不用) 线程通信:多个线程因为在同一个进程中,所以互相通信比较容易的。 线程通信的经典模型:生产者与消费者问题。 生产者负责生成商品,消费者负责消费商品。 生产不能过剩,消费不能没有。 模拟一个案例: 小明和小红有一个共同账户:共享资源 他们有3个爸爸(亲爸,岳父,干爹)给他们存钱。 模型:小明和小红去取钱,如果有钱就取出,然后等待自己,唤醒他们3个爸爸们来存钱 他们的爸爸们来存钱,如果发现有钱就不存,没钱就存钱,然后等待自己,唤醒孩子们来取钱。 做整存整取:10000元。 分析: 生产者线程:亲爸,岳父,干爹 消费者线程:小明,小红 共享资源:账户对象。 注意:线程通信一定是多个线程在操作同一个资源才需要进行通信。 线程通信必须先保证线程安全,否则毫无意义,代码也会报错! 线程通信的核心方法: public void wait(): 让当前线程进入到等待状态 此方法必须锁对象调用. public void notify() : 唤醒当前锁对象上等待状态的某个线程 此方法必须锁对象调用 public void notifyAll() : 唤醒当前锁对象上等待状态的全部线程 此方法必须锁对象调用 小结: 是一种等待唤醒机制。 必须是在同一个共享资源才需要通信,而且必须保证线程安全。 */ public class ThreadCommunication { public static void main(String[] args) { // 1.创建一个账户对象。 Account acc = new Account("ICBC-1313113",0); // 2.创建2个取钱线程。 new DrawThread(acc , "小明").start(); new DrawThread(acc , "小红").start(); // 3.创建3个存钱线程。 new SaveThread(acc , "亲爹").start(); new SaveThread(acc , "干爹").start(); new SaveThread(acc , "岳父").start(); } }
5.6.1、线程通信总结
- 线程通信是一种等待唤醒机制。
- 线程安全必须早同一个共享资源才需要通信,而且必须保证线程安全。
5.7、总结
- 线程安全,性能差。
- 线程不安全性能好,假如开发中不会存在多线程的安全问题,建议使用线程不安全的设计类。
六、 volatile关键字
6.1、问题引入
public class VolatileThread extends Thread { // 定义成员变量 private boolean flag = false ; public boolean isFlag() { return flag;} @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 将flag的值更改为true this.flag = true ; System.out.println("flag=" + flag); } } public class VolatileThreadDemo {// 测试类 public static void main(String[] args) { // 创建VolatileThread线程对象 VolatileThread volatileThread = new VolatileThread() ; volatileThread.start(); // main方法 while(true) { if(volatileThread.isFlag()) { System.out.println("执行了======"); } } } }
6.2、多线程下变量的不可见性
6.2.1、概述
在介绍多线程并发修改变量不可见现象的原因之前,我们先看看另一种Java内存模型(和Java并发编程有关的模型):JMM。
JMM(Java Memory Model):Java内存模型是Java虚拟机规范中定义的一种内存模型,Java内存模型是标准化的,他屏蔽了底层不同计算机的硬件的不同
Java内存模型描述了Java程序中各种变量(线程共享变量)的访问规则以及在JVM中将变量存储到内存和从内存中读取变量的底层细节。
JMM有以下规定:
- 所有的共享变量都存储于主内存(这里的变量是指实例变量和类变量,不包含局部变量,因为局部变量的线程是私有的,不存在竞争的问题)
- 每一个线程都有自己独立的工作内存,线程的工作内存保留了被线程使用的变量的工作副本
- 线程对变量的所有操作(读、取)都必须在工作内存中完成,而不能直接读写主内存的变量。
本地内存和主内存之间的关系:
6.2.2、问题分析
- 子线程1从主内存中读取到数据并复制到其对应的工作内存。
- 修改flag的值为true,但是这个时候flag的值还并没有写会主内存。
- 此时main方法读取到了flag的值为false。
- 当子线程1将flag的值写回去之后,由于main函数中的
while(true)
调用的是系统底层的代码,速度快,快到没有时间再去读取主内存中的值,所以此时while(true)
读取到的值一直是flag = false
。 - 此时我们能想到的办法是,如果main线程从主内存中读取到了flag最新的值,那么if语句就可以执行了。
6.2.3、多线程下变量的不可见性的原因
- 每个线程都有自己的工作内存,线程都是从主内存中拷贝到共享变量的副本值
- 每个线程都是在自己的工作内存中操作共享变量的。
6.2.4、解决方案
6.2.4.1、加锁
while(true){ synchronized(t){ if(t.isFlag()){ System.out.print("主线程进入循环") } } }
第一个线程进入synchronized
代码块前后,执行过程如下:
- 线程获得锁
- 清空工作内存
- 从主内存中拷贝共享变量的最新值变成副本
- 执行代码
- 将修改后的值重新放回主内存中
- 线程释放锁
6.2.4.2、对共享变量使用volatile关键字修饰
JMM中主内存和本地内存-第 2 页我们还可以对共享变量用volatile
关键字修饰,volatile
关键字的作用是在多线程并发下修改共享变量实现可见性。,一旦一线程修改了volatile
修饰的变量,另一个线程可以立即读取到最新值。
6.2.5、volatile和synchronized
volatile
只能修饰实例变量和类变量,而synchronized
可以修饰方法以及代码块volatile
保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全),而synchronized
是一种排他互斥的机制,可以保证线程安全。
七、原子性
所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。
7.1、问题引入
public class VolatileAtomicThread implements Runnable { // 定义一个int类型的遍历 private int count = 0 ; @Override public void run() { // 对该变量进行++操作,100次 for(int x = 0 ; x < 100 ; x++) { count++ ; System.out.println("count =========>>>> " + count); } } } public class VolatileAtomicThreadDemo { public static void main(String[] args) { // 创建VolatileAtomicThread对象 VolatileAtomicThread volatileAtomicThread = new VolatileAtomicThread() ; // 开启100个线程对count进行++操作 for(int x = 0 ; x < 100 ; x++) { new Thread(volatileAtomicThread).start(); } } }
执行结果:不保证一定是10000
7.2、问题原理说明
以上问题主要是发生在count++操作上,count++操作包含3个步骤:
- 从主内存中读取数据到工作内存
- 对工作内存中的数据进行++操作
- 将工作内存中的数据写回到主内存
count++操作不是一个原子性操作,也就是说在某一个时刻对某一个操作的执行,有可能被其他的线程打断。
1)假设此时x的值是100,线程A需要对改变量进行自增1的操作,首先它需要从主内存中读取变量x的值。由于CPU的切换关系,此时CPU的执行权被切换到了B线程。A线程就处于就绪状态,B线程处于运行状态
2)线程B也需要从主内存中读取x变量的值,由于线程A没有对x值做任何修改因此此时B读取到的数据还是100
3)线程B工作内存中x执行了+1操作,但是未刷新之主内存中
4)此时CPU的执行权切换到了A线程上,由于此时线程B没有将工作内存中的数据刷新到主内存,因此A线程工作内存中的变量值还是100,没有失效。A线程对工作内存中的数据进行了+1操作
5)线程B将101写入到主内存
6)线程A将101写入到主内存
虽然计算了2次,但是只对A进行了1次修改。
7.3、volition的原子性
在多线程环境下,volatile关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性(在多线程环境下volatile修饰的变量也是线程不安全的)。
在多线程环境下,要保证数据的安全性,我们还需要使用锁机制。
7.4、问题解决办法
7.4.1、使用锁机制(加锁)
我们可以给count++操作添加锁,那么count++操作就是临界区的代码,临界区只能有一个线程去执行,所以count++就变成了原子操作。
缺点:性能差。
public class VolatileAtomicThread implements Runnable { // 定义一个int类型的变量 private volatile int count = 0 ; private static final Object obj = new Object(); @Override public void run() { // 对该变量进行++操作,100次 for(int x = 0 ; x < 100 ; x++) { synchronized (obj) { count++ ; System.out.println("count =========>>>> " + count); } } } }
7.4.2、使用原子类
7.4.2.1、概述
Java从JDK5开始提供了java.util.concurrent.atomic
包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。我们可以使用原子类来保证原子性操作,从而保证线程安全。
7.4.2.2、常用API
我们以Integer的原子类进行讲解。
方法 | 概述 |
public AtomicInteger(): | 初始化一个默认值为0的原子型Integer |
public AtomicInteger(int initialValue): | 初始化一个指定值的原子型Integer |
int get(): | 获取值 |
int getAndIncrement(): | 以原子方式将当前值加1,注意,这里返回的是自增前的值。 |
int incrementAndGet(): | 以原子方式将当前值加1,注意,这里返回的是自增后的值。 |
int addAndGet(int data): | 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。 |
int getAndSet(int value): | 以原子方式设置为newValue的值,并返回旧值。 |
public class VolatileAtomicThread implements Runnable { // 定义一个int类型的变量,默认值是0,我们也可以指定长度 private AtomicInteger atomicInteger = new AtomicInteger() ; @Override public void run() { // 对该变量进行++操作,100次 for(int x = 0 ; x < 100 ; x++) { int i = atomicInteger.getAndIncrement(); System.out.println("count =========>>>> " + i); } } }
7.5、原子类CAS机制
CAS的全成是: Compare And Swap
(比较再交换); 是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。CAS可以将read-modify-check-write转换为原子操作,这个原子操作直接由处理器保证。
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
7.5.1、CAS机制详解
- 在内存地址V当中,存储着值为10的变量。
- 此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。
- 在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。
- 线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,说明值已经被更改过了,提交失败。
- 线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。
- 这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的,说明并没有人修改过值。
- 线程1进行SWAP(交换),把地址V的值替换为B,也就是12。
7.6、乐观锁和悲观锁
CAS和Synchronized都可以保证多线程环境下共享数据的安全性。那么他们两者有什么区别?
7.6.1、悲观锁
Synchronized是从悲观的角度出发,是一个典型的悲观锁。
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。因此Synchronized
我们也将其称之为悲观锁。jdk中的ReentrantLock
也是一种悲观锁。性能较差!
7.6.2、乐观锁
CAS是从乐观的角度出发,总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。
CAS这种机制我们也可以将其称之为乐观锁。综合性能较好!很多数据库都会使用到乐观锁机制。
作者:XiaoLin_Java
链接:https://juejin.cn/post/6984190169708494879
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。