• JUC必要掌握(Synchronized,Lock,可重入锁ReentrantLock,可重入锁,读写锁,自旋锁,线程间通信,集合的线程安全)


    本文已参与「新人创作礼」活动,一起开启掘金创作之路

    1.锁(Synchronized和lock)

    1.1 Synchronized

    (1)Synchronized是Java内置的关键字,是Java内置的锁机制。 (2)Synchronized的作用域: 方法: 实例方法:被锁定的是类的实例对象。 静态方法:类对象,也就是Class对象。 代码块: 实例对象:锁的对象是类的实例对象。 class对象:锁的对象是Class对象。 任意实例对象object:锁的对象是实例对象。 (3)Synchronized是可重入锁,非公平锁,不可以中断,锁的释放由JVM决定。

    (4)Synchronized获取锁的线程释放锁只会有两种情况:

             1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
    
             2)线程执行发生异常,此时 JVM 会让线程自动释放锁。
    复制代码

    1.2 Lock

    (1)Lock是JUC包的接口。 (2)Lock的内置方法: lock():加锁(锁不可中断)。 unlock():解锁。 tryLock():尝试获取锁。 lockInterruptibly():加锁(锁可以通过Interruptibly进行中断)。 newCondition():新建一个监视器。

    Synchronized和Lock锁的区别
    1、Synchronized 内置的Java关键字, Lock 是一个Java接口。
    2、Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁。
    3、Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁。
    4、Synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去。
    5、Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以 判断锁,非公平(可以 自己设置)。
    6、Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码!
    复制代码

    2.线程安全问题:

    2.1 电影院的售票实例:

    package com.example.onlyqi.juc;
     
    /**
     * @author only-qi
     * @date 2022/5/30 10:16
     * @description
     */
    public class Ticket implements Runnable {
        private int ticket = 100;
     
        /*
         * 执行卖票操作
         */
        @Override
        public void run() {
             //每个窗口卖票的操作
             //窗口 永远开启
            while (true) {
       
                    if (ticket > 0) {//有票 可以卖
     
                        try {
                            //出票操作
                            //使用sleep模拟一下出票时间
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //获取当前线程对象的名字
                        String name = Thread.currentThread().getName();
                        System.out.println(name + "正在卖票:" + ticket--);
                    }
                }
         
        }
    }
    
    package com.example.onlyqi.juc;
     
    /**
     * @author onlyqi
     * @date 2022/5/30 10:18
     * @description
     */
    public class Demo {
        public static void main(String[] args) {
            Ticket ticket = new Ticket();
    //创建三个窗口对象
            Thread t1 = new Thread(ticket, "窗口1");
            Thread t2 = new Thread(ticket, "窗口2");
            Thread t3 = new Thread(ticket, "窗口3");
    //同时卖票
            t1.start();
            t2.start();
            t3.start();
        }
    }
    复制代码

    出现的问题:

    3.线程同步解决线程安全问题

    3.1 同步代码块:

    synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

    格式:

    synchronized(同步锁){
    需要同步操作的代码
    }
    复制代码

    同步锁:

    对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.

    1. 锁对象 可以是任意类型。

    2. 多个线程对象 要使用同一把锁。

    注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着

    public class Ticket implements Runnable {
        private int ticket = 100;
        Integer lock = new Integer(1);
        /*
         * 执行卖票操作
         */
        @Override
        public void run() {
            //每个窗口卖票的操作
            //窗口 永远开启
            while (true) {
                synchronized (lock) {
                    if (ticket > 0) {//有票 可以卖
                        try {
                            //出票操作 //使用sleep模拟一下出票时间
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //获取当前线程对象的名字
                        String name = Thread.currentThread().getName();
                        System.out.println(name + "正在卖票:" + ticket--);
                    }
                }
            }
        }
    }
    复制代码

    3.2 同步方法

    同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外 等着。

    格式:

    public synchronized void method(){
    可能会产生线程安全问题的代码
    }
    复制代码

    同步锁是谁?

    对于非static方法,同步锁就是this。 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。

    package com.example.onlyqi.juc;
     
    /**
     * @author onlyqi
     * @date 2022/5/30 10:16
     * @description
     */
    public class Ticket implements Runnable {
        private int ticket = 100;
        /*
         * 执行卖票操作
         */
        @Override
        public void run() {
            while(true) {
                sellTicket();
            }
     
        }
        public synchronized void sellTicket(){
            //每个窗口卖票的操作
            //窗口 永远开启
            if (ticket > 0) {//有票 可以卖
                try {
                    //出票操作 //使用sleep模拟一下出票时间
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //获取当前线程对象的名字
                String name = Thread.currentThread().getName();
                System.out.println(name + "正在卖票:" + ticket--);
            }
     
        }
    }
    复制代码

    3.3 Lock锁

    package com.example.onlyqi.juc;
     
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
     
    /**
     * @author onlyqi
     * @date 2022/5/30 10:16
     * @description
     */
    public class Ticket implements Runnable {
        private int ticket = 100;
        Lock lock = new ReentrantLock();
     
        /*
         * 执行卖票操作
         */
        @Override
        public void run() {
    //每个窗口卖票的操作
    //窗口 永远开启
            while (true) {
                lock.lock();
                if (ticket > 0) {//有票 可以卖
                 //出票操作
                   //使用sleep模拟一下出票时间
                    try {
                        Thread.sleep(50);
                        //获取当前线程对象的名字
                        String name = Thread.currentThread().getName();
                        System.out.println(name + "正在卖:" + ticket--);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        lock.unlock();
                    }
                 
                }
            }
        }
    }
     
    复制代码

    4. ReentrantLock(可重入锁)

    4.1 ReentrantLock默认是非公平锁

    小知识点:

    1、公平锁
     
    定义:多个线程按照先到先得的策略获取锁。
     
    优点:所有线程都有机会获得锁,不会饿死
     
    缺点:由于所有线程都会经历阻塞态,因此唤醒阻塞线程的开销会很大。
     
    2、非公平锁
     
    定义:所有的线程拼运气,谁运气好,谁就获取到锁
     
    优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高
     
    缺点:可能会有线程长时间甚至永远获取不到锁,导致饿死。
    复制代码

    ReentrantLock类不仅实现了Lock接口里的方法,还新增了一些其他的方法,ReenTrantLock提供的方法如下:

    // 创建一个 ReentrantLock ,默认是“非公平锁”
    ReentrantLock()
     
    // 创建策略是fair的 ReentrantLock。fair为true表示是公平锁,fair为false表示是非公平锁
    ReentrantLock(boolean fair)
        
    // 查询当前线程保持此锁的次数
    int getHoldCount()
     
    // 返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null
    protected Thread getOwner()
     
    // 返回一个collection,它包含可能正等待获取此锁的线程。
    protected Collection<Thread> getQueuedThreads()
     
    // 返回正等待获取此锁的线程估计数
    int getQueueLength()
     
    // 返回一个 collection,它包含可能正在等待与此锁相关给定条件的那些线程。
    protected Collection<Thread> getWaitingThreads(Condition condition)
     
    // 返回等待与此锁相关的给定条件的线程估计数
    int getWaitQueueLength(Condition condition)
        
    // 查询给定线程是否正在等待获取此锁
    boolean hasQueuedThread(Thread thread)
        
    // 查询是否有些线程正在等待获取此锁。
    boolean hasQueuedThreads()
        
    // 查询是否有些线程正在等待与此锁有关的给定条件。
    boolean hasWaiters(Condition condition)
        
    // 如果是“公平锁”返回true,否则返回false
    boolean isFair()
        
    // 查询当前线程是否保持此锁。
    boolean isHeldByCurrentThread()
        
    // 查询此锁是否由任意线程保持。
    boolean isLocked()
        
    // 获取锁。
    void lock()
        
    // 如果当前线程未被中断,则获取锁。
    void lockInterruptibly()
        
    // 返回用来与此 Lock 实例一起使用的 Condition 实例。
    Condition newCondition()
        
    // 尝试获取锁,仅当锁没有被其他线程获取时才能获取到锁
    boolean tryLock()
        
    // 如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。
    boolean tryLock(long timeout, TimeUnit unit)
        
    // 试图释放此锁。
    void unlock()
    复制代码

    4.2 ReenTrantLock类常用的方法的使用案例:

    (1)void lock()

    package com.example.onlyqi.juc;
     
     
     
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    /**
     * @author onlyqi
     * @date 2022/5/30 13:51
     * @description
     */
    public class LocklMain {
        // 公共写的数据
        private static List<Integer> list = new ArrayList<>();
        private static Lock lock= new ReentrantLock();
        public static void main(String[] args) {
            final LocklMain locklMain = new LocklMain();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    insert();
                }
            },"tread01").start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    insert();
                }
            },"tread02").start();
        }
        private static void insert()
        {
            lock.lock();
            try{
                System.out.println(Thread.currentThread().getName() + " 开始添加元素");
                for (int i =0; i < 5; ++i)
                {
                    list.add(i);
                    System.out.println(Thread.currentThread().getName() + " 添加了元素 " + String.valueOf(i));
                }
                System.out.println(Thread.currentThread().getName() + " 线程添加元素完毕");
            }
            finally {
                System.out.println(Thread.currentThread().getName() + " 释放了锁");
                lock.unlock();
            }
        }
    }
    复制代码

    例子中在main方法里初始化一个final实例和一个公共写的list,起两个线程,每个线程里都调用这个final实例的insert方法向list里add元素。由于实例方法insert()是需要同步的方法,之前可用synchronized关键字直接声明insert方法,这里在insert方法里通过调用lock.lock()和lock.unlock()对insert()方法里需要同步的部分进行加锁,从结果看达到了同步的目的:Thread01先调用lock.lock()上锁,向list add元素,add完成后调用lock.unlock()方法释放锁,接着Thread02再调用lock.lock()上锁,向list add元素,添加完后再释放锁。

    注意private Lock lock=newReentrantLock();这个语句,不应该在insert方法里声明,而是全局声明(可以使用单例模式创建一个全局唯一的锁对象),不然两个线程调用insert()方法时,都会new一个lock出来,两个lock各自锁各自调用的insert()方法,到不到同步的目的。

    (2) boolean tryLock()

    package com.example.onlyqi.juc;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    /**
     * @author onlyqi
     * @date 2022/5/30 14:03
     * @description
     */
     
    public class TryLockMain {
        // 公共写的数据
        private static List<Integer> list = new ArrayList<>();
        private static Lock lock= new ReentrantLock();
        public static void main(String[] args) {
            final TryLockMain tryLocklMain = new TryLockMain();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    insert();
                }
            },"tread01").start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    insert();
                }
            },"tread02").start();
        }
        private static void insert()
        {
            if (lock.tryLock())
            {
                System.out.println(Thread.currentThread().getName() + " 获得锁成功");
                try{
                    System.out.println(Thread.currentThread().getName() + " 开始添加元素");
                    for (int i =0; i < 5; ++i)
                    {
                        list.add(i);
                        System.out.println(Thread.currentThread().getName() + " 添加了元素 " + String.valueOf(i));
                    }
                    System.out.println(Thread.currentThread().getName() + " 线程添加元素完毕");
                }
                finally {
                    System.out.println(Thread.currentThread().getName() + " 释放了锁");
                    lock.unlock();
                }
            }
            else {
                System.out.println(Thread.currentThread().getName() + " 获取锁失败");
            }
        }
    }
    复制代码

    运行结果:

    跟上面例子很相似,不同的是在需要同步的insert()方法里用tryLock()而不是lock(),同样起两个工作线程,调用final实例对象tryLocklMain的insert()方法向list里add元素。Thread01先获得lock锁,进行for循环打印,此时Thread02也调用tryLock()方法尝试获取lock锁,由于Thread01此时持有该锁,Thread02获取锁失败,无需等待直接返回false,走else语句打印“获取锁失败”。

    4.3 可重入锁

    ReentrantLock和sychronized是可重入锁

    可重入锁:指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。

    package com.example.onlyqi.juc;
     
    /**
     * @author onlyqi
     * @date 2022/5/30 14:28
     * @description
     */
    public class ReentrantTest implements Runnable {
     
        public synchronized void get() {
            System.out.println(Thread.currentThread().getName());
            set();
        }
     
        public synchronized void set() {
            System.out.println(Thread.currentThread().getName());
        }
     
        public void run() {
            get();
        }
     
        public static void main(String[] args) {
            ReentrantTest rt = new ReentrantTest();
            for(int i=0;i<100;i++){
                new Thread(rt).start();
            }
        }
    }
    复制代码

    整个过程没有发生死锁的情况,输出结果如下

    不可重入锁:不可重入锁,与可重入锁相反,不可递归调用,递归调用就发生死锁。

    4.4 读写锁

    独占锁:指该锁一次只能被一个线程所持有。对reentrantLock和synchronized都是独占锁
     
    共享锁:指该锁可以被多个线程所持有
    复制代码

    读写锁

    ReentrantReadWriteLock 实现了 ReadWriteLock 接口。
     
    对ReentrantReadWriteLock其读锁是共享,其写锁是独占
     
    写的时候只能被一个线程持有,读的时候能被多个线程所持有
     
    读写场景时:读的时候不应该用独占锁,影响并发性,读的时候不会造成数据不一致问题,因此可以多个人共享读
     
    ● 读-读:能共存
    ● 读-写:不能共存
    ● 写-写:不能共存
    
    public class ReadWirteSync {
     
        /**
         * 资源类
         */
        static class MyCache {
            
            private volatile Map<String, Object> map = new HashMap<>();
            // private Lock lock = null;
     
            /**
             * 定义写操作
             * 满足:原子 + 独占
             *
             * @param key
             * @param value
             */
            public void put(String key, Object value) {
                System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
                try {
                    // 模拟网络拥堵,延迟0.3秒
                    TimeUnit.MILLISECONDS.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                map.put(key, value);
                System.out.println(Thread.currentThread().getName() + "\t 写入完成");
            }
     
            public void get(String key) {
                System.out.println(Thread.currentThread().getName() + "\t 正在读取:");
                try {
                    // 模拟网络拥堵,延迟0.3秒
                    TimeUnit.MILLISECONDS.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Object value = map.get(key);
                System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);
            }
     
     
        }
     
        public static void main(String[] args) {
     
     
            MyCache myCache = new MyCache();
            // 线程操作资源类,5个线程写
            for (int i = 0; i < 5; i++) {
                // lambda表达式内部必须是final
                final int tempInt = i;
                new Thread(() -> {
                    myCache.put(tempInt + "", tempInt + "");
                }, String.valueOf(i)).start();
            }
            // 线程操作资源类, 5个线程读
            for (int i = 0; i < 5; i++) {
                // lambda表达式内部必须是final
                final int tempInt = i;
                new Thread(() -> {
                    myCache.get(tempInt + "");
                }, String.valueOf(i)).start();
            }
        }
     
    }
    复制代码

    运行的结果:

    在写入的时候,写入完成时被别的线程打断了。还没写完,就读取了

    解决方法:

    上面多线程代码是没有加锁的,在线程进行写入操作时,被其他线程频繁打断,从而不具备原子性

    使用synchronized锁,又太重量级,以为读的时候可以并发读。

    所以,使用读写锁

    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
     
     
     
     
    // 创建一个写锁
    rwLock.writeLock().lock();
     
    // 写锁 释放
    rwLock.writeLock().unlock();
     
     
     
    //读操作的时候,转换为读锁
     
    // 创建一个读锁
    rwLock.readLock().lock();
     
    // 读锁 释放
    rwLock.readLock().unlock();
    复制代码

    上代码:

    package com.example.onlyqi.juc;
     
    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
     
    /**
     * @author onlyqi
     * @date 2022/5/30 14:57
     * @description
     */
    public class ReadWirteSync {
     
     
     
        /**
         * 资源类
         */
        static class MyCache {
     
     
            private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
            private volatile Map<String, Object> map = new HashMap<>();
            // private Lock lock = null;
     
            /**
             * 定义写操作
             * 满足:原子 + 独占
             *
             * @param key
             * @param value
             */
            public void put(String key, Object value) {
                rwLock.writeLock().lock();
                System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
                try {
                    // 模拟网络拥堵,延迟0.3秒
                    TimeUnit.MILLISECONDS.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                map.put(key, value);
                System.out.println(Thread.currentThread().getName() + "\t 写入完成");
                rwLock.writeLock().unlock();
            }
     
            public void get(String key) {
                rwLock.readLock().lock();
                System.out.println(Thread.currentThread().getName() + "\t 正在读取:");
                try {
                    // 模拟网络拥堵,延迟0.3秒
                    TimeUnit.MILLISECONDS.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Object value = map.get(key);
                System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);
                rwLock.readLock().unlock();
            }
     
     
        }
     
        public static void main(String[] args) {
     
     
            MyCache myCache = new MyCache();
            // 线程操作资源类,5个线程写
            for (int i = 1; i < 5; i++) {
                // lambda表达式内部必须是final
                final int tempInt = i;
                new Thread(() -> {
                    myCache.put(tempInt + "", tempInt + "");
                }, "写线程"+i+":").start();
            }
            // 线程操作资源类, 5个线程读
            for (int i = 1; i < 5; i++) {
                // lambda表达式内部必须是final
                final int tempInt = i;
                new Thread(() -> {
                    myCache.get(tempInt + "");
                }, "读线程"+i+":").start();
            }
        }
     
    }
     
     
     
     
     
     
     
     
     
     
     
     
    复制代码

    运行结果:

    4.5 Java自旋锁

    1.自旋锁:自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。

    2.为什么要使用自旋锁

    CAS全称呼Compare-And-Swap,它是一条CPU并发原语,俗称比较并交换!
    ​
    
    
    多个线程对同一个变量一直使用CAS操作,那么会有大量修改操作,从而产生大量的缓存一致性流量,因为每一次CAS操作都会发出广播通知其他处理器,从而影响程序的性能。
    复制代码

    3. 线程自旋与线程阻塞 阻塞的缺点显而易见,线程一旦进入阻塞(Block),再被唤醒的代价比较高,性能较差。自旋的优点是线程还是Runnable的,只是在执行空代码。当然一直自旋也会白白消耗计算资源,所以常见的做法是先自旋一段时间,还没拿到锁就进入阻塞。JVM在处理synchrized实现时就是采用了这种折中的方案,并提供了调节自旋的参数。

    4.首先来对比一下互斥锁和自旋锁。 互斥锁:从等待到解锁过程,线程会从sleep状态变为running状态,过程中有线程上下文的切换,抢占CPU等开销。 自旋锁:从等待到解锁过程,线程一直处于running状态,没有上下文的切换。 5.虽然自旋锁效率比互斥锁高,但它会存在下面两个问题 自旋锁一直占用CPU,在未获得锁的情况下,一直运行,如果不能在很短的时间内获得锁,会导致CPU效率降低。 试图递归地获得自旋锁会引起死锁。递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。

    由此可见,我们要慎重的使用自旋锁,自旋锁适合于锁使用者保持锁时间比较短并且锁竞争不激烈的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。

    public class SpinLocakDemo {
     
     
        /**
         * 手写一个自旋锁
         * 循环比较获取直到成功为止,没有类似于wait的阻塞
         * 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒,B随后进来发现当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到
         */
        public static class SpinLockDemo {
     
            // 现在的泛型装的是Thread,原子引用线程
            AtomicReference<Thread> atomicReference = new AtomicReference<>();
     
            public void myLock() {
                // 获取当前进来的线程
                Thread thread = Thread.currentThread();
                System.out.println(Thread.currentThread().getName() + "\t come in ");
     
                // 开始自旋,期望值是null,更新值是当前线程,如果是null,则更新为当前线程,否者自旋
                while (!atomicReference.compareAndSet(null, thread)) {
     
                }
            }
     
            /**
             * 解锁
             */
            public void myUnLock() {
     
                // 获取当前进来的线程
                Thread thread = Thread.currentThread();
     
                // 自己用完了后,把atomicReference变成null
                atomicReference.compareAndSet(thread, null);
     
                System.out.println(Thread.currentThread().getName() + "\t invoked myUnlock()");
            }
     
        }
     
     
        public static void main(String[] args) {
     
            SpinLockDemo spinLockDemo = new SpinLockDemo();
     
            // 启动t1线程,开始操作
            new Thread(() -> {
     
                // 开始占有锁
                spinLockDemo.myLock();
     
     
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
     
                // 开始释放锁
                spinLockDemo.myUnLock();
     
            }, "t1").start();
     
     
            // 让main线程暂停1秒,使得t1线程,先执行
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
     
            // 1秒后,启动t2线程,开始占用这个锁
            new Thread(() -> {
     
                // 开始占有锁
                spinLockDemo.myLock();
                // 开始释放锁
                spinLockDemo.myUnLock();
     
            }, "t2").start();
     
        }
    }
    复制代码

    5. 线程间通信

    5.1 线程间通信必知点

    关键字 synchronized 与 wait()/notify()这两个方法一起使用可以实现等待/通知模式,

    Lock锁的 newContition()方法返回 Condition 对象,Condition 类 也可以实现等待/通知模式。

    用 notify()通知时,JVM 会随机唤醒某个等待的线程, 使用 Condition 类可以 进行选择性通知, Condition 比较常用的两个方法:

    • await()会使当前线程等待,同时会释放锁,当其他线程调用 signal()时,线程会重 新获得锁并继续执行。

    • signal()用于唤醒一个等待的线程。

    注意:在调用 Condition 的 await()/signal()方法前,也需要线程持有相关 的 Lock 锁,调用 await()后线程会释放这个锁,在 singal()调用后会从当前 Condition 对象的等待队列中,唤醒 一个线程,唤醒的线程尝试获得锁, 一旦 获得锁成功就继续执行。

    5.2 线程间通信demo

    场景---两个线程,一个线程对当前数值加 1,另一个线程对当前数值减 1,要求 用线程间通信

    5.2.1 synchronized 方案
    package com.example.onlyqi.juc;
     
    /**
     * @author onlyqi
     * @date 2022/5/30 16:11
     * @description
     */
    /**
     * volatile 关键字实现线程交替加减
     */
    public class TestVolatile {
        /**
         * 交替加减
         * @param args
         */
        public static void main(String[] args){
            DemoClass demoClass = new DemoClass();
            new Thread(() ->{
                for (int i = 0; i < 5; i++) {
                    demoClass.increment();
                }
            }, "线程 A").start();
            new Thread(() ->{
                for (int i = 0; i < 5; i++) {
                    demoClass.decrement();
                }
            }, "线程 B").start();
        }
    }
     
    class DemoClass{
        //加减对象
        private int number = 0;
        /**
         * 加 1
         */
        public synchronized void increment() {
            try {
                while (number != 0){
                    this.wait();
                }
                number++;
                System.out.println("--------" + Thread.currentThread().getName() + "加一成功----------,值为:" + number);
                notifyAll();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        /**
         * 减一
         */
        public synchronized void decrement(){
            try {
                while (number == 0){
                    this.wait();
                }
                number--;
                System.out.println("--------" + Thread.currentThread().getName() + "减一成功----------,值为:" + number);
                notifyAll();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    复制代码

    运行结果:

    5.2.2 Lock方案
    package com.example.onlyqi.juc;
     
    /**
     * @author onlyqi
     * @date 2022/5/30 16:11
     * @description
     */
     
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
     
    /**
     * volatile 关键字实现线程交替加减
     */
    public class TestVolatile {
        /**
         * 交替加减
         * @param args
         */
        public static void main(String[] args){
            DemoClass demoClass = new DemoClass();
            new Thread(() ->{
                for (int i = 0; i < 5; i++) {
                    demoClass.increment();
                }
            }, "线程 A").start();
            new Thread(() ->{
                for (int i = 0; i < 5; i++) {
                    demoClass.decrement();
                }
            }, "线程 B").start();
        }
    }
     
    class DemoClass{
        //加减对象
        private int number = 0;
        //声明锁
        private Lock lock = new ReentrantLock();
        //声明钥匙
        private Condition condition = lock.newCondition();
        /**
         * 加 1
         */
        public void increment() {
            try {
                lock.lock();
                while (number != 0){
                    condition.await();
                }
                number++;
                System.out.println("--------" + Thread.currentThread().getName() + "加一成功----------,值为:" + number);
                condition.signalAll();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
        /**
         * 减一
         */
        public void decrement(){
            try {
                lock.lock();
                while (number == 0){
                    condition.await();
                }
                number--;
                System.out.println("--------" + Thread.currentThread().getName() + "减一成功----------,值为:" + number);
                condition.signalAll();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    }
    复制代码

    5.3 线程间定制化通信

    ==问题: A 线程打印 5 次 A,B 线程打印 10 次 B,C 线程打印 15 次 C,按照 此顺序循环 2 轮==

    package com.example.onlyqi.juc;
     
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
     
    /**
     * @author onlyqi
     * @date 2022/5/30 16:21
     * @description
     */
    public class DemoClass {
        //通信对象:0--打印 A 1---打印 B 2----打印 C
        private int number = 0;
        //声明锁
        private Lock lock = new ReentrantLock();
        //声明钥匙 A
        private Condition conditionA = lock.newCondition();
        //声明钥匙 B
        private Condition conditionB = lock.newCondition();
        //声明钥匙 C
        private Condition conditionC = lock.newCondition();
        /**
         * A 打印 5 次
         */
        public void printA(int j){
            try {
                lock.lock();
                while (number != 0){
                    conditionA.await();
                }
                System.out.println(Thread.currentThread().getName() + "输出 A,第" + j + "轮开始");
                        //输出 5 次 A
                for (int i = 0; i < 5; i++) {
                    System.out.println("A");
                }
                //开始打印 B
                number = 1;
                //唤醒 B
                conditionB.signal();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
        /**
         * B 打印 10 次
         */
        public void printB(int j){
            try {
                lock.lock();
                while (number != 1){
                    conditionB.await();
                }
                System.out.println(Thread.currentThread().getName() + "输出 B,第" + j + "轮开始");
                        //输出 10 次 B
                for (int i = 0; i < 10; i++) {
                    System.out.println("B");
                }
                //开始打印 C
                number = 2;
                //唤醒 C
                conditionC.signal();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
        /**
         * C 打印 15 次
         */
        public void printC(int j){
            try {
                lock.lock();
                while (number != 2){
                    conditionC.await();
                }
                System.out.println(Thread.currentThread().getName() + "输出 C,第" + j + "轮开始");
                        //输出 15 次 C
                for (int i = 0; i < 15; i++) {
                    System.out.println("C");
                }
                System.out.println("-----------------------------------------");
                //开始打印 A
                number = 0;
                //唤醒 A
                conditionA.signal();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    }
     
    /**
     * volatile 关键字实现线程交替加减
     */
    class TestVolatile {
        /**
         * 交替加减
         * @param args
         */
        public static void main(String[] args){
            DemoClass demoClass = new DemoClass();
            new Thread(() ->{
                for (int i = 1; i <= 2; i++) {
                    demoClass.printA(i);
                }
            }, "A 线程").start();
            new Thread(() ->{
                for (int i = 1; i <= 2; i++) {
                    demoClass.printB(i);
                }
            }, "B 线程").start();
            new Thread(() ->{
                for (int i = 1; i <= 2; i++) {
                    demoClass.printC(i);
                }
            }, "C 线程").start();
        }
    }
     
    复制代码

    运行结果:

    6. 集合的线程安全

    请看我之前写的一篇文章,传送门:

    java集合:线程安全的实现方式与分析 传送门:

    JUC必要掌握,学习第一天
    JUC必要掌握(Callable&Future、JUC 三大辅助类、 阻塞队列),学习第三天

    来源:https://juejin.cn/post/7117208351053709349
  • 相关阅读:
    React全家桶+AntD 共享单车后台管理系统开发
    eclipse中通过Properties Editor插件查看配置文件中Unicode内容
    修改eclipse的编码格式
    后端接收前端数据中文乱码解决方案
    MySQL基础
    wordpress个人常用标签调用
    4gl游标cursor
    尝试写一写4gl与4fd
    foreach循環體控制
    保护wordpress后台登录地址
  • 原文地址:https://www.cnblogs.com/konglxblog/p/16756868.html
Copyright © 2020-2023  润新知