多线程与高并发day02

本文介绍了如何优雅地结束Java线程,对比了stop(), suspend()与resume(), volatile以及interrupt()方法,并分析了各自的优缺点。同时,探讨了并发编程中的可见性特性,通过实例解释了volatile关键字如何确保线程间变量的可见性,并讨论了缓存行和缓存一致性协议对并发的影响。
摘要由CSDN通过智能技术生成

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

个人学习笔记,仅供参考!

1.如何优雅的结束线程

  1. 可使用stop()方法结束线程。stop()方法会直接结束线程并释放做资源,以下代码可进行验证。但是由于stop()方法过于粗暴,无论线程处于何时都会直接结束线程,因此可能在同步方法中进行一半时,被结束,且无善后。所以可能导致数据一致性出现问题。现已废弃,了解即可。
public class day04 {
    static class SleepHelper {
        public static void sleepSeconds(int seconds) {
            seconds *= 1000;
            try {
                Thread.sleep(seconds);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

    static final Object o = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (o) {
                while (true) {
                    System.out.println("t1 go on");
                    SleepHelper.sleepSeconds(1);
                }
            }

        });

        Thread t2 = new Thread(() -> {
            synchronized (o) {
                while (true) {
                    System.out.println("t2 go on");
                    SleepHelper.sleepSeconds(1);
                }
            }

        });

        t1.start();
        SleepHelper.sleepSeconds(1);
        t2.start();
        
        SleepHelper.sleepSeconds(5);

        t1.stop();
    }
}
  1. 使用suspend();与resume();方法组合,可以使线程停止和恢复。但此方法同样过于粗暴,且线程停止时不会释放锁。以下代码可进行验证。在代码中容易因使用不当或特殊情况,造成死锁。已废弃,了解即可。
public class day04 {
    static class SleepHelper {
        public static void sleepSeconds(int seconds) {
            seconds *= 1000;
            try {
                Thread.sleep(seconds);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

    static final Object o = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (o) {
                while (true) {
                    System.out.println("t1 go on");
                    SleepHelper.sleepSeconds(1);
                }
            }

        });

        Thread t2 = new Thread(() -> {
            synchronized (o) {
                while (true) {
                    System.out.println("t2 go on");
                    SleepHelper.sleepSeconds(1);
                }
            }

        });

        t1.start();
        SleepHelper.sleepSeconds(1);
        t2.start();

        SleepHelper.sleepSeconds(5);

        t1.suspend();
        SleepHelper.sleepSeconds(5);

        t1.resume();
    }
}
  1. 使用volatile结束线程。volatile是一种较为优雅的结束线程的一种方式。尽可能不依赖于循环中,中间状态,例如在往一个容器中加入元素,加到第四个时停止,这种情况volatile很难做到精确控制,容易出现偏差;或者说如果,在代码中使用了wait();则可能导致阻塞,无法跳入下一次循环而导致线程无法结束。在特定情况下volatile可以起到不错的作用,且使用较为简便。
public class day04 {
    static class SleepHelper {
        public static void sleepSeconds(int seconds) {
            seconds *= 1000;
            try {
                Thread.sleep(seconds);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

    static final Object o = new Object();
    private static volatile boolean running = true;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (o) {
                while (running) {
                    System.out.println("t1 go on");
                    SleepHelper.sleepSeconds(1);
//                    try {
//                        o.wait();
//                    } catch (InterruptedException e) {
//                        e.printStackTrace();
//                    }
                }
            }

        });

//        Thread t2 = new Thread(() -> {
//            synchronized (o) {
//                while (true) {
//                    System.out.println("t2 go on");
//                    SleepHelper.sleepSeconds(1);
//                }
//            }
//
//        });

        t1.start();
        SleepHelper.sleepSeconds(1);
//        t2.start();

        SleepHelper.sleepSeconds(5);

        running = false;
    }
}
  1. interrupt结束线程,这是一种非常优雅的结束线程的方式。且就算代码处于sleep();或处于wait();时只要可以正确处理因为interrupt();产生的报错,线程依然能优雅的结束。
public class day04 {
    static class SleepHelper {
        public static void sleepSeconds(int seconds) {
            seconds *= 1000;
            try {
                Thread.sleep(seconds);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }


    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
                while (!Thread.currentThread().isInterrupted()) {
                    System.out.println("t1 go on");
//                    try {
//                        Thread.sleep(1000);
//                    } catch (InterruptedException e) {
//                        e.printStackTrace();
//                        Thread.currentThread().interrupt();
//                    }
                }
            System.out.println("end");
        });


        t1.start();
        SleepHelper.sleepSeconds(5);
        t1.interrupt();

    }
}

对于线程结束的方式,以上四种为比较普通的结束方式。如果要做到精确的结束,就需要业务线程与其他线程配合。一些比较精细或者高级的方法,之后会进行记录与讲解。

2.并发编程三大特性

  1. 可见性(visibility)
    从一个程序谈起:
    以下程序,注释volatile与打印语句时,线程不会得到停止。但放开任一注释,线程即可跳出循环继续执行直到结束。这是由于打印语句内部使用了synchronize关键字,而volatile可以保障可见性。
public class day04 {
    static class SleepHelper {
        public static void sleepSeconds(int seconds) {
            seconds *= 1000;
            try {
                Thread.sleep(seconds);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

    private static /*volatile*/ boolean running = true;
    private static  void m(){
        System.out.println("m start");
        while (running){
            //System.out.println("hello");
        }
        System.out.println("m end");
    }
    public static void main(String[] args) {
        new Thread(day04::m,"t1").start();
        SleepHelper.sleepSeconds(1);
        running = false;

    }
}

在这里插入图片描述
在主存中的running为true,当两个线程读取running时,会取出以后copy一份放在线程缓存中, 之后while时读的都是缓存中的copy值。所以线程可见性:一个线程对共享变量值得修改,能够及时的被其他线程看到。

线程可见性原理:
线程一对共享变量的改变想要被线程二看见,就必须执行下面两个步骤:
将工作内存1中的共享变量的改变更新到主内存中
将主内存中最新的共享变量的变化更新到工作内存2中。

关于synchronized:1.线程解锁前,必须把共享变量的最新值刷新到主内存。2.线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中读取最新的值。(加锁与解锁是同一把锁)

关于volatile保障可见性:volatile变量每次被线程访问时,都强迫从主内存中读取该变量的值,而当变量发生变化时会强迫线程将最新的值刷新到主内存中,这样不同的变量总能看到最新的值。

volatile关键字:
能够保证volatile变量的可见性。
只能保证单个volatile变量的原子性,对于volatile++这种复合操作不具有原子性。
volatile引用类型(包括数组)只能保证引用本身的可见性,不能保证内部字段的可见性。

volatile实现共享变量内存可见性有一个条件,就是对共享变量的操作必须具有原子性。比如一个整数变量num ; 这个操作具有原子性,但是 num++ 或者num–由3步组成,并不具有原子性,所以是不行的。例如:如有一个num,此时有线程1从主内存中获取num的值,并执行++,但还未修改写入主内存,又有线程2取得num的值,进行++操作,造成丢失修改,执行了2次++,num的值只增加1。所以volatile不具有原子性,不适用于计数场景,可以保证原子性操作时,可以尽量的选择使用volatile。在其他不能保证其操作的原子性时,考虑使用synchronized。

关于缓存:
在这里插入图片描述

这是一个多核CPU,左侧为CPU1,右侧为CPU2,L1、L2位于核内部,L3位于CPU内部。几个CPU共享主存。当寄存器需要一个数据时。会去L1读取,L1没有去L2,L2没有去L3,L3也没有就会去主存中读取。当放数据时也是一样,从L3放到L1再到寄存器。我们可见性中说的缓存就是这里的缓存,并不是说的ThreadLocal。

在这里插入图片描述

缓存行概念:计算机将数据从主存读入Cache时,是把要读取数据附近的一部分数据都读取进来,这样一次读取的一组数据就叫做CacheLine,每一级缓存中都能放很多的CacheLine,缓存行越大,局部空间效率越高,读取时间越慢!缓存行越小,局部空间效率越低,读取时间越快!
一个缓存行存储的字节是2的倍数。不同机器上,缓存行大小也不一样,通常来说为64字节。。
空间局部性原理:按块读取,程序局部性原理,可以提高效率,充分发挥总线CPU针脚等一次性读取更多数据的能力。

我们可以通过一个小程序来认识:缓存一致性。

public class day04 {
    public static long COUNT = 1000_0000_0000L;

    private static class T{
//        private long p1=1L,p2=1L,p3=1L,p4=1L,p5=1L,p6=1L,p7=1L;
        public long x = 0L;
//        private long p9=1L,p10=1L,p11=1L,p12=1L,p13=1L,p14=1L,p15=1L;
    }
    public static T[] arr = new T[2];
    static {
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(2);

        Thread t1 = new Thread(() -> {
            for (long i = 0; i < COUNT; i++) {
                arr[0].x = i;
            }
            latch.countDown();
        });

        Thread t2 = new Thread(() -> {
            for (long i = 0; i < COUNT; i++) {
                arr[1].x = i;
            }
            latch.countDown();
        });
        final long start = System.nanoTime();
        t1.start();
        t2.start();
        latch.await();
        System.out.println((System.nanoTime()-start)/100_000L);
    }
}

当我们放开注释的两行代码时,会发现效率提升了。这就是由于缓存一致性所产生的现象。
在这里插入图片描述
但如果将注释放开,数据被进行了填充,则两条数据不可能位于统一缓存行,则会变成下列这种情况。

在这里插入图片描述
在Disruptor中也使用到过这种特点。
在这里插入图片描述

在这里插入图片描述

在JDK8中提供了一个注解:@Contended,被这个注解修饰的变量不会和其他数据处于同一缓存行,相当于自动为其填充了空白数据。使用时需要添加参数:-XX:-RestrictContended (但只有JDK1.8有)

MESI 是Cache一致性协议中的一种,是Inter的Cpu设计,比较有名。
在这里插入图片描述MESI协议是一个基于失效的缓存一致性协议,是支持写回(write-back)缓存的最常用协议。与写穿(write through)缓存相比,回写缓冲能节约大量带宽。总是有“脏”(dirty)状态表示缓存中的数据与主存中不同。MESI协议要求在缓存不命中(miss)且数据块在另一个缓存时,允许缓存到缓存的数据复制。与MSI协议相比,MESI协议减少了主存的事务数量。这极大改善了性能。


总结

本次学习,学习了,如何结束一个线程,以及并发编程三大特性之一的可见性。由此学习了volatile保障线程可见性,缓存行的概念和机制,以及大概了解了缓存一致性协议。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值