JUC学习笔记(一)

125 阅读11分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情

JUC笔记

JUC简介

JUC主要包在jdk中的位置

主要有:Condition、Lock、ReadWriteLock几个接口

多线程回顾

业务:普通的线程代码 继承Thread类

Runnable接口 没有返回值,效率相比Callable较低,使用Callable比Runnable多

进程和线程

进程是指在系统中正在运行的应用程序,是执行程序的依次执行过程,是一个动态概念,是资源分配(内存、外设等)的最小单位

线程是进程的基本执行单元,是CPU资源分配的最小单位

区别

  • 进程之间是独立的地址空间,同一个进程的多个线程共享地址空间
  • 同一进程内的线程共享本进程的所有资源,进程之间则是独立的

Java默认有两个进程:主进程main和GC(垃圾回收)守护进程。Java是无法开启线程的,是通过start0()这个本地方法调用底层的C++才能开启

并发和并行

并发: 对于单处理机而言,多条线程快速交替执行,宏观上好像是在同时运行,微观上依然是串行的

并行: 对于多处理机而言,其他资源充足的情况下,多个线程可以分别跑在一个处理机上,互不干扰同时进行,无论宏观上还是微观上,都是同时进行的

public class Test {
    public static void main(String[] args) {
        // 获取CPU的核数
        // CPU密集型,IO密集型
        System.out.println(Runtime.getRuntime().availableProcessors());
    }
}

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

线程状态

public enum State {
    // 新生态
    NEW,
    // 运行时状态
    RUNNABLE,
    // 阻塞状态
    BLOCKED,
    // 等待,死等,直到其他线程执行指定动作,通知结束等待
    WAITING,
    // 一段时间的等待,等待另一个线程执行动作达到指定时间
    TIMED_WAITING,
    // 终止状态
    TERMINATED;
}

wait和sleep的区别

1、来自不同的类,wait来自Object,sleep来自Thread类

2、wait方法会释放锁,sleep不会释放锁

3、适用范围不同,wait必须在同步代码块中使用,sleep可以在任何地方使用

4、wait不需要捕获超时异常,sleep需要捕获超时异常

线程方法

线程优先级

Lock锁

Lock是一个接口,里面有ReentrantLock、ReadLock、WriteLock几个实现类。

在需要加锁的代码块中进行显示加锁和释放锁

可重入锁

参考:segmentfault.com/a/119000002…

ReentrantLock就是一个可重入锁Re-Entrant-Lock:即表示可重新反复进入的锁,但仅限于当前线程;锁的粒度更小,更加灵活

公平锁

先到先得,不可抢占

非公平锁

非严格先到先得,可以抢占。Java中默认为非公平锁

使用步骤

1、Lock lock = new ReentrantLock();定义一个可重入锁 2、lock.lock();添加锁 3、finally => lock.unlock();释放锁

public class TicketLock {
    private int ticketNum;
    // 定义一个可重入锁
    Lock lock = new ReentrantLock();

    public TicketLock(int ticketNum) {
        this.ticketNum = ticketNum;
    }

    public int getTicketNum() {
        return ticketNum;
    }

    public void setTicketNum(int ticketNum) {
        this.ticketNum = ticketNum;
    }

    // 这里改用显式加锁方法
    public void sale() {
        // 显式添加锁
        lock.lock();
        // 尝试获取锁
        // lock.tryLock();
        try {
            // 业务代码
            if (ticketNum > 0) {
                System.out.println(Thread.currentThread().getName() + "买了第" + ticketNum-- + "票");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 释放锁
            lock.unlock();
        }
    }
}

synchronized和Lock锁区别

1、synchronized是内置关键字,而Lock是一个类

2、synchronized无法判读获取锁的状态,Lock可以判断是否获取到了锁

3、synchronized会自动释放锁,用完就释放;Lock需要手动释放锁,不释放可能会导致死锁

4、synchronized得到锁之后,如果阻塞,那么另外的线程会一直等待;Lock锁就不会一直等待,可以尝试使用tryLock()尝试获取锁

5、synchronized是可重入锁,不可中断,非公平的;Lock锁是可重入锁,可以设置是否是公平/非公平锁,通过设置ReentrantLock构造函数的参数

6、synchronized适合锁少量的同步代码问题;Lock适合锁大量的同步代码

生产者和消费者问题

使用synchronized锁实现的生产者和消费者问题

步骤:判断等待(while代码块中使用wait()方法)、业务处理、通知(notify/notifyAll()方法)

多线程笔记中已经实现,这里注意虚假唤醒问题,等待(wait()方法)需要放到while代码块中

代码如下:

/**
 * 生产者、消费者问题,信号量法
 * 这里假设演员表演节目,观众观看节目不能同时进行
 */
public class ProducerAndConsumer2 {
    public static void main(String[] args) {
        // 节目
        TV tv = new TV();
        new Actor(tv).start();
        new Audience(tv).start();
    }
}

// 生产者 -- 演员
class Actor extends Thread {
    TV tv;

    public Actor(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {
                tv.show("央视新闻");
            } else {
                tv.show("广告");
            }
        }
    }
}

// 消费者 -- 观众
class Audience extends Thread {
    TV tv;

    public Audience(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            tv.watch();
        }
    }
}

// 产品 -- 节目

class TV {
    // 演员表演,观众等待
    // 观众观看,演员等待

    // 表演的节目
    private String showName;
    // 表演/观看,默认是表演,为true
    private boolean flag = true;

    public synchronized void show(String showName) {
        // 这里需要使用while循环,避免发生虚假唤醒。如果存在多个消费者进程,如果同时有多个消费者等待之后被唤醒,但执行有先后顺序,前一个消费者执行之后条件可能不再满足其他消费者执行。
        // 如果使用的是if,在等待之前已经判断过了,唤醒之后不会再进行判断,则会发生错误,所以需要使用while
        // 虚假唤醒问题:https://www.cnblogs.com/jichi/p/12694260.html
        while (!flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("演员表演了:" + showName);
        // 通知观众观看
        this.notifyAll();
        this.showName = showName;
        this.flag = !this.flag;
    }

    public synchronized void watch() {
        while (flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("观众观看了:" + showName);
        // 通知演员表演
        this.notifyAll();
        this.flag = !this.flag;
    }
}

使用Lock锁实现的生产者和消费者问题

需要使用到Condition代替传统的wait、notify等方法,关于Condition:www.liaoxuefeng.com/wiki/125259…

代码如下:

public class Product {
    // 产品数量
    private int number = 0;

    // Lock锁
    private Lock lock = new ReentrantLock();
    // 得到Condition
    Condition condition = lock.newCondition();

    // 改用Lock锁配合Condition实现生产者、消费者问题
    // 生产
    public void produce() {
        // 添加锁
        lock.lock();
        try { // 这里需要使用while循环,避免发生虚假唤醒。如果存在多个消费者进程,如果同时有多个消费者等待之后被唤醒,但执行有先后顺序,前一个消费者执行之后条件可能不再满足其他消费者执行。
            // 如果使用的是if,在等待之前已经判断过了,唤醒之后不会再进行判断,则会发生错误,所以需要使用while
            // 虚假唤醒问题:https://www.cnblogs.com/jichi/p/12694260.html
            while (number != 0) {
                // 等待
                condition.await();
            }
            this.number++;
            System.out.println(Thread.currentThread().getName() + "=>" + number);
            // 唤醒全部
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 解锁
            lock.unlock();
        }
    }

    // 消费
    public void consunme() {
        // 添加锁
        lock.lock();
        try { // 这里需要使用while循环,避免发生虚假唤醒。如果存在多个消费者进程,如果同时有多个消费者等待之后被唤醒,但执行有先后顺序,前一个消费者执行之后条件可能不再满足其他消费者执行。
            // 如果使用的是if,在等待之前已经判断过了,唤醒之后不会再进行判断,则会发生错误,所以需要使用while
            // 虚假唤醒问题:https://www.cnblogs.com/jichi/p/12694260.html
            while (number == 0) {
                // 等待
                condition.await();
            }
            this.number--;
            System.out.println(Thread.currentThread().getName() + "=>" + number);
            // 唤醒全部
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 解锁
            lock.unlock();
        }
    }
}

精准通知和唤醒线程

控制执行顺序,在Lock锁实现消费者、生产者问题的基础上实现精准通知,假设需要执行顺序为A->B->C->D

需要Condition配对,不同的Condition监视不同的等待和唤醒

代码如下:

public class DataCondition {
    // 定义Lock锁
    private Lock lock = new ReentrantLock();
    // 获取Condition
    private Condition conditionA = lock.newCondition();
    private Condition conditionB = lock.newCondition();
    private Condition conditionC = lock.newCondition();
    // 资源类数量 为1时A执行,2时B,3时C
    private int number = 1;

    // 输出A
    public void printA() {
        // 加锁
        lock.lock();
        // 判断等待、业务处理、唤醒
        try {
            while (number != 1) {
                conditionA.await();
            }

            System.out.println(Thread.currentThread().getName() + "=>" + "AAA");
            // 设置2,去唤醒B
            number = 2;
            // 唤醒指定的Condition,这里设置number=2并唤醒conditionB
            conditionB.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    // 输出B
    public void printB() {
        // 加锁
        lock.lock();
        // 判断等待、业务处理、唤醒
        try {
            while (number != 2) {
                conditionB.await();
            }

            System.out.println(Thread.currentThread().getName() + "=>" + "BBB");
            // 设置3,去唤醒C
            number = 3;
            // 唤醒指定的Condition,这里设置number=2并唤醒conditionC
            conditionC.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    // 输出C
    public void printC() {
        // 加锁
        lock.lock();
        // 判断等待、业务处理、唤醒
        try {
            while (number != 3) {
                conditionC.await();
            }

            System.out.println(Thread.currentThread().getName() + "=>" + "CCC");
            // 设置1,去唤醒A
            number = 1;
            // 唤醒指定的Condition,这里设置number=2并唤醒conditionA
            conditionA.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

什么是锁?

锁的一般是对象(类的实例)和类(Class)

8锁现象(关于锁的8个问题)理解锁是什么?

1、一般情况下,顺序调用synchronized锁实现的两个同步方法,哪个会先执行?

先获得锁的方法会先执行,通常调用的方法会先获得锁,这里是sendMsg方法先执行

代码示例:

资源类

public class Phone01 {
    public synchronized void sendMsg() {
        System.out.println("发短信");
    }

    // synchronized锁的是方法的调用者,是当前类的一个实例
    public synchronized void call() {
        System.out.println("打电话");
    }
}

测试类

public class PhoneTest01 {
    public static void main(String[] args) throws InterruptedException {
        Phone01 phone = new Phone01();

        // 这里就算sendMsg()方法休眠3秒,也是sendMsg()先执行,因为先调用sendMsg(),所以该方法先获得了phone这个对象的锁
        // 谁先获得锁谁先执行
        // 这里的phone::sendMsg是在lamda表达式里面使用方法引用(方法引用由::双冒号操作符标示)的方式,参考:https://www.bbsmax.com/A/x9J2Pj1nd6/#stream--parallelstream
        // 方法签名只看参数类型和返回类型,不看方法名称,也不看类的继承关系。
        new Thread(phone::sendMsg, "A").start();
        // JUC包下的sleep,这里是睡眠1秒
        TimeUnit.SECONDS.sleep(1);
        new Thread(phone::call, "B").start();
    }
}

2、在 1 的基础上,sendMsg方法中添加延迟,延迟3秒,哪个方法先执行?

依然是先获得锁的方法先执行,就算该方法中添加了延迟,这里还是sendMsg方法先执行

修改 1 中的资源类

public class Phone01 {
    public synchronized void sendMsg() {
        try {
            // 休眠3秒
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    // synchronized锁的是方法的调用者,是当前类的一个实例
    public synchronized void call() {
        System.out.println("打电话");
    }
}

3、在 2 的基础上添加一个普通方法hello,哪个方法先执行?

hello方法先执行,然后是sendMsg,普通方法不需要获得资源类对象的锁,在有CPU资源是就可以直接执行

代码如下:

资源类:

public class Phone01 {
    public synchronized void sendMsg() {
        try {
            // 休眠3秒
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    // synchronized锁的是方法的调用者,是当前类的一个实例
    public synchronized void call() {

        System.out.println("打电话");
    }

    // 非同步方法不受锁的影响
    public void hello() {
        System.out.println("非同步方法");
    }
}

测试类:

public class PhoneTest01 {
    public static void main(String[] args) throws InterruptedException {
        Phone01 phone = new Phone01();

        // 谁先获得锁谁先执行
        new Thread(phone::sendMsg, "A").start();
        // JUC包下的sleep,这里是睡眠1秒
        TimeUnit.SECONDS.sleep(1);
        new Thread(phone::call, "B").start();
        // hello()方法不需要获取锁,所以不需要等待A或B执行完,可以自主执行
        new Thread(phone::hello, "C").start();
    }
}

4、在 2 的基础上,再添加一个资源类,两个线程分别调用两个资源类中的不同方法,哪个方法先执行?

call先执行,因为sendMsg中有一个延迟。这时有两个对象,则有两把锁,phone和phone1的锁互不干扰,即使sendMsg先获得了phone的锁,但不影响phone1, 但因为sendMsg中有一个3秒的延迟,所以call 会获得CPU 资源,call先执行

代码示例:

资源类:

public class Phone01 {
    public synchronized void sendMsg() {
        try {
            // 休眠3秒
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    // synchronized锁的是方法的调用者,是当前类的一个实例
    public synchronized void call() {
        System.out.println("打电话");
    }
}

测试类:

public class PhoneTest01 {
    public static void main(String[] args) throws InterruptedException {
        Phone01 phone = new Phone01();
        // 再添加一个资源类,这时就有了两个对象,两把锁
        Phone01 phone1 = new Phone01();

        // 谁先获得锁谁先执行
        new Thread(phone::sendMsg, "A").start();
        // JUC包下的sleep,这里是睡眠1秒
        TimeUnit.SECONDS.sleep(1);
        new Thread(phone1::call, "B").start();
    }
}

5、在 2 的基础上,将普通同步方法改为静态同步方法之后,哪个方法先执行?

sendMsg先执行,这时由于是静态方法,则在类一加载时就被加载进内存了,这时synchronized锁的是Class,所以先调用sendMsg,则sendMsg方法先获得锁,先执行

代码示例:

资源类:

public class Phone02 {
    // 修改为静态同步方法,类一加载时方法就加载到内存中了,这时synchronized锁的是Class,不再是对象
    public static synchronized void sendMsg() {
        try {
            // 休眠3秒
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    // synchronized锁的是方法的调用者,是当前类的一个实例
    public static synchronized void call() {

        System.out.println("打电话");
    }
}

测试类:

public class PhoneTest02 {
    public static void main(String[] args) throws InterruptedException {
        // 这里就算sendMsg()方法休眠3秒,也是sendMsg()先执行,因为先调用sendMsg(),所以该方法先获得了Phone02这个Class的锁
        // 谁先获得锁谁先执行
        new Thread(Phone02::sendMsg, "A").start();
        // JUC包下的sleep,这里是睡眠1秒
        TimeUnit.SECONDS.sleep(1);
        new Thread(Phone02::call, "B").start();
    }
}

6、在 5 的基础上,先构建两个资源类对象,两个线程再通过这两个资源类分别调用不同的方法,哪个方法先执行?

sendMsg先执行,这时由于是静态方法,则在类一加载时就被加载进内存了,这时synchronized锁的是Class,所以先调用sendMsg,则sendMsg方法先获得锁,先执行。这时无论几个对象,都是sendMsg 方法先执行,因为这时synchronized锁定的是Class(唯一的),而不是具体的对象,所以和具体对象的个数无关。甚至不需要构建对象,可以直接通过类名调用对应的方法。

7、在 5 的基础上修改,call改为非静态方法,调用顺序不变,哪个方法先执行?

call方法先执行,sendMsg中有延迟。这是因为sendMsg是静态方法,锁的的是Class;call是非静态方法,锁的是phone这个对象。二者不是同一个锁,而且互不干扰,但sendMsg存在延迟,所以call先执行

代码示例:

资源类:

public class Phone02 {
    // 修改为静态同步方法,类一加载时方法就加载到内存中了,这时synchronized锁的是Class,不再是对象
    public static synchronized void sendMsg() {
        try {
            // 休眠3秒
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    // synchronized锁的是方法的调用者,是当前类的一个实例
    public synchronized void call() {
        System.out.println("打电话");
    }
}

测试类:

public class PhoneTest02 {
    public static void main(String[] args) throws InterruptedException {
        Phone02 phone = new Phone02();
        // 谁先获得锁谁先执行
        new Thread(Phone02::sendMsg, "A").start();
        // JUC包下的sleep,这里是睡眠1秒
        TimeUnit.SECONDS.sleep(1);
        new Thread(phone::call, "B").start();
    }
}

8、在 7 的基础上,再添加一个资源类,一个调用静态方法sendMsg,一个调用非静态方法call,哪一个先执行?

call先执行,原理同 7 类似,增加资源类调用静态方法,同样获得是Class(唯一)的锁,多个对象调用静态方法和使用类名调用静态方法,效果是相同的。而非静态方法获得的phone对象的锁仍然不受Class锁的影响,sendMsg 存在延迟,所以call先执行。