Java 并发

目录

一, 认识线程

1.1 进程和线程

1.2 线程的优点

1.3 进程与线程的区别

二、使用线程

2.1 创建线程

2.2 线程状态

2.3 常用的线程方法

 2.4 常用的线程属性

三、线程安全

3.1 线程不安全的例子:

3.2 原子性

3.3 可见性

3.4 wait 和 notify

四、多线程实案例

4.1 单例模式

4.2 阻塞队列

4.3 定时器 

4.4 线程池

五 常见锁策略

5.1 乐观锁 vs 悲观锁

5.2 读写锁

5.3 重量级锁 vs 轻量级锁

5.4 自旋锁 vs 挂起等待锁

5.5 公平锁 vs 非公平锁

5.6 可重入锁 vs 不可重入锁

六 CAS

6.1 什么是CAS

6.2 CAS 应用

6.3 ABA 问题

七 Synchronized 原理

7.1 特性:

7.2 加锁工作过程

7.3 其他的优化操作

八 Callable 接口

8.1 Callable 的用法

九 Java.util.concurrent 的常见类

9.1 ReentrantLock

9.2 原子类

9.3 信号量 Semaphore

9.4 CountDownLatch

十 线程安全的集合类

10.1 CopyOnWriteArrayList

10.2 队列:

10.3 哈希表:


一, 认识线程

1.1 进程和线程

        1) 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。

        2) 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.

        3) 进程是系统分配资源的最小单位,线程是系统调度的最小单位。

        4) 线程比进程更轻量

进程与线程图:

1.2 线程的优点

        1. 创建一个新线程的代价要比创建一个新进程小得多
        2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
        3. 线程占用的资源要比进程少很多
        4. 能充分利用多处理器的可并行数量
        5. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
        6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
        7. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。


1.3 进程与线程的区别

        1. 进程是系统进行资源分配和调度的一个独立单位,线程是程序执行的最小单位。
        2. 进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈。
        3. 由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信。
        4. 线程的创建、切换及终止效率更高 

二、使用线程

2.1 创建线程

方法1 匿名内部类创建 Thread 子类对象
// 使用匿名类创建 Thread 子类对象
Thread t1 = new Thread() {
    @Override
    public void run() {
        System.out.println("使用匿名类创建 Thread 子类对象");
   }
};

 方法2 匿名内部类创建 Runnable 子类对象

// 使用匿名类创建 Runnable 子类对象
Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("使用匿名类创建 Runnable 子类对象");
   }
});

方法3 lambda 表达式创建 Runnable 子类对象

// 使用 lambda 表达式创建 Runnable 子类对象
Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象"));

2.2 线程状态

        线程的状态是一个枚举类型 Thread.State 

示例:

public class ThreadState {
    public static void main(String[] args) {
        for (Thread.State state : Thread.State.values()) {
            System.out.println(state);
       }
   }
}

1) New 新建线程

2) Runable 可运行

3) Blocked 阻塞 

4) Waiting 等待

5) Timed Waiting 计时等待

6) Terminated 终止

获得线程状态:

        getState()

图:

2.3 常用的线程方法

启动线程---start()

        新建的线程调用start() 方法即进入可运行状态。

备注: 

        直接调用run()方法只会在同一个线程中执行任务,但并没有启动新的线程。start()方法会创建一个执行run方法的新线程。

等待一个线程 -- join()

         等待一个线程完成它的工作后,才能进行自己的下一步工作。

示例:

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Runnable target = () -> {
            System.out.println(Thread.currentThread().getName() + ": 我结束了!");
        };
        Thread thread1 = new Thread(target, "李四");
        Thread thread2 = new Thread(target, "王五");
        System.out.println("先让李四开始工作");
        thread1.start();
        thread1.join();
        System.out.println("李四工作结束了,让王五开始工作");
        thread2.start();
        thread2.join();
        System.out.println("王五工作结束了");
   }
}

 休眠当前线程 -- Thread.sleep():

        线程的调度是不可控的,所以,这个方法只能保证实 际休眠时间是大于等于参数设置的休眠时间

示例:

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(3 * 1000);
   }
}

Thread.yield() 方法:

        使当前正在执行的线程交出运行权。

 2.4 常用的线程属性

线程引用:

        Thread.currentThread(): 获取当前线程引用

示例:

public class ThreadDemo {
    public static void main(String[] args) {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName());
   }
}

中断属性:

       Thread.currentThread.isInterrupted():  获得当前线程是否设置了中断状态

示例:

while(!Thread.currentThread().isInterrupted() && 自定义标志位){
    do more work
}

        如果线程被阻塞, 就无法检查中断状态, 即会抛出InterruptedException 异常。如: 使用sleep()方法时, 当设置为中断状态时线程不会休眠, 而是清除中断状态并抛出异常, 因此,如此循环中使用了sleep, 则不要检测中断状态, 而是捕获异常

示例:

    private static class MyRunnable implements Runnable {
        @Override
        public void run() {
            // 两种方法均可以
            while (!Thread.interrupted()) {
            //while (!Thread.currentThread().isInterrupted()) {
                System.out.println(Thread.currentThread().getName()
                        + ": 别管我,我忙着转账呢!");
                try {
                    Thread.sleep(1000);
               } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println(Thread.currentThread().getName()
                            + ": 有内鬼,终止交易!");
                    // 注意此处的 break
                    //Thread.currentThread().interrupt();  //设置为中断
                    break;
               }
           }
            System.out.println(Thread.currentThread().getName()
                    + ": 啊!险些误了大事");
       }
   }
    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "李四");
        System.out.println(Thread.currentThread().getName()
                + ": 让李四开始转账。");
        thread.start();
        Thread.sleep(10 * 1000);
        System.out.println(Thread.currentThread().getName()
                + ": 老板来电话了,得赶紧通知李四对方是个骗子!");
        thread.interrupt();
   }
}

Thread.interrupted() 与 isInterrupted() 区别:

        前者为类对象,调用后将会重置中断状态为false, 后者是线程对象的属性, 调用不改变中断状态。

线程名:

        setName()方法。

线程优先级:

        setPriority()方法可以设置线程的优先级, 默认优先级为5 (NORM_PRIORITY)。

三、线程安全

3.1 线程不安全的例子:

static class Counter {
    public int count = 0;
    void increase() {
        count++;
   }
}
public static void main(String[] args) throws InterruptedException {
    final Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
       }
   });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
       }
   });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(counter.count);
}

不安全原因: 没有保证账户的count的原子性。 

3.2 原子性

        在一个操作中, CPU不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。

synchronized 关键字

特性:

1) 互斥
        synchronized 会起到互斥效果 , 某个线程执行到某个对象的 synchronized 中时 , 其他线程如果也执行到同一个对象 synchronized 就会 阻塞等待。 进入 synchronized 修饰的代码块 , 相当于 加锁
退出 synchronized 修饰的代码块 , 相当于 解锁。
2) 刷新内存
        即保证可见性
3) 可重入
        即可重复加锁,在可重入锁的内部 , 包含了 " 线程持有者 " " 计数器 " 两个信息。解锁的时候计数器递减为 0 的时候 , 才真正释放锁。 加锁时, 只有持有者的才可重复加锁。
可重入示例:
static class Counter {
    public int count = 0;
    synchronized void increase() {
        count++;
   }
    synchronized void increase2() {
        increase();
   }
}

使用:

1) 直接修饰普通方法

public class SynchronizedDemo {
    public synchronized void methond() {
   }
}
2) 修饰静态方法
public class SynchronizedDemo {
    public synchronized static void method() {
   }
}

3) 锁当前对象

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            
       }
   }
}

4) 锁类对象

public class SynchronizedDemo {
    public void method() {
        synchronized (SynchronizedDemo.class) {
       }
   }
}

3.3 可见性

        可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到

编译器优化问题:

static class Counter {
    public int flag = 0;
}
public static void main(String[] args) {
    Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        while (counter.flag == 0) {
            // do nothing
       }
        System.out.println("循环结束!");
   });
    Thread t2 = new Thread(() -> {
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入一个整数:");
        counter.flag = scanner.nextInt();
   });
    t1.start();
    t2.start();
}
// 执行效果
// 当用户输入非0值时, t1 线程循环不会结束

        由上述例子可得, 虽然 flag !=1, 但线程也不会跳出while循环, 这是因为编译器的优化, 即多次读取同一个变量的值, 且该变量不变, 则编译器将不在从内存中读取, 而是将内存中 flag 的值存到了寄存器中, 从寄存器中读取变量值。

volatile 关键字
        上述例子的ans变量加上volatile关键字后,  即可保证内存可见性, 但不能保证原子性。
示例:
static class Counter {
    public volatile int flag = 0;
}

3.4 wait 和 notify

wait()方法:

1) 使当前执行代码的线程进行等待. (把线程放到等待队列中)
2) 释放当前的锁
3) 满足一定条件时被唤醒, 重新尝试获取这个锁

4) 要搭配 synchronized 来使用

wait 结束等待的条件:

1) 其他线程调用该对象的 notify 方法. 
2) wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间). 
3) 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常

示例:

public static void main(String[] args) throws InterruptedException {
    Object object = new Object();
    synchronized (object) {
        System.out.println("等待中");
        object.wait();
        System.out.println("等待结束");
   }
}

notify()方法

        唤醒等待的线程

notifyAll()方法

        notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程, 但都需要竞争锁。

wait 和 sleep 的对比

        wait 需要搭配 synchronized 使用 sleep 不需要

        wait 是 Object 的方法,  sleep 是 Thread 的静态方法

四、多线程实案例

4.1 单例模式

饿汉模式

class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
   }
}

懒汉模式

class Singleton {
    private static volatile Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {                 //非null则无需排队阻塞
            synchronized (Singleton.class) {    //保证原子性
               if (instance == null) {          //若为null,则创建对象
                   instance = new Singleton();
               }
            }
        }
        return instance;
    }
}

4.2 阻塞队列

标准库中的阻塞队列:

BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// 入队列
queue.put("abc");
// 出队列. 如果没有 put 直接 take, 就会阻塞. 
String elem = queue.take();

生产者消费者模型

public static void main(String[] args) throws InterruptedException {
    BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<Integer>();
    Thread customer = new Thread(() -> {
        while (true) {
            try {
                int value = blockingQueue.take();
                System.out.println("消费元素: " + value);
           } catch (InterruptedException e) {
                e.printStackTrace();
           }
       }
   }, "消费者");
    customer.start();
    Thread producer = new Thread(() -> {
        Random random = new Random();
        while (true) {
            try {
                int num = random.nextInt(1000);
                System.out.println("生产元素: " + num);
                blockingQueue.put(num);
                Thread.sleep(1000);
           } catch (InterruptedException e) {
                e.printStackTrace();
           }
       }
   }, "生产者");
    producer.start();
    customer.join();
    producer.join();
}

阻塞队列实现;

class MyQueue {
    int[] deque;
    int front;
    int last;

    public MyQueue(int k) {
        deque = new int[k + 1];
        front = 0;
        last = 0;
    }

    public void enQueue(int value) throws InterruptedException {
        synchronized (this) {
            if (isFull())
                this.wait();
            last = (last + 1) % deque.length;
            deque[last] = value;
            this.notify();

        }

    }

    public void deQueue() throws InterruptedException {
        synchronized (this) {
            if (last == front)
                this.wait();
            front = (front + 1) % deque.length;
            this.notify();
        }

    }

    public int Front() {
        if (last == front)
            return -1;
        return deque[(front + 1) % deque.length];

    }

    public int Rear() {
        if (last == front)
            return -1;
        return deque[last];
    }

    public boolean isEmpty() {
        if (last == front)
            return true;
        return false;
    }

    public boolean isFull() {
        if (((last + 1) % deque.length) == front)
            return true;
        return false;
    }
}

4.3 定时器 

标准库中的定时器:

Timer timer = new Timer();
timer.schedule(new TimerTask() {
    @Override
    public void run() {
        System.out.println("hello");
   }
}, 3000);

实现定时器:

class MyTimer {
    private PriorityBlockingQueue<MyTast> queue = new PriorityBlockingQueue<>();
    Object object = new Object();

    public void schedule(Runnable runnable, long time) {
        MyTast task = new MyTast(runnable, time);
        queue.add(task);
        synchronized (object) {
            object.notify();
        }
    }

    public MyTimer() {
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    MyTast task = queue.take();
                    if (task.getTime() >= System.currentTimeMillis()) {
                        task.run();
                    } else {
                        queue.add(task);
                        synchronized (object) {
                            object.wait(Math.abs(task.getTime() - System.currentTimeMillis()));
                        }
                    }

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

class MyTast implements Comparable<MyTast> {
    private Runnable runnable;
    private long time;

    public MyTast(Runnable runnable, long time) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + time;
    }

    public void run() {
        runnable.run();
    }

    public long getTime() {
        return time;
    }

    @Override
    public int compareTo(MyTast o) {           /*实现比较规则*/
        return (int) (this.time - o.time);
    }
}

4.4 线程池

标准库中的线程池:

public class Work_1 {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);      /*指定线程池个数*/
        Executors.newCachedThreadPool();      /*自动扩容*/
        Executors.newSingleThreadExecutor();  /*只有一个线程的线程池*/
        Executors.newScheduledThreadPool(10);   /*带有定时器功能的线程池。*/
        pool.submit(() -> {                  /* Runnable() */
            System.out.println("aa");
        });
    }
}

实现线程池:

class MyTreadPool {
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

    static class Worker extends Thread {
        private BlockingQueue<Runnable> queue = null;

        public Worker(BlockingQueue<Runnable> queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            while (true) {
                try {
                    Runnable runnable = queue.take();
                    runnable.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private List<Thread> workers = new ArrayList<>();

    public MyTreadPool(int n) {
        for (int i = 0; i < n; i++) {
            Worker worker = new Worker(queue);
            worker.start();
            workers.add(worker);
        }
    }

    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }
}

五 常见锁策略

5.1 乐观锁 vs 悲观锁

        Synchronized 初始使用乐观锁策略. 当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略

悲观锁:

        悲观锁认为多个线程访问同一个共享变量冲突的概率较大, 会在每次访问共享变量之前都去真正加锁。

乐观锁:

        乐观锁认为多个线程访问同一个共享变量冲突的概率不大. 并不会真的加锁, 而是直接尝试访问数据。在访问的同时识别当前的数据是否出现访问冲突。乐观锁的实现可以引入一个版本号, 借助版本号识别出当前的数据访问是否冲突

总结:

        线程阻塞情况少, 使用乐观锁效率高; 阻塞情况多, 使用乐观锁会导致耗费额外资源, 如自旋锁

5.2 读写锁

        Synchronized 不是读写锁

       一个线程对于数据的访问, 主要存在两种操作: 读数据 和 写数据,数据的读取方之间不会产生线程安全问题, 两种场景下都用同一个锁,就会产生极大的性能损耗。

        1) 读加锁和读加锁之间, 不互斥
        2) 写加锁和写加锁之间, 互斥
        3) 读加锁和写加锁之间, 互斥

总结:

        适合于 "频繁读, 不频繁写" 的场景中。

5.3 重量级锁 vs 轻量级锁

        synchronized 开始是一个轻量级锁. 如果锁冲突比较严重, 就会变成重量级锁

synchronized来源: 

重量级锁:

        加锁机制重度依赖了 OS 提供了 mutex, 大量的内核态用户态切换, 很容易引发线程的调度

轻量级锁:

        加锁机制尽可能不使用 mutex, 而是尽量用用户态代码, 不太容易引发线程调度

 5.4 自旋锁 vs 挂起等待锁

        synchronized 中的轻量级锁策略大概率就是通过自旋锁的方式实现的

自旋锁伪代码:

while (抢锁(lock) == 失败) {}

挂起等待锁:

        如定时器中的wait()方法。

自旋锁优缺点:

        优点: 没有放弃 CPU 资源, 一旦锁被释放就能第一时间获取到锁, 更高效. 在锁持有时间比较短的场景下非常有用

        缺点: 如果锁的持有时间较长, 就会浪费 CPU 资源

5.5 公平锁 vs 非公平锁

        synchronized 是非公平锁

公平锁:

        遵守 "先来后到". B 比 C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁

非公平锁:

        不遵守 "先来后到". B 和 C 都有可能获取到锁

5.6 可重入锁 vs 不可重入锁

        synchronized 是可重入锁

可重入锁:

        即允许同一个线程多次获取同一把锁 (递归锁)

六 CAS

6.1 什么是CAS

        CAS: 全称Compare and swap,字面意思:”比较并交换

涉及到以下操作:

        1. 比较 A 与 V 是否相等。(比较)
        2. 如果比较相等,将 B 写入 V。(交换)
        3. 返回操作是否成功。

CAS 伪代码:

boolean CAS(address, expectValue, swapValue) {
 if (address == expectedValue) {
        address = swapValue;
        return true;
   }
    return false;
}

上面写的代码不是原子的, 真实的 CAS 是一个原子的硬件指令完成的

6.2 CAS 应用

1) 实现原子类

        标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的

AtomicInteger 类示例:

AtomicInteger atomicInteger = new AtomicInteger(0);
// 相当于 i++
atomicInteger.getAndIncrement();

2) 实现自旋锁

自旋锁伪代码:

public class SpinLock {
    private Thread owner = null;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有. 
        // 如果这个锁已经被别的线程持有, 那么就自旋等待. 
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
       }
   }
    public void unlock (){
        this.owner = null;
   }
}

6.3 ABA 问题

        如: t1 执行这两个操作之间, t2 线程可能把 num 的值从 A 改成了 B, 又从 B 改成了 A

图:

 解决方案:

        给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期, 修改成功有版本号+1

七 Synchronized 原理

        结合上面的锁策略, 我们就可以总结出, Synchronized 具有以下特性

7.1 特性:

        1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
        2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁. 
        3. 实现轻量级锁的时候大概率用到的自旋锁策略
        4. 是一种不公平锁
        5. 是一种可重入锁
        6. 不是读写锁

7.2 加锁工作过程

        JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁状态。会根据情况,进行依次升级。

图:

 1) 偏向锁

       1) 偏向锁不是真的 "加锁", 只是给对象头中做一个 "偏向锁的标记", 记录这个锁属于哪个线程 
       2) 如果后续没有其他线程来竞争该锁, 就不用进行其他同步操作了(避免了加锁解锁的开销)
       3) 如果后续有其他线程来竞争该锁, 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态. 
       4) 偏向锁本质上相当于 "延迟加锁" . 能不加锁就不加锁, 尽量来避免不必要的加锁开销. 

2) 轻量级锁

        随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁, CAS 来实现),自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了

3) 重量级锁

        如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁, 此处的重量级锁就是指用到内核提供的 mutex 。即执行加锁操作, 先进入内核态; 在内核态判定当前锁是否已经被占用;  如果该锁没有占用, 则加锁成功, 并切换回用户态;  如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒

7.3 其他的优化操作

1) 锁消除

        编译器+JVM 判断锁是否可消除. 如果可以, 就直接消除, 如单线程下的StringBuilder

2) 锁粗化

        一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化

如:

八 Callable 接口

8.1 Callable 的用法

        用于创建线程, 相比于Runnable 创建线程的方法, 主要的区别在于callable接口是有返回值的。 

        Callable 通常需要搭配 FutureTask 来使用。 FutureTask 用来保存 Callable 的返回结果. 因为Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定

Thread 返回值示例:

static class Result {
    public int sum = 0;
    public Object lock = new Object();
}
public static void main(String[] args) throws InterruptedException {
    Result result = new Result();
    Thread t = new Thread() {
        @Override
        public void run() {
            int sum = 0;
            for (int i = 1; i <= 1000; i++) {
                sum += i;
           }
            synchronized (result.lock) {
                result.sum = sum;
                result.lock.notify();
           }
       }
   };
    t.start();
    synchronized (result.lock) {
        while (result.sum == 0) {
            result.lock.wait();
       }
        System.out.println(result.sum);
   }
}

Callable 示例:

Callable<Integer> callable = new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 1000; i++) {
            sum += i;
       }
        return sum;
   }
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
int result = futureTask.get();
System.out.println(result);

九 Java.util.concurrent 的常见类

9.1 ReentrantLock

        可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全

ReentrantLock 的用法:

        lock(): 加锁, 如果获取不到锁就死等. 
        trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁. 
        unlock(): 解锁

示例:

ReentrantLock lock = new ReentrantLock(); 
lock.lock();   
try {    
 // working    
} finally {    
 lock.unlock()    
}  

ReentrantLock 和 synchronized 的区别;

        1) synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准库的一个类, 在 JVM 外实现的(基于 Java 实现)

        2) synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock

        3) synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃

        3) synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式.

        4) 更强大的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指
定的线程. 

总结:

        锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便. 
        锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等. 
        如果需要使用公平锁, 使用 ReentrantLock.

9.2 原子类

        原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。

原子类:
        1) AtomicBoolean
        2) AtomicInteger
        3) AtomicIntegerArray
        4) AtomicLong
        5) AtomicReference
        6) AtomicStampedReference

示例:

addAndGet(int delta);   i += delta;
decrementAndGet(); --i;
getAndDecrement(); i--;
incrementAndGet(); ++i;
getAndIncrement(); i++;

9.3 信号量 Semaphore

        信号量, 用来表示 "可用资源的个数". 本质上就是一个计数器.

理解信号量;
        1) 可以把信号量想象成是停车场的展示牌: 当前有车位 100 个. 表示有 100 个可用资源. 
        2) 当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 
        3) 当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 
        4) 如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源. 

示例:

Semaphore semaphore = new Semaphore(4);
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        try {
            System.out.println("申请资源");
            semaphore.acquire();
            System.out.println("我获取到资源了");
            Thread.sleep(1000);
            System.out.println("我释放资源了");
            semaphore.release();
       } catch (InterruptedException e) {
            e.printStackTrace();
       }
   }
};
for (int i = 0; i < 20; i++) {
    Thread t = new Thread(runnable);
    t.start();
}

        synchronized 相当于信号量为1的特殊情况

9.4 CountDownLatch

        同时等待 N 个任务执行结束(好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩)。

示例:

public class Demo {
    public static void main(String[] args) throws Exception {
        CountDownLatch latch = new CountDownLatch(10);
        Runnable r = new Runable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(Math.random() * 10000);
                    latch.countDown();
               } catch (Exception e) {
                    e.printStackTrace();
               }
           }
       };
        for (int i = 0; i < 10; i++) {
            new Thread(r).start();
       }
   // 必须等到 10 人全部回来
        latch.await();
        System.out.println("比赛结束");
    }    
}

代码解析: 

       1) 构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成. 
       2) 任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减. 
       3) 主线程中使用 latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0 了

十 线程安全的集合类

10.1 CopyOnWriteArrayList

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

优点:
        在读多写少的场景下, 性能很高, 不需要加锁竞争. 
缺点:
        1. 占用内存较多. 
        2. 新写的数据不能被第一时间读取到

10.2 队列:

 1) ArrayBlockingQueue
        基于数组实现的阻塞队列

2) LinkedBlockingQueue
        基于链表实现的阻塞队列

3) PriorityBlockingQueue
        基于堆实现的带优先级的阻塞队列

4) TransferQueue

        最多只包含一个元素的阻塞队列

10.3 哈希表:

Hashtable

        只是简单的把关键方法加上了 synchronized 关键字. 

ConcurrentHashMap

        相比于 Hashtable 做出了一系列的改进和优化

ConcurrentHashMap优点:

        1) 读操作没有加锁(但是使用了 volatile 保证从内存读取结果), 只对写操作进行加锁. 加锁的方式仍然是是用 synchronized, 但是不是锁整个对象, 而是 "锁桶" (用每个链表的头结点作为锁对象), 大大降低了锁冲突的概率. 

        2) 充分利用 CAS 特性. 比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况. 

        3) 发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去. 扩容期间, 新老数组同时存在. 后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小部分元素. 搬完最后一个元素再把老数组删掉. 这个期间, 插入只往新数组加. 这个期间, 查找需要同时查新数组和老数组。

比较图:

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值