线程的状态
查看线程的所有状态
线程的状态是一个枚举类型 Thread.State
我们可以通过foreach
的方法来查看Thread中包含哪几种状态
public class ThreadState {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
状态 | 说明 |
---|---|
NEW | 安排了工作,还未开始启动 |
RUNNABLE | 可工作的,可分为就绪状态和工作中 |
BLOCKED | 等待 |
WAITING | 等待 |
TIMED_WAITING | 等待 |
TERMINATED | 工作完成 |
- BLOCKED表示等待获取锁,WAITING和TIMED_WAITING表示等待其他线程发来通知
- TIMED_WAITING线程在等待唤醒,但设置了时限;WAITING线程在无限等待唤醒
yield()
短暂让出CPU
线程安全问题
线程不安全的例子
public class ThreadState {
static class Counter{
int count = 0;
void increase(){
count++;
}
}
public static void main(String[] args) {
final Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter.count);
}
运行上述代码,我们会发现得到的结果总是小于100000,而我们预期得到的结果应该是100000,这种情况引出了本节要讨论的重点——线程的安全性。
如果在多线程中代码运行的结果与单线程环境得到的结果(单线程为应该得到的结果)不一致,则说这个程序是线程不安全的。
接下来,我们将会讨论线程不安全的原因。
线程不安全的原因
1.修改共享数据
在上述线程不安全的例子中,t1
和t2
同时对counter.count
数据进行修改,即在t1
对数据进行读写的时候,t2
对数据也在进行读写。
举个例子,t1
读取内存中的数据10,同时t2
也读取内存中的数据10,然后他们同时加1,再写入内存中,t1
写入内存的是11,t2
写入内存的也是11,这样内存中的数据便是11,按照预期的结果,做了两次加法,存入内存的数据应该是12。这样便造成了线程的不安全
2.原子性
原子性是指一系列操作的一个整体性,不可以打乱,也不可以切割执行某一部分。
在上述例子中,t1
在读写数据的时候t2
也在进行读写,这就导致了线程的不安全。
值得注意的是:
一条java语句不一定是原子的,也不一定只是一条指令。
比如n++,其实是由三步操作组成的:
- 1.从内存把数据读到CPU
- 2.进行数据更新
- 3.把数据写回道CPU
3.可见性
可见性是指一个线程对共享变量值的修改,能够及时的被其他线程看到。
public static void main(String[] args) {
int n = 0;
for (int i = 0; i < 50000; i++) {
n++;
}
}
观察上面这段代码,这段代码大量重复着读内存,更新数据,写内存的操作,这时编译器会自动进行优化,更新为读内存,更新数据,更新数据。。。写内存的操作。
在单线程的情况下,这么优化没有任何问题。但在多线程的情况下,则可能出现内存中的数据不及时更新的问题。
4.代码重排序
一段代码是这样的顺序:1->2->3
编译器可能会对代码顺序进行优化,而变成1->3->2
线程安全问题的解决
synchronized关键字-监视器锁monitor lock
synchronized的特性
1)互斥
synchronized会起到互斥的效果,当某个线程执行到某个对象的synchronized中时,其他线程如果执行到同一个对象的synchronized就会阻塞等待。
2)刷新内存
synchronized的工作过程:
- 1.获得互斥锁
- 2.从主内存拷贝变量的最新副本到工作的内存
- 3.执行代码
- 4.将更改后的共享变量的值刷新到主内存
- 5.释放互斥锁
synchronized也能保证内存的可见性。
3)可重入锁
不可重入锁:一个线程没有释放锁,在第二次加锁的时候就会阻塞等待,直到第一次的锁被释放,才能获取到锁。可是,如果释放锁和等待获取锁的是同一个线程,那么此时就会陷入死锁的困境,这种情况就是不可重入锁。
synchronized是一个可重入锁,在可重入锁的内部,包含了“线程持有者”和“计数器”两个信息
- 如果某个线程加锁的时候,发现锁已经被占用,但是恰好占用的正是自己,那么仍然可以继续获取到锁,并且让计数器自增
- 解锁的时候计数器递减为0的时候,才真正释放锁。
synchronized的使用
- 修饰普通方法:锁的对象
public class SynchronizedDemo {
public synchronized void methond() {
}
}
- 修饰静态方法:锁的类
public class SynchronizedDemo {
public synchronized static void methond() {
}
}
- 修饰代码块:明确指定锁的哪个对象
//锁当前对象
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
//锁类对象
public class SynchronizedDemo {
public void method() {
synchronized (Synchronized.class) {
}
}
}
volatile关键字
volatile修饰的变量,能够保证“内存可见性”。
加上volatile,会让修饰的变量强制读写内存
wait和notify
wait()
:让当前线程进入等待状态notify()、notifyAll()
:唤醒在当前对象上等待的线程
wait的工作原理
- 使当前执行代码的线程进行等待
- 释放当前的锁
- 满足一定条件时被唤醒,重新尝试获取这个锁
wait要搭配synchronized来使用,脱离synchronized使用wait会直接抛出异常
wait结束等待的条件
- 其他线程调用该对象的notify方法
- wait等待时间超时
- 其他线程调用该等待线程的interrupted方法,导致wait抛出InterruptedException异常
public class Locker {
private static Object locker = new Object();
static class WaitTask implements Runnable{
@Override
public void run() {
synchronized (locker){
System.out.println("wait start");
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("wait end");
}
}
}
static class NotifyTask implements Runnable{
@Override
public void run() {
synchronized (locker){
Scanner in = new Scanner(System.in);
System.out.println("notify");
in.next();
locker.notify();
System.out.println("notify end");
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new WaitTask());
Thread t2 = new Thread(new NotifyTask());
t1.start();
t2.start();
}
}
wait和sleep的对比
wait和sleep是完全没有可比性的,因为一个是用于线程间的通信,而另一个是让线程阻塞一段时间
其相同点 是都可以让线程放弃执行一段时间
其不同点有
1.wait需要搭配synchronized使用,sleep不需要
2.wait是Object的方法,sleep是Thread的方法