二—17:JUC_锁_信号量_并发集合

一、Synchronized同步锁

1. 锁介绍

在Java中每个对象或类都可以当做锁使用,这些锁称为内置锁

Java中内置锁都是互斥锁。也就是说一个线程获取到锁,其他线程必须等待或阻塞。 如果占用锁的线程不释放锁,其他线程将一直等待下去。锁在同一时刻,只能被一个线程持有

如果锁是作用于对象,称对象锁。如果锁作用整个类称为类锁

2. synchronized介绍

  1. synchronized是Java中的关键字。使用synchronized关键字是锁的一种实现。

  2. synchronized的加锁和解锁过程不需要程序员手动控制,只要执行到synchronized作用范围会自动加锁(获取锁/持有锁),执行完成后会自动解锁(释放锁)加锁的代码出现异常,会自动解锁。

  3. synchronized可以保证可见性,因为每次执行到synchronized代码块时会清空线程区(工作内存或高速缓存)。

  4. synchronized 不禁用指令重排,但可以保证有序性。因为同一个时刻只有一个线程能操作。

  5. synchronized 可以保证原子性,一个线程的操作一旦开始,就不会被其他线程干扰,只能当前线程执行完,其他线程才可以执行。

  6. synchronized 在Java老版本中属于重量级锁(耗费系统资源比较多的锁),随着Java的不停的更新、优化,在Java8中使用起来和轻量级锁(耗费系统资源比较少的锁)已经几乎无差别了。

  7. 主要分为下面几种情况:

    1. 修饰实例方法,非静态方法(对象锁) 需要在类实例化后,再进行调用。

    2. 修饰静态方法(类锁)静态方法属于类级别的方法,静态方法可以类不实例化就使用。

    3. 修饰代码块(对象锁、类锁)。
      代码演示:

package com.lyx.test;

public class Test02 {
    static int a = 0;
    public static void main(String[] args) throws InterruptedException {
        Test02 test02 = new Test02();
        for (int i = 0; i < 3; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
              test02.test01();
                }
            }).start();
        }
        Thread.sleep(3000);
        System.out.println(a);
    }
    /*
     * 加锁:执行到加锁的方法,自动加锁
     * 释放锁:执行完加锁的方法,自动解锁,出现异常自动解锁
     * 锁范围:加锁的方法
     * 锁类型:对象锁(同一个对象生效)
     *        多个线程争夺同一个对象锁
     * 锁失效:多个对象失效
     * */
   public  synchronized void  test01(){
       for (int i = 0; i < 10000; i++) {
           a++;
       }
   }
    /*
     * 加锁:执行到加锁的方法,自动加锁
     * 释放锁:执行完加锁的方法,自动解锁,出现异常自动解锁
     * 锁范围:加锁的方法
     * 锁类型:类锁(同一个类生效,和对象无关)
     *        多个线程争夺同一个类锁
     * */
    public static synchronized void test2(){
        for (int i = 0; i < 10000; i++) {
            a++;
        }
    }
    /*
     * 加锁:执行到加锁的方法,自动加锁
     * 释放锁:执行完加锁的方法,自动解锁,出现异常自动解锁
     * 锁范围:加锁的代码块
     * 锁类型:类锁(同一个固定值)
     * 锁失效:多个值
     * */
    final String LOCK = "锁";
    public void test3(){
        int a = 0;
        System.out.println(a);
        synchronized (LOCK){
            for (int i = 0; i < 10000; i++) {
                a++;
            }
        }
    }

    public void test4(String str){
        synchronized (str){
            for (int i = 0; i < 10000; i++) {
                a++;
            }
        }
    }
    /*
     * 加锁:执行到加锁的方法,自动加锁
     * 释放锁:执行完加锁的方法,自动解锁,出现异常自动解锁
     * 锁范围:加锁的代码块
     * 锁类型:对象锁(同一个对象)
     * 锁失效:多个对象
     * */
    public void test5(){
        synchronized (this){
            for (int i = 0; i < 10000; i++) {
                a++;
            }
        }
    }
    /*
     * 加锁:执行到加锁的方法,自动加锁
     * 释放锁:执行完加锁的方法,自动解锁,出现异常自动解锁
     * 锁范围:加锁的代码块
     * 锁类型:类锁(同一个类对象生效)
     * */
    public void test6() {
        synchronized (Test02.class) {
            for (int i = 0; i < 10000; i++) {
                a++;
            }
        }
    }
}

3. 对象锁和类锁(面试题)

当synchronized修饰静态方法或代码块参数为Class时或代码块参数为固定值,锁为类锁,作用整个类。同一个类使用,锁生效。

当synchronized修饰实例方法或代码块参数为this时,为对象锁,只对当前对象有效。

体现在:

​ 多个对象使用时,锁生效,使用类锁。

​ 同一对象使用时,所生效,使用对象锁。

4. 什么是可重入锁(面试题)

某个线程已经获得了某个锁,允许再次获得锁,就是可重入锁。如果不允许再次获得锁就称为不可重入锁。

synchronized为可重入锁。但可重入锁不仅仅只有synchronized。后面还会学习ReentrantLock也是可重入锁。

4.1 代码演示
public class Demo22 {
  public static void main(String[] args) {
    Demo22 demo = new Demo22();
    new Thread(new Runnable() {
      @Override
      public void run() {
        demo.test1();
      }
    }).start();
  }

  public void test1(){
    synchronized (this){
      System.out.println("test1执行");
      test2();
    }
  }
  public void test2(){
    synchronized (this){
      System.out.println("test2执行");
    }
  }
}
4.2 可重入锁底层原理

可重入锁底层原理特别简单,就是计数器。

当一个线程第一次持有某个锁时会由monitor(监控器)对持有锁的数量加1,当这个线程再次需要碰到这个锁时,如果是可重入锁就对持有锁数量再次加1(如果是不可重入锁,发现持有锁为1了,就不允许多次持有这个锁了,阻塞),当释放锁时对持有锁数量减1,直到减为0,表示完全释放了这个锁。

二、生命周期图回顾

在这里插入图片描述

线程生命周期从新建到死亡共包含五种状态:

新建状态、就绪状态、运行状态、阻塞状态、死亡状态

1 .新建状态

​ 当实例化Thread对象后,线程就处于新建状态. 这时线程并没有执行。

public static void main(String[] args) {
  Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
      System.out.println("run()执行开始");
      System.out.println("run()执行结束");
    }
  });
}

2. 就绪状态

​ 只要在代码中启动了线程,就会从新建状态,变为就绪状态。

thread.start();

​ 就绪状态属于一种临时状态。处于就绪状态的线程会去抢占CPU,只要抢占成功就会切换到运行状态,失去了cpu执行权,回到就绪状态。

​ 线程抢占CPU的场景和超市中大妈早上去抢菜的场景是一样。

3. 运行状态

​ 运行状态就是开始执行线程的功能。具体就是执行run()方法

Thread thread = new Thread(new Runnable() {
  @Override
  public void run() {
    System.out.println("run()执行开始");
    try {
      Thread.sleep(2000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("run()执行结束");
  }
});
thread.start();

​ 在代码执行过程中分为三种情况:

​ 1. 如果碰到sleep() / wait() / join()等方法会让线程切换为阻塞状态。

​ 2. 如果调用yield()方法或失去CPU执行权限会切换为就绪状态。

​ 3. 如果run()方法成功执行完成,或出现问题或被停止(中断)会切换为死亡状态。

4. 阻塞状态

​ 阻塞状态时,线程停止执行。让出CPU资源。

​ 处于阻塞状态的线程需要根据情况进行判断是否转换为就绪状态:

​ 1. 如果是因为sleep()变为阻塞,则休眠时间结束自动切换为就绪状态。

​ 2. 如果是因为wait()变为阻塞状态,需要调用notify()或notifyAll()手动切换为就绪状态。

​ 3. 如果因为join()变为阻塞状态,等到join线程执行完成,自动切换为就绪状态。

​ 4. (已过时)如果是因为suspend()暂停的线程,需要通过resume()激活线程。

5. 死亡状态

​ 死亡状态即线程执行结束。

三、线程中相关方法回顾

1. stop()介绍(已过时)

1.1 stop()介绍(已过时)

​ stop()可以停止一个线程。让线程处于死亡状态,stop()已经过时

1.2 stop()弃用的原因

stop()太绝对了,什么情况下都能停,并没有任何的提示信息,可能导致混乱结果。

​ 推荐使用interrupt()停止一个长时间wait的线程。

1.3 stop()代码演示
package com.zqwl.test;

/*
* stop():
*   结束阻塞,运行状态的线程
*
* */
public class Test04 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程开始执行");
                /*for (int i = 0; i < 999999999; i++) {
                    System.out.println(i);
                }*/
                try {
                    Thread.sleep(500000000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("子线程执行结束");
            }
        });
        thread.start();

        Thread.sleep(1000);

        thread.stop();

2. interrupt()介绍

interrupt()只能中断当前线程状态带有InterruptedException异常的线程,当程序执行过程中,如果被强制中断会出现Interrupted异常。

interrupt() 负责打断处于阻塞状态的线程。防止出现死锁或长时间wait(等待)。

2.1 interrupt()代码演示
package com.lyx.test;
/*
* interrupt()
* 中断阻塞状态的线程,抛出中断异常,线程继续执行
* 运行状态线程不能被中断
* 注意:
* 只能中断声明了InterruptedException异常状态的线程
* *具体来说,当对一个线程,调用 interrupt() 时,
* ① 如果线程处于被阻塞状态(处于sleep, wait, join 等状态),线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。
* ② 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true。被设置中断标志的线程将继续正常运行,不受影响。
* interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。
* */
public class Test05 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("开始");
             /*   for (int i = 0; i < 99999999; i++) {
                    System.out.println(i);
                }*/
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                  e.printStackTrace();
                }
                System.out.println("结束");
            }
        });
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}

3. suspend()和resume()

3.1 suspend()介绍(已过时)

​ suspend()可以挂起、暂停线程,让线程处于阻塞状态,是一个实例方法,已过时。

挂起时,不会释放锁

3.2 resume()介绍(已过时)

​ resume()可以让suspend()的线程唤醒,变成就绪状态,已过时。

3.3 代码演示
public static void main(String[] args) {
  Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
      while (true){
        System.out.println("run()开始执行");
        System.out.println("run()执行结束");
      }
    }
  });
  try {
    //启动线程
    thread.start();
    Thread.sleep(1000);//主线程阻塞状态
    thread.suspend();//挂起线程
    Thread.sleep(2000);
    thread.interrupt();//唤醒线程
  } catch (InterruptedException e) {
    e.printStackTrace();
  }
  thread.resume();
  //结束处于运行状态的线程
  thread.interrupt();
}
3.4 被弃用的原因

​ 如果线程A持有锁(假设锁叫做L),对线程A做了suspend,让线程A挂起。在线程A没有resume之前,线程B无论如何也是无法获得锁的,也就出现了死锁。因为suspend时没有释放锁。

四、线程通信回顾

1. 什么是线程通信

需要多个线程配合完成一件事情,如何让多个线程能够合理的切换就是线程通信需要考虑的问题,重点在于配合

2. 线程通信的几种方式(面试题)

  1. wait()和notify() | notifyAll() 方式

  2. join()方式

  3. Condition 方式

3. wait()和notify() | notifyAll()

3.1 介绍

​ wait() 是Object中的方法。调用wait()后会让线程从运行状态变为阻塞状态。

​ 在Object类中提供了wait()的重载方法 。

​ 1. 使用wait()和notify() | notifyAll()要求必须有锁。

​ 2. wait()、notify()、notifyAll() 都是放入锁的代码中。

​ 3. wait()和notify() | notifyAll() 配合使用。

3.2 代码演示
package com.lyx.test;

/*
 * t1:白日依山尽,
 * t2:黄河入海流。
 * t1:欲穷千里目,
 * t2:更上一层楼。
 * */
public class Test09 {
    static final String LOCK = "锁";

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                synchronized (LOCK) {
                    System.out.println("t1:白日依山尽,");
                        LOCK.wait();
                    System.out.println("t1:欲穷千里目,");
                    LOCK.notifyAll();
                        }
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    synchronized (LOCK) {
                        System.out.println("t2:黄河入海流。");
                        LOCK.notifyAll();
                        LOCK.wait();
                        System.out.println("t2:更上一层楼。");
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();
    }
}
3.3 wait()和sleep()区别(常见面试题)
  1. 所属类不同

​ wait(long) 是Object中方法

​ sleep(long)是Thread的方法

  1. 唤醒机制不同

​ wait() 没有设置最大时间情况下,必须等待notify() | notifyAll()

​ sleep()是到指定时间自动唤醒

  1. 锁机制不同

​ wait(long)释放锁

​ sleep(long)只是让线程休眠,不会释放锁

  1. 使用位置不同

​ wait()必须持有对象锁

​ sleep()可以使用在任意地方

  1. 方法类型不同

​ wait()是实例方法

​ sleep()是静态方法

4. join()

4.1 介绍

​ join() 把线程加入到另一个线程中。在哪个线程内调用join(),就会把对应的线程加入到当前线程中。

​ join()后,会让当前线程挂起,变成阻塞状态,直到新加入的线程执行完成。当前线程才会继续执行。

public static void main(String[] args) {
  Thread thread1 = new Thread() {
    @Override
    public void run() {
      System.out.println("我是子线程");
      try {
        Thread.sleep(3000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  };
  try {
    thread1.start();
    //thread1线程加入主线程,主线程挂起,thread1执行完主线程继续执行
    thread1.join();
    System.out.println("我是主线程");
  } catch (Exception e) {
    e.printStackTrace();
  }
}

五、 JUC中的locks包

1. locks包介绍

java.util.concurrent.locks:JUC中对锁支持的工具包 。
在这里插入图片描述

六、JUC的锁机制

1. AQS

1.1 介绍

AQS全名AbstractQueuedSynchronizer,是并发容器JUC(java.util.concurrent)下locks包内的一个类。
在这里插入图片描述

1.2 工作原理

AQS的核心思想为如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是使用队列实现的锁,即将暂时获取不到锁的线程加入到队列中。

AQS使用一个int state成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。

AQS使用CAS对该同步状态进行原子操作实现对其值的修改,当state大于0的时候表示锁被占用,如果state等于0时表示没有占用锁。

2. 锁机制介绍

JUC中锁的底层使用的就是AQS

  1. ReentrantLock:Lock接口的实现类,可重入锁。相当于synchronized同步锁。

  2. ReentrantReadWriteLock:ReadWriteLock接口的实现类。类中包含两个静态内部类,ReadLock读锁、WriteLock写锁。

  3. Condition:是一个接口,都是通过lock.newCondition()实例化。属于wait和notify的替代品。提供了await()、signal()、singnalAll()与之对应。

  4. LockSupport:和Thread中suspend()和resume()相似。

3. 锁机制详解

3.1 ReentrantLock重入锁

ReentrantLock是JUC中对重入锁的标准实现。作用相当于synchronized。

加锁和解锁过程都需要由程序员手动控制,使用很灵活。

提供了2种类型的构造方法。

​ 1. ReentrantLock():创建非公平锁的重入锁。

​ 2. ReentrantLock(boolean):创建创建锁。取值为true表示公平锁,取值为false表示非公平锁。

公平锁:多线程操作共一个资源时,严格按照顺序执行。

非公平锁:多线程在等待时,可以竞争,谁竞争成功,谁获取锁。

非公平锁的效率要高于公平锁。ReentrantLock默认就是非公平锁。

语法:

创建:

ReentrantLock rk = new ReentrantLock();

​ 加锁:

//无返回值  阻塞代码
rk.lock(); 
//有返回值 不会阻塞代码
boolean b = rk.tryLock()

​ 解锁:

rk.unLock();

注意:

​ 1. ReentrantLock出现异常时,不会自动解锁

​ 2. 多线程的情况下,一个线程出现异常,并没有释放锁,其他线程也获取不到锁,容易出现死锁

​ 3. 建议把解锁方法finally{}代码块中

​ 4. synchronized加锁与释放锁不需要手动的设置,遇到异常时,会自动的解锁
代码演示:

public class Test01 {
  static int a = 0;
  public static void main(String[] args) throws InterruptedException {
    ReentrantLock rl = new ReentrantLock(false);
    for (int i = 0; i < 5; i++) {
      new Thread() {
        @Override
        public void run() {
          rl.lock();//加锁
          test();
          //重入锁
          /*rl.lock();  
            test();
            rl.unlock();*/
          rl.unlock();//解锁
        }
      }.start();
    }
    Thread.sleep(2000);
    System.out.println(a);
  }
  public static void test() {
    for (int i = 0; i < 10000; i++) {
      a++;
    }
  }
}

注意:

避免死锁,需要将解锁放到finally{}中

3.2 Condition等待 | 唤醒

wait和notify是针对synchronized的,Condition是针对Lock的

语法:

创建:

ReentrantLock rk = new ReentrantLock();
Condition condition = rk.newCondition();

线程等待

condition.await(); 

唤醒一个线程 | 唤醒所有线程:

condition.signal(); //唤醒一个线程
condition.signalAll(); //唤醒所有线程
3.2.1 代码演示
public class Test03 {
  public static void main(String[] args) {
    ReentrantLock rl = new ReentrantLock();
    Condition condition = rl.newCondition();
    new Thread(){
      @Override
      public void run() {
        try {
          //加锁
          rl.lock();
          System.out.println("白日依山尽");
          //线程等待 -> 阻塞状态  释放锁
          condition.await();
          System.out.println("欲穷千里目");
          //唤醒其他线程
          condition.signal();
        } catch (InterruptedException e) {
          e.printStackTrace();
        } finally{
          rl.unlock();
        }
      }
    }.start();

    new Thread(){
      @Override
      public void run() {
        try {
          //加锁
          rl.lock();
          System.out.println("黄河入海流");
          //唤醒其他线程
          condition.signal();
          //线程等待 -> 阻塞状态  释放锁
          condition.await();
          System.out.println("更上一层楼");
        } catch (Exception e) {
          e.printStackTrace();
        } finally{
          rl.unlock();
        }
      }
    }.start();
  }
}

3.3 ReadWriteLock读写锁

ReadWriteLock为接口,实现类为ReentrantReadWriteLock

ReadLock 读锁,又称为共享锁。允许多个线程同时获取该读锁

WriteLock 写锁,又称为独占锁。只有一个线程能获取,其他的线程等待,避免死锁

注意:

​ 读写锁,实际含义为是否能有多个线程同时获取

语法:

创建:

ReentrantReadWriteLock rk = new ReentrantReadWriteLock();

读锁:

//获取读锁
ReentrantReadWriteLock.ReadLock readLock = rrw.readLock();
//加锁
readLock.lock();
boolean b = readLock.tryLock();
//解锁
readLock.unlock();

写锁:

//获取写锁
ReentrantReadWriteLock.WriteLock writeLock = rrw.writeLock();
//加锁
writeLock.lock();
boolean b = writeLock.tryLock();
//解锁
writeLock.unlock();

代码演示

/*
*ReadWriteLock:
* ReadLock 读锁,又称为共享锁。允许多个线程同时获取该读锁,相当于没有锁。
* WriteLock 写锁,又称为独占锁。只有一个线程能获取,其他写的线程等待,避免死锁。(与lock锁相似)
* */
public class Test14 {
    public static void main(String[] args) {
        //获取读写锁对象
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        //读锁
        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        //写锁
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
//             writeLock.lock();
               readLock.lock();
                    test(Thread.currentThread().getName());
                } catch (Exception e) {
                   e.printStackTrace();
                } finally {
//              writeLock.unlock();
                readLock.unlock();
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                   readLock.lock();
//                 writeLock.lock();
                    test(Thread.currentThread().getName());
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                  readLock.unlock();
//                writeLock.unlock();
                }
            }
        }).start();
    }
    public static void test(String name) {
        for (; ; ) {
            try {
                System.out.println(name);
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
3.4 LockSupport 暂停 | 恢复

LockSupport是Lock中实现线程暂停和线程恢复。suspend()和resume()是synchronized中的暂停和恢复。

注意:暂停恢复不会释放锁,避免死锁问题

语法:

暂停

LockSupport.park();

恢复:

LockSupport.unpark(t1);
3.4.1 代码演示
package com.lyx.test;

import java.util.concurrent.locks.LockSupport;

public class Test15 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("程序开始");
                for (int i = 0; i < 10; i++) {
                    try {
                        System.out.println(i);
                        if (i == 6){
                            LockSupport.park(); //暂停线程
                        }
                        Thread.sleep(3);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("程序结束");
            }
        });

        thread.start();
        Thread.sleep(5000);
        LockSupport.unpark(thread); //恢复线程
    }
}

3.5 synchronized和lock的区别(面试题)
  1. 类型不同

​ synchronized是关键字。修饰方法,修饰代码块

​ Lock是接口

  1. 加锁和解锁机制不同

​ synchronized是自动加锁和解锁,程序员不需要控制。

​ Lock必须由程序员控制加锁和解锁过程,解锁时,需要注意出现异常不会自动解锁

  1. 异常机制

​ synchronized碰到没有处理的异常,会自动解锁,不会出现死锁。

​ Lock碰到异常不会自动解锁,可能出现死锁。所以写Lock锁时都是把解锁放入到finally{}中。

  1. Lock功能更强大

​ Lock里面提供了tryLock()/isLocked()方法,进行判断是否上锁成功。synchronized因为是关键字,所以无法判断。

  1. Lock性能更优

​ 如果多线程竞争锁特别激烈时,Lock的性能更优。如果竞争不激烈,性能相差不大。

  1. 线程通信方式不同

​ synchronized 使用wait()和notify()线程通信。

​ Lock使用Condition的await()和signal()通信。

  1. 暂停和恢复方式不同

​ synchronized 使用suspend()和resume()暂停和恢复,这俩方法过时了。

​ Lock使用LockSupport中park()和unpark()暂停和恢复,这俩方法没有过时。

七、JUC中的Tools

1. Tools介绍

Tools也是JUC中的工具类,其中包含了CountDownLatch、CyclicBarrier、Semaphore

2. CountDownLatch计数器

在开发中经常遇到在主线程中开启多个线程去并行执行任务,并且主线程需要等待所有子线程执行完毕后再进行汇总的场景。之前是使用join() | 主线程休眠实现的,但是不够灵活,某些场合和还无法实现,所以开发了CountDownLatch这个类。底层基于AQS。

CountDown是计数递减的意思,Latch是门闩的意思。内部维持一个递减的计数器。可以理解为初始有n个Latch,等Latch数量递减到0的时候,结束阻塞,执行后续操作。

创建:

CountDownLatch cdl= new CountDownLatch(数字);

线程等待:

//当前线程等待,直到到Latch计数到零,或者被interrupt
cdl.await()

计数器递减:

//减少Latch的计数,如果计数达到零,释放等待的线程
cdl.countDown( )
2.1 代码实现:
public class Test07 {
  int a = 0;
  public static void main(String[] args) throws InterruptedException {
    CountDownLatch countDownLatch = new CountDownLatch(5);
    ReentrantLock rl = new ReentrantLock();
    Test11 test05 = new Test11();
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    for (int i = 0; i < 5; i++) {
      executorService.execute(new Runnable() {
        @Override
        public void run() {
          rl.lock();
          test05.test();
          countDownLatch.countDown(); //计数器-1
          rl.unlock();
        }
      });
    }
    countDownLatch.await(); //计数器值不为0阻塞,为0 恢复执行
    System.out.println(test05.a);
  }
  public void test() {
    for (int i = 0; i < 10000; i++) {
      a++;
    }
  }
}

3. CyclicBarrier回环屏障

CountDownLatch优化了join()在解决多个线程同步时的能力,但CountDownLatch的计数器是一次性的。计数递减为0之后,再调用countDown()、await()将不起作用。为了满足计数器可以重置的目的,JDK推出了CyclicBarrier类。

await()方法表示当前线程执行时计数器值不为0则等待。如果计数器为0则继续执行。每次await()之后计算器会减少一次。当减少到0下次await从初始值重新递减。

3.1 代码实现

假设多个任务都有三个阶段组成,多个线程分别指向一个任务,必须保证每个任务的一个阶段结束后,才进入下一个阶段。此时使用CyclicBarrier正合适

/*
 * 1.裁判:比赛开始
 * 2.球员:开始行动
 * 3.球员:球员犯规
 * 4.裁判:吹哨
 * 5.裁判:出示红牌
 * 6.球员:接受红牌
 * */
public class Test08 {
  public static void main(String[] args) {
    //回环屏障
    CyclicBarrier cb = new CyclicBarrier(2);
    //裁判线程
    new Thread() {
      @Override
      public void run() {
        try {
          System.out.println("1.裁判:比赛开始");
          cb.await(); //2-1 = 1  线程阻塞
          System.out.println("4.裁判:吹哨");
          cb.await(); //1-1 = 0 代码继续执行
          System.out.println("5.裁判:出示红牌");
          cb.await(); //2-1 = 1 阻塞
        } catch (InterruptedException e) {
          e.printStackTrace();
        } catch (BrokenBarrierException e) {
          e.printStackTrace();
        }
      }
    }.start();
    //球员线程
    new Thread() {
      @Override
      public void run() {
        try {
          System.out.println("2.球员:开始行动");
          cb.await(); // 1-1 = 0  代码继续执行
          System.out.println("3.球员:球员犯规");
          cb.await(); //2-1 = 1 线程阻塞
          System.out.println("6.球员:接受红牌");
          cb.await(); //1-1=0 代码继续执行
        } catch (InterruptedException e) {
          e.printStackTrace();
        } catch (BrokenBarrierException e) {
          e.printStackTrace();
        }
      }
    }.start();
  }
}

4. Semaphore 信号量

CountDownLatch和CyclicBarrier的计数器递减的,而Semaphore的计数器是可加可减的,并可指定计数器的初始值,并且不需要事先确定同步线程的个数,等到需要同步的地方指定个数即可。且Semaphore也具有回环重置的功能,这一点和CyclicBarrier很像。底层也是基于AQS。

语法:

创建:

Semaphore sp= new Semaphore(数字);

获取信号量的值:

int i = sp.availablePermits();

增加信号量:

//信号量+1  
sp.release();  
//信号量+n
sp.release(n); 

减少信号量:

sp.acquire(); //信号量-1,无返回值 
sp.tryAcquire(); //信号量-1,有返回值

sp.acquire(n); //信号量-n,无返回值 
sp.tryAcquire(n); //信号量-n,有返回值
4.1 代码实现:
public class Test09 {
  public static void main(String[] args) {
    try {
      Semaphore sp = new Semaphore(10);
      System.out.println(sp.availablePermits()); //当前信号量的值 10
      sp.release(); //信号量+1
      System.out.println(sp.availablePermits()); //当前信号量的值 11
      sp.release(10); //信号量+10
      System.out.println(sp.availablePermits()); //当前信号量的值 21
      sp.acquire(); //信号量-1
      System.out.println(sp.availablePermits()); //当前信号量的值 20
      boolean b = sp.tryAcquire(15);
      System.out.println(b+"-"+sp.availablePermits()); //当前信号量的值 true-5,值若小于0,返回0。
      /*
       * acquire(n):信号量不足 <0,阻塞等待
       * sp.tryAcquire(n):信号量不足 <0,返回false,继续执行
       * */
      sp.acquire(5); 
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

八、并发集合类

1. 介绍

并发集合类:主要是提供线程安全的集合。

比如:

​ 1. ArrayList对应的并发类是CopyOnWriteArrayList

​ 2. HashSet对应的并发类是 CopyOnWriteArraySet

​ 3. HashMap对应的并发类是ConcurrentHashMap

这些类的方法API和之前学习的ArrayList、HashSet、HashMap的API是相同的,所以重在实现原理上,而不是API的使用上。

2. CopyOnWriteArrayList

2.1 ArrayList

ArrayList是最常用的集合之一,大小不固定,可以随着元素的增多可以自动扩容。

储存的数据为有序,可重复. 底层实现是基于数组,线程不安全。

2.2. CopyOnWriteArrayList

使用方式和ArrayList相同, 当时CopyOnWriteArrayList线程为安全的。

写时复制

​ 通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后往新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。

​ 这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

​ 对于读操作远远多于写操作的应用非常适合,特别在并发情况下,可以提供高性能的并发读取。

​ CopyOnWrite容器只能保证数据的最终一致性,不能保证数据实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
<Alt

2.3 CopyOnWriteArrayList重点源码
public class CopyOnWriteArrayList<E> implements List<E>RandomAccess, Cloneable, java.io.Serializable {
  //创建不可改变的对象
  final transient Object lock = new Object();
  //volatile修饰的Object类型的数组, 保证了数组的可见性,有序性
  private transient volatile Object[] array;

  //获取元素,根据下标获取元素,支持多线程查询
  public E get(int index) {
    return elementAt(getArray(), index);
  }   

  //设置数组
  final void setArray(Object[] a) {
    array = a;
  }

  //添加元素,写时复制
  public boolean add(E e) {
    //加锁
    synchronized (lock) {
      //获取当前数组
      Object[] es = getArray();
      //获取数组的长度
      int len = es.length;
      //复制旧数组,长度+1,创建一个新数组
      es = Arrays.copyOf(es, len + 1);
      //根据下标,将添加的元素放入
      es[len] = e;
      //将新数组设置为当前的数组
      setArray(es);
      return true;
    }
  }

  //修改元素
  public E set(int index, E element) {
    //加锁
    synchronized (lock) {
      //获取当前数组
      Object[] es = getArray();
      //根据传递的下标,获取数组中的元素
      E oldValue = elementAt(es, index);
      //数组中该下标存储的元素和修改的元素不一致
      if (oldValue != element) {
        es = es.clone();
        //修改元素
        es[index] = element;
      }
      //将新数组设置为当前的数组
      setArray(es);
      return oldValue;
    }
  }
	
  //删除元素
  public E remove(int index) {
    //加锁
    synchronized (lock) {
      //获取当前数组
      Object[] es = getArray();
      //获取数组长度
      int len = es.length;
      //根据传递的下标,获取数组中的元素
      E oldValue = elementAt(es, index);
      int numMoved = len - index - 1;
      Object[] newElements;
      //最有一个元素
      if (numMoved == 0)
        newElements = Arrays.copyOf(es, len - 1);
      else {
        newElements = new Object[len - 1];
        System.arraycopy(es, 0, newElements, 0, index);
        System.arraycopy(es, index + 1, newElements, index,
                         numMoved);
      }
      setArray(newElements);
      return oldValue;
    }
  }
}

3. CopyOnWriteArraySet源码分析

3.1 HashSet

HashSet无序,无下标,元素不可重复的集合,线程不安全,底层实现为(HashMap)。

3.2 CopyOnWriteArraySet

它是线程安全的HashSet,CopyOnWriteArraySet则是通过"动态数组(CopyOnWriteArrayList)"实现的,并不是散列表

CopyOnWriteArraySet在CopyOnWriteArrayList 的基础上使用了Java的装饰模式,所以底层是相同的。而CopyOnWriteArrayList本质是个动态数组队列,所以CopyOnWriteArraySet相当于通过动态数组实现的Set,CopyOnWriteArrayList中允许有重复的元素;但CopyOnWriteArraySet是一个Set集合,所以它不能有重复数据。因此,CopyOnWriteArrayList额外提供了addIfAbsent()和addAllAbsent()这两个添加元素的API,通过这些API来添加元素时,只有当元素不存在时才执行添加操作!

3.3 CopyOnWriteArraySet重点源码
public class CopyOnWriteArraySet<E> extends AbstractSet<E> implements java.io.Serializable {
  //声明CopyOnWriteArrayList
  private final CopyOnWriteArrayList<E> al;
  //无参构造方法
  public CopyOnWriteArraySet() {
    al = new CopyOnWriteArrayList<E>();
  }

  //添加元素
  public boolean add(E e) {
    return al.addIfAbsent(e);
  }
}
public class CopyOnWriteArrayList<E> implements List<E>RandomAccess, Cloneable, java.io.Serializable {
  private transient volatile Object[] array;

  //获取当前的数组
  final Object[] getArray() {
    return array;
  }

  public boolean addIfAbsent(E e) {
    Object[] snapshot = getArray();
    /*
     * &&逻辑与,只要第一个返回false,直接返回false
     *   第一个判断,当前添加的元素是否存在:
     *       存在返回这个元素的下标
     *       不存在返回-1
     *
     *   第二个判断,如果不存在添加元素
     *       添加成功,true
     *       添加失败,false
     * */
    return indexOfRange(e, snapshot, 0, snapshot.length) < 0
      && addIfAbsent(e, snapshot);
  }
  
  private boolean addIfAbsent(E e, Object[] snapshot) {
    //加锁
    synchronized (lock) {
      //获取数组
      Object[] current = getArray();
      //获取数组长度
      int len = current.length;
      //数组发生过修改
      if (snapshot != current) {
        // Optimize for lost race to another addXXX operation
        int common = Math.min(snapshot.length, len);
        for (int i = 0; i < common; i++)
          if (current[i] != snapshot[i]
              && Objects.equals(e, current[i]))
            return false;
        if (indexOfRange(e, current, common, len) >= 0)
          return false;
      }
      //数组没有发生修改 数组拷贝,原数组长度+1
      Object[] newElements = Arrays.copyOf(current, len + 1);
      //添加元素
      newElements[len] = e;
      //新数组替换原数组
      setArray(newElements);
      return true;
    }
  }
}

4. ConcurrentHashMap

4.1 HashMap

HashMap也是使用非常多的集合,线程不安全,以key-value的形式存在。

在HashMap中,底层实现为哈希表,系统会根据hash算法来计算key的存储位置,我们可以通过key快速地存、取value,允许一个key-value为null

​ 1. HashMap JDk1.7以及1.7之前

​ HashMap 底层是基于** 数组+链表** 组成的

​ 头插

​ 2. HashMap JDk1.8以及1.8之后

​ HashMap 底层是基于 数组+链表+红黑树 组成的,当 Hash 冲突严重时,在数组上形成的链表会变的越来越长,这样在查询时的效率就会越来越低,达到一定的条件,就会由链表转换为红黑树,提高查询的效率

​ 尾插

4.2 HashTable

HashTable和HashMap的实现原理几乎一样,差别无非是

​ 1. HashTable不允许key和value为null

​ 2. HashTable是线程安全的,但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时,只要有一个线程访问操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差

Alt

4.3 ConcurrentHashMap1.7及之前

ConcurrentHashMap采用了非常精妙的"分段锁"策略。

Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,使用多个锁来控制对hash表的不同部分(段segment)进行的修改,如果多个修改操作发生在不同的段上,他们就可以并发进行,从而提高了效率。
Alt

4.4 ConcurrentHashMap1.8及之后

ConcurrentHashMap在JDK8中进行了巨大改动。它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用synchronized + CAS,如果没有出现hash冲突,使用CAS直接添加数据,只有出现hash冲突的时候才会使用同步锁添加数据,又提升了效率,它底层由"数组"+链表+红黑树的方式思想(JDK8中HashMap的实现), 为了做到并发,又增加了很多辅助的类,例如TreeBin,Traverser等对象内部类。
Alt

4.5 ConcurrentHashMap1.8及之后重点源码
final V putVal(K key, V value, boolean onlyIfAbsent) {
  //key-value都为空, 抛出异常
  if (key == null || value == null) throw new NullPointerException();
  //计算key的hash值
  int hash = spread(key.hashCode());
  /*
   * 使用链表保存时,binCount记录结点数;
   */
  int binCount = 0;
  //循环遍数组
  for (Node<K,V>[] tab = table;;) {
    Node<K,V> f; int n, i, fh; K fk; V fv;
    //判断当前桶是否为空,空的就需要初始化
    if (tab == null || (n = tab.length) == 0)
      tab = initTable();
    //计算 key 的 hash 值,通过(n - 1) & hash计算key存放的位置, 存储的位置为空,使用cas直接插入
    else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
      if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
        break;                   // no lock when adding to empty bin
    }
    //发现是ForwardingNode结点,说明此时table正在扩容,则尝试协助数据迁移
    else if ((fh = f.hash) == MOVED)
      tab = helpTransfer(tab, f);
    else if (onlyIfAbsent // check first node without acquiring lock
             && fh == hash
             && ((fk = f.key) == key || (fk != null && key.equals(fk)))
             && (fv = f.val) != null)
      return fv;
    else { //出现hash冲突,也就是table[i]桶中已经曾经添加了Node节点,加锁,添加数据
      V oldVal = null;
      synchronized (f) {
        if (tabAt(tab, i) == f) {
          if (fh >= 0) {
            binCount = 1;
            for (Node<K,V> e = f;; ++binCount) {
              K ek;
              // 出现hash冲突,就会找到“相等”的结点,判断是否需要更新value值
              if (e.hash == hash &&
                  ((ek = e.key) == key ||
                   (ek != null && key.equals(ek)))) {
                oldVal = e.val;
                if (!onlyIfAbsent)
                  e.val = value;
                break;
              }
              Node<K,V> pred = e;
              if ((e = e.next) == null) {
                //插入数据
                pred.next = new Node<K,V>(hash, key, value);
                break;
              }
            }
          }
          //如果当前桶为红黑树,那就要按照红黑树的方式写入数据
          else if (f instanceof TreeBin) {
            Node<K,V> p;
            binCount = 2;
            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                  value)) != null) {
              oldVal = p.val;
              if (!onlyIfAbsent)
                p.val = value;
            }
          }
          else if (f instanceof ReservationNode)
            throw new IllegalStateException("Recursive update");
        }
      }
      // 如果链表中节点个数达到阈值,数组长度大于64,链表转化为红黑树
      if (binCount != 0) {
        if (binCount >= TREEIFY_THRESHOLD)
          treeifyBin(tab, i);
        if (oldVal != null)
          return oldVal;
        break;
      }
    }
  }
  // 计数值加1
  addCount(1L, binCount);
  return null;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值