多线程知识点小结2

pcb里的状态字段:系统设定的状态有就绪状态和阻塞状态,在Java中,我们把上述状态又进一步作出细分了,如下

  NEW:安排了工作,还未开始行动

  RUNNABLE:可工作的,又可以分成正在工作中和即将开始工作,就绪状态,可以分成两种情况.1.线程正在CPU上运行.2.线程在这里排队,随时都可以在CPU上运行

  BLOCKED:这几个都表示排队等着其他事情(因为锁(synchronized)产生了阻塞)

  WAITING:这几个都表示排队等着其他事情(因为调用了wait产生了阻塞)

  TIMED_WAITING:这几个都表示排队等着其他事情(因为sleep产生了阻塞)

  TERMINATED:工作完成了

下面的代码是查看线程运行前的状态和线程运行完的状态

public class Demo11 {
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(t.getState());//获取线程启动前的状态
        t.start();
        t.join();
        System.out.println(t.getState());//t线程执行之后的状态
    }
}

执行的结果为

 只要实际运行结果和预期结果(需求结果)不一致,就可以称为是一个bug

在多线程下,发现由于多线程执行,导致的bug,统称为"线程安全问题",如果某个代码,在单线程下执行没有问题,多线程下执行也没有问题,则称为"线程安全",反之也可以称为"线程不安全"

以下代码就说明了这个情况

class Counter{
    public int count=0;
    public void increaes(){
        count++;
    }
}
public class Demo12 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter=new Counter();
        Thread t1=new Thread(()->{
            for (int i=0;i<50000;i++){
                counter.increaes();
            }
        });
        Thread t2=new Thread(()->{
            for (int i=0;i<50000;i++){
                counter.increaes();
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(counter.count);

    }
}

我运行了三次结果如图

 很明显三次结果都不一样,这是怎么回事呢?

count++这个操作本质上是三个步骤

1.把内存中的数据,加载到CPU的寄存器中(load)(把count拿出来执行自己的操作)

2.把寄存器中的数据进行加一(add)(虽然一个CPU核心上,寄存器就这么一组,但是两个线程,可以视为是各自有各自的一组寄存器,本质上是"分时复用"的,就当做两个寄存器吧)

3.把寄存器中的数据写回到内存中(save)

所以说两个线程都分别有这三个步骤,那就是六个步骤,当两个线程a和b一起执行的时候,这六个步骤就不一定是按顺序(a线程先执行完三个步骤,b线程继续执行三个步骤,交替实现),有可能a线程先load,然后把count进行add加一,这时候b线程也load,然后把count进行add加一,然后a线程再把count放回去,b线程也把count放回去,这样两个线程都执行了一轮以后,count按理来说应该就是2,,结果造化弄人啊还是1

所以这就是个不安全的线程

让我们把代码改一下,把join换了位置

class Counter{
    public int count=0;
    public void increaes(){
        count++;
    }
}
public class Demo12 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter=new Counter();
        Thread t1=new Thread(()->{
            for (int i=0;i<50000;i++){
                counter.increaes();
            }
        });
        Thread t2=new Thread(()->{
            for (int i=0;i<50000;i++){
                counter.increaes();
            }
        });
        t1.start();
        t1.join();//先执行线程1

        t2.start();
        t2.join();//后执行线程2

        System.out.println(counter.count);

    }
}

这样运行结果就是100000啦

改完以后就是先执行第一个线程,先把count变成50000,再用下一个线程把count加到100000

 线程安全问题的原因:

1.(根本原因)多个线程之间的调度顺序是"随机的",操作系统使用"抢占式"的策略来调度线程                  以往只需要考虑代码在一个固定的顺序下执行,执行正确即可,现在需要考虑多线程下,N种执行顺序下,代码执行都得正确

2.多个线程同时修改同一个变量,容易产生线程安全问题

3.进行的修改,不是"原子的",如果修改操作,能够按照原子的方式来完成,此时也不会有线程安全问题  (count++不是原子的      =是直接赋值,视为原子       if =这种先判定再赋值,也不是原子)

4.内存可见性,引起的线程安全问题(以后说)

5.指令重排序,引起的线程安全问题(以后说)

解决线程安全问题最主要的切入手段就是把修改操作变成原子的,那就是加锁,加锁相当于把一组的操作给打包成一个"原子"的操作,此处这里的原子,则是通过锁,进行"互斥",我这个线程工作的时候,其他线程无法进行工作

打个比方就是上厕所的时候我们锁门,通过这个锁就限制了同一时刻只有一个人能使用这个厕所       代码中的锁就是多个线程一起执行的时候,只有一个线程能使用这个变量

Java引入了一个synchronized关键字

进入方法(代码块)就会加锁(lock),出了方法(代码块)就会解锁(unlock)

加锁目的是为了互斥使用资源(互斥地修改变量)

当a线程加锁之后,b线程也尝试加锁,b线程就会阻塞等待,这个阻塞会一直持续到a线程把锁释放之后,b才能加锁成功

此处b线程的阻塞等待,就把b线程的针对count的++操作推迟到了等a线程完成了count++,b线程才能够真正执行count++

 把"穿插执行"变成了"串行执行"

让我们把代码修改一下看看结果

class Counter{
    public int count=0;
    synchronized public void increaes(){
        count++;
    }
}
public class Demo12 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter=new Counter();
        Thread t1=new Thread(()->{
            for (int i=0;i<50000;i++){
                counter.increaes();
            }
        });
        Thread t2=new Thread(()->{
            for (int i=0;i<50000;i++){
                counter.increaes();
            }
        });
        t1.start();
        t2.start();
        
        t1.join();
        t2.join();

        System.out.println(counter.count);

    }
}

成功运行出100000

这里有个问题,我们通过加锁操作之后,把并行执行变成串行执行了,此时多线程还有别的意义吗?

当然是有的,我们加锁只是把局部操作进行加锁了,我们单看一个for循环,里面的increase因为加锁成了串行的了,但是for循环并没有加锁,不涉及到线程安全问题,for循环中的变量i是栈上的一个局部变量,两个线程,是有两个独立的栈空间,也就是完全不同的变量

 因此,这两个线程,有一部分代码是串行执行的,有一部分是并发执行的,仍然要比纯粹的串行执行效率更高

synchronized还有另外一种写法

 使用synchronized的时候,其实是指定了某个具体的对象进行加锁,当synchronized直接修饰方法,此时就相当于是针对this加锁(修饰方法相当于上述代码的简化写法)不存在所谓的"同步方法"这样的概念

这里的this指向的是counter对象

当两个线程针对同一个对象加锁的时候,就会出现锁竞争/锁冲突                                                         一个线程能先拿到锁,另一个线程就会阻塞等待(BLOCKED)                                                               直到第一个线程释放锁之后,第二个线程才可能获取到锁,才能继续向下执行

这个对象是谁无所谓,我们唯一关注的就是两个线程是否在针对同一个对象加锁,和这个synchronized修饰不修饰方法,和synchronized里头访问哪个对象的属性啥的,都没关系

打个比方,我追一个人,不管这个人好看与否或是高矮胖瘦都无所谓,最重要的是这个人有没有对象(锁是不是已经被别人占有)

锁对象(就是括号里面的)可以是任意对象,通常会写成this这种,锁的里面{}的代码可以是任意的,不一定非得是访问this的成员

如果两个线程针对不同的对象进行加锁,就不会有阻塞等待,也就不会让两个线程按照串行的方式进行count++,也就依然会存在线程安全问题,看如下代码

class Counter{
    private Object locker=new Object();
    public int count=0;
    public void increaes(){
        synchronized (this) {
            count++;
        }
    }
    public void increase2(){
        synchronized (locker) {
            count++;
        }
    }
}
public class Demo12 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter=new Counter();
        Thread t1=new Thread(()->{
            for (int i=0;i<50000;i++){
                counter.increaes();
            }
        });
        Thread t2=new Thread(()->{
            for (int i=0;i<50000;i++){
                counter.increase2();
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(counter.count);

    }
}

 第一个for循环是调用increase方法,第二个for循环我们调用increase2方法,两个方法都是对不同的对象加锁,所以还是解决不了问题,运行出来的结果依然是错误的

 如果一个代码里,一个线程加锁了,另一个线程没加锁,此时是否会存在安全问题?

会的,没加锁的那个执行的时候有可能会被加了锁的捣乱,结果还是会有安全问题(我自己是这么理解的)

总而言之,如果两个线程针对同一个对象进行加锁,就会出现锁竞争/锁冲突(一个线程能加锁成功,另一个线程阻塞等待)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值