Java遨游在多线程的知识体系中

Posted 意愿三七

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java遨游在多线程的知识体系中相关的知识,希望对你有一定的参考价值。

前言:

这一篇文章是接着上一篇的,因为上一篇篇幅有点长了,太长会影响阅读,所以也是重新写一篇,这个多线程系列会一直更下去,也期待完成的那一天,还是老样子,有问题直接评论区留言or私信我,看见都会解决的。


一、分析上篇多线程不安全原因

上一篇是讲到线程不安全,典型的案列是:

当两个线程并发针对count变量进行自增,此时会出现bug

1. count++ 操作是三个步骤,load add save


2. 多个线程之间的调度是无序的,两个线程的上述三个操作可能存在多种不同的相对顺序

  • 解决方案,加锁synchronize,进入synchronize会先加锁,出了这个方法会解锁,如果当前线程占用这把锁,其他锁来占用会出现阻塞等待

线程不安全的原因:

  1. 【根本原因】线程的抢占式执行(对于这个原因无可奈何)
  2. 两个线程在修改同一个变量

3. 线程针对变量的修改不是原子的

(不可以拆分的最小单位,像++ 拆分变成 load add save)


4. 内存可见性

内存可见性:
场景:有一个变量,一个线程循环快速的读取这个变量的值,另一个线程会在一定时间之后,修改这个变量(来看代码)

public class Demo16 
    private static int isQuit =0;
    public static void main(String[] args) 
        Thread t =new Thread(()->
           while(isQuit==0) 

           
            System.out.println("t 线程结束");
        );
        t.start();

        //在主线程中,通过scanner 让用户输入一个整数,把输入的值赋值给isQuit,从而影响到t线程退出。

        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入一个整数");
        isQuit = scanner.nextInt();
        System.out.println("main 线程结束");
    


按道理来说把输入的值赋给isQuit的话 就while结束了,但是还是没有这就是一个bug

为什么呢?

这里就要提到java中的编译器优化了,程序猿写的代码,java编译器在编译的时候,并不是原封不动的逐字翻译,会在保证原有的逻辑不变的前提下,动态调整要执行的指令内容,这个调整的过程,是需要保证原有的逻辑不变的,但是这个调整能提高效率~

主流的编译器都会这类编译器的优化,这样的优化会大幅度提高代码效率,但是上面的代码就是优化过头了,我们来看看上面代码的图流程:

  • 在多线程的环境下 isQuit的值可能会发生改变,但是编译器在进行的时候,没法对于多线程的代码做出过多的预判只是看到了t线程内部没有地方在修改isQuit,
  • 并且这个t线程里,要反复执行太多次操作(读内存,我们以前说过从内存读数据,要比在寄存器读内存要慢个3-4个数量级)
  • 在t中,编译器的直观感受,就是反复进行太多次的load,太慢了,同时,这里load得到结果还一直不变,所以编译器做出了大胆的优化操作,直接省略这里的load(只保留第一次)后续的操作,都不再重新读内存了,而是直接从寄存器读

所以大概的意思就是被优化了,isQuit的数改了也没有反应,一直在0和0在比。


针对内存可见性的问题,解决方案有两种:

  1. 还是synchronize,就会禁止编译器在synchronize内部产生上述的优化
  2. 还可以使用另一个关键字volatile

要要用这个关键字修饰对应的变量即可,一旦有了这个关键字,编译器优化的时候,就知道了会禁止进行上述的读内存的优化,会保证每次都重新从内存读,哪怕速度慢一些,(主要还是保证内存可见性)

public class Demo16 
    private static volatile int isQuit =0;
    public static void main(String[] args) 
        Thread t =new Thread(()->
           while(isQuit==0) 

           
            System.out.println("t 线程结束");
        );
        t.start();

        //在主线程中,通过scanner 让用户输入一个整数,把输入的值赋值给isQuit,从而影响到t线程退出。

        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入一个整数");
        isQuit = scanner.nextInt();
        System.out.println("main 线程结束");
    



5.指令重排序

这个也可能会引起线程不安全的问题,这个也是和编译器存在关联的(也是一种优化手段)

触发指令重排序的前提,也是要保证代码的逻辑不变

上面的图,结果一样的,但是只要调整了顺序就会快好多,指令重排序,就是保证代码原有逻辑不变,调整了程序指令的执行顺序,从而提高了效率,(如果是单线程)判断还比较准确,但是在多线程这里判断节不一定准确了


二、synchronize 关键字-监视器锁

1.synchronized 的特性

  1. 互斥

  2. 保证内存可见性

  3. 禁止指令重排序


2. synchronized 使用示例

  1. 直接修饰普通方法: 锁的 SynchronizedDemo 对象
public class SynchronizedDemo 
public synchronized void methond() 

	


2.加到一个代码块上,需要手动的指定一个“锁对象”

我们重点要理解,synchronized 锁的是什么. 两个线程竞争同一把锁, 才会产生阻塞等待.

我们可以看见2个滑稽哥,去针对一个ATM去争,第一个ATM机上锁,后面的人进不去,但是我们可以去隔壁的ATM机,所以说上锁也是要分对象的。

只有针对这个指定的对象上了锁之后,此时多个线程尝试操作相关的对象才会产生竞争,代码来看:

class Count
    public int count = 0;

    public void increase()
      synchronized (this)
          count++;
      
    

在java中,任何一个继承自object的类的对象,都可以作为锁对象(synchronize加锁操作,本质上是操作object对象头中的一个标志位)


3.修饰静态方法

加到一个static方法上,此时相当于指定了当前的类对象,为锁对象

类对象:里面包含了这个类中的一些关键信息 这些关键信息就支持了java的反射机制,类对象和普通对象一样,也是可以被加锁的

public class Demo17 
    //使用这个对象来作为加锁的对象(任意对象都可以作为锁对象)
    private static Object locker1  = new Object();
    public static void main(String[] args) 
        Thread t1= new Thread(()->

            Scanner scanner = new Scanner(System.in);

           while(true)
               synchronized (locker1)
                   //获取到锁让他在这里阻塞,通过scanner阻塞
                   System.out.println("请输入一个整数");
                   int a =  scanner.nextInt();
                   System.out.println("a ="+a);
               
           
        );
        t1.start();

        //为了保证t1可以先拿到锁 然后t2再运行
        try 
            Thread.sleep(1000);
         catch (InterruptedException e) 
            e.printStackTrace();
        

        Thread t2 = new Thread(()->
            synchronized (locker1)
                System.out.println("hello t2");
            
        );
        t2.start();
    

让t1先拿到locker1这个锁,然后就阻塞(这里的scanner只是为了阻塞达到的效果)

t2在尝试获取到这个锁的时候,由于t1已经占用了锁,所以t2线程无法获取到锁,就只能阻塞等待


没有打印t2线程日志,当前t2是阻塞的

上面说是到第40行产生了阻塞状态,导致Blocked

尝试让t2拿锁,发现也是没有用

代码调整一下:

t2已经执行了:

由于没有竞争同一把锁,这就意味着两个线程之间不会有任何锁的竞争。


4.可重入锁

是synchronize的一个重要的特性,如果synchronize不是可重入,那么就很容易出现“死锁”的情况

class Count
    public int count = 0;

  synchronized  public void increase()
      synchronized (this)
          count++;
      
    

  1. 调用increase的时候,先进行加锁操作,针对this来加锁(此时this就是一个锁定的状态了,把this对象中的标志位给设置上了)
  2. 继续执行到下面的代码块的时候,也会尝试再次加锁(由于此时this已经是锁定状态,按照之前的理解 这里的加锁操作就会出现阻塞)

这里的阻塞要等到之前的代码把锁释放了,要执行完这个方法,锁才能释放,但是由于此时的阻塞,导致当前这个方法没法继续执行了(僵住了)

  • java设计锁的大佬考虑到了这样的情况,于是就把当前的synchronize设计成可重入锁,针对同一个锁连续加锁多次,不会有负面效果:

  • 锁中持有这样的两个信息:

  1. 当前这个锁被哪个线程给持有了
  2. 当前这个锁被加锁几次了
    (当前线程t如果已经被加锁了,就会自己判断出,当前的锁就是t持有的,第二加锁并不是真的加锁,只是进行了一个修改计数(1->2))
    (后续往下执行的时候,出了synchronize代码块,就触发一次解锁,也不是真的解锁而是计数-1),在外层方法执行完了之后,再次解锁,再次计数-1,计数为0才进行真正的解锁

死锁出现的情况,不仅仅是上述的这一种情况(针同一把锁,连续加锁两次)

  1. 一个线程,一把锁
  2. 两个线程,两把锁
  3. N个线程,N把锁

三、 Java 标准库中的线程安全类

  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder

上面的这些谨慎在多线程环境下使用,尤其是一个对象被多个线程修改的时候,

Vector (不推荐使用)
HashTable (不推荐使用)
ConcurrentHashMap
StringBuffer

上面几个类线程是安全的,核心方法上带有synchronize关键字,更加放心的多线程环境下使用的

还有String也是线程安全的,比较特殊,是因为String是不可变对象(不能被修改)因此就不能在多线程中修改同一个String 了


四、volatile 和synchronize区别

volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性


加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了

没加volatile 关键字

public class Demo16 
    private static  int isQuit =0;
    public static void main(String[] args) 
        Thread t =new Thread(()->
           while(isQuit==0) 

           
            System.out.println("t 线程结束");
        );
        t.start();

        //在主线程中,通过scanner 让用户输入一个整数,把输入的值赋值给isQuit,从而影响到t线程退出。

        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入一个整数");
        isQuit = scanner.nextInt();
        System.out.println("main 线程结束");
    


线程结束不了。

加上volatile关键字:


class Count
    public volatile int count = 0;

    public void increase()
          count++;
    


public class Demo15 

    private static Count counter =new Count();

    public static void main(String[] args) throws InterruptedException 
        //创建两个线程,两个线程分别对counter调用5w次 increase操作
        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();

        //阻塞等待线程累加结束
        // 如果是t2先执行完t1后执行完也没事, t1.join ,main就会阻塞等待t1线程,这个时候t2执行完了,t1还没有执行完
        // 过了一会,t1线程执行完了,于是t1.join就返回,继续调用t2.join,由于t2已经执行完了的t2.join 可以立马返回,不必阻塞等待

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

        System.out.println(counter.count);
    

加上synchronize,结果正确了


synchronized 既能保证原子性, 也能保证内存可见性

public class Demo16 
    private static volatile   int isQuit =0;
    public static void main(String[] args) 
        Thread t =new Thread(()->
           while(true) 
                synchronized (Demo16.class)
                    if (isQuit !=0)
                        break;
                    
                
           
            System.out.println("t 线程结束");
        );
        t.start();

        //在主线程中,通过scanner 让用户输入一个整数,把输入的值赋值给isQuit,从而影响到t线程退出。

        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入一个整数");
        isQuit = scanner.nextInt();
        System.out.println("main 线程结束");
    


注意while(true)地方!!


五、wait 和 notify

由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知.
但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺

通过wait, notify机制,当某个线程调用了wait之后,就阻塞等待,直到其他某个线程调用了notify,才把这个线程唤醒为止


假设有两个线程,t1,t2,希望先执行t1,t1执行了一些工作,再执行t2,就可以先让t2wait,然后t1执行一些工作,调用notify唤醒t2

注意: wait, notify, notifyAll 都是 Object 类的方法

public class Demo18 
    public static void main(String[] args) throws InterruptedException 
        Object o = new Object();
        System.out.println("等待之前");
        o.wait();
        System.out.println("等待之后");
    


为什么会有这个报错,因为这个o这个对象的锁状态不对,针对一个没有加锁的对象进行解锁操作,就会出现上述的异常

wait这个方法里会做三件事情:

  1. 先针对o解锁
  2. 进行等待
  3. 当通知到来之后,就会被唤醒,同时尝试重新获取到锁,然后再继续执行
  • 正因为wait里面做了这几件事,所以wait才需要搭配synchronize来使用

代码修改变成这样的就可以了。

public class Demo18 
    public static void main(String[] args) throws InterruptedException 
        Object o = new Object();
        synchronized(O)
			System.out.println("等待之前");
        	o.wait();
        	System.out.println("等待之后");
		
    


注意: wait, notify, notifyAll 都是 Object 类的方法

notify()方法

哪个对象调用的wait,就需要哪个对象调用notify来唤醒

比如 O1.wait()就需要01.notify 来唤醒
o1.wait(),使用o2.notify,没啥效果
notify同样要搭配synchronize来使用

如果有多个线程都在等待,调用一次notify,只能唤醒其中一个线程,具体唤醒的是谁,不知道(随机的),

如果没有任何线程等待,直接调用notify,不会有副作用

public class Demo19 

    private static Object locker = new Object();

    public static void main(String[] args) throws InterruptedException 
        Thread waiter  = new Thread(() ->
            while(true)
                synchronized (locker)
                    System.out.println("wait 开始");
                    try 
                        locker.wait();
                     catch (InterruptedException e) 
                        e.printStackTrace();
                    
                    System.out.println("wait 结束");
                
            
        );
        waiter.start();

        Thread.sleep(3000);

        Thread notifier = new Thread(()->
           synchronized (locker)
               System.out.println("notify 之前");
               locker.notify();
               System.out.println("notify 之后");
           
        );
        notifier.start();
    

结合wait和notify就可以针对多个线程之间的执行顺序进行一定的控制,java中除了刚才的notify(一次随机唤醒一个线程)之外,还有一个操作叫做notifyAll(一下子全部唤醒,唤醒之后,这些线程再尝试竞争这同一个锁)


notifyAll()方法

notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程.+

理解 notify 和 notifyAll
notify 只唤醒等待队列中的一个线程. 其他线程还是乖乖等着

notify场景:


六、多线程相关的代码案例

基于我们上面学的,我们可以使用多线程来写一个多线程相关的代码案例

案列一:实现一个线程安全版本的单例模式代码(设计模式)

在有些程序中,某些类在代码只应该有一个实例,而不应该有多个就可以称为是单例。实现一个代码,保证这个类不会被创建多个实例,此时这样的代码就叫单例模式。单例模式分为饿汉模式和懒汉模式

6.1 饿汉模式

class SingletonDataSource

    //类属性 被static修饰的成员 是类的属性
    //一个类对象在一个程序中也就存在一份(JVM)保证的
    private static SingletonDataSource instance  = new SingletonDataSource();

    //构造方法 设置为private 外面的人没有办法调用构造方法 无法创建实例
    private SingletonDataSource ()

    

    //外界使用访问的唯一通道
    public static SingletonDataSource getInstance()
        return  getInstance();
    


public class Demo1 
    public static void main(String[] args) 
        //无论在代码中的哪个地方来调用这里的 getInstance得到的都是同一个实例。
        SingletonDataSource dataSource = SingletonDataSource.getInstance();


    
Java遨游在多线程的知识体系中

Java遨游在多线程的知识体系中

Java多线程之~~~~synchronized 方法

重大事件;人工智能击败王牌飞行员,空战迎来无人时代?有3大问题必须解决

java 多线程中synchronized 机制

Java并发编程:synchronized