美文网首页
JUC并发编程

JUC并发编程

作者: 勿念及时雨 | 来源:发表于2020-03-04 19:05 被阅读0次

    JUC是什么?

    JUC,即java.util.concurrent包的缩写,是java原生的并发包和一些常用的工具类。

    JUC

    线程基础知识

    线程和进程
    进程:计算机中运行中的程序,如QQ.exe等。
    线程:进程中执行的具体的任务,如打字、自动保存等。
    一个进程可以包含多个线程,一个进程至少有一个线程。Java程序至少有两个线程:GC线程Main线程
    并发和并行
    并发:多个线程操作同一个资源并且交替执行的过程。
    并行:多个线程同时执行,只有在多核CPU下才能完成。
    使用多线程或者并发编程的目的:提高效率,让CPU一直工作,达到最高的处理性能。
    线程的状态
    线程有6种状态,我们可以从源码中查看具体是哪6种状态。

      public enum State {
        // java能够创建线程吗? 不能!
        // 新建
        NEW,
    
        // 运行
        RUNNABLE,
    
        // 阻塞
        BLOCKED,
    
        // 等待
        WAITING,
    
        // 延时等待
        TIMED_WAITING,
    
        // 终止!
        TERMINATED;
    }
    

    很显然,线程的六种状态分别是:新建(NEW)、运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITTING)、延时等待(TMED_WAITTING)、终止(TERMINATED)。
    wait和sleep的区别

    • 类不同
      wait是属于Object类的方法,sleep是Thread类的方法。在JUC编程中,线程休眠的实现代码是:
    TimeUnit.SECONDS.sleep(3)
    
    • 是否会释放资源
      sleep会一直持有锁,不会释放锁,wait则会释放锁。
    • 使用范围不同
      wait和notify是一组,一般在线程通信的时候使用。sleep是单独的方法,在任何地方都可以使用。
    • 是否需要捕获异常
      sleep需要捕获中断异常,wait不需要。

    Lock锁

    传统方式一般采用synchronized关键字来加锁,如以下代码:

    package com.coding.demo01;
    
    // 传统的 Synchronized
    // Synchronized 方法 和 Synchronized 块
    
    /*
     * 我们的学习是基于企业级的开发进行的;
     * 1、架构:高内聚,低耦合
     * 2、套路:线程操作资源类,资源类是单独的
     */
    public class Demo01 {
        public static void main(String[] args) throws InterruptedException {
            // 1、新建资源类
            Ticket ticket = new Ticket();
    
            // 2、线程操纵资源类
            new Thread(new Runnable() {
                public void run() {
                    for (int i = 1; i <=40; i++) {
                        ticket.saleTicket();
                    }
                }
            },"A").start();
    
    
            new Thread(new Runnable() {
                public void run() {
                    for (int i = 1; i <=40; i++) {
                        ticket.saleTicket();
                    }
                }
            },"B").start();
    
            new Thread(new Runnable() {
                public void run() {
                    for (int i = 1; i <=40; i++) {
                        ticket.saleTicket();
                    }
                }
            },"C").start();
        }
    }
    
    // 单独的资源类,属性和方法!
    // 这样才能实现复用!
    class Ticket{
    
        private int number = 30;
    
        // 同步锁,厕所 =>close=>
        public synchronized void saleTicket(){
            if (number>0){
                System.out.println(Thread.currentThread().getName() + "卖出第"+(number--)+"票,还剩:"+number);
            }
        }
    }
    

    现在,我们也可以使用Lock来加锁。

    Lock lock=new ReentrantLock()
    

    ReentrantLock,即可重入锁(相当于回家的时候只要开了大门的锁,卧室,厕所不需要解锁就能进入),其默认是非公平锁(不公平,后面的线程可以插队)。如以下代码:

    package com.coding.demo01;
    
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    /*
     * JUC之后的操作
     * Lock锁 + lambda表达式!
     */
    public class Demo02 {
        public static void main(String[] args) {
            // 1、新建资源类
            Ticket2 ticket = new Ticket2();
            // 2、线程操作资源类 , 所有的函数式接口都可以用 lambda表达式简化!
            // lambda表达式 (参数)->{具体的代码}
            new Thread(()->{for (int i = 1; i <= 40 ; i++) ticket.saleTicket();},"A").start();
            new Thread(()->{for (int i = 1; i <= 40 ; i++) ticket.saleTicket();},"B").start();
            new Thread(()->{for (int i = 1; i <= 40 ; i++) ticket.saleTicket();},"C").start();
    
        }
    }
    
    
    // 依旧是一个资源类
    class Ticket2{
        // 使用Lock,它是一个对象
        // ReentrantLock 可重入锁:回家:大门 (卧室门,厕所门...)
        // ReentrantLock 默认是非公平锁!
        // 非公平锁: 不公平 (插队,后面的线程可以插队)
        // 公平锁: 公平(只能排队,后面的线程无法插队)
        private Lock lock = new ReentrantLock();
    
        private int number = 30;
    
        public void saleTicket(){
            lock.lock(); // 加锁
            try {
                // 业务代码
                if (number>0){
                    System.out.println(Thread.currentThread().getName() + "卖出第"+(number--)+"票,还剩:"+number);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock(); // 解锁
            }
        }
    }
    

    synchronized和Lock的区别
    1.synchronized是一个关键字,Lock是一个对象。
    2.synchronized无法尝试获取锁,Lock可以尝试获取锁并判断。
    3.synchronized会自动释放锁(a线程执行完毕,b如果出现异常也会释放锁),Lock锁必须手动进行释放,不释放就会变成死锁。
    4.使用synchronized时,如果线程a获得锁并阻塞,线程b会一直进行等待,使用Lock则可以尝试获取锁,失败了之后就放弃。

    tryLock方法
    5.synchronized一定是非公平的,但Lock锁可以是公平的,需要通过参数进行设置。
    6.代码量特别大时,一般使用Lock实现精准控制,synchronized适合代码量较小的同步问题。

    生产者消费者问题

    线程和线程之间本来是不能通信的,但有时我们需要线程之间进行协调操作。
    比如有两个线程:A、B ,还有一个值初始为0,实现两个线程交替执行,对该变量 + 1,-1;交替10次。
    先来看使用synchronized实现线程之间通信的版本,代码如下:

    package com.coding.demo01;
    
    // Synchronized 版
    /*
    目的: 有两个线程:A  B ,还有一个值初始为0,
           实现两个线程交替执行,对该变量 + 1,-1;交替10次
     */
    public class Demo03 {
        public static void main(String[] args) {
            Data data = new Data();
    
            // +1
            new Thread(()->{
                for (int i = 1; i <=10 ; i++) {
                    try {
                        data.increment();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"A").start();
    
            // -1
            new Thread(()->{
                for (int i = 1; i <=10 ; i++) {
                    try {
                        data.decrement();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"B").start();
        }
    }
    
    // 资源类
    // 线程之间的通信: 判断  执行  通知
    class Data{
    
        private int number = 0;
    
        // +1
        public synchronized void increment() throws InterruptedException {
            if (number!=0){ // 判断是否需要等待
                this.wait();
            }
            number++; // 执行
            System.out.println(Thread.currentThread().getName()+"\t"+number);
            // 通知
            this.notifyAll(); //唤醒所有线程
        }
    
        // -1
        public synchronized void decrement() throws InterruptedException {
            if (number==0){ // 判断是否需要等待
                this.wait();
            }
            number--; // 执行
            System.out.println(Thread.currentThread().getName()+"\t"+number);
            // 通知
            this.notifyAll(); //唤醒所有线程
        }
    }
    

    那么问题来了,这四条线程可以实现交替吗?答案是不能!因为会产生虚假唤醒问题,jdk文档中对该问题也有说明。


    虚假唤醒

    需要特别注意的if和while的区别,当两个线程同时执行if判断,if只会判断一次,而while会对每一个线程都进行判断。显然,上面的if应该改为while,代码如下:

    package com.coding.demo01;
    
    // Synchronized 版
    /*
    目的: 有两个线程:A  B ,还有一个值初始为0,
           实现两个线程交替执行,对该变量 + 1,-1;交替10次       
    传统的 wait 和 notify方法不能实现精准唤醒通知!
     */
    public class Demo03 {
        public static void main(String[] args) {
            Data data = new Data();
    
            // +1
            new Thread(()->{
                for (int i = 1; i <=10 ; i++) {
                    try {
                        data.increment();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"A").start();
            new Thread(()->{
                for (int i = 1; i <=10 ; i++) {
                    try {
                        data.increment();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"C").start();
    
            // -1
            new Thread(()->{
                for (int i = 1; i <=10 ; i++) {
                    try {
                        data.decrement();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"B").start();
            new Thread(()->{
                for (int i = 1; i <=10 ; i++) {
                    try {
                        data.decrement();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"D").start();
    
    
        }
    }
    
    // 资源类
    // 线程之间的通信: 判断  执行  通知
    class Data{
    
        private int number = 0;
    
        // +1
        public synchronized void increment() throws InterruptedException {
            while (number!=0){ // 判断是否需要等待
                this.wait();
            }
            number++; // 执行
            System.out.println(Thread.currentThread().getName()+"\t"+number);
            // 通知
            this.notifyAll(); //唤醒所有线程
        }
    
        // -1
        public synchronized void decrement() throws InterruptedException {
            while (number==0){ // 判断是否需要等待
                this.wait();
            }
            number--; // 执行
            System.out.println(Thread.currentThread().getName()+"\t"+number);
            // 通知
            this.notifyAll(); //唤醒所有线程
        }
    }
    

    问题又来了,从测试的结果可以看出,传统的 wait 和 notify方法不能实现精准唤醒通知
    这时我们就需要考虑使用JUC来实现了,先来看看JUC中的一个重要的接口Condition的文档说明。

    lSynchronized和Lock的线程通信示意图
    Condition.png
    我们使用Lock锁和Condition来实现精准唤醒线程,代码如下:
    package com.coding.demo01;
    
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    /*
    实现线程交替执行!
    主要的实现目标:精准的唤醒线程!
        三个线程:A B C
        三个方法:A p5  B p10   C p15 依次循环
     */
    public class Demo04 {
        public static void main(String[] args) {
            Data2 data = new Data2();
    
            new Thread(()->{
                for (int i = 1; i <= 10; i++) {
                    try {
                        data.print5();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"A").start();
    
            new Thread(()->{
                for (int i = 1; i <= 10; i++) {
                    try {
                        data.print10();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"B").start();
    
            new Thread(()->{
                for (int i = 1; i <= 10; i++) {
                    try {
                        data.print15();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"C").start();
        }
    }
    
    // 资源类
    class Data2{
        private int number = 1; // 1A 2B  3C
        private Lock lock = new ReentrantLock();
        // 实现精准访问
        private Condition condition1 = lock.newCondition();
        private Condition condition2 = lock.newCondition();
        private Condition condition3 = lock.newCondition();
    
        public void print5() throws InterruptedException {
    
            lock.lock();
    
            try {
                // 判断
                while (number!=1){
                    condition1.await();
                }
                // 执行
                for (int i = 1; i <= 5; i++) {
                    System.out.println(Thread.currentThread().getName() + "\t" + i);
                }
                // 通知第二个线程干活!
                number = 2;
                condition2.signal(); // 唤醒
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock(); // 一定要解锁
            }
        }
    
        public void print10() throws InterruptedException {
            lock.lock();
            try {
                // 判断
                while (number!=2){
                    condition2.await();
                }
                // 执行
                for (int i = 1; i <= 10; i++) {
                    System.out.println(Thread.currentThread().getName() + "\t" + i);
                }
                // 通知3干活
                number = 3;
                condition3.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
    
        }
    
        public void print15() throws InterruptedException {
            lock.lock();
            try {
                // 判断
                while (number!=3){
                    condition3.await();
                }
                // 执行
                for (int i = 1; i <= 15; i++) {
                    System.out.println(Thread.currentThread().getName() + "\t" + i);
                }
                // 通知 1 干活
                number = 1;
                condition1.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
    

    测试结果说明,使用Lock锁很容易就解决上述问题,由此我们可以得到一个结论:一个新技术的出现,一定是为了替换一些旧的技术的!

    锁对象的判断方法

    1.被synchronized修饰的方法,锁的对象是方法的调用者,当两个方法调用的对象是同一个时,先调用的先执行。
    2.没有被synchronized修饰的方法,不是同步方法,不受锁的影响。
    3.只要方法被static修饰,不管是否同时被synchronized修饰,锁的对象就是Class模板对象,这个对象是全局唯一的。
    4.synchronized锁的是调用的对象,static锁的是这个类的Class模板,这是两个不同的锁。

    不安全的集合类

    只要在并发环境下,List、Map、Set这些类都是不安全的。
    List不安全的代码示例:

    package com.coding.unsafe;
    
    import java.util.*;
    import java.util.concurrent.CopyOnWriteArrayList;
    
    /**
     * 故障现象:ConcurrentModificationException 并发修改异常
     * 导致原因:add方法没有锁!
     * 解决方案:
     * 1、List<String> list = new Vector<>(); //jdk1.0 就存在的!效率低
     * 2、List<String> list = Collections.synchronizedList(new ArrayList<>());
     * 3、List<String> list = new CopyOnWriteArrayList<>();
     * 
     * 什么是 CopyOnWrite; 写入是复制 (思想 COW)
     * 多个调用者同时要相同的资源;这个有一个指针的概念。
     * 读写分离的思想:
     */
    public class UnSafeList {
    
        public static void main(String[] args) {
    //        List<String> list = Arrays.asList("a", "b", "c");
    //        list.forEach(System.out::println);
    //        List<String> list = new ArrayList<>();
    
            List<String> list = new CopyOnWriteArrayList<>();
    
            for (int i = 1; i <= 30; i++) {
                new Thread(()->{
                    list.add(UUID.randomUUID().toString().substring(0,3));
                    System.out.println(list);
                },String.valueOf(i)).start();
            }
        }
    }
    

    如上述代码所示,解决List不安全问题的方法有两种:

    List<String> list = Collections.synchronizedList(new ArrayList<>());
    List<String> list = new CopyOnWriteArrayList<>();
    

    CopyOnWrite(COW),写入是复制,多个调用者同时要相同的资源,这是一种读写分离的思想,其源码如下:

    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
    

    Set不安全的代码示例:

    package com.coding.unsafe;
    
    import java.util.Collections;
    import java.util.HashSet;
    import java.util.Set;
    import java.util.UUID;
    import java.util.concurrent.CopyOnWriteArraySet;
    
    // ConcurrentModificationException
    public class UnSafeSet {
        public static void main(String[] args) {
            // HashSet 底层是什么 就是 HashMap
            // add,就是 HashMap 的 key;
            Set<String> set = new HashSet<>();
    
    //        Set<String> set = Collections.synchronizedSet(new HashSet<>());
    //        Set<String> set = new CopyOnWriteArraySet();
    
            for (int i = 1; i <=30 ; i++) {
                new Thread(()->{
                    set.add(UUID.randomUUID().toString().substring(0,3));
                    System.out.println(set);
                },String.valueOf(i)).start();
            }
        }
    }
    

    如上述代码所示,解决Set不安全问题的方法有两种:

    Set<String> set = Collections.synchronizedSet(new HashSet<>());
    Set<String> set = new CopyOnWriteArraySet();
    

    Map不安全的代码示例:

    package com.coding.unsafe;
    
    import java.util.HashMap;
    import java.util.Map;
    import java.util.UUID;
    import java.util.concurrent.ConcurrentHashMap;
    
    //ConcurrentModificationException
    public class UnsafeMap {
        public static void main(String[] args) {
            // new HashMap<>() 工作中是这样用的吗? 不是
            // 加载因子0.75f;,容量 16; 这两个值工作中不一定这样用!
            // 优化性能!
            // HashMap 底层数据结构,链表 + 红黑树
            // = = = = = = =
    //        Map<String, String> map = new HashMap<>();
            Map<String, String> map = new ConcurrentHashMap<>();
    
            // 人生如程序,不是选择就是循环,时常的自我总结十分重要!
            for (int i = 1; i <=30 ; i++) {
                new Thread(()->{
                    map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,3));
                    System.out.println(map);
                },String.valueOf(i)).start();
            }
    
        }
    }
    

    解决Map不安全问题的方法是使用ConcurrentHashMap来替代HashMap:

    Map<String, String> map = new ConcurrentHashMap<>();
    

    综上所述,要解决一般集合的线程不安全的问题,核心思路就是使用JUC并发包下面的并发安全的集合去替代这些不安全的集合。

    相关文章

      网友评论

          本文标题:JUC并发编程

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