文章目录
前言
在前面的博客中介绍了进程和线程,以及线程中的一些常用方法,sleep() 休眠当前线程/ Thread.currentThread.getName()获取当前线程名 /join()等待线程等,下面来说说线程状态及线程安全问题
一、什么是线程状态
线程状态是一个枚举类型Thread.State
线程的六种状态:
- NEW :Tread对象刚创建,还没在系统中创建线程,实例创建之后,调用start()之前
- RUNNABLE : 就绪状态,随时可能调度到CPU上执行,或在CPU上正在执行
- BLOCKED :阻塞状态
- WAITING: 阻塞状态
- TIMED_WAITING: 阻塞状态
- TERMINATED:系统中线程结束了(入口方法执行完毕)Thread对象销毁之前
状态及状态转移
状态变化可以说是因为方法的执行,调用了一些sleep()/notify()等方法会导致线程状态发生改变
- 放两张比较容易理解的图:
-isAlive()方法,是否存活,可以认为除了NEW 和TERMINATED的状态都是存活着的
-yield() 上图中从运行到就绪,他表示调用yield()主动让出CPU资源,但是状态还是RUNNABLE
二、线程安全问题
什么是线程安全问题?
- 当某线程操作过程中,尚未操作完成时,其他线程也参与进来,使结果偏离预期
- 比如抢车票,如果一个人进入发现还有最后一张票,立马订购,在还没订购完成,又有一个人进来看到也乘一张票,两人都定了票,但是票只有一张,这就属于线程安全问题
为什么会有线程安全问题?
- 线程的抢占式执行,是操作系统内核觉得,属于不可控因素
- 多个线程修改同一个变量
- 修改操作不是“原子的” (保证操作的原子性是保存线程安全问题的主要手段)
- 内存的可见性:由于编译器的优化使当前修改的数据没有涉及写入到内存中,另一个线程就读不到最新数据,就读取的是未修改的数据
- 代码顺序性 (指令重排序):由于编译器优化,自动对执行顺序调整,就类似于把多个相同的重复操作比如i++ 放在一起在缓存或者寄存器中加,然后在最后写入内存,这样很容易发生线程安全问题
如何解决线程安全问题?
-
利用synchronized关键字
- 使用在同步代码块中
- synchronized(同步监视器){
//需要被同步的代码
}
其中被同步的代码是共享数据 例如多个线程共同操作的变量(ticket)
同步监视器(锁)任何一个类的对象都可以充当锁,多个线程必须要共用一把锁,否则线程安全依然不能保证
- synchronized(同步监视器){
- 同步方法中
- 如果操作共享数据的代码完整的声明在一个方法中,可以将此方法声明为同步的
- 仍要使用同步监视器:
- 1.非静态的同步方法:同步监视器为:this
- 2.静态的同步方法:同步监视器为当前类本身 (类.class)
- 使用在同步代码块中
-
也可以使用Lock锁 JDK5.0新增
- 1.实例化Lock ReentrantLock lock= new ReentrantLock();
- 2.调用lock.lock();
- 3.调用解锁方法,lock.unlock();
-volatile 关键字 比synchronized更高效 不涉及竞争,不涉及线程调度- volatile能够禁止指令重排序,保证内存可见性,但不保证原子性
- 所以volatile适用于一个线程读一个线程写的情况,不适应两个线程都写的情况
关于可重入锁
多个线程连续加锁两次,synchronized会做特殊处理,不会出现死锁
synchronized内部记录了当前这把锁是哪个线程持有的,如果当前加锁的线程和持有锁的线程相同,此时不是真正的进行加锁,而是把计数器++,如果调用到解锁操作,不是立刻释放锁,而是计数器–,直到减到0,才会释放。
synchronized和lock的异同?
1. 同:二者都可以解决线程安全问题
2. 不同:synchronized机制在执行完相应的同步代码块以后,自动释放同步监视器(锁)
lock需要手动的启动同步(lock())同时结束同步也需要手动释放(unlock());
总结
线程安全问题无论是应用到开发环境中还是面试中,都是很高频的问题,需要我们重视,可以写一些简单的线程不安全代码体会一下,在进行加锁试试,体会线程安全后的结果与之前有哪些不同,上述方法解决了线程安全问题,但操作代码时,只能一个线程参与,其余线程等待,相当于单线程过程,效率低