JUC

141 阅读8分钟

1、进程线程

  • 进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
  • 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个进程中可以并发多个线程,每条线程并行执行不同的任务。

2、synchronized关键字

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
  3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;

注意synchronized关键字不能被继承。

2.1、synchronized锁

  • 对于普通同步方法,锁是当前实例对象
  • 对于静态同步方法,锁是当前类的Class对象
  • 对于同步代码块,锁是synchronized括号中配置的对象

3、Lock锁

3.1、newCondition

Lock 锁的 newContition()方法返回 Condition 对象,Condition 类可以实现等待/通知模式。 Condition 比较常用的两个方法:

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

3.2、ReentrantLock

  • ReentrantLock,意思是“可重入锁”,ReentrantLock 是唯一实现了 Lock 接口的类。
  • new ReentrantLock() 默认创建的为非公平锁,如果要创建公平锁可以使用new ReentrantLock(true)。

常用方法

  • lock():用于获取锁
  • unlock():用于释放锁
  • tryLock():尝试获取锁
  • getHoldCount():查询当前线程执行 lock() 方法的次数
  • getQueueLength():返回正在排队等待获取此锁的线程数
  • isFair():该锁是否为公平锁

3.3、ReadWriteLock

可以多个线程同时进行读操作,但是只允许一个线程写操作。 注意点:

  • 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
  • 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

4、synchronized和Lock区别

  1. Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现;
  2. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;
  3. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用synchronized 时,等待的线程会一直等待下去,不能够响应中断;
  4. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
  5. Lock 可以提高多个线程进行读操作的效率。在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于synchronized。

5、volatile关键字

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  2. 禁止进行指令重排序。普通变量仅仅能保证在该方法执行过程中,得到正确结果,但是不保证程序代码的执行顺序。 注意:volatile关键字无法保证操作的原子性。

5.1、volatile适用场景

  1. 一个变量被多个线程共享,线程直接给这个变量赋值。
  2. 值得说明的是对volatile变量的单次读/写操作可以保证原子性的,如long和double类型变量,但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。在某些场景下可以代替Synchronized。但是,volatile的不能完全取代Synchronized的位置,只有在一些特殊的场景下,才能适用volatile。

总体来说,需要必须同时满足下面两个条件时才能保证并发环境的线程安全:

  1. 对变量的写操作不依赖于当前值(比如 i++),或者说是单纯的变量赋值(boolean flag = true)。
  2. 该变量没有包含在具有其他变量的不变式中,也就是说,不同的volatile变量之间,不 能互相依赖。只有在状态真正独立于程序内其他内容时才能使用volatile。

6、集合安全

6.1、 Vector 中的操作是线程安全的,因为方法被synchronized关键字修饰。

6.2、 Collections 提供了方法 synchronizedList 保证 list 是同步线程安全的。

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

6.3、 CopyOnWriteArrayList

它相当于线程安全的 ArrayList。和 ArrayList 一样,它是个可变数组;但是和 ArrayList 不同的时,它具有以下特性:

  1. 它最适合于具有以下特征的应用程序:List 大小通常保持很小,遍历操作比较多,需要在遍历期间防止线程间的冲突。
  2. 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove()等等)的开销很大。 线程安全原因:
  3. 动态数组机制
  • 它内部有个“volatile 数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile 数组”, 这就是它叫做 CopyOnWriteArrayList 的原因。
  • 由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList 效率很低;但是单单只是进行遍历查找的话,效率比较高。
  1. 线程安全机制
  • 通过 volatile 和互斥锁来实现的。
  • 通过volatile 数组”来保存数据的。一个线程读取 volatile 数组时,总能看到其它线程对该 volatile 变量最后的写入;就这样,通过 volatile 提供了“读取到的数据总是最新的”这个机制的保证。
  • 通过互斥锁来保护数据。在“添加/修改/删除”数据时,会先“获取互斥锁”,再修改完毕之后,先将数据更新到“volatile 数组”中,然后再“释放互斥锁”,就达到了保护数据的目的。

7、三大辅助类

JUC 中提供了三种常用的辅助类,通过这些辅助类可以很好的解决线程数量过多时 Lock 锁的频繁操作。这三种辅助类为:

  • CountDownLatch: 减少计数
  • CyclicBarrier: 循环栅栏
  • Semaphore: 信号灯

7.1、减少计数 CountDownLatch

CountDownLatch(闭锁)可以看作一个只能做减法的计数器,可以让一个或多个线程等待执行。 CountDownLatch 有两个重要的方法:

  • countDown():使计数器减 1;
  • await():当计数器不为 0 时,则调用该方法的线程阻塞,当计数器为 0时,可以唤醒等待的一个或者全部线程。
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        //数值为6的计时器
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "离开了");
                //计数器减一
                countDownLatch.countDown();
            },"同学" + i).start();
        }

        //计数器值不为0,主线程等待
        countDownLatch.await();

        System.out.println("全部离开了,锁门,现在的计数器为" + countDownLatch.getCount());
    }
}

7.2、循环栅栏 CyclicBarrier

在使用中CyclicBarrier 的构造方法第一个参数是目标障碍数,每次执行 CyclicBarrier 一次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后的语句。可以将 CyclicBarrier 理解为加 1 操作。

public class CyclicBarrierDemo {

    //定义神龙召唤需要的龙珠总数
    private final static int NUMBER = 7;

    /**
     * 集齐七颗龙珠可以召唤神龙
     * @param args
     */
    public static void main(String[] args) {
        //定义循环栅栏
        CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, () ->{
            System.out.println("集齐" + NUMBER + "颗龙珠,现在召唤神龙!!!!!!!!!");
        });
        //定义 7 个线程分别去收集龙珠
        for (int i = 1; i <= 7; i++) {
            new Thread(()->{
                try {
                    if(Thread.currentThread().getName().equals("龙珠3号")){
                        System.out.println("龙珠 3 号抢夺战开始,孙悟空开启超级赛亚人模式!");
                        Thread.sleep(5000);
                        System.out.println("龙珠 3 号抢夺战结束,孙悟空打赢了,拿到了龙珠 3号!");
                    }else{
                        System.out.println(Thread.currentThread().getName() + "收集到了!!!!");
                    }
                    cyclicBarrier.await();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }, "龙珠" + i + "号").start();
        }
    }

}

7.3、信号量Semaphore

Semaphore 的构造方法中传入的第一个参数是最大信号量(可以看成最大线程池),每个信号量初始化为一个最多只能分发一个许可证。使用 acquire 方法获得许可证,release 方法释放许可。

public class SemaphoreDemo {

    public static void main(String[] args) {
        //设置两个信号,相当于2个停车位
        Semaphore semaphore = new Semaphore(2);

        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "抢到了车位");

                    //设置随机停车时间
                    TimeUnit.SECONDS.sleep(new Random().nextInt(5));

                    System.out.println(Thread.currentThread().getName() + "离开了车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            },i + "辆车").start();
        }
    }
}

7.4、CyclicBarrier 与 CountDownLatch 有什么区别?

CyclicBarrier 与 CountDownLatch 本质上都是依赖 volatile 和 CAS 实现的,它们区别如下:

  • CountDownLatch 只能使用一次,而 CyclicBarrier 可以使用多次。
  • CountDownLatch 是手动指定等待一个或多个线程执行完成再执行,而CyclicBarrier 是 n 个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。