Thread中线程的状态:(获取线程的状态 t.getState())
NEW: 把Thread对象创建好了,但还没开始行动public static void main(String[] args) { Thread t=new Thread(()->{ }); System.out.println(t.getState());//获取线程的状态,此时是NEW状态,因为任务安排好了,但还没有创建线程 t.start(); }
TERMINATED: 操作系统中的线程已经执行完毕,销毁了,但是Thread对象还在
public static void main(String[] args) throws InterruptedException { Thread t=new Thread(()->{ }); t.start(); Thread.sleep(1000);//因为t线程啥都没干,此时1s之后t线程肯定执行完了,但是Thread对象还在 System.out.println(t.getState());//此时TERMINATED }
RUNNABLE: 就绪状态,处于这个状态的线程,就是在就绪队列中,随时可以被调度到CPU上。如果代码中没有进行sleep,也没有进行其他的可能导致阻塞的操作,大概率是处于Runnable状态
public static void main(String[] args) throws InterruptedException { Thread t=new Thread(()->{ while(true){ //这里面啥都不能有!!! } }); t.start(); Thread.sleep(1000); System.out.println(t.getState());//Runnable,因为是一直持续不断的执行循环,随时系统想调度它上个CPU都是随时可以的 }
TIMED_WAITING:代码中调用了sleep,join(时间)就会进入到TIMED_WAITING,意思就是当前的线程在一定时间之内是阻塞的状态
public static void main(String[] args) throws InterruptedException { Thread t=new Thread(()->{ while(true){ try { Thread.sleep(1000);//有sleep,进入了阻塞状态 } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); Thread.sleep(1000); System.out.println(t.getState());//TIMED_WAITING }
BLOCKED: 当前线程在等待锁,导致了阻塞(阻塞状态之一)
WAITING: 当前线程在等待唤醒,导致了阻塞(阻塞状态之一)
线程状态转换简图:
线程安全问题:(重点)
线程安不安全:指的是在调度线程时(操作系统调度线程是随机的)程序有没有引入bug,引入bug就认为是不安全的
一个线程不安全的案例:
使用两个线程对同一个整形变量进行自增操作,每个线程自增5W次,看最终的结果
class Conter{ //这个变量就是两个线程要去自增的变量 public int count; public void increase(){ count ++; } } public class Demo15 { private static Conter conter=new Conter(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(()->{ for (int i = 0; i < 50000; i++) {//让t1线程自增50000次 conter.increase(); } }); Thread t2=new Thread(()->{ for (int i = 0; i < 50000; i++) {//让t2线程自增50000次 conter.increase(); } }); t1.start(); t2.start(); //必须要在t1和t2都执行完了之后再打印count的结果,因为t1 t2和main线程是并发的关系,可能会导致t1和t2还没执行完,就先执行了main线程的打印操作, // 所以需要等待操作,等t1和t2都执行完之后再执行main的打印操作 t1.join(); t2.join(); //在main中打印一下两个线程自增完成之后,得到的count结果 System.out.println(conter.count); } }
此时发现用心两个线程分别执行自增50000次,最后并没有达到想要的100000次结果,出现了bug
此时就需要找到到底是哪里出现了问题
首先从源头上考虑下count++到底干了啥?
站在CPU的角度看,count++实际上包含了三个指令
1、把内存中count的值加载到CPU寄存器中(load)
2、把寄存器中的值+1(add)
3、把寄存器中的值写回到内存的count中(save)
因为“抢占式执行”导致两个线程同时执行这三个指令的时候,顺序上会充满了随机性
时若是这种情况,还比较幸运的两个线程自增1最终得到了2~
但若是下面这种情况,就不那么幸运了~
此时会发现,如果是这种情况,两个线程并发相加,但最终结果还是1,相当于+了两次,但只有一次生效了~
这也是产生bug的根源,就会导致线程不安全的问题~
解决线程不安全问题:给方法直接加上synchronized关键字,此时进入方法就会自动加锁,离开方法就会自动解锁。当一个线程加锁成功的时候,其他线程尝试加锁,就会触发阻塞等待(此时对应的线程,就处在BLOCKED状态)阻塞会一直持续到占用所的线程把锁释放为止
class Conter{ public int count; synchronized public void increase(){//加锁 count ++; } } public class Demo15 { private static Conter conter=new Conter(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(()->{ for (int i = 0; i < 50000; i++) { conter.increase(); } }); Thread t2=new Thread(()->{ for (int i = 0; i < 50000; i++) { conter.increase(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(conter.count); } }
总结:产生线程不安全的原因
1、线程是抢占式执行,线程间的调度充满随机性(虽然这是根本原因,但是也无可奈何)
2、多个线程对同一个变量进行修改操作(可以通过调整代码结构,使不同线程操作不同变量)
3、针对变量的操作不是原子的(可以通过枷锁操作,使用synchronized关键字把多个操作打包成一个原子的操作)
4、内存可见性,也会影响到线程安全(使用synchronized关键字或者使用volatile关键字)
出现内存优化就可能还会出现内存可见性问题,当然至于编译器什么情况优化什么情况不优化,我们也不知道~没有优化就没有内存可见性问题~
5、指令重排序,也会影响到线程安全(使用synchronized关键字)
也是编译器优化中的一种操作,没有优化就没有指令重排序问题~
关于内存可见性的例子:
针对同一个变量,一个线程 t1 进行读操作(循环进行很多次),一个线程 t2 进行修改操作(合适的时候执行依次),此时就会出现内存可见性问题。
出现问题的原因:
由于 t1 在频繁的读取内存的值,非常低效(因为读取内存的操作相比于读取寄存器来说是一个非常低效的操作),再加上如果 t2 线程迟迟不修改,t1线程每次读到的值是一样的值,因此有可能在JAVA编译器进行代码优化后 t1 线程不再从内存中读数据了,而是直接从寄存器里读,此时万一 t2 修改了值, t1 也感知不到了
private static int isQuit=0; public static void main(String[] args) { Thread t=new Thread(()->{ while(isQuit==0) {//t线程在循环都isQuit } System.out.println("循环结束!t线程退出!"); }); t.start(); Scanner scanner=new Scanner(System.in); System.out.println("请输入一个isQuit的值:"); isQuit=scanner.nextInt();//main线程在进行修改isQuit,但不是一直修改,也不一定啥时候修改,只有当用户输入的时候才修改 System.out.println("main线程执行完毕!"); } } //此时就会出现内存可见性问题,由于t线程一直在读,此时编译器就可能会做出优化,让t不在从内存中读了,直接从内存中读,此时就算main线程修改了isQuit的值,t线程也感知不到isQuit被修改了,所以t线程还会继续执行不受影响
没有显示 "循环结束!t线程退出!"证明 t 线程还在继续执行。没有受到影响
解决内存可见性问题的方法:
1、使用synchronized关键字:相当于手动禁用了编译器的优化
2、使用volatile关键字: 使用sychronized既可以保证指令的原子性还可以保证内存可见性,但是volatile只能保证内存可见性,禁用了编译器的优化
private static volatile int isQuit=0;//加volatile关键字,禁用了编译器的优化,这样编译器每次执行判定相等,都会从内存读取isQuit的值 public static void main(String[] args) { Thread t=new Thread(()->{ while(isQuit==0) {//t线程在循环都isQuit } System.out.println("循环结束!t线程退出!"); }); t.start(); Scanner scanner=new Scanner(System.in); System.out.println("请输入一个isQuit的值:"); isQuit=scanner.nextInt();//main线程在进行修改isQuit,但不是一直修改,也不一定啥时候修改,只有当用户输入的时候才修改 System.out.println("main线程执行完毕!"); }
关于指令重排序的问题的例子:
就像去超市买菜一样,提前把菜单列好,但是超市里摆放菜的位置是不一样的,有可能你菜单上第一个要买的菜离门口很远,第二个要买的菜离门口很近,此时如果先买第一个菜再买第二个菜还要走回头路,因此这时编译器就会重新规划出一个菜单,按照这个新排号的菜单来买菜就会不走弯路
虽然编译器在进行指令重排序会提高程序的效率,但也可能出现问题(因为必须是保证逻辑不变的前提下再去调整顺序)如果代码是单线程的程序,编译器的判定一般都很准,但若是多线程的,编译器可能产生误判(会使逻辑改变,导致程序执行错误)
解决指令重排序的问题的方法:使用synchronized关键字禁止指令重排序
synchronized关键字的作用:保证原子性,保证内存可见性,禁止指令重排序
虽然synchronized可以解决很多问题,但也不能无脑用,因为synchronized很容易使线程阻塞,一旦线程阻塞(放弃CPU),下次回到CPU的时间就不可控了,如果调度不回来,对应的任务的执行时间就拖慢了
也就是一旦使用了synchronized,这个代码大概率就和高性能无缘了~
而volatile不会引起线程阻塞
synchronized的用法:
synchronized中文意思是:同步
在多线程的线程安全中“同步”指的是“互斥”
在IO或者网络编程中,“同步”相对的此叫做“异步,此处的“同步”和“互斥”没有任何关系,和线程也没有关系了,表示的是消息的发送方,如何获取到结果~
1、直接修饰普通的方法
使用synchronized的时候,本质上是针对某个“对象”进行加锁,此时锁对象是this
class Conter{ public int count; synchronized public void increase(){//此时就是针对this来加锁 count ++; } }
public class SynchronizedDemo { public synchronized void methond() { } }
2、修饰一个代码块
需要显示指令针对哪个对象加锁(JAVA中的任意对象都可以作为锁对象)
class Conter{ public int count; public void increase(){//加锁 synchronized (this){//此时this是锁对象,当然还可以是其他的,因为JAVA中的任意对象都可以作为锁对象 count ++; } } }
3、修饰一个静态方法
相当于对当前类的类对象加锁 Counter.class(反射)
public class SynchronizedDemo { public synchronized static void method() { } }