javaEE基础 —— 线程的状态和安全
目录
一、线程的状态
先前,我们大概介绍了一下线程的两个状态:阻塞和就绪。严格来说线程并不只有这两种状态,上述的两种状态是在系统层面上的线程状态,在Java中,尤其是Thread类之中一共将线程的状态表分成了六种。
1.new
当Thread对象创建好了,但线程并未执行
示例代码:
public static void main(String[] args) {
Thread t = new Thread(() -> {
});
System.out.println(t.getState());
}
运行结果:
2.terminated
线程已经运行结束,但是Thread对象还在
示例代码:
public static void main(String[] args) {
Thread t = new Thread(() -> {
});
t.start();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t.getState());
}
运行结果:
需要注意的是,上述两个状态是Java自身定义的,和操作系统中的PCB状态没有关系。
3.runnable
这就是我们时常说的就绪状态,这时线程有两种情况:
1.正在被执行
2.没有被调度执行,但随时可以去调度它
示例代码:
public static void main(String[] args) {
Thread t = new Thread(() -> {
});
t.start();
System.out.println(t.getState());
}
运行结果:
4.timed_waiting
代码中因调用了sleep()或是join(等待时间),就会进入这个状态,即阻塞状态。
代码示例:
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
Thread.sleep(500);
System.out.println(t.getState());
}
运行结果:
5.blocked
当前线程在等待锁而进入了阻塞状态,也是阻塞状态的一种。
6.waiting
当前线程在阻塞当中且被等待唤醒。一般我们使用wait使某个线程处于阻塞状态就会触发这种状态。
小结
上述三种状态:TIMED_WAITING、BLOCKED、WAITING,它们都是阻塞状态。
在系统里阻塞状态只有一种,但在Java又进行了进一步的细分:根据不同的原因,分成了不同的状态。
这样子做是有利于程序员理解代码是怎么执行的。
我们可以将这几种状态转移的关系简单地画下面这个图:
二、线程安全
1.线程安全的概念
想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
2.线程不安全的原因
说到线程不安全的原因就不得不提原子性操作,我们先来了解一下什么是原子性操作:
我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个操作就是不具备原子性的。
我们来看一段代码:
class count {
public static int num;
public void increase() {
num++;
}
}
class Main2 {
public static void main(String[] args) throws InterruptedException {
count c = new count();
Thread t1 = new Thread(() -> {
Object o = new Object();
for(int i = 0; i < 50000; i++) {
c.increase();
}
});
Thread t2 = new Thread(() -> {
Object o = new Object();
for(int i = 0; i < 50000; i++) {
c.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count.num);
}
}
我们定义了两个线程,每一个线程都会使num自增50000次,那么按照代码的逻辑最后num的值应该是100000,那么实际结果真的是这样子吗?
执行结果:
我们会发现 结果跟我们认为的是有很大差距的,那么这种结果的原因是什么呢?我们来分析一下。
我们在前面提到了原子性操作,而引发这个问题的恰恰也是原子性的问题。
num++;看起来是只有一个命令,实际上计算机在执行的时候是分成下面3个命令来执行的:
1.从寄存器中读取num当前值
2.在当前基础上加一
3.将++后的值存回寄存器
正因为这样,多个线程在读取储存和修改自增的时候,有极大的概率是在无效自增,比如A线程读取值为3,这时B线程也进来读取数据,读到的也是3(这个时候A线程还没有自增完成),当A加一结束将4存回寄存器后,B线程也将自增后的4存回去,但这个时候,按我们之前约定的应当是两个线程结束之后增加了2才对,但现在只有增加了1
除了 非原子性操作带来的线程不安全之外,线程的抢占式执行才是线程不安全的万恶之源。
而在上述代码之中,我们针对自增的是同一个对象,如果我们可以使两个线程去执行不同变量的自增最后将结果加起来,一样可以避免出现线程不安全的问题。
当然,我们也可以使用synchronized关键字,对代码的某些关键操作上锁,将非原子性操作打包成一个原子性的操作
优化后的代码:
class count {
public static int num;
synchronized public void increase() {//加锁
num++;
}
}
class Main2 {
public static void main(String[] args) throws InterruptedException {
count c = new count();
Thread t1 = new Thread(() -> {
Object o = new Object();
for(int i = 0; i < 50000; i++) {
c.increase();
}
});
Thread t2 = new Thread(() -> {
Object o = new Object();
for(int i = 0; i < 50000; i++) {
c.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count.num);
}
}
此外,内存可见性也会导致线程安全
代码示例:
public static int a = 1;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while(a == 1) {
}
System.out.println("循环结束");
});
t.start();
Scanner scan = new Scanner(System.in);
System.out.println("输入一个数字:");
a = scan.nextInt();
System.out.println("main结束!");
}
执行结果:
我们发现,在我们修改了a的值之后,直到主线程结束t线程也没有结束,这就是内存可见性导致的线程不安全
当某个线程高频率地读取某个数值,但多次读取发现该值一直不变,那么Java自带的优化策略就会直接从内存当中读取,以提高执行效率。所以,在某些比较重要的变量我们可以使用volatile关键字进行修饰。
修改后:
volatile public static int a = 1;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while(a == 1) {
}
System.out.println("循环结束");
});
t.start();
Scanner scan = new Scanner(System.in);
System.out.println("输入一个数字:");
a = scan.nextInt();
System.out.println("main结束!");
}
此外,volatile关键字还可以防止指令重排序。
以上就是今天的全部内容了,喜欢的话就点个赞吧!