多线程—线程状态与线程安全

线程的状态

之前讨论的“就绪”和“阻塞”都是针对系统层面上的线程的状态(相当于针对一个进程只有一个线程的情况),但更常见的情况是一个进程中包含了多个线程,所谓的状态,其实是绑定在线程上。

NEW

安排了工作,还未开始行动
把Thread对象创建好了,但还没开始调用start

public class Demo14 {
    public static void main(String[] args) {
        Thread t=new Thread(() ->{

        });
        System.out.println(t.getState());
        t.start();
    }
}

在这里插入图片描述
运行效果
在这里插入图片描述

TERMINATED

工作完了
就是操作系统的线程已经执行完毕,销毁了,但是Thread对象还在,获取到的状态。

public class Demo14_1 {
    public static void main(String[] args) throws InterruptedException {
        Thread  t=new Thread(() ->{
           
        });
        t.start();
        Thread.sleep(1000);
        System.out.println(t.getState());
    }
}

执行效果
在这里插入图片描述

RUNNABLE

就绪状态
处于这个状态的线程,就是在就绪队列中,随时可以被调度到CPU上
如果代码中没有sleep,也没有其他可能导致阻塞的状态,代码大概率是处在Runnable状态的。

ublic class Demo14_2 {
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(() ->{
            while(true){
                //这里什么都不写
            }
        });
        t.start();
        Thread.sleep(1000);
        System.out.println(t.getState());
    }
}

执行效果
在这里插入图片描述

TIMED_WAITING

代码中调用了sleep或者join(超时时间),就会进入到此状态。
意思是当前的线程在一定时间之内是阻塞的状态【一定时间到了之后,阻塞解除】。(阻塞状态之一)

public class Demo14_3 {
   
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(() ->{
            while(true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        Thread.sleep(1000);
        System.out.println(t.getState());
    }
}

执行效果
在这里插入图片描述

BLOCKED

当前线程在等待锁,导致阻塞(阻塞状态之一)
synchronized

WAITING

当前线程在等待唤醒,导致阻塞(阻塞状态之一)

线程状态转换图

在这里插入图片描述

线程安全

概念

操作系统,线程调度的时候,是随机的(抢占式执行),正是因为这样的随机性,就可能导致程序执行出现一些bug.
如果因为这样的调度随机性引入了bug,就认为线程是不安全的,如果是因为这样的调度随机性,也没有带来bug,就认为是线程安全的。

线程不安全的典型案例

使用两个线程,对同一个整型变量,进行自增操作,每个线程自增5w次,看最终的结果。

class Counter {
    public int count;
    public void increase(){
        count++;
    }
}

public class Demo15 {
    private static Counter counter=new Counter();
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(() ->{
           for(int i=0;i<50000;i++){
               counter.increase();
           }
        });
        Thread t2=new Thread(() ->{
            for(int i=0;i<50000;i++){
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        //必须要在t1  t2都执行完了之后,在打印count的结果
        //否则,main 和 t1  t2之间都是并发执行的关系,导致t1 t2还没执行完,就先执行了下面的打印操作
        t1.join();
        t2.join();

        //在main中打印一下两个线程自增完成之后,得到的count结果
        System.out.println(counter.count);


    }
}

执行效果
在这里插入图片描述
预期是10w,但实际是63826
这里累加的结果是5w到10w之间
这5w对并发相加中,有时候是串行的(+2),有时候是交错的(+1),具体串行多少次,交错多少次,是未知的
极端情况下:
如果所有的操作是串行的,此时结果就是10w
如果所有的操作是交错的,此时结果就是5w

原因:
在这里插入图片描述

如何解决上述问题?
加锁
在这里插入图片描述

加锁之后,就变成串行了,和单线程就没啥区别了。
但是在实际开发过程中,一个线程中药做很多任务,例如:线程里要执行步骤1 ,步骤2,步骤3 ,步骤4,其中很可能只有 步骤4才涉及到线程安全问题,针对步骤4加锁即可,此时上面的步骤1 ,步骤2,步骤3 都可以并发执行。

如何加锁
使用synchronized 关键字

class Counter {
    public int count;
    synchronized public void increase(){
        count++;
    }
}

执行效果
在这里插入图片描述
给方法直接synchronized关键字,此时进入方法,就会自动加锁,离开方法,就会自动解锁。
当一个线程加锁成功的时候,其他线程尝试加锁,就会触发阻塞等待(此时对应的线程,就处在BLOCKED状态),阻塞会一直持续到占用锁的线程把锁释放为止。

线程不安全的原因

不是所有的多线程都要加锁(如果这样,多线程的并发能力就形同虚设)
1.线程是抢占式执行的,线程间的调度充满随机性【线程不安全的万恶之源】根本原因,无可奈何
2.多个线程对同一个变量进行修改操作(如果是多个线程针对不同的变量进行修改,没事;如果是多个线程针对同一个线程读,也没事)通过调整代码结构,使不同线程操作不同变量
3.针对变量的操作不是原子的
有些操作,比如读取变量的值,只是针对一条机器指令,此时这样的操作本身就可以视为是原子的。
通过加锁操作,也就是把好几个指令给打包成一个原子的了。
4.内存可见性,也会影响到线程安全。
在这里插入图片描述

public class Demo16 {
    private static int isQuite=0;
    public static void main(String[] args) {
        Thread t=new Thread(() ->{
            while(isQuite==0){
                
            }
            System.out.println("循环结束!t线程退出");
        });
        t.start();

        Scanner scanner=new Scanner(System.in);
        System.out.println("请输入一个isQuit的值:");
        isQuite=scanner.nextInt();
        System.out.println("main线程执行完毕");
        
    }
}

执行效果
在这里插入图片描述
此时,这里没有输出这句话。System.out.println(“循环结束!t线程退出”);

那么如何解决呢?
(1)使用synchronized关键字
synchronized不光能保证指令的原子性,同时也能保证内存可见性。被synchronized包裹起来的代码,编译器就不敢有上述的行为!相当于手动禁用了编译器的优化!
(2)使用volatile关键字
volatile和原子性无关,但能保证内存的可见性。
禁止编译器做出上述优化。编译器每次执行判定相等的时候,都会重新从内存中读取isQuite的值

 private static  volatile int isQuite=0;

执行效果
在这里插入图片描述

5.指令重排序,也会影响到线程安全问题
指令重排序,也是编译器优化中的一种操作。

在这里插入图片描述
那么,如何解决这样的问题呢?
使用synchronized关键字
synchronized不仅能保证原子性,同时也能保证内存可见性,同时还能禁止指令重排序。

synchronized的用法

synchronized翻译为同步的。
“同步”在计算机中存在多种意思。
在多线程中,线程安全中,同步指“互斥”。
在I/O或者网络编程中,同步相对的词叫“异步”,此处的同步和互斥没有关系和线程也没有关系,表示的是消息的发送方如何获取到结果。

直接修饰普通方法

使用synchronized的时候,本质上是针对某个“对象”进行加锁。
在这里插入图片描述

修饰一个代码块

需要显示指定针对哪个对象加锁(Java中的任意对象都有锁对象)

class Counter {
    public int count;
     public void increase(){
         synchronized (this){
             count++;
         }

    }
}

public class Demo15 {
    private static Counter counter=new Counter();
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(() ->{
           for(int i=0;i<50000;i++){
               counter.increase();
           }
        });
        Thread t2=new Thread(() ->{
            for(int i=0;i<50000;i++){
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        //必须要在t1  t2都执行完了之后,在打印count的结果
        //否则,main 和 t1  t2之间都是并发执行的关系,导致t1 t2还没执行完,就先执行了下面的打印操作
        t1.join();
        t2.join();

        //在main中打印一下两个线程自增完成之后,得到的count结果
        System.out.println(counter.count);


    }
}


修饰一个静态方法

相当于针对当前类对象加锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值