11.线程状态
在操作系统中的线程,自身是有一个状态的,但是呢在Java Thread是对系统线程的封装。线程的状态是一个枚举类型Thread.State。
public class Demo01 {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
通过上述代码我们可以看见线程的所有状态,下面我们将针对这些状态进行解释。
(1)NEW
系统中的线程还没创建出来,只是有一个Thread对象,也就是说我没有thread.start();
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (true) {
try {
// System.out.println("hello");
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
});
System.out.println("启动线程之前获取线程状态:" + thread.getState());
(2)TERMINATED
系统中的线程已经执行完了,Thread对象还在
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
// System.out.println("hello");
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
});
System.out.println("启动线程之前获取线程状态:" + thread.getState());
thread.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程执行完了,但Thread对象还在:" + thread.getState());
}
这里的话我把循环去掉了,怕被冲没了。
(3)RUNNABLE
表示就绪状态,有两种情况,一个是正在CPU上运行,一个是准备好随时去CPU上运行。
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
Thread.sleep(3000);
System.out.println("hello");
} catch (Exception e) {
e.printStackTrace();
}
},"thread1");
System.out.println("启动线程之前获取线程状态:" + thread.getState());
thread.start();
System.out.println("此时" + thread.getName() + "的线程状态:" + thread.getState());
try {
System.out.println("主线程");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// System.out.println("线程执行完了,但Thread对象还在:" + thread.getState());
}
从运行结果可以看到我们在开启thread线程之前是NEW,在开启线程之后也就是创建了线程对象要开始了,此时thread的状态是RUNNABLE,就绪了,我们此时让它休眠了3秒后执行,所以就会看到在打印hello后结束了。
(4)TIMED_WAITING
指定时间等待,也就是sleep方法
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
Thread.sleep(3000);
System.out.println("hello");
} catch (Exception e) {
e.printStackTrace();
}
},"thread1");
System.out.println("启动线程之前获取线程状态:" + thread.getState());
thread.start();
System.out.println("此时" + thread.getName() + "的线程状态:" + thread.getState());
try {
System.out.println("主线程");
Thread.sleep(1000);
System.out.println("在主线程休眠1秒后,thread1还没有开始还在休眠中3秒:" + thread.getState());
} catch (InterruptedException e) {
e.printStackTrace();
}
// System.out.println("线程执行完了,但Thread对象还在:" + thread.getState());
}
(5)BLOCKED
表示等待锁出现的状态,也就是这几个线程都排着队等着其它事情。
当一个线程试图获取一个对象的锁,但是呢这个锁已经被其他线程占用了,此时这个线程就会阻塞等待其它线程释放锁,直到拿到锁。
(6)WAITING
使用wait方法出现的等待
12.线程状态的意义
13.线程安全(重要)
一些代码在多线程环境下会出现bug,本质上是因为线程之间的调度顺序是不确定的。
我们先看一段代码:
package safethread;
public class Demo01 {
static class Counter {
public int count = 0;
void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException{
final Counter counter = new Counter();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(counter.count);
}
}
通过运行这段代码我们可以发现运行的结果是一个随机的,不确定的,但是我们预期结果是10w,所以预期与实际的结果不同,那么有问题,所以这就是由多线程引起的,线程不安全问题。如果单独开启一个线程,那么运行结果就和我我们预期的一致50000。那么为什么会出现呢?原因在于线程的调度是无序调度的,不确定的,也就是说多个线程它是抢占式执行的,谁抢到资源谁就执行,等待其中一个释放后才可以去执行。所以这个问题的出现是和线程调度随机性有密切关系的。解释如下:
我们上面的count++来举例:
count++操作,它本质上是三个cpu指令构成,分别如下
(1)load:把内存中的数据读取到cpu寄存器中
(2)add:把寄存器中的值进行 + 1操作
(3)save:把寄存器中的值写回到内存中
分析如下:我们现在比如说有两个线程t1,t2,那么它们开始执行的话,就会抢占式执行,无序随机的,而我们的的都是用同一个变量去作为他们共同的资源它们对这个数count进行操作,所以按照排列组合,它们产生的情况是很多的。下面简单列举几种说明:
所以通过上面的这几个例子可以看出我们不知道线程在执行的时候它是怎么无序过去调度的,也不知道发生了什么,所以也就出现了我们认为的10w,它是小于10w的随机的数字。总之因为线程的无需调度,导致了线程执行顺序的不确定性,导致了结果不一样了。
2.引起线程不安全的原因
通过上面的概述我们可以知道线程不安全的原因有以下几个:
(1)抢占式执行,线程调度是无序的,谁抢到资源谁执行
(2)多个线程修改同一个变量,对于上面的count++操作,可以看出count的值基本每次都是不一样的。在这里有如下的几条总结:
(a)一个线程修改同一个变量变量是安全的
(b)多个线程读取同一个线程是安全的
©多个线程修改不同的变量是安全的
(d)多个线程修改同一个变量是不安全的
(修改也就是写操作,那么它的反就是读取操作)
(3)修改操作不是原子的(原子也就是最小的单位数据项,不可划分)
也就是和我们的count++操作一样的,它虽然是一条语句,看上去是原子的,但是它其实是有3个步骤实现的,读到内存,数据更新,存储到cpu。
(4)内存可见性,也就是一个线程对于共享变量的修改,能够及时的被其他线程看到。
线程之间的共享变量存储在主内存(main memrory)。
每一个线程都有自己的工作内容(working memory)。
当线程要读取一个共享变量的时候,就会先把这个变量从主内存拷贝到工作内存中,再从工作内存中读取数据。
当线程要修改一个共享变量的时候,也会先修改工作内存中的副本,在同步到主内存中。
但是呢由于每个线程都有自己的工作内存,这些工作内存的内容相当于同一个共享变量中的副本,此时如果我们修改A线程的工作内存中的值,线程B的工作内存不一定会及时的改变。所以此时就会产生问题。
主内存是真正的硬件角度的内存,而工作内存指的是CPU的寄存器和告诉缓存。
那么问题来了,内存不可见性是如何影响线程安全问题的?先看一段代码:
public class Demo03 {
public static int flag = 0;
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
while (flag == 0) {
// System.out.println("lala");
}
System.out.println("flag!=0退出");
});
Thread thread2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数:");
flag = scanner.nextInt();
});
thread1.start();
thread2.start();
}
}
从这段代码中我们预期的是当我们输入一个非0数字后那么就线程1退出循环,但是结果并不是这样的,我们发现输入非0后还在一直循环中。
那么为什么会出现这样的问题呢?原因在于我们的比较条件,在cpu寄存器中有个叫比较寄存器的我们用cmp表示,当我们要去读取load这个flag时候,此时这里的load时间开销是远远高于cmp的,(读内存虽然比读硬盘快,但是读寄存器比读内存又要快),所以编译器就发现load的开销很大但是呢每次load的结果都是一样的,所以呢就把load给去掉了,只有第一次执行load才真正执行了,后面的循环都只是cmp,不load了,也就是重复用之前寄存器中load过的值。所以才会出现一直循环的问题。
这里的操作叫做编译器优化的手段,但是呢编译器它对于一些单线程下判断是非常准确的,多线程就不一定。所以内存可见性,它是在多线程环境下,编译器对于代码优化产生误判,引起的bug。那我们如何解决这个问题呢?这里我们就要引入关键字volatile。请看后面内容解决。
(5)指令重排序
简单来讲指令重排序也就是不按照规定的顺序来走,我看谁方便先走哪个。
可以看出第二种重新排序后的要高效许多。
14.如何解决线程不安全问题?
(1)从原子角度出发,如何将一个线程变成原子的呢?没有其他线程去打扰他,所以很简单例子,上厕所记得锁门!!!
锁的话它能够保证线程的原子性,它的核心操作也就是加锁,解锁。
一旦某个线程对某个资源进行了加锁,那么其它的线程也想使用这个资源,就只能阻塞等待它解锁(释放资源),才能用,不能直接上去就用。
那该如何进行加锁呢?synchronized关键字
package safethread;
public class Demo02 {
static class Count {
public int count = 0;
public void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException{
final Count counts = new Count();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counts.increase();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counts.increase();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(counts.count);
}
}
针对之前的这个代码,结果是一个随机的结果,是小于等于10w的一个数。现在我们将其加锁看。
会发现此时我们的结果和我们预想的10w一样了,我们在进行加加的这个操作中上锁,谁拿到了就锁起来,其它的想要去访问,只能阻塞等待。
这里的话我们会有一个叫锁对象的东西,锁对象,也就是说针对哪个对象进行加锁,如果多个线程针对同一个对象加锁,那么就会出现“锁竞争”(一个线程先拿到了锁,其它线程阻塞等待),那如果多个线程针对不同对象加锁,就不会产生锁竞争,会各自用自己的。
我们也可以这样去写加锁:
其中写了this,和之前的this一样,表示指向当前引用的对象,也就是这个counts。而上面的写法是针对于给类对象加锁,这里要注意的是()里的锁对象,可以是任意一个Object对象,但是内置的类型不可以)。
在我上述的代码中就是出现了锁竞争,所以当线程1获取到,那么就对++操作进行上锁,线程2就阻塞等待解锁后拿。
在这里我们也还可以手动指定锁对象加锁,然后里面的参数随便写,只要是Object的实例就行(内置类型不可)
volatile关键字保证内存可见性
还是看这个图,当我们的代码在写入volatile关键字修饰的变量的时候,编译器此刻就能保证每次都是重新从内存读取新的变量值,也就是上述flag的值。
可以清楚的看见当输入非0就退出了,此时thread2把变量修改非0了,thread1再去重新从内存读取,就是新的值了。
总结:volatile(保证内存可见性)
(1)代码再写入volatile修饰的变量的时候
改变线程工作内存中volatile变量副本的值,将改变后的副本的值从工作内存刷新到主内存。
(2)代码在读取volatile修饰的变量的时候
从主内存中读取volatile变量的最新值到线程的工作内存中,从工作内存中督导volatile变量的副本。