认识线程安全

线程安全

必须要深刻理解线程安全的相关内容.
通过一段代码了解线程不安全

static class Counter {
       private static int count;
       public static void increse(){
           count++;
       }
   }

   public static void main(String[] args) throws InterruptedException {
       Thread t1 = new Thread() {
           @Override
           public void run(){
               for(int i = 0; i < 50000; i++) {
                   Counter.increse();
               }
           }
       };
       Thread t2 = new Thread() {
           @Override
           public void run(){
               for(int i = 0; i < 50000; i++) {
                   Counter.increse();
               }
           }
       };
       Counter counter = new Counter();
       t1.start();
       t2.start();
       t1.join();
       t2.join();
       System.out.println(Counter.count);
   }

正确的累加结果应该为100000
计算结果:
第一次输出: 100000
第二次输出: 68562
第三次输出: 87667

极端情况下,t1和t2每次++都是串行执行,那么结果就是100000
t1和t2每次++都是并发执行,那么结果就是50000

这样的结果出现的原因是

1.线程是抢占式执行的.(线程不安全的万恶之源)

  • 线程之间的调度完全由内核负责,用户代码中感知不到,也无法去控制
  • 线程之间谁先执行,谁后执行,谁执行到哪里从CPU上下来都是用户无法感知到的

2.自增操作不是原子的

  • 原子性:原子是一个不可切分的单位,要么执行完,要么全都不执行
    每次++都能拆分成三个步骤:

    1. 把内存中的数据读取到CPU中 LOAD
    2. 在CPU中把数据+1 ++
    3. 把计算结束的数据写会到内存中 SAVE
    4. 必须保证线程1 SAVE结束了,线程2再LOAD,此时计算结果才是正确的,但这件事情不好保证
  • 当CPU执行到上面三个步骤中的任何一步时,都可能会被调度器调度走
    让给其他线程来执行.

  • 如果两个线程是串行执行的,此时的计算结果是正确的
    如果是并行执行,线程进行++一半的时候,线程2也在++
    此时发现,明明自增了两次,但是结果还是1,这就产生了线程不安全的情况

3.多个线程尝试修改一个变量

  • 如果是一个线程修改一个变量.线程安全.
  • 如果是多个线程尝试读取同一个变量.线程安全
  • 如果多个线程尝试修改不同的变量.线程安全
  • 如果有多个线程,一个线程读数据,一个线程修改数据,此时也是可能导致线程不安全

4.内存可见性导致线程安全问题

5.指令重排序(java编译器在编译代码时,会针对指令进行优化,调整指令的先后顺序,起到保证原有逻辑不变的情况下,提高程序的运行效率)

前三个原因更加重要,后两个原因也有影响,但是没有前三个影响范围那么广

如何解决线程不安全问题呢?

  1. 抢占式执行(这个没办法解决,操作系统内核实现的)
  2. 自增操作非原子(这个有办法,可以给自增操作加上锁) 适用范围最广
  3. 第一个线程同时修改同一个变量(这个能不能有办法,也不一定,得看具体的需求)

这里的锁和现实的锁类似
锁的特点:
1.加锁(获取锁)lock
2.解锁(释放锁)unlock

Java中使用锁,需要一个关键字synchronized (英文原意 同步)
加锁解锁都由一个关键字包办了,这样的好处就是避免出现忘记解锁的情况
尝试加锁的时候并不一定能马上成功,如果发现当前的锁已经被占用了,那么
要等到之前的线程释放锁,此时剩下的线程再重新竞争锁

synchronized public static void increse(){
            count++;
        }

在进入increase 方法之前,会尝试加锁,increase方法执行完后自动解锁
尝试加锁的时候不一定能立刻就成功,如果发现当前的锁已经被占用了,那么该代码就会阻塞等待.
一直等到之前的线程释放锁,才可能获取到这个锁.

有锁的优点:
哪怕线程1执行了一半被调度走了,线程2也想尝试进行++,也会因为线程1没有释放锁而阻塞,不会对线程1的修改操作产生任何影响.
这样的话线程1的自增操作就能一鼓作气的执行完,中间也不会受到干扰,也就相当于保证了++操作的原子性.

锁这个东西用起来也没那么容易:
1.使用的时候一定要按照正确的方式来使用,否则就很容易出现各种问题
2.一旦使用锁,那么这个代码基本就和高性能无缘了

锁的等待时间是不可等待的,可能会等很久

理解 synchronized具体使用:
用法可以灵活的指定某个对象来加锁,而不仅仅是把锁加到方法上
如果把synchronized写到方法前,相当于给当前对象(this)加锁,
(所谓的锁,其实是针对某个指定的对象来加锁)

对象 new出的对象会给这个对象申请一块内存空间
认识线程安全_第1张图片
在这里插入图片描述
此处的synchronized就是针对couter这个对象来加锁,
进入到increase方法内部,就把加锁状态设为true
退出increase方法后,就把加锁状态设为false

synchronized几种常见用法:
1.加到普通方法前:表示锁this
2.加到静态方法前:表示锁当前类的对象
3.加到某个代码块之前,显示指定给某个对象加锁

public static void main(String[] args) {
   //下面的代码意义是
   //演示锁的相关特性
   //如果线程1获取到锁以后,不进行输入操作的话,就会占着锁不放
   //此时线程2是不能获取到锁的,等到线程1释放锁以后,线程2才能获取到锁
   Object locker = new Object();
   Thread t1 = new Thread() {
       @Override
       public void run(){
           Scanner sc = new Scanner(System.in);
           synchronized(locker){ //先尝试加锁.然后读取数据
               System.out.println("线程1获取到锁");
               System.out.println("请输入一个整数:");
               int num = sc.nextInt(); //如果用户不输入,那么就会一直阻塞在nextInt,这个锁就会一直被占有
               System.out.println(num);
           }
       }
   };
   Thread t2 = new Thread() {
       @Override
       public void run(){
           while(true){
               synchronized (locker){
                   System.out.println("线程2获取到锁");
                   try {
                       Thread.sleep(1000);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }

           }
       }
   };
   t1.start();
   t2.start();
}

认识线程安全_第2张图片
一旦线程1获取到锁没有释放的话,那么线程2 就会在锁这里阻塞等待

利用jconsole查看线程的情况
认识线程安全_第3张图片
↑这个就是线程1的调用栈,可以看到他阻塞在nextInt方法,在等待用户输入.

认识线程安全_第4张图片
↑这是线程2的调用栈,并且 Java_0608.TestDemo3$2.run(TestDemo3.java:42) 说明阻塞在代码的第42行
执行到41行之后触发了阻塞,即将运行42行时被阻塞.

同时上面的BLOCKED也说明是等待锁而导致的阻塞

等待锁的线程会进入到BLOCKED这样的状态

当输入一个整数,让线程1释放锁之后,线程2才能继续执行.

如果把上面的代码改一下,对不同的对象进行加锁会如何
两个线程之间会独立运行,不会影响.
也就是说t1获取到锁以后,t2仍在运行,两个线程之间没有竞争.

你可能感兴趣的:(认识线程安全)