美文网首页
多线程笔记

多线程笔记

作者: small瓜瓜 | 来源:发表于2019-06-24 15:28 被阅读0次

    1. volatile

    1.1 volatile介绍

    volatile保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。它在某些情况下比synchronized的开销更小。

    举个例子我们来分析下面的代码:

    public class Main {
        public static void main(String[] args) {
            VolatileTest volatileTest = new VolatileTest();
            new Thread(volatileTest).start();
    
            while (true) {
                if (volatileTest.isFlag()){
                    System.out.println("over");
                    break;
                }
            }
        }
    }
    
    class VolatileTest implements Runnable {
        private boolean flag;
    
        @Override
        public void run() {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = true;
            System.out.println("flag=" + flag);
        }
    
        public boolean isFlag() {
            return flag;
        }
    }
    

    上面的代码最后输出结果是:

    flag=true
    

    这个结果是令人诧异的,程序会一直执行while循环不结束,flag已经为true了,为什么while循环还是不结束呢?说明这里的flag同时有了两个值

    • 在主线程中:flag=false
    • 在副线程中:flag=true

    变量实际是一段内存空间,并不存在同时有两种信息的状态。其实线程在操作主存中的变量数据时,首先会将数据复制到线程私有内存中,当操作完成后才会将数据写回主存,当多个线程操作一个共享变量时,由于线程的修改,导致数据不一致性。就发生了上述结果。

    为了解决共享变量的不一致性,使得多线程对共享变量的修改的可见。上述结果我们可以使用synchronized关键字来解决。如下:

     while (true) {
                synchronized (volatileTest){
                    if (volatileTest.isFlag()){
                        System.out.println("over");
                        break;
                    }
                }
            }
    

    当是这样解决又有一个很大的问题,synchronized是悲观锁,使得多线程堵塞等待,极大的降低多线程的效率。那有没有一个更好的解决办法呢?这里就可以用到volatile关键字了,修改如下:

        private volatile boolean flag;
    

    只需要将变量flag声明时,用volatile修饰就可以保证共享变量flag的可见性。再次运行就不会发生堵塞数据不一致的问题了。

    注意 : 如果将代码改成下面的,运行结果也是没有问题的,导致上面的结果还有一个重要的原因,while循环中执行的太快,导致主线程来不及去主存中刷新数据。

            while (true) {
                // 只要是需要消耗一定的时间,让主线程能从主存读取数据即可
                System.out.println("no over"); 
                if (volatileTest.isFlag()) {
                    System.out.println("over");
                    break;
                }
            }
    

    1.2 volatile的三大特性:

    1. 可见性
    2. 不保证原子性
    3. 禁止指令重排

    具体是如何做到的可以参考以下博客
    死磕Java——volatile的理解

    2. Atomic

    jdk1.5java.util.concurrent.atomic包下提供了常用的原子操作类,什么是原子操作呢?顾名思义,就是不可分割的操作。

    1. i++的原子性问题:i++的操作实际上分为三个步骤"读-改-写"
      int i=10;
      i=i++; //10
    // 上面的代码等同于下面的
      int i = 10;
      int temp=i;
      i=i+1;
      i=temp;
    // 所以最后i的值为10
    
    1. 原子变量:jdk1.5后java.util.concurrent.atomic包下提供了常用的原子变量:


      java.util.concurrent.atomic包
    • volatile 保证内存可见性
    • CAS(Compare-And-Swap)算法保证数据的原子性CAS算法是硬件对于并发操作共享数据的支持
      CAS包含了三个操作数:
      • 内存值V
      • 预估值A
      • 更新值B
        当且仅当V==A时,V = B,否则将不做任何操作

    可参考博客:
    Java中atomic包中的原子操作类总结

    CAS的实现需要硬件指令集的支撑,在JDK1.5后虚拟机才可以使用处理器提供的CMPXCHG指令实现。

    3. ConcurrentHashMap

    3.1 ConcurrentHashMap 采用"锁分段"机制

    Java5.0java.util.concurrent包中提供了多种并发容器类来改进同步容器的性能。ConcurrentHashMap同步容器类是Java5增加的一个线程安全的哈希表。对与多线程的操作,介于HashMapHashtable之间。内部采用“锁分段”机制替代Hashtable的独占锁。进而提高性能。此包还提供了设计用于多线程上下文中的Collection实现:
    ConcurrentHashMapConcurrentSkipListMapConcurrentSkipListSetCopyOnWriteArrayListCopyOnWriteArrayset。当期望许多线程访问一个给定collection时,ConcurrentHashMap通常优于同步的HashMapConcurrentSkipListMap通常优于同步的TreeMap。当期望的读数和遍历远远大于列表的更新数时,CopyOnWriteArrayList优于同步的ArrayList

    4. CountDownLatch

    4.1 CountDownLatch闭锁

    CountDownLatch一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
    闭锁可以延迟线程的进度直到其到达终止状态,闭锁可以用来确保某些活动直到其他活动都完成才继续执行:

    • 确保某个计算在其需要的所有资源都被初始化之后才继续执行;
    • 确保某个服务在其依赖的所有其他服务都已经启动之后才启动;
    • 等待直到某个操作所有参与者都准备就绪再继续执行。

    CountDownLatch使用实例代码:

    public class Main {
        public static void main(String[] args) throws InterruptedException {
            CountDownLatch latch = new CountDownLatch(10);
            LatchDemo ld = new LatchDemo(latch);
            long start = System.currentTimeMillis();
    
            for (int i = 0; i < 10; i++) {
                new Thread(ld).start();
            }
    
            latch.await();
            long end = System.currentTimeMillis();
    
            System.out.println("总时长为:" + (end - start));
        }
    }
    
    class LatchDemo implements Runnable {
    
        private CountDownLatch latch;
    
        public LatchDemo(CountDownLatch latch) {
            this.latch = latch;
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                double random = Math.random() * 100;
                if (random > 99) {
                    System.out.println(random);
                }
            }
            latch.countDown();
        }
    }
    

    Callable

    Callable介绍

    Runnable是执行工作的独立任务,但是它不返回任何值。在Java SE5中引入的Callable是一种具有类型参数的泛型,泛型类型是方法call()的返回的值类型。

    四种执行线程方式的介绍

    种数 种类 说明
    1 实现Runnable接口 通过Thread实例启动它
    2 继承Thread 重写Threadrun方法
    3 实现Callable接口 通过FutureTask包装,然后再通过Thread启动
    4 实现Callable接口 ExecutorServices.submit()

    可参考博客:
    彻底理解Java的Future模式
    Future模式添加Callback及Promise 模式

    Lock

    用于解决多线程安全问题的方式:

    • synchronized:隐式锁、重量级

      1. 同步代码块
      2. 同步方法
    • jdk 1.5后,Lock:轻量级

      1. 同步锁Lock
        注意:是一个显示锁,需要通过lock()方法上锁,必须通过unlock()方法进行释放锁

    多线程安全问题演示

    买票案例代码演示:

    public class Main {
        public static void main(String[] args) {
            Ticket ticket = new Ticket();
            new Thread(ticket,"一号售票窗口").start();
            new Thread(ticket,"二号售票窗口").start();
            new Thread(ticket,"三号售票窗口").start();
        }
    }
    
    class Ticket implements Runnable {
    
        private int num = 100;
    
        @Override
        public void run() {
            while (true) {
                if (num > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("卖出一张票,剩余还有:" + --num);
                }else if (num == 0){
                    break;
                }
            }
        }
    }
    

    上面的代码存在线程安全问题 ,多线程下对同一共享变量进行修改。
    用第一种保证安全性:

    while (true) {
                synchronized (this){
                    if (num > 0) {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("卖出一张票,剩余还有:" + --num);
                    }else if (num == 0){
                        break;
                    }
                }
            }
    

    但是这样效率严重降低。
    用第三种方式保证安全性:

    while (true) {
                lock.lock();
                try{
                    if (num > 0) {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("卖出一张票,剩余还有:" + --num);
                    } else if (num == 0) {
                        break;
                    }
                }finally {
                    lock.unlock();
                }
            }
    

    Condition

    编写一个程序,开启3个线程,这三个线程的ID分别为A、B、C,每个线程将自己的ID在屏幕上打印10遍,要求输出的结果必须按顺序显示。
    如:ABCABCABC….依次递归

    public class Main {
        public static void main(String[] args) {
            Test test = new Test();
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    test.LoopA();
                }
            },"A").start();
    
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    test.LoopB();
                }
            },"B").start();
    
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    test.LoopC();
                }
            },"C").start();
        }
    }
    
    class Test {
    
        private int id = 1;
        Lock lock = new ReentrantLock();
        Condition condition1 = lock.newCondition();
        Condition condition2 = lock.newCondition();
        Condition condition3 = lock.newCondition();
    
        public void LoopA() {
            lock.lock();
            try {
                while (id != 1) {
                    condition1.await();
                }
                System.out.println("A");
                id = 2;
                condition2.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
        public void LoopB() {
            lock.lock();
            try {
                while (id != 2) {
                    condition2.await();
                }
                System.out.println("B");
                id = 3;
                condition3.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
        public void LoopC() {
            lock.lock();
            try {
                while (id != 3) {
                    condition3.await();
                }
                System.out.println("C");
                id = 1;
                condition1.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
    

    ReadWriteLock 读写锁

    写写/读写 需要“互斥”
    读读 不需要互斥

    public class Main {
        public static void main(String[] args) {
            Test test = new Test();
            for (int i = 0; i < 10; i++) {
                int j = i;
                new Thread(() -> {
                    test.write("" + j, new Random().nextInt(10));
                }).start();
            }
            for (int i = 0; i < 100; i++) {
                int j = i;
                new Thread(() -> {
                    test.read("" + j);
                }).start();
            }
        }
    }
    
    class Test {
    
        private int id = 1;
        private ReadWriteLock rwl = new ReentrantReadWriteLock();
    
        public void read(String name) {
            rwl.readLock().lock();
            try {
                System.out.println(String.format("名字:%s,读出的数据:%d", name, id));
            } finally {
                rwl.readLock().unlock();
            }
        }
    
        public void write(String name, int id) {
            rwl.writeLock().lock();
            try {
                this.id = id;
                System.out.println(String.format("我是写锁,名字:%s,改写数据为:%d", name, id));
            } finally {
                rwl.writeLock().unlock();
            }
        }
    }
    

    线程八锁

    1. 两个普通同步方法,两个线程,标准打印,打印?//one two
    2. 新增 Thread.sleep()给getone(),打印?//one two
    3. 新增普通方法 getThree(),打印?//three one two
    4. 两个普通同步方法,两个Number对象,打印?//two one
    5. 修改 getone()为静态同步方法,打印?//two one
    6. 修改两个方法均为静态同步方法,一个Number对象?//one two
    7. 一个静态同步方法,一个非静态同步方法,两个Number对象?//two one
    8. 两个静态同步方法,两个Number对象?//one two

    线程八锁的关键:

    • 非静态方法的锁默认为this,静态方法的锁为对应的Class实例
    • 某一个时刻内,只能有一个线程持有锁,无论几个方法。

    线程池

    线程池介绍

    线程池:提供了一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁额外开销,提高了响应的速度。

    线程池的体系结构:

    java.util.concurrent.Executor:负责线程的使用与调度的根接口

      |--**ExecutorService子接口:线程池的主要接口
            |--ThreadPoolExecutor 线程池的实现类
            |--ScheduledExecutorService 子接口:负责线程的调度
                  |--ScheduledThreadPoolExecutor:继承 ThreadPoolExecutor,
                                                    实现 ScheduledExecutorService
    

    工具类:Executors

    ExecutorService newFixedThreadPool():创建固定大小的线程池
    ExecutorService newCachedThreadPool():缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量。
    ExecutorService newSingleThreadExecutor():创建单个线程池。线程池中只有一个线程
    ScheduledExecutorService newScheduledThreadPool():创建固定大小的线程,可以狂迟或定时的执行任务。
    

    相关文章

      网友评论

          本文标题:多线程笔记

          本文链接:https://www.haomeiwen.com/subject/thpbqctx.html