多线程问题2-关于线程的状态及其线程安全问题~

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() {
   }
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值