java---JUC并发包详解

目录

前言

一、atomic包

AtomicInteger类

AtomicReference类

AtomicStampedReference类

二、locks包

接口

Condition

Lock 

ReadWriteLock

实现类

ReentrantLock类

ReentrantReadWriteLock类

三、CountDownLatch

四、Semaphore(信号量)

总结


前言

JUC是java.util.concurrent包的简称,在Java5.0添加,目的就是为了更好的支持高并发任务。让开发者进行多线程编程时减少竞争条件和死锁的问题!


一、atomic包

方便程序员在多线程环境下,无锁的进行原子操作。Atomic包里的类基本都是使用Unsafe实现的包装类,核心操作是CAS原子操作。

AtomicInteger类

举个例子就是我们平时的  i++,不是原子的操作,在多线程环境下会发生错误,但是使用atomic提供的原子类,就可以很好的避免这个问题发生。

代码演示:

public class Test {
   static  AtomicInteger atomicInteger  = new AtomicInteger(0);
     static int count = 100000000;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
           for(int i = 0; i < count; i++){
               //相当于i++
                atomicInteger.getAndIncrement();
           }
        });
        t.start();
        Thread t2 = new Thread(() -> {
            for(int i = 0; i < count; i++){
                //相当于i--
              
                atomicInteger.getAndDecrement();
            }
        });
        t2.start();
        t.join();
        t2.join();
        System.out.println(atomicInteger);
    }
}

运行结果

可以看到在一亿这个数量级上都是0,足以说明了原子性得到了保持。

AtomicReference类

CAS(关于CAS之前的博客有详细的介绍) 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。所以将多个变量封装成对象,通过AtomicReference来保证原子性。

AtomicReference类内部对变量的表示:

 private volatile V value;

AtomicStampedReference类

由于使用CAS会遇到ABA问题(关于ABA问题),所以使用AtomicStampedReference类实现了用版本号作比较的CAS机制。

AtomicStampedReference类内部对变量的表示

  private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }  

    private volatile Pair<V> pair;

    /**
     * Creates a new {@code AtomicStampedReference} with the given
     * initial values.
     *
     * @param initialRef the initial reference   //变量的引用
     * @param initialStamp the initial stamp     //这个就是版本号
     */
    public AtomicStampedReference(V initialRef, int initialStamp) {
        pair = Pair.of(initialRef, initialStamp);
    }

二、locks包

我们选出几个比较重要接口和实现类讲一下:

接口

上面可以看到一共有三个接口分别是 Condition、Lock、ReadWriteLock。

Condition

condition主要是为了在JUC框架中提供和Java传统的监视器风格的wait,notify和notifyAll方法类似的功能。condition一般和lock一起使用,就像synchronized与wait、notify使用一样

代码展示:

public class Test {
     static Lock lock = new ReentrantLock();
      static Condition condition = lock.newCondition();


    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            lock.lock();
            try {
                condition.await();
                System.out.println("子线程醒了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });
        t.start();
        System.out.println("两秒之后叫醒子线程");
        Thread.sleep(2000);
        
        lock.lock();
        condition.signal();
        lock.unlock();
    }
}

运行结果

Lock 

在 Lock 接口中,获取锁的方法有 4 个:lock()、tryLock()、tryLock(long,TimeUnit)、lockInterruptibly(),为什么需要这么多方法?这些方法都有什么区别?接下来我们一起来看。

lock()

lock 方法是 Lock 接口中最基础的获取锁的方法,当有可用锁时会直接得到锁并立即返回,当没有可用锁时会一直等待,直到获取到锁为止,它的基础用法如下: 

Lock lock = new ReentrantLock();
// 获取锁
lock.lock();
try {
    // 执行业务代码...
} finally {
    //释放锁
    lock.unlock();   
}

lockInterruptibly() 

lockInterruptibly 方法和 lock 方法类似,当有可用锁时会直接得到锁并立即返回,如果没有可用锁会一直等待直到获取锁,但和 lock 方法不同,lockInterruptibly 方法在等待获取时,如果遇到线程中断会放弃获取锁。它的基础用法如下:

Lock lock = new ReentrantLock();
try {
    // 获取锁
    lock.lockInterruptibly();
    try {
        // 执行业务方法...
    } finally {
        // 释放锁
        lock.unlock();
    }
} catch (InterruptedException e) {
    e.printStackTrace();
}

使用 t.interrupt() 方法可以中断线程执行。 

tryLock() 

与前面的两个方法不同,使用无参的 tryLock 方法会尝试获取锁,并立即返回获取锁的结果(true 或 false),如果有可用锁返回 true,并得到此锁,如果没有可用锁会立即返回 false。它的基础用法如下:

Lock lock = new ReentrantLock();
// 获取锁
boolean result = lock.tryLock();
if (result) {
    try {
        // 获取锁成功,执行业务代码...
    } finally {
        // 释放锁
        lock.unlock();
    }
} else {
    // 执行获取锁失败的业务代码...
}

tryLock(long,TimeUnit)  

有参数的 tryLock(long,TimeUnit) 方法需要设置两个参数,第一个参数是 long 类型的超时时间,第二个参数是对参数一的时间类型描述(比如第一参数是 3,那么它究竟是 3 秒还是 3 分钟,是第二个参数说了算的)。在这段时间内如果获取到可用的锁了就返回 true,如果在定义的时间内,没有得到锁就会返回 false它的基础用法如下: 

Lock lock = new ReentrantLock();
try {
    // 获取锁(最多等待 3s,如果获取不到锁就返回 false)
    boolean result = lock.tryLock(3, TimeUnit.SECONDS);
    if (result) {
        try {
            // 获取锁成功,执行业务代码...
        } finally {
            // 释放锁
            lock.unlock();
        }
    } else {
        // 执行获取锁失败的业务代码...
    }
} catch (InterruptedException e) {
    e.printStackTrace();
}

synchronized和Lock的区别

1.synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。

2.synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。

3.通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

ReadWriteLock

首先ReentrantLock(后面介绍)某些时候有局限,如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。

因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

两个线程都是读:


public class Main {
    public static void main(String[] args) {
        ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        Lock readLock = readWriteLock.readLock();
        Lock writeLock = readWriteLock.writeLock();

        // “写”的角色,请求写锁
        // "只读“角色,请求读锁

       readLock.lock();        // 读锁已经有了
        // writeLock.lock();       // 写锁锁上
        Thread t = new Thread() {
            @Override
            public void run() {
                readLock.lock();
       //      writeLock.lock();
                System.out.println("子线程也可以加锁成功");
            }
        };
        t.start();
    }
}

运行结果

一个线程读一个线程写 

public class Main {
    public static void main(String[] args) {
        ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        Lock readLock = readWriteLock.readLock();
        Lock writeLock = readWriteLock.writeLock();

        // “写”的角色,请求写锁
        // "只读“角色,请求读锁

       readLock.lock();        // 读锁已经有了
        // writeLock.lock();       // 写锁锁上
        Thread t = new Thread() {
            @Override
            public void run() {
            //    readLock.lock();
            writeLock.lock();
                System.out.println("子线程也可以加锁成功");
            }
        };
        t.start();
    }
}

此时由于读锁还没有解锁,所以写锁会阻塞在哪里,不会有输出。 

实现类

ReentrantLock类

这个类前面我们已经用到了。首先根据字面意思表示可重入锁

什么是可重入锁:先看一下代码

public class Main {
    public static void main(String[] args) {
        // 我们是主线程
        Lock lock = new ReentrantLock();    // 名字已经说明,这把锁是可重入锁

        lock.lock();        // 锁已经被 main 线程锁了

        lock.lock();
        System.out.println("说明允许可重入");
    }
}

也就是是否允许持有锁的线程成功请求到同一把锁,这里得是同一个线程。 

对与synchrnized来说,也是可重入锁:代码如下:

public class Main2 {
    public static void main(String[] args) {
        Object lock = new Object();

        synchronized (lock) {   // main 线程已经对 lock 加锁了
            synchronized (lock) {   // main 线程再次对 lock 请求锁(处于已经锁上状态)
                System.out.println("这里打印了就说明了 sync 锁是可重入锁");
            }
        }
    }
}

ReentrantLock默认是不公平锁,但是可以修改。synchronized是不公平锁。

public class Main {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock(true);    // fair = true:使用公平锁模式

        Lock lock1 = new ReentrantLock(false);    // fair = false:使用不公平锁模式

        Lock lock2 = new ReentrantLock();               // 默认情况下是不公平的
    }
}

Synchronized与ReentrantLock 

1.两者都是可重入锁

可重入锁:重入锁,也叫做递归锁,可重入锁指的是在一个线程中可以多次获取同一把锁,比如: 一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁, 两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

2.synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API

  • synchronized 是依赖于  JVM 实现的,虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的。
  • ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成)

3.ReentrantLock 比 synchronized 增加了一些高级功能

相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)

  • 等待可中断.通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
  • ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
  • ReentrantLock类线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”

4.使用选择

  • 除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。
  • synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。

ReentrantReadWriteLock类

首先ReentrantLock某些时候有局限,如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。

因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。其实都是前面说的~~~。

三、CountDownLatch

CountDownLatch是一个同步工具类,用来协调多个线程之间的同步,或者说起到线程之间的通信(而不是用作互斥的作用)。

CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一(当然这里一个线程也可以返回多个)。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。

代码演示:

public class Main {
    // count: 计数器为 3 个,只有 3 个全部报到了,门闩才会打开
    static CountDownLatch countDownLatch = new CountDownLatch(3);


    static class MyThread extends Thread {
        @Override
        public void run() {
            countDownLatch.countDown();
            countDownLatch.countDown();
            countDownLatch.countDown();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start();

        countDownLatch.await();

        System.out.println("门闩被打开了");
    }
}

四、Semaphore(信号量)

Semaphore也叫信号量,在JDK1.5被引入,可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。

Semaphore内部维护了一组虚拟的许可,许可的数量可以通过构造函数的参数指定。

  • 访问特定资源前,必须使用acquire方法获得许可,如果许可数量为0,该线程则一直阻塞,直到有可用许可。
  • 访问资源后,使用release释放许可。

Semaphore和ReentrantLock类似,获取许可有公平策略和非公平许可策略,默认情况下使用非公平策略。

Semaphore可以用来做流量分流,特别是对公共资源有限的场景,比如数据库连接。
假设有这个的需求,读取几万个文件的数据到数据库中,由于文件读取是IO密集型任务,可以启动几十个线程并发读取,但是数据库连接数只有10个,这时就必须控制最多只有10个线程能够拿到数据库连接进行操作。这个时候,就可以使用Semaphore做流量控制。

代码展示:

public class SemaphoreTest {
    private static final int COUNT = 40;
    private static Executor executor = Executors.newFixedThreadPool(COUNT);
    private static Semaphore semaphore = new Semaphore(10);
    public static void main(String[] args) {
        for (int i=0; i< COUNT; i++) {
            executor.execute(new ThreadTest.Task());
        }
    }

    static class Task implements Runnable {
        @Override
        public void run() {
            try {
                //读取文件操作
                semaphore.acquire();
                // 存数据过程
                semaphore.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
            }
        }
    }
}


总结

加油哦~~

  • 6
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值