目录
一.线程状态
我们知道都线程一般有两个状态,就是就绪状态和阻塞状态,这其实都是说的系统层面上的(就是一个进程只包含一个线程),但是我们在进程中更常见的缓释多线程的,因此对于线程的状态是由更具体的划分的!
首先获取线程的方法是通过对象调用getState()方法获得到该线程的状态的,下面我介绍一下线程的具体状态:
NEW状态:安排了工作,但是还没有开始行动,也就是系统中目前还没有线程
package thread;
public class Demo12 {
public static void main(String[] args) {
Thread t = new Thread(()->{
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
System.out.println(t.getState());//此时系统中还没有线程所以其状态应该是NEW
t.start();
}
}
RUNNABLE状态:可工作的,又可以分成准备开始的和已经开始的,也就是开始了线程的工作但是还没有结束
public class Demo12 {
public static void main(String[] args) {
Thread t = new Thread(()->{
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
System.out.println(t.getState());//开始线程但没有结束状态就是RUNNABLE
}
}
TERMINATED状态:工作结束,线程内容执行完毕
public class Demo12 {
public static void main(String[] args) {
Thread t = new Thread(()->{
});
t.start();
System.out.println(t.getState());//线程结束了但t还在状态就是TERMINATED
}
}
TIMED_WAITING状态:在排队等待中,也就是当前的线程在一定时间内是阻塞的状态
public class Demo12 {
public static void main(String[] args) {
Thread t = new Thread(()->{
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t.getState());//当前的线程在一定时间内是阻塞的状态 TIMED_WAITING
}
}
BLOCKED状态:当前线程在等待锁中,导致的阻塞,这里暂不详细介绍后面会再次细说的
WAITING状态:当前线程再等到唤醒,导致阻塞了,后面也是会再次提起的!
简单的线程状态转化图
二.线程安全是什么
线程安全是整个多线程中最重要的问题,面试的时候只要提到多线程,那么线程安全是一定不会躲过去的,那么到底什么是线程安全呢?首先线程安全问题是由于多线程中的并发编程所引起的(先不细说,后面会介绍到),由于操作系统调度线程的时候,是随机的(抢占式执行),所以就很有可能导致程序出现一些bug(可以说是程序本身的),如果因为这样的调度随机性引入了bug,我们就认为代码的线程是不安全的,没有引入bug的话就是安全的线程
这里举一个典型的案例:
两个线程对同一变量进行自增5w次操作得到的结果并不是10w
//线程不安全的典例
class Add{
public int count;
public void increase(){
count++;
}
}
public class Demo13 {
public static Add add = new Add();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
for (int i = 0; i < 5_0000; i++) {
add.increase();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
add.increase();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();//这两个join谁在前谁在后都是没有关系的因为我们并不知道t1,t2哪个先结束 但是并不影响因为main一定是会等到t1和t2都结束才会打印count的
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(add.count);
}
}
结果并不是5w+5w = 10w 那这是为什么呢?
我们分析一下这个count++到底是怎么运行的,实际上我们可以吧count++分成三个CPU指令(1.(load)把内存中的count值加载到CPU寄存器中,2.(add)把寄存器中的值+1,3.(save)把寄存器的值写回到内存的count中去)正因为前面所说的抢占式执行,这就导致了两个线程同事执行这三个指令的时候,顺序上充满了随机性,就会有一下的情况
这是正是我们所想要达到的结果,但由于读取顺序是随机的,有时候串行的(+2),有时候是交错的(+1),因此这种情况不是一定的下面情况也是很有可能会发生的
这只是乱序的其中一种,这样并行操作的话就会产生bug了,就不能达到我们所预期的结果了,那么要怎么样才能解决这个bug呢?
三.解决线程安全问题
正是由于线程的"抢占式执行",所导致的线程安全问题,那么要怎样解决这个问题呢,简单粗暴,"加锁",类似于这样:
t1加了锁之后,t2就会一直等到t1执行完了,通过这样的阻塞,就把乱序的并发,变成了一个串行操作,这个时候运算结果也就对了,这样就可以简单的解决这个bug了,加了锁之后并发程度就降低了,数据就更靠谱了,速度也变慢了,正所谓"有得就有失,有失就有得"!具体使用代码如何写,java中加锁的方式右很多种,最常使用的是synchronized(这个单词十分重要)这样的关键字
class Add{
public int count;
synchronized public void increase(){
count++;
}
}
public class Demo13 {
public static Add add = new Add();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
for (int i = 0; i < 5_0000; i++) {
add.increase();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
add.increase();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(add.count);
}
}
给方法直接加上synchronized关键字后,只要进入此方法,就会自动加锁,离开方法就会自动解锁,当一个线程加锁成功的时候其他线程想要尝试加锁,就会出发阻塞等待,此时所对应的状态正是我们上面所提到的BLOCKED状态,阻塞会一直持续到,占用锁的线程把锁释放为止
四.产生线程不安全问题的原因
1.线程是"抢占式执行",线程调度充满了未知,充满了随机性,这也是产生线程不安全的源头("万恶之源",但是你又拿它没办法!)
2.多个线程对同一变量进行修改操作,(如果多个线程对多个变量进行操作的话,就不会产生那样的bug了,多个线程对同一变量度也是没有问题的,但是就是不能同时修改)
3.针对变量的操作不是原子性的,比如读变量的值,就只是针对一条机器指令,这样的操作本身就可以视为是原子的,但要自增的话,就如上面所说的,这是好几步的机器指令才能完成一次自增,然后指令的乱序就会产生线程不安全问题了,而要加锁的话就是针对这个几条机器指令,把它们打包成一个整体,一个原子,再去操作,就不会有上面的问题出现了
4.内存可见性,也会影响到线程安全
简单举个例子:
针对同一变量,一个线程只进行读操作,另一个线程进行自增操作
编译器的优化就是编译器自身会对程序员写出来的代码做出一些调整,在保证原有逻辑不变的前提下,程序的执行效率会大大提升!(原有逻辑在一般的情况下都是可以保证的,但是在多线程中,是不能保证的,多线程执行时的不确定性,在编译器编译阶段,很难预知执行行为的,对其进行的优化就很可能发生误判了)
import java.util.Scanner;
public class Demo15 {
private static int falg = 0;
public static void main(String[] args) {
Thread t = new Thread(()->{
while(falg == 0){//多次进行读操作
}
System.out.println("循环结束,t线程结束");
});
t.start();
System.out.println("请输入一个falg的值:");
Scanner input = new Scanner(System.in);
falg = input.nextInt();//main线程进行修改值
//此时我们想达到的结果就是线程推出循环,然后t线程结束
System.out.println("main线程结束");
}
}
此时并没有达到我们所预期的结果(t线程结束),这就和我们上述举的例子是一样的,内存可见性导致了线程安全问题,那么怎么解决内存可见性问题呢?我们有一种办法使用volatile关键字,volatile和原子性无关,但是能够解决内存可见性问题,是用了这个关键字后,它禁止了编译器做出上述的优化,编译器每次执行判定相等,都会重新从内存中读取falg的值,再进行比较
import java.util.Scanner;
public class Demo15 {
//private static int falg = 0;
private volatile static int falg = 0;
public static void main(String[] args) {
Thread t = new Thread(()->{
while(falg == 0){//多次进行读操作
}
System.out.println("循环结束,t线程结束");
});
t.start();
System.out.println("请输入一个falg的值:");
Scanner input = new Scanner(System.in);
falg = input.nextInt();//main线程进行修改值
//此时我们想达到的结果就是线程推出循环,然后t线程结束
System.out.println("main线程结束");
}
}
这样内存可见性问题也就解决了, 编译器的优化对于普通程序员来说,是非常玄学的,我们并不知道这样的优化发生在什么时候,假如我们不使用关键字,给循环中加个休眠,然后内存可见性也就消失了(这就是由于不加休眠的时候,编译器自身会发现一直在读取无意义的数据这是非常不好的,我们加上休眠之后,每隔一秒再进行读取,这对于计算机本身来说,时间是非常宽裕的,编译器自身就不会进行优化了)
5.指令重排序也会影响到线程安全问题
指令重排序也是编译器优化的一种操作,比如去买超市东西的话,我们不会按照自己列的商品顺序去买,而是看到什么拿什么,买完直接结账走人,这样的效率也是最快的,而不是在超市中到处乱逛,耗费时间!就类似于这样,我们所写的代码,彼此的顺序谁在前谁在后也是无所谓的,编译器就智能的调整这里代码的先后顺序,当然这里也是在保证原有逻辑的情况下进行调整的,但是多线程的情况下,编译器也是有可能产生误判的,那么要怎么解决这样的问题呢?
也是synchronized关键字,synchronized不仅可以保证原子性,内存可见性,还是可以保证指令不会重排序的,因此这个synchronized关键字真的很重要!后面我也会再详细介绍synchronized和volatile关键字以及wait()和notify()方法的具体使用的!