多线程-基础-1

线程安全:多个线程同时访问这个类时,始终返回正常的行为,称为线程安全。

1、Synchronized

1.1 多个线程一个锁

/**同步:synchronized
 *    同步的概念就是共享,我们要牢记“共享”,如果不是共享的资源就没必要进行同步
 * 异步:asynchronized
 *    异步就是独立,相互之间不受任何制约。就好像我们学习http的时候,在页面发起ajax请求
 *  我们还可以继续浏览页面或者操作页面的其他内容,二者之间没有任何关系。
 *
 * 同步的目的就是为了线程安全,其实对于线程安全来说,需要满足两个特性
 *   原子性(同步)
 *   可见性
 */

1.1.1 示例代码:

public class MyObject {
    public synchronized void method1(){
        try {
            System.out.println(Thread.currentThread().getName());
            Thread.sleep(4000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
    public   void method2(){
        try {
            System.out.println(Thread.currentThread().getName());
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    /**
     * 一个实例对象,他的方法加锁了,则这个锁是加在实例对象上的。
     *  ①:同一个对象在访问被synchronized修饰的代码块(方法)时,需要等待锁释放。(public synchronized void method1(),public synchronized void method2(),)
     *  ②:若同一个对象访问未被synchronized修饰的代码块(方法)时,无锁。(public synchronized void method1(),public void method2())
     * @param args
     */
    public static void main(String[] args) {
        MyObject t1 = new MyObject();
        new Thread(() -> t1.method1(),"t1").start();
        new Thread(() -> t1.method2(),"t2").start();
    }
}

    /**
     * 一个实例对象,他的方法加锁了,则这个锁是加在实例对象上的。
     *  ①:同一个对象在访问被synchronized修饰的代码块(方法)时,需要等待锁释放。(public synchronized void method1(),public synchronized void method2(),)
     *  ②:若同一个对象访问未被synchronized修饰的代码块(方法)时,无锁。(public synchronized void method1(),public void method2())
     * @param args
     */
    public static void main(String[] args) {
        MyObject t1 = new MyObject();
        new Thread(() -> t1.method1(),"t1").start();
        new Thread(() -> t1.method2(),"t2").start();
    }
}

1.1.2 实例说明:

 /**
 *实例总结:
 *    A线程先持有object对象的Lock锁,B线程如果在这个时候调用对象中的同步
 *  (synchronized)方法则需要等待,也就是同步
 *    A线程先持有object对象的Lock锁,B线程可以异步的方式调用对象中的非
 * (synchronized)修饰的方法。
 */

 

1.2 多个线程多个锁

/**
 * 多个线程多个锁:多个线程,每个线程都可以拿到自己指定的锁,分别获得锁之后,执行synchronized方法体的内容。
 */

1.2.1 示例代码:

public class MutiThread {
    /**
     * 全局的属性
     */
    private static int num = 0;
    public static synchronized void printNum(String tag){
        try {
           if (tag.equals("a")) {
               num = 100;
               System.out.println("tag a ,set num over ! ");
               Thread.sleep(4000);
           }else {
               num =  200;
               System.out.println("tag b, set num over !");
               Thread.sleep(100);
           }
            System.out.println("tag "+ tag + ", num = " + num);
          } catch (InterruptedException e) {
              e.printStackTrace();
      }
    }

 /**
     * ①:两个线程获取的是两个对象的锁,互不影响,不会有同步效果【public synchronized void printNum(String tag)】
     * ②:多个对象之间如果想要共用一把锁,需要类级别的锁(方法被static修饰)【public static synchronized void printNum(String tag)】
     * 主方法
     * @param args
     */
    public static void main(String[] args) {
        //定义两个不同的对象
        MutiThread m1 = new MutiThread();
        MutiThread m2 = new MutiThread();
        new Thread(()-> m1.printNum("a")).start();
        new Thread(()-> m2.printNum("b")).start();
    }
}
​

 

1.2.2 执行结果:

tag a ,set num over ! 
tag b, set num over !
tag b, num = 200
tag a, num = 200
​
Process finished with exit code 0

1.2.3 实例总结:

​
/**
 * 实例总结:
 *       ①:关键字synchronized取得的锁都是对象锁,而不是把一段代码(方法)当作锁。
 *   所以示例代码中那个线程先执行synchronized关键字的方法,那个线程就持有该方法
 *   所属对象的锁(Lock),两个对象,线程获得获得的就是两个不同对象的锁,他们互不影响。
 *       ②:有一种情况则是相同的锁,即在静态方法上加synchronized关键字,表示锁定.class类,
 *   类一级别的锁(独占.class类)。
 */

1.3 synchronized 锁对象选择:

1.3.1 尽量不要使用字符串常量作为锁对象,容易出现死循环

public class StringLock {
    public void method1(){
        String lock = new String("就这样吧");
        synchronized ("就这样吧")
        //synchronized (lock)
                           {
            try {
                while (true) {
                    System.out.println("当前线程:" + Thread.currentThread().getName() + "开始");
                    Thread.sleep(1000);
                    System.out.println("当前线程:" + Thread.currentThread().getName() + "结束");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
​
    public static void main(String[] args) {
       final StringLock stringLock = new StringLock();
        new Thread(() -> stringLock.method1(), "t1").start();
        new Thread(() -> stringLock.method1(), "t2").start();
    }
}
当前线程:t1开始
当前线程:t1结束
当前线程:t1开始
......

1.3.2 String作为锁对象,被重新赋值之后,对象引用发生变化,导致锁变化。

**
 * 字符串常量在内部发生变化,引用对象发生变化【System.out.println(lock.getBytes());】,引用的不是同一个对象,所以两个线程同时进入了
 */
public class ChangeLock {
    private String lock = "lock";
    private void method(){
        synchronized (lock) {
            try {
                System.out.println("当前线程:"+ Thread.currentThread().getName() +"开始");
                System.out.println(lock.getBytes());
                lock = "change lock";
                Thread.sleep(2000);
                System.out.println("当前线程: "+ Thread.currentThread().getName() +"结束");
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        ChangeLock changeLock = new ChangeLock();
        new Thread(() -> changeLock.method(), "t1").start();
        new Thread(() -> changeLock.method(), "t2").start();
    }
}
当前线程:t1开始
[B@8193e5
当前线程: t1结束
当前线程:t2开始
[B@25fef1
当前线程: t2结束

1.3.1 对象作为锁对象,即使对象内部属性发生变化,则引用的是同一对象。

/**
 * 对象内部属性发生变化,则引用的是同一对象
 */
public class ModifyObject {
    public String name;
    public int age;
​
    private synchronized void changeAttribute(String name ,int age){
        try {
            System.out.println("当前线程:"+ Thread.currentThread().getName() +"开始");
            this.name = name;
            this.age = age;
            Thread.sleep(2000);
            System.out.println("当前线程: "+ Thread.currentThread().getName() +"结束");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        ModifyObject modifyObject = new ModifyObject();
        new Thread(() -> modifyObject.changeAttribute("张三", 18), modifyObject.toString()).start();
        new Thread(() -> modifyObject.changeAttribute("李四",  25), modifyObject.toString()).start();
    }
}
当前线程:com.qiulin.study.thread.day01.ModifyObject@91c18f开始
当前线程: com.qiulin.study.thread.day01.ModifyObject@91c18f结束
当前线程:com.qiulin.study.thread.day01.ModifyObject@91c18f开始
当前线程: com.qiulin.study.thread.day01.ModifyObject@91c18f结束
​

1.4 锁重入:

1.4.1 方法里面调用被synchronized修饰的方法。

/**
 * synchorized锁重入
 */
public class SyncDubbo1 {
​
    public synchronized void  method1(){
        System.out.println("method1.....");
        method2();
    }
    public synchronized void method2(){
        System.out.println("method2.....");
        method3();
    }
    public synchronized void method3(){
        System.out.println("method3.....");
    }
    
    public static void main(String[] args) {
        SyncDubbo1 t1 = new SyncDubbo1();
        new Thread(() -> t1.method1()).start();
    }
}
method1.....
method2.....
method3.....

1.4.2 父子类

static class Parent{
        public int i = 10;
        public synchronized void operationSup(){
            try {
                i--;
                System.out.println("Parent print i = "+ i);
                Thread.sleep(100);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
​
    static class Child extends Parent{
        public synchronized void operationSub(){
            try {
                while (i>0){
                    i--;
                    System.out.println("Child print i =" + i);
                    Thread.sleep(100);
                    this.operationSup();
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
​
    public static void main(String[] args) {
        Child child = new Child();
        new Thread(() -> child.operationSub()).start();
    }
Child print i =2
Parent print i = 1
Child print i =0
Parent print i = -1

2. Volatile 关键字的主要作用是使变量在多个线程之间可见

/**
* 在java 中,每一个线程都会有一块内存区,其中存放着所有线程共享的主内存中的变量的拷贝。
* 当线程执行时,他在自己的工作内存中操作这些变量。为了存取一个共享的变量,
* 一个线程通常先获取锁并去清除它的内存工作区,把这些共享变量从所有线程的共享内存区中
* 正确的装入到他自己的工作内存中,当线程解锁时该工作内存中变量的值写回到共享内存中。
*
*一个线程可以执行的操作有使用(use)、赋值(assign)、装载(load)、存储(store)、
* 锁定(lock)、解锁(unlock)。
* 而主内存可以执行的操作有读(read)、写(write)、锁定(lock)、解锁(unlock),
* 每个操作都是原子的。
*
* volatile的作用就是强制线程到主内存(共享内存)里去读取变量,而不去线程工作内存区里去读取,
* 从而实现了多个线程之间的变量可见,也就是满足线程安全的可见性。
*/
public class VoliteRunThread  {
    private  boolean isRunning = true;
​
    private void setRunning(boolean isRunning) {
        this.isRunning = isRunning;
    }
​
    public void method() {
        System.out.println("进入run方法...");
        try {
            while (isRunning == true) {
                System.out.println("线程开始工作...");
                Thread.sleep(200);
            }
            System.out.println("线程结束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
​
    }
​
    public static void main(String[] args) throws InterruptedException {
        VoliteRunThread runThread = new VoliteRunThread();
        new  Thread(() -> runThread.method() ).start();
        Thread.sleep(1000);
        new  Thread(() -> runThread.setRunning(false)).start();
    }
}
进入run方法...
线程开始工作...
线程开始工作...
线程开始工作...
线程开始工作...
线程开始工作...
线程结束

3. 线程间通信

/**
 * 线程之间的通信:线程是操作系统中独立的个体,但这些个体如果 不经过特殊的处理就不能
 * 成为一个整体,线程间的通信就是成为整体的必用方式之一。当线程存在通信指挥,系统间的交互性
 * 会更强大,在提高CPU利用率的同时还会使开发人员对线程任务在处理的过程中进行
 * 有效的把控与监督。
 * <p>
 * 使用wait/notify方法实现线程间的通信。(注意这两个方法都是object的类的方法,换句话说
 * java 为所有的对象都提供这两个方法。)
 * <p>
 * 1. wait 和 notify 必须配合synchronized 关键字使用
 * 2. wait释放锁,notify方法不释放锁。
 */
3.1 volatile 变量修饰,while空轮询检测

public class ListAdd1 {
    private volatile List list = new ArrayList();
​
    public void listAdd() {
        list.add("我最帅");
    }
​
    public int getListSize() {
        return list.size();
    }
​
    /**
     * 1.线程间通信,不足:while 空轮询,浪费资源
     *
     * @param args
     */
    public static void main(String[] args) {
​
        ListAdd2 obj = new ListAdd2();
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + "添加一个元素...");
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                obj.listAdd();
            }
        }, "t1").start();
​
​
        new Thread(() -> {
            while (true) {
                if (obj.getListSize() == 2) {
                    System.out.println(Thread.currentThread().getName() + "中断了....");
                    throw new RuntimeException("线程中断....");
                }
            }
        }, "t2").start();
    }
}
t1添加一个元素...
t1添加一个元素...
t1添加一个元素...
t2中断了....
Exception in thread "t2" java.lang.RuntimeException: 线程中断....
    at com.qiulin.study.thread.day02.ListAdd1.lambda$main$1(ListAdd1.java:54)
    at java.lang.Thread.run(Thread.java:745)
t1添加一个元素...
t1添加一个元素...

3.1.2 使用wait /notify 方式实现【缺点:锁被占据,做不到实时通信(必须等到方法执行完毕,锁释放后,线程2才中断)】

/**
 * 使用wait /notify  方式实现
 * <p>
 * 缺点:锁被占据,做不到实时通信【必须等到方法执行完毕,锁释放后,线程2才中断】
 */
public class ListAdd2 {
    private volatile List list = new ArrayList();
​
    public void listAdd() {
        list.add("我最帅");
    }
​
    public int getListSize() {
        return list.size();
    }
​
    public static void main(String[] args) {
        ListAdd1 list2 = new ListAdd1();
        Object lock = new Object();
​
        new Thread(() -> {
            try {
                synchronized (lock) {
                    if (list2.getListSize() != 5) {
                        lock.wait();
                    }
                    System.out.println(Thread.currentThread().getName() + "中断了....");
                    throw new RuntimeException("线程中断....");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2").start();
​
        new Thread(() -> {
            try {
                synchronized (lock) {
                    for (int i = 0; i < 10; i++) {
                        list2.listAdd();
                        System.out.println(Thread.currentThread().getName() + "添加一个元素...");
                        Thread.sleep(100);
                        if (list2.getListSize() == 5) {
                            System.out.println("发出通知....");
                            lock.notify();
                        }
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1").start();
​
    }
}

3.1.3 CountDownLatch【内部以计数器控制】CountDownLatch的两个关键方法 await 和 countDown 的定义,其实CountDownLatch只是简单的利用了 AQS 的 state 属性(表示锁可重入的次数),CountDownLatch 的内部类 sync 重写了 AQS 的 tryAcquireShared,CountDownLatch 的 tryAcquireShared 方法的定义是:

public int tryAcquireShared(int acquires) {
    return getState() == 0? 1 : -1;
}

state的初始值就是初始化 CountDownLatch 时的计数器,在 sync 调用 AQS 的 acquireSharedInterruptibly的时候会判断 tryAcquireShared(int acquires) 是否大于 0,如果小于 0,会将线程挂起。

/**
 * 实时通信  CountDownLatch
 */
public class ListAdd3 {
    private volatile List list = new ArrayList();
​
    public void listAdd() {
        list.add("我最帅");
    }
​
    public int getListSize() {
        return list.size();
    }
​
    public static void main(String[] args) {
        ListAdd3 list2 = new ListAdd3();
        CountDownLatch downLatch = new CountDownLatch(1);//计数器减为0时,阻塞结束
​
        new Thread(() -> {
            try {
                if (list2.getListSize() != 2) {
                   downLatch.await();
                }
                System.out.println(Thread.currentThread().getName() + "中断了....");
                throw new RuntimeException("线程中断....");
​
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2").start();
        
        new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    list2.listAdd();
                    System.out.println(Thread.currentThread().getName() + "添加一个元素...");
                    Thread.sleep(100);
                    if (list2.getListSize() == 2) {
                        System.out.println("发出通知....");
                        downLatch.countDown();
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1").start();
​
    }
}
t1添加一个元素...
t1添加一个元素...
发出通知....
t1添加一个元素...
t2中断了....
Exception in thread "t2" java.lang.RuntimeException: 线程中断....
    at com.qiulin.study.thread.day02.ListAdd3.lambda$main$0(ListAdd3.java:31)
    at java.lang.Thread.run(Thread.java:745)
t1添加一个元素...
t1添加一个元素...

4. 使用wait 和notify 实现一个阻塞队列

 
/**
 * 使用wait 和notify 实现一个阻塞队列
 *
 * 1.需要一个承装元素的集合
 */
public class MyQueue {
    // 1.需要一个承装元素的集合
    private final LinkedList<Object> list = new LinkedList<>();
​
    //2.需要一个计数器
    private AtomicInteger count = new AtomicInteger();
​
    //3. 需要制定上限和下限
    private int minSize = 0;
    //上限可以自定义指定
    private int maxSize ;
​
    //4.构造方法
    public MyQueue(int size){
        this.maxSize = size;
    }
​
    //5.初始化一个对象,用于加锁
    private final  Object lock = new Object();
​
    //put(将对象加入到队列)
    public void put(Object obj){
        synchronized (lock){
            while (maxSize == count.get()){  //当前容器已经满了
                try {
                    lock.wait();
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
            //1.加入元素
            list.add(obj);
            //2.计数器累加
            count.incrementAndGet();
            System.out.println("新加入的元素"+obj);
            //3.通知其他线程【容器满载的情况】
            lock.notify();
        }
    }
​
    //get(从队列中获取对象)
    public  Object take(){
        Object result = null;
        synchronized (lock){
            while (count.get() == this.minSize ){
                try {
                    lock.wait();
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
                //1.移除元素
                result = list.removeFirst();
                System.out.println("移除的元素为:"+result);
                //2.计数器递减
                count.decrementAndGet();
                //3.唤醒其他线程【容器为空的时候】
                lock.notify();
​
        }
        return result;
    }
​
    public int getSize(){
        return list.size();
    }
​
    public static void main(String[] args) {
        MyQueue myQueue = new MyQueue(5);
        myQueue.put("a");
        myQueue.put("b");
        myQueue.put("c");
        myQueue.put("d");
        myQueue.put("e");
        System.out.println("当前队列长度:"+ myQueue.getSize());
​
        new Thread(() -> myQueue.put("f"),"t1").start();
        new Thread(() ->myQueue.take(),"t2").start();
    }
}
新加入的元素a
新加入的元素b
新加入的元素c
新加入的元素d
新加入的元素e
当前队列长度:5
移除的元素为:a
新加入的元素f

5、ThreadLocal

/**
 * ThreadLocal:线程局部变量,是一种多线程并发访问变量的解决方案。与其synchronized
 * 等加锁的方式不同,ThreadLocal完全不提供锁,而使用空间换时间的手段,
 * 为每个线程提供独立副本,以保证线程安全。
 *
 * 从性能上说,ThreadLocal不具有绝对的优势,在并发不是很高的情况下,加锁的性能
 * 会更好,但是作为一套与锁完全无关的解决方案,在并发量或者竞争激烈的额场景,使用
 * ThreadLocal可以在一定程度上减少锁竞争。
 *
 * 加锁:以时间换空间
 * ThreadLocal:以空间换时间
 */
public class ThreadLocalDemo {
    private static  ThreadLocal<String> threadLocal = new ThreadLocal<>();
​
    public void setValue(String value){
        threadLocal.set(value);
    }
​
    public String getValue(){
       return threadLocal.get();
    }
​
    public static void main(String[] args) {
        ThreadLocalDemo localDemo = new ThreadLocalDemo();
​
        new Thread(() ->{
            localDemo.setValue("张三");
            System.out.println("当前线程"+Thread.currentThread().getName()+",ThreadLocal值为:"+localDemo.getValue());
        },"t1").start();
​
        new Thread(() ->{
            System.out.println("当前线程"+Thread.currentThread().getName()+",ThreadLocal值为:"+localDemo.getValue());
        },"t2").start();
    }
}
结果:
当前线程t1,ThreadLocal值为:张三
当前线程t2,ThreadLocal值为:null
​

6、单例模式

/**
 * 单例模式:最常见的额就是饥饿模式和懒汉模式
 * 饥饿模式:直接实例化对象,类加载的时候创建出来
 * 懒汉模式:在调方法时进行实例化对象
 *
 * 在多线程模式中,考虑到性能和线程安全问题,我们一般选择下面两种
 * 比较经典的单例模式,在性能提高的同时,又保证了线程安全。
 *
 * ① dubble check instance 双重空值判断
 * ② static inner class 内部类
 */

6.1 、懒汉模式

/**
 * 懒汉模式:在调用方法的时候被创建出来
 */
public class LazySingleton {
    private static LazySingleton instance = null;
    public LazySingleton(){
​
    }
    public static LazySingleton getInstance(){
        if (instance == null){
            instance = new LazySingleton();
        }
        return instance;
    }
}

​6.2、 饿汉模式

/**
 * 饿汉模式:在类被加载时创建
 */
public class HungrySingleton {
    private static HungrySingleton instance = new HungrySingleton();
    private HungrySingleton(){
​
    }
    public static HungrySingleton getInstance(){
        return instance;
    }
}

6.3、双重空值判断

  • /**
     * 双重检测
     */
    public class DubbleSingleton {
        private static DubbleSingleton  instance;
    ​
        public static DubbleSingleton getInstance(){
            if (instance == null){
                try {
                    //模拟初始化对象准备的时间
                    Thread.sleep(3000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                synchronized (DubbleSingleton.class){
                   /* if (instance == null) {
                        instance = new DubbleSingleton();
                    }*/
                    instance = new DubbleSingleton();
            }
            }
            return instance;
        }
    ​
        public static void main(String[] args) {
            new Thread(()->{
                System.out.println(DubbleSingleton.getInstance().hashCode());
            },"t1").start();
    ​
            new Thread(()->{
                System.out.println(DubbleSingleton.getInstance().hashCode());
            },"t2").start();
        }
        
    26103236
    21414242

    ​6.4、静态内部类

/**
 * 静态内部类实现单例
 */
public class InnerSingleton {
    private static class Singleton{
        private static InnerSingleton instance = new InnerSingleton();
    }
    public static InnerSingleton getInstance(){
        return Singleton.instance;
    }
}

​7、同步容器类

        同步容器类都是线程安全的,但是在某些场景下可能需要加锁来保护符合操作。复合类操作如:迭代(反复访问某个元素,遍历完容器中所有的元素)、跳转(根据指定的顺序找到当前元素的下一个元素)、以及条件运算。这些复合操作在多线程并发地修改容器时,可能会表现出意外的行为,最经典的是ConcurrentModificationException,原因是当容器迭代的过程中,被并发的修改了内容,这是由于早期迭代器设计的时候并没有考虑并发修改的问题。

        同步类容器:如Vector、HashTable。这些容器的同步功能其实都是JDK的Collections.synchronized***等工厂方法去创建实现的。其底层的机制无非就是用传统的synchronized关键字对每个共用的方法都进行同步,使得每次只能有一个线程访问容器的状态。这很明显不满足高并发的需求,在保证县城安全的同时,也必须要保证性能。

7.1、将非线程安全的容器转换成线程安全的容器,只需要在容器外面使用Collections.sysnchronized***包装即可。返回的容器为同步类容器。

 HashMap<Integer,String> map = new HashMap();
  Map<Integer, String> synchronizedMap = Collections.synchronizedMap(map);

8、并发类容器

        Jdk5.0之后提供了许多种并发类容器来替代同步类容器从而改善性能。同步类容器的状态都是串行化的。他们虽然实现了线程安全,但是严重降低了并发性,在多线程环境下,严重降低了应用程序的吞吐量。

        并发类容器是专门针对并发设计的,使用ConcurrentHashMap来代替HashTable,而且在ConcurrentHashMap中,添加了一些常见的复合操作的支持。以及使用了CopyOnWriteArrayList代替Vecor,并发的CopyonWriteArraySet,以及并发的Queue,ConcurrentLinkedQueue和LinkedBlockQueue,前者是高性能的队列,后者是阻塞形式的队列。还有ArrayBlockingQueue、PriortyQueue、SynchronousQueue等。

8.1、ConcurrentMap 接口有两个重要的实现:

8.1.1、ConcurrentHashMap

        ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的HashTable,他们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。把一个整体分成了16个段(Segment)。也就是最高支持16个线程并发修改操作。这也是在多线程场景时减小锁的力度从而降低锁竞争的一种方案。并且代码中大多共享变量使用Volatile关键字声明,目的是第一时间获取修改的内容。

8.1.2、ConcurrentSkipListMap(支持并发排序功能,弥补ConcurrentHashMap),类似于TreeMap。

8.2、Copy-On-Write容器,可以在非常多的并发场景中用到。(读多写少场景合适,写需要copy,耗时)

Copy-On-Write容器,其实是一种用于程序设计中的优化策略。

8.2.1、CopyOnWriteArrayList

8.2.2、CopyOnWriteArraySet

        CopyOnWrite容器即写时复制的容器。我们再往容器中添加一个元素时,不能直接往容器里添加,而是先将容器进行Copy,复制出一个新的容器,然后新的容器再添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写在不同的容器。

  • 注意:*在多个写(add,remove)的时候,其实方法里面是有重入锁保证了数据的一致性。*

9、并发Queue

9.1、ConcurrentLinkedQueue:高性能队列

        适用于高并发场景,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue性能好于BlockingQueue。它是一个基于链接节点的无界线程安全的队列。遵循先进先出原则。队列不允许null元素。

9.1.1 重要方法:

add()/offer() 添加元素,在ConcurrentLinkedQueue中,这两个方法没有任何区别。

poll()/peek() 取出元素,poll()会队列中删除元素,peek()不会删除。

9.2、BlockingQueue:阻塞队列

9.2.1 ArrayBlockQueue

        基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长的数组,以便缓存队列中的数据对象,其内部没实现读写分离,也就意味着生产和消费不能完全并行,长度是需要定义的,可以制定先进先出或者先进后出,也叫有界队列。

9.2.2 LinkedBlockingQueue

        基于链表的阻塞队列,通ArrayBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),LinkedBlockingQueue之所以能够高效的并发处理数据,是因为其内部实现采用分离锁(读写分离两个锁),从而实现生产者和消费者操作的完全并行运行。他是一个无界队列。

9.2.3 SynchronousQueue

        一种没有缓冲的队列,生产者产生的数据直接被消费者获取并消费。SynchronousQueue 它是一个对于元素来说空了才能存入,存在才能取出的队列,只保留一个元素在queue里。

/** 
注意1:它一种阻塞队列,其中每个 put 必须等待一个 take,反之亦然。
    同步队列没有任何内部容量,甚至连一个队列的容量都没有。
注意2:它是线程安全的,是阻塞的。
注意3:不允许使用 null 元素。
注意4:公平排序策略是指调用put的线程之间,或take的线程之间。
公平排序策略可以查考ArrayBlockingQueue中的公平策略。
注意5:SynchronousQueue的以下方法很有趣:
    * iterator() 永远返回空,因为里面没东西。
    * peek() 永远返回null。
    * put() 往queue放进去一个element以后就一直wait直到有其他thread进来把这个element取走。
    * offer() 往queue里放一个element后立即返回,如果碰巧这个element被另一个thread取走了,offer方法返回true,认为offer成功;否则返回false。
    * offer(2000, TimeUnit.SECONDS) 往queue里放一个element但是等待指定的时间后才返回,返回的逻辑和offer()方法一样。
    * take() 取出并且remove掉queue里的element(认为是在queue里的。。。),取不到东西他会一直等。
    * poll() 取出并且remove掉queue里的element(认为是在queue里的。。。),只有到碰巧另外一个线程正在往queue里offer数据或者put数据的时候,该方法才会取到东西。否则立即返回null。
    * poll(2000, TimeUnit.SECONDS) 等待指定的时间然后取出并且remove掉queue里的element,其实就是再等其他的thread来往里塞。
    * isEmpty()永远是true。
    * remainingCapacity() 永远是0。
    * remove()和removeAll() 永远是false。
 **/
SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>();
​
            new Thread(()->{
                try {
                    for (int i=0 ; i< 5; i++){
                        String s = UUID.randomUUID().toString();
                        synchronousQueue.put(s);
                        System.out.println("生产一个数据:" + s);
                        Thread.sleep(1000);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
​
​
            new Thread(() -> {
                while (true) {
                    try {
                        Thread.sleep(500);
                        System.out.println("消费数据:" + synchronousQueue.take());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
​
​
消费数据:cb02c171-775d-4389-a5d4-b24358e66dff
生产一个数据:cb02c171-775d-4389-a5d4-b24358e66dff
生产一个数据:e89fed09-a57c-4a15-9433-cd5eef0be956
消费数据:e89fed09-a57c-4a15-9433-cd5eef0be956
生产一个数据:ea6725a7-de26-4ec6-abab-2f2181a0c921
消费数据:ea6725a7-de26-4ec6-abab-2f2181a0c921
生产一个数据:35a6eef6-805f-4422-ad2f-f876f35f10c8
消费数据:35a6eef6-805f-4422-ad2f-f876f35f10c8
生产一个数据:ac74809f-647e-421a-9191-28e726311d82
消费数据:ac74809f-647e-421a-9191-28e726311d82

9.3.4 PriorityBlockingQueue

        基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定,也就是说传入的对象必须实现Comparable接口),在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁,他是一个无界队列。

/**
*注意:队列添加元素时,先将元素添加到数组末尾,在取出元素时,采用“上冒”的方式将该元素尽量往上冒。即:在添加的时候元素没有排序,在取出的时候,元素按照类似冒泡的方式进行排序。
*/
public static void main(String[] args) {
        PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
​
​
        queue.add(new Task(1, "task1"));
        queue.add(new Task(3, "task3"));
        queue.add(new Task(2, "task2"));
        queue.add(new Task(4,"task4"));
        System.out.println(queue.toString());
        System.out.println("取出第一个元素:"+queue.poll());
        System.out.println(queue.toString());
    }
​
​
    static class Task implements Comparable<Task>{
      private int id;
      private String name;
​
        public int getId() {
            return id;
        }
​
        public void setId(int id) {
            this.id = id;
        }
​
        public String getName() {
            return name;
        }
​
        public void setName(String name) {
            this.name = name;
        }
​
        public Task(int id, String name) {
            this.id = id;
            this.name = name;
        }
​
        @Override
        public int compareTo(Task task) {
            return this.id>task.id?1 :(this.id ==  task.id ? 0 : -1);
        }
​
        @Override
        public String toString() {
            return "Task{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    
[Task{id=1, name='task1'}, Task{id=3, name='task3'}, Task{id=2, name='task2'}, Task{id=4, name='task4'}]
取出第一个元素:Task{id=1, name='task1'}
[Task{id=2, name='task2'}, Task{id=3, name='task3'}, Task{id=4, name='task4'}]

9.3.5 DalayQueue

       带有延时时间的Queue,其中的元素只有当其指定的延时时间到了,才能够从队列中获取到该元素。DelayQueue中的元素必须实现Delayed接口,DelayedQueue是一个没有大小限制的队列,应用场景很多,如:对缓存超时的数据进行移除、任务超时处理、空闲连接的关闭等。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章