JUC基础学习(上)

217 阅读17分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

1. 什么是JUC

JUC就是java.util.concurrent工具包的简称。这是一个处理线程的工具包,JDK1.5之后出现。

我们平常在使用线程时一般会使用以下几种方式

  1. 继承Thread
  2. 实现Runnable接口
  3. 实现Callable接口

以前写过一篇博客,相信大家看完之后可以对多线程有一个清晰地认识:

java基础之多线程解析

2. 线程和进程

  • 进程:一个程序,QQ.exe,Music.exe程序的集合
    • 一个进程往往包含多个线程,至少一个(正在运行的程序可以理解为进程,那么线程就是一个程序的不同的操作)
    • java默认有两个线程,main、GC
  • 线程:开了一个进程Typora,写字、保存这就是不同的线程。
    • 在java中Thread、Runnable、Callable操作线程
# 我们经常用java程序写多线程的例子,那么java真的可以开启多线程吗?
- 答案是不,我们可以从源码中查看答案。(java中开启线程的方式是start()方法,所以我们深入start()方法的源码看一下)

public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();										//本地方法,调用底层c++,java无法直接操作硬件
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

并发和并行

  • 并发:单核cpu,模拟出来多条线程,快速交替。

    • 简单地说,就是一个人做多件事,举个例子,如果许多个用户同时访问服务器,如果采取串行方式,那么用户体验感会极差,但是如果采用并发,那么很快会给每一个用户带来一定的反馈,为了维护用户的体验感,也为了更合理cpu的资源,对一个人的访问还没有全部响应,就去执行对另一个人的响应。
    • 并发在一定情况下是可以提高cpu的利用率的,比如说在有I/O等待的情况下,在I/O等待的时候去做另外的事情就提高了cpu的利用率。
  • 并行:多核cpu,多个线程同时执行。

    • 简单地说,就是多个人做多件事。
//下面这段代码可以获取cpu的核数
public static void main(String[] args) {
        System.out.println(Runtime.getRuntime().availableProcessors());
    }

并发编程的本质:充分利用cpu的资源

线程的状态数(6个)

//下面是源码中的呈现出的线程状态数
public enum State {
	//线程新生状态
    NEW,

    //线程运行
    RUNNABLE,

    //线程阻塞
    BLOCKED,

    //线程等待,死死的等
    WAITING,

    //线程等待,超过一定的时间就不等了
    TIMED_WAITING,

    //线程中止状态
    TERMINATED;
}

wait和sleep的区别

  1. 二者来自不同的类

    • wait -> Object
    • sleep -> Thread
  2. 关于锁的释放

    • wait会释放锁
    • sleep睡觉了会抱着锁,不会释放锁
  3. 使用的范围是不同的

    • wait必须在同步代码块中使用
    • sleep可以在任何地方睡觉

3. Synchronized锁

传统的Synchronized锁

package juc.demo;

// synchronized
//下面是一个卖票的例子

public class demo1 {
    /*
    真正的多线程开发,公司中的开发
    多线程就是一个单独的资源类,没有任何附属的方法
    属性,方法(资源类中要包含必要的方法,不包含不必要的(附属)方法)
     */
    public static void main(String[] args) {
        //并发,多个线程同时操作一个资源类,把资源类丢入线程
        Ticket ticket = new Ticket();

        new Thread(() -> {
            for(int i = 0; i < 60; i++){
                ticket.sale();
            }
        }, "A").start();
        new Thread(() -> {
            for(int i = 0; i < 60; i++){
                ticket.sale();
            }
        }, "B").start();
        new Thread(() -> {
            for(int i = 0; i < 60; i++){
                ticket.sale();
            }
        }, "C").start();
    }
}

//资源类 OOP
class Ticket{
    //属性、方法
    private int number = 50;
    //卖票的例子
    //synchronized本质:队列、锁
    public synchronized void sale(){
        if(number > 0){
            System.out.println(Thread.currentThread().getName() + "卖出了" + (number--) + "票,剩余" + number + "票");
        }
    }
}

我们可以在自己的电脑上运行一下这个程序,结果是符合我们的实际情况的,也就说明了我们上面的编程是合理的。

4. Lock锁

我们看一下API中关于Lock接口的信息。

在这里插入图片描述

我们看到Lock有三个实现类,有可重入锁、读锁、写锁。

这里我们说一下什么是可重入锁:

下面是
可重入锁是一种特殊的互斥锁,它可以被同一个线程多次获取,而不会产生死锁。

1. 首先它是互斥锁:任意时刻,只有一个线程锁。即假设A线程已经获取了锁,在A线程释放这个锁之前,B线程是无法获取到这个锁的,B要获取这个锁就会进入阻塞状态。

2. 其次,它可以被同一个线程多次持有。即,假设A线程已经获取了这个锁,如果A线程在释放锁之前又一次请求获取这个锁,那么是能够获取成功的。

我们来看一下可重入锁ReentrantLock的一部分源码:

public ReentrantLock() {
        sync = new NonfairSync();			//非公平锁,线程可以插队
    }

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
       	//这里的new FairSync()是公平锁,线程不可以插队
    }
  • 公平锁:十分公平,线程执行顺序按照先来后到顺序
  • 非公平锁:十分不公平;线程可以插队

接下来我们可以将上面的用Synchronized修饰的方法卖票例子改为使用Lock实现,并认识一下Lock和Synchronized的区别和联系

package juc.demo;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
// Lock锁
public class demo02 {
    public static void main(String[] args) {
        Ticket02 ticket = new Ticket02();

        new Thread(() -> {
            for(int i = 0; i < 60; i++){
                ticket.sale();
            }
        }, "A").start();
        new Thread(() -> {
            for(int i = 0; i < 60; i++){
                ticket.sale();
            }
        }, "B").start();
        new Thread(() -> {
            for(int i = 0; i < 60; i++){
                ticket.sale();
            }
        }, "C").start();
    }
}
/*
Lock加锁的使用方式

1. 首先创建实现类,new ReentrantLock();
2. lock.lock();加锁
3. 在try/catch中写业务代码
4. 在finally中解锁,lock.unlock();
*/
class Ticket02{
    //属性、方法
    private int number = 50;
    Lock lock = new ReentrantLock();
    public void sale(){
        lock.lock();            //加锁
        try{
            if(number > 0){
                System.out.println(Thread.currentThread().getName() + "卖出了" + (number--) + "票,剩余" + number + "票");
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();      //解锁
        }
    }
}

Synchronized和Lock的区别

  1. Synchronized是java内置的关键字,Lock是一个java接口
  2. Synchronized无法判断获取锁的状态,Lock可以判断是否获取到了锁
  3. Synchronized会自动释放锁,Lock必须要手动释放锁(如果不释放锁,死锁)。
  4. Synchronized线程1(获得锁,如果线程1阻塞)、线程2(傻傻的等);Lock锁就不一定会等待下去
  5. Synchronized是可重入锁不可以中断非公平;Lock,可重入锁可以判断锁可以公平也可以非公平(自己设置,不加参数默认非公平)。
  6. Synchronized适合锁少量的代码同步问题,Lock适合锁大量的同步代码
额,,,这里用一句话简单的总结一下,Synchronized就是汽车自动挡,Lock就是汽车手动挡。

5. 生产者和消费者问题

# 首先解释一下什么是生产者和消费者问题

生产者消费者问题也称有限缓冲问题,是一个多线程同步问题的经典案例。该问题描述了共享固定大小的两个线程——即所谓的“生产者”和“消费者”
,在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据
。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者不会在缓冲区空时消耗数据。

要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据时,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能推广到多个生产者
和消费者的情形。

该问题需要注意以下几点:
- 在缓冲区为空时,消费者不能再进行消费。
- 在缓冲区为满时,生产者不能再进行生产。
- 在一个线程进行生产或消费时,其余线程不能再进行生产或消费等操作,即保持线程间的同步。
- 注意条件变量与互斥锁的顺序。

接下来我们看一下比较简单情况下的生产者和消费者问题。

Synchronized版的生产者和消费者问题

//这是Synchronized版的生产者和消费者问题
package juc.demo;

// 线程通信
// 传统的生产者、消费者模式
// if改为while : 防止虚假唤醒
public class demo03 {
    public static void main(String[] args) {
        A a = new A();
        new Thread(() -> {
            for(int i = 0; i < 10; i++){
                try {
                    a.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();

        new Thread(() -> {
            for(int i = 0; i < 10; i++){
                try {
                    a.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();

        new Thread(() -> {
            for(int i = 0; i < 10; i++){
                try {
                    a.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();

        new Thread(() -> {
            for(int i = 0; i < 10; i++){
                try {
                    a.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "D").start();
    }
}
class A {
    private int number = 0;
    public synchronized void increment() throws InterruptedException {
        while(number != 0){				//不等于0让其等待
            //等待
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName() + "->" + number);
        //通知其他线程,我+1完毕了
        this.notify();
    }

    public synchronized void decrement() throws InterruptedException {
        while(number == 0){				//等于0让其等待
            //等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName() + "->" + number);
        //通知其他线程,我-1完毕了
        this.notify();
    }
}
/*
这里说一下虚假唤醒
虚假唤醒就是在多线程执行过程中,线程间的通信未按照我们幻想的顺序执行,故出现数据不一致等不符合我们预期的结果。
在上面那两个同步方法中我们分别使用了以下的代码,这里使用while的原因是为了防止虚假唤醒。
我们举个例子,我们将whiel改为if,A、C执行加线程,B、D执行减线程,在某一时刻,number为1,此时A线程抢到,但是由于判断,
A线程会等待,之后A又抢到了,且此时number为1,此时if语句之前已经执行过了,A线程不会等待,直接加1,number为2,这显然与
我们的预期不符,这就是虚假唤醒产生的原因,将if改为while可以很好地解决虚假唤醒问题。
while(number != 0){				//不等于0让其等待
            //等待
            this.wait();
        }
        
while(number != 0){				//不等于0让其等待
            //等待
            this.wait();
        }
        
*/

Lock版的生产者和消费者问题

//这是Lock版的生产者和消费者问题
package juc.demo;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

// 线程通信
// Lock版的生产者、消费者模式
// if改为while : 防止虚假唤醒
public class demo04 {
    public static void main(String[] args) {
        AA a = new AA();
        new Thread(() -> {
            for(int i = 0; i < 10; i++){
                try {
                    a.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();

        new Thread(() -> {
            for(int i = 0; i < 10; i++){
                try {
                    a.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();

        new Thread(() -> {
            for(int i = 0; i < 10; i++){
                try {
                    a.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();

        new Thread(() -> {
            for(int i = 0; i < 10; i++){
                try {
                    a.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "D").start();
    }
}

class AA {
    private int number = 0;
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    public  void increment() throws InterruptedException {
        lock.lock();

        try {
            while(number != 0){
                //等待
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "->" + number);
            //通知其他线程,我+1完毕了
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public  void decrement() throws InterruptedException {
        lock.lock();
        try {
            while(number == 0){
                //等待
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + "->" + number);
            //通知其他线程,我-1完毕了
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
# ReentrantLock使用方法 (juc底层这些知识等学完juc基础用法,就会单独看源码学习)
- 提供了无条件的,可轮询的,定时的以及可中断的锁获取操作
- 加锁和解锁都是显式的
- 与Synchronized一样都是可重入锁
  
  Lock lock = new ReentrantLock();
  try{
      lock.lock();//加锁操作
  }finally{
      lock.unlock();
  }
  
# 介绍一下Condition类
 首先我们需要明白condition对象是依赖于lock对象的,意思就是说condition对象需要通过lock对象进行创建出来(调用Lock对象的newCondition()方法)。consition的使用方式非常的简单。但是需要注意在调用方法前获取锁。Condition接口提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式。

Condition精准的通知和唤醒线程

//让A、B、C线程轮流操作
package juc.demo;

import jdk.nashorn.internal.ir.ContinueNode;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class demo05 {

    public static void main(String[] args) {
        Data03 date = new Data03();
        new Thread(()->{
            for(int i = 0; i <10; i++){
                date.printA();
            }
        }, "A").start();

        new Thread(()->{
            for(int i = 0; i <10; i++){
                date.printB();
            }
        }, "B").start();

        new Thread(()->{
            for(int i = 0; i <10; i++){
                date.printC();
            }
        }, "C").start();
    }
}
class Data03{
    private int number = 1;
    Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();

    public void printA(){
        lock.lock();
        try {
            //业务判断->执行->通知
            while(number != 1){
                condition1.await();
            }

            System.out.println(Thread.currentThread().getName() + "->" + number);
            number = 2;
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB(){
        lock.lock();
        try {
            while(number != 2){
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName() + "->" + number);
            number = 3;
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printC(){
        lock.lock();
        try {
            while (number != 3){
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName() + "->" + number);
            number = 1;
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
//生产线:下单->支付->物流

6. 8锁现象

Synchronized锁的对象是方法的调用者

例子1:

package juc.lock8;

import java.util.concurrent.TimeUnit;

/**
 * 8锁:就是关于锁的8个问题
 * 1、标准情况下,两个线程先打印 发短信还是 先打印 打电话? 1/发短信  2/打电话
 * 1、sendSms延迟4秒,两个线程先打印 发短信还是 打电话? 1/发短信  2/打电话
 */
public class Main {
    public static void main(String[] args) {
        Phone phone = new Phone();

        // 锁的存在
        new Thread(()->{
            phone.sendSms();
        },"A").start();

        // 捕获
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone.call();
        },"B").start();
    }
}

class Phone{

    // synchronized 锁的对象是方法的调用者!、
    // 两个方法用的是同一个对象调用(同一个锁),谁先拿到锁谁执行!
    public synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);// 抱着锁睡眠
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public synchronized void call(){
        System.out.println("打电话");
    }

}
//线程在执行前必须先拿到锁才可以执行,执行完之后要释放锁。
/*
上面代码的执行结果是先发短信后打印,因为对于Synchronized修饰的普通方法锁的是对象,发短信线程先拿到锁,发短信执行完之后释放锁,
之后打电话线程才可以拿到锁执行。
*/

例子2:

package juc.lock8;

import java.util.concurrent.TimeUnit;

/**
 * 3、 增加了一个普通方法后!先执行发短信还是Hello?
 * 4、 两个对象,两个同步方法, 发短信还是 打电话? 
 */
public class Main  {
    public static void main(String[] args) {
        // 两个对象,两个调用者,两把锁!
        Phone2 phone1 = new Phone2();
        Phone2 phone2 = new Phone2();

        //锁的存在
        new Thread(()->{
            phone1.sendSms();
        },"A").start();

        // 捕获
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone2.call();
        },"B").start();

        new Thread(()->{
            phone2.hello();
        },"C").start();
    }
}

class Phone2{

    // synchronized 锁的对象是方法的调用者!
    public synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public synchronized void call(){
        System.out.println("打电话");
    }

    // 这里没有锁!不是同步方法,不受锁的影响
    public void hello(){
        System.out.println("hello");
    }
}
/*
结果是:
先打电话
在说hello
最后发短信
*/

例子3:

对于静态方法来说,Synchronized锁的是类的Class模板

package juc.lock8;

import java.util.concurrent.TimeUnit;

/**
 * 5、增加两个静态的同步方法,只有一个对象,先打印 发短信?打电话?
 * 6、两个对象!增加两个静态的同步方法, 先打印 发短信?打电话?
 */
public class Main  {
    public static void main(String[] args) {
        // 两个对象的Class类模板只有一个,static,锁的是Class
        Phone3 phone1 = new Phone3();
        Phone3 phone2 = new Phone3();

        //锁的存在
        new Thread(()->{
            phone1.sendSms();
        },"A").start();

        // 捕获
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone2.call();
        },"B").start();
    }
}

// Phone3唯一的一个 Class 对象
class Phone3{

    // synchronized 锁的对象是方法的调用者!
    // static 静态方法
    // 锁的是Class
    public static synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public static synchronized void call(){
        System.out.println("打电话");
    }
}
/*
虽然是两个不同的对象,但是由于锁的是静态方法,锁的对象是类的Class模板,也就是说,锁只有一个,
必须等其中一个拿到锁的线程执行完,另一个线程才能继续执行。
*/

例子4:

package juc.lock8;

import java.util.concurrent.TimeUnit;

/**
 * 7、1个静态的同步方法,1个普通的同步方法 ,一个对象,先打印 发短信?打电话?
 * 8、1个静态的同步方法,1个普通的同步方法 ,两个对象,先打印 发短信?打电话?
 */
public class Main  {
    public static void main(String[] args) {
        // 两个对象的Class类模板只有一个,static,锁的是Class
        Phone4 phone1 = new Phone4();
        Phone4 phone2 = new Phone4();
        //锁的存在
        new Thread(()->{
            phone1.sendSms();
        },"A").start();

        // 捕获
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone2.call();
        },"B").start();
    }
}

// Phone3唯一的一个 Class 对象
class Phone4{

    // 静态的同步方法 锁的是 Class 类模板
    public static synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    // 普通的同步方法  锁的调用者(对象),二者锁的对象不同,所以不需要等待
    public synchronized void call(){
        System.out.println("打电话");
    }
}

/*
结果:
先打电话后发短信,因为发短信线程会等4s
*/
  • 对于Synchronized修饰的普通方法来说,Synchronized锁的是对象
  • 对于Synchronized修饰的静态方法来说,Synchronized锁的是类的Class

7. 集合类不安全

# 并发修改异常
并发就是同一时刻发生,并发修改的意思就是同一时刻发生并修改。当方法检测到对象的并发修改,但不允许这种修改时,会抛出此异常。

List不安全

List、ArrayList等在并发多线程条件下,不能实现数据共享。多个线程调用同一个list对象就会出现并发修改异常

java.util.ConcurrentModificationException:并发修改异常

package juc.unsafe;

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
//java.util.ConcurrentModificationException并发修改异常
/**
 * 解决方案:
 * 1. List<String> list = new Vector<>();
 * 2. List<String> list = Collections.synchronizedList(new ArrayList<>());
 * 3. List<String> list = new CopyOnWriteArrayList<>();
 */
public class ListTest {

    public static void main(String[] args) {
        List<String> list = new CopyOnWriteArrayList<>();
        for(int i = 0; i < 10; i++){
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(list);
            },String.valueOf(i)).start();

        }
    }
}

Set不安全

package juc.unsafe;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * java.util.ConcurrentModificationException 并发修改异常
 * 解决方案:
 * 1. Set<String> set = Collections.synchronizedSet(new HashSet<>());
 * 2.
 */
public class SetTest {
    public static void main(String[] args) {
        Set<String> set = new CopyOnWriteArraySet<>();

        for(int i = 0; i < 100; i++){
            new Thread(() -> {
                set.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(set);
            }, String.valueOf(i)).start();
        }

    }
}

HashSet底层就是HashMap,HashMap底层的知识我以前写过博客,具体可以看我以前写过的博客啊

HashSet源码分析

HashMap源码分析

Map不安全

package juc.unsafe;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * java.util.ConcurrentModificationException 并发修改异常
 * 解决方案:
 * 1. Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
 * 2. Map<String, String> map = new ConcurrentHashMap<>();
 */
public class MapTest {

    public static void main(String[] args) {
        Map<String, String> map = new ConcurrentHashMap<>();

        for(int i = 0; i < 30; i++){
            new Thread(() -> {
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 5));
                System.out.println(map);
            }, String.valueOf(i)).start();
        }
    }
}

8. Callable

  • Callable 是 java.util 包下 concurrent 下的接口,有返回值,可以抛出被检查的异常
  • Runable 是 java.lang 包下的接口,没有返回值,不可以抛出被检查的异常
  • 二者调用的方法不同,run()/ call()

同样的 LockSynchronized 二者的区别,前者是java.util 下的接口 后者是 java.lang 下的关键字。

使用Callable的步骤:

  1. 首先创建一个Callable的实现类,并实现call()方法(将要执行的逻辑写在其中)
  2. 创建一个FutureTask对象并将Callable的实现类作为参数传递给它的构造方法
  3. 创建一个Thread对象并将上面的FutureTask对象作为参数传递给它的构造方法
  4. Thread执行**start()**方法
package juc.clllable;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        CallableTestImpl call = new CallableTestImpl();
        FutureTask futureTask = new FutureTask(call);
        new Thread(futureTask, "A").start();
        new Thread(futureTask, "B").start();
        Integer o = (Integer) futureTask.get();
        System.out.println(o);
    }
}

class CallableTestImpl implements Callable<Integer> {
    @Override
    public Integer call(){
        System.out.println("call");
        return 1024;
    }
}

9. 常用的辅助类

9.1 CountDownLatch

CountDownLatch是一个减法计数器,实现调用几次其他线程后再出发某一个任务,

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

import java.util.concurrent.CountDownLatch;

public class CountDownLanchTest {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for(int i = 0;i < 6; i++){
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " go out");
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }

        countDownLatch.await();
        System.out.println("close door");
    }
}

原理:

  • CountDownLatch countDownLatch = new CountDownLatch(6); 设置需要等待的线程数为6
  • countDownLatch.countDown();计数器减一
  • countDownLatch.await();等待计数器归零再向下执行

[CountDownLatch学习](CountDownLatch的理解和使用 - Shane_Li - 博客园 (cnblogs.com))

9.2 CyclicBarrier

加法计数器

让指定数量的线程等待完成后才能执行下一步的动作

package juc.add;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {

    public static void main(String[] args) {
        /**
         * 集齐7颗龙珠召唤神龙
         */

        // 召唤龙珠线程
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
            System.out.println("召唤神龙成功");
        });

        for(int i = 1; i <= 7; i++){
            final int temp = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "收集" + temp + "个龙珠");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }

    }
}

9.3 Semaphore

Semaphore 通常我们叫它信号量, 可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。

package juc.add;

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class SemaphoreDemo {

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);
        for(int i = 1; i <= 6; i++){
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "抢到车位");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName() + "离开车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }

            }, String.valueOf(i)).start();
        }
    }
}
  • semaphore.acquire();获得,假设已经满了则等待,等待其他线程释放.
  • semaphore.release();释放,会将当前信号的释放量加1,然后等待其他线程争抢。

10. ReadWriteLock读写锁

读可以被多个线程共享,写只能有一个线程去写

ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁。

package juc.rw;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWritedemo {

    public static void main(String[] args) {
        MyCacheLock myCache = new MyCacheLock();

        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.put(temp + "", temp + "");
            }, String.valueOf(i)).start();
        }

        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.get(temp + "");
            }, String.valueOf(i)).start();
        }
    }
}

class MyCache{

    private volatile Map<String, Object> map = new HashMap<>();

    //存、写
    public void put(String key, Object value){
        System.out.println(Thread.currentThread().getName() + "写入" + key);
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "写入OK");

    }

    //取、读
    public void get(String key){
        System.out.println(Thread.currentThread().getName() + "读取"  + key);
        Object o = map.get(key);
        System.out.println(Thread.currentThread().getName() + "读取OK");
    }

}

class MyCacheLock{

    private volatile Map<String, Object> map = new HashMap<>();
    ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    //存、写
    public void put(String key, Object value){
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "写入" + key);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写入OK");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    //取、读
    public void get(String key){
        readWriteLock.readLock().lock();

        try {
            System.out.println(Thread.currentThread().getName() + "读取"  + key);
            Object o = map.get(key);
            System.out.println(Thread.currentThread().getName() + "读取OK");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }

    }

}

11. BlockingQueue

BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利。

阻塞队列 在这里插入图片描述

四组API:

方式抛出异常有返回值,不抛出异常阻塞 等待超时等待
添加addoffer()put()offer(,)
移除removepoll()take()poll(,)
检测队首元素elementpeek()--
package juc.bq;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;

import static java.util.concurrent.TimeUnit.*;

public class BlockQueue {

    public static void main(String[] args) throws InterruptedException {
        test4();
    }

    public static void test1(){
        ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);
        System.out.println(queue.add("a"));
        System.out.println(queue.add("b"));
        System.out.println(queue.add("c"));

        System.out.println(queue.element());
        //队列已满,抛出异常java.lang.IllegalStateException: Queue full
        //System.out.println(queue.add("d"));

        System.out.println(queue.remove());
        System.out.println(queue.remove());
        System.out.println(queue.remove());

        //队列为空, 抛出异常java.util.NoSuchElementException
        //System.out.println(queue.remove());
    }

    public static void test2(){
        ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);
        System.out.println(queue.offer("a"));
        System.out.println(queue.offer("b"));
        System.out.println(queue.offer("c"));

        //队列已满
        //System.out.println(queue.offer("d"));

        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.peek());
        //队列为空
        System.out.println(queue.poll());
    }

    public static void test3() throws InterruptedException {
        ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);
        queue.put("a");
        queue.put("b");
        queue.put("c");

        //queue.put("d");

        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println(queue.take());
    }

    public static void test4() throws InterruptedException {
        ArrayBlockingQueue queue = new ArrayBlockingQueue<>(3);
        System.out.println(queue.offer("a"));
        System.out.println(queue.offer("b"));
        System.out.println(queue.offer("c"));
        System.out.println(queue.offer("d", 2, SECONDS));


        System.out.println(queue.poll(2, TimeUnit.SECONDS));
        System.out.println(queue.poll(2, TimeUnit.SECONDS));
        System.out.println(queue.poll(2, TimeUnit.SECONDS));
        System.out.println(queue.poll(2, TimeUnit.SECONDS));
    }
}

SynchronizedQueue

同步队列

没有容量,当put进去一个元素之后,必须take消费一个才能继续put(简而言之就是容量为1的ArrayBlockingQueue)

package juc.bq;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

/**
 * Synchronized同步队列
 * put了一个元素,必须从里面先删除一个元素,否则不能继续put值
 */
public class SynchronizedQueueDemo {

    public static void main(String[] args) {
        BlockingQueue<Integer> queue = new SynchronousQueue<>();
        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + " put 1");
                queue.put(1);
                System.out.println(Thread.currentThread().getName() + " put 2");
                queue.put(2);
                System.out.println(Thread.currentThread().getName() + " put 3");
                queue.put(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }, "A").start();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName() + "->" + queue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName() + "->" + queue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName() + "->" + queue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "B").start();
    }
}