1、简介

什么是JUC

JUC是java.util.concurrent 的简写,在并发编程中使用的工具类。

在jdk官方手册中可以看到juc相关的jar包有三个。

用中文概括一下,JUC的意思就是java并发编程工具包

实现多线程有三种方式:Thread、Runnable、Callable,其中Callable就位于concurrent包下

进程和线程

进程 / 线程是什么?

进程:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

线程:通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义,线程可以利用进程所有拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程小,基本上不拥有系统资源, 故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。

白话:

进程:就是操作系统中运行的一个程序,QQ.exe,music.exe,word.exe ,这就是多个进程

线程:每个进程中都存在一个或者多个线程,比如用word写文章时,就会有一个线程默默帮你定时自动保存。

并发 / 并行是什么?

做并发编程之前,必须首先理解什么是并发,什么是并行。

并发和并行是两个非常容易混淆的概念。它们都可以表示两个或多个任务一起执行,但是偏重点有点不同。并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的。并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。然而并行的偏重点在于”同时执行”。

严格意义上来说,并行的多个任务是真实的同时执行,而对于并发来说,这个过程只是交替的,一会运行任务一,一会儿又运行任务二,系统会不停地在两者间切换。但对于外部观察者来说,即使多个任务是串行并发的,也会造成是多个任务并行执行的错觉。

实际上,如果系统内只有一个CPU,而现在而使用多线程或者多线程任务,那么真实环境中这些任务不可能真实并行的,毕竟一个CPU一次只能执行一条指令,这种情况下多线程或者多线程任务就是并发的,而不是并行,操作系统会不停的切换任务。真正的并发也只能够出现在拥有多个CPU的系统中(多核CPU)。

并发的动机:在计算能力恒定的情况下处理更多的任务, 就像我们的大脑, 计算能力相对恒定, 要在一天中处理更多的问题, 我们就必须具备多任务的能力. 现实工作中有很多事情可能会中断你的当前任务, 处理这种多任务的能力就是你的并发能力。

并行的动机:用更多的CPU核心更快的完成任务. 就像一个团队, 一个脑袋不够用了, 一个团队来一起处理 一个任务。

例子:
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。 (不一定是
同时的)
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。

线程的状态

Java的线程有6种状态:可以分析源码:

public enum State {
    //线程刚创建
    NEW,
    
    //在JVM中正在运行的线程
    RUNNABLE,
    
    //线程处于阻塞状态,等待监视锁,可以重新进行同步代码块中执行
    BLOCKED,
    
    //等待状态
    WAITING,
    
    //调用sleep() join() wait()方法可能导致线程处于等待状态
    TIMED_WAITING,
    
    //线程执行完毕,已经退出
    TERMINATED;
}
image-20220817222250406

wait / sleep 的区别

1、来自不同的类

这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object类。

sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用了b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。

2、有没有释放锁(释放资源)

最主要是sleep方法没有释放锁

而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。

sleep是线程被调用时,占着cpu去睡觉,其他线程不能占用cpu,os认为该线程正在工作,不会让出系统资源,wait是进入等待池等待,让出系统资源,其他线程可以占用cpu。

sleep(100L)是占用cpu,线程休眠100毫秒,其他进程不能再占用cpu资源,wait(100L)是进入等待池中等待,交出cpu等系统资源供其他进程使用,在这100毫秒中,该线程可以被其他线程notify,但不同的是其他在等待池中的线程不被notify不会出来,但这个线程在等待100毫秒后会自动进入就绪队列等待系统分配资源,换句话说,sleep(100)在100毫秒后肯定会运行,但wait在100毫秒后还有等待os调用分配资源,所以wait100的停止运行时间是不确定的,但至少是100毫秒。
就是说sleep有时间限制的就像闹钟一样到时候就叫了,而wait是无限期的除非用户主动notify。

3、使用范围不同

wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用

synchronized(x){
    //或者wait()
    x.notify()
}

4、是否需要捕获异常

sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常

2、Lock锁

synchronized锁

public class SaleTicketTest1 {
    /*
     * 题目:三个售票员 卖出 30张票
     * 多线程编程的企业级套路:
     * 1. 在高内聚低耦合的前提下, 线程 操作(对外暴露的调用方法) 资源类
     */

    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 40; i++) {
                    ticket.saleTicket();
                }
            }
        }, "A").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <=40; i++) {
                    ticket.saleTicket();
                }
            }
        }, "B").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 40; i++) {
                    ticket.saleTicket();
                }
            }
        }, "C").start();

    }
    
}

class Ticket { // 资源类
    private int number = 30;

    public synchronized void saleTicket() {
        if (number > 0) {
            System.out.println(Thread.currentThread().getName() + "卖出第 " + (number--) + "票,还剩下:" + number);
        }
    }
}

Lock 锁

public class SaleTicketTest2 {
    public static void main(String[] args) {
        Ticket2 ticket2 = new Ticket2();

        new Thread(() -> {
            for (int i = 1; i <= 40; i++) {
                ticket2.saleTicket();
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 1; i <= 40; i++) {
                ticket2.saleTicket();
            }
        }, "B").start();

        new Thread(() -> {
            for (int i = 1; i <= 40; i++) {
                ticket2.saleTicket();
            }
        }, "C").start();

    }
}

class Ticket2 { // 资源类
    private Lock lock = new ReentrantLock();

    private int number = 30;

    public void saleTicket() {
        lock.lock();

        try {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出第 " + (number--) + "票,还剩下:" + number);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }
}

区别

  1. 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
  2. synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
  3. synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放
    锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
  4. 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1
    阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以
    不用一直等待就结束了;
  5. synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
  6. Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

3、生产者和消费者

synchroinzed

生产者和消费者 synchroinzed 版

public class ProducerConsumer {
    /**
     * 题目:现在两个线程,可以操作初始值为0的一个变量
     * 实现一个线程对该变量 + 1,一个线程对该变量 -1
     * 实现交替10次
     * <p>
     * 诀窍:
     * 1. 高内聚低耦合的前提下,线程操作资源类
     * 2. 判断 、干活、通知
     */

    public static void main(String[] args) {
        Data data = new Data();

        //A线程增加
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();

        //B线程减少
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
    }
}

class Data {
    private int number = 0;

    public synchronized void increment() throws InterruptedException {
        // 判断该不该这个线程做
        if (number != 0) {
            this.wait();
        }
        // 干活
        number++;
        System.out.println(Thread.currentThread().getName() + "\t" + number);
        // 通知
        this.notifyAll();
    }

    public synchronized void decrement() throws InterruptedException {
        // 判断该不该这个线程做
        if (number == 0) {
            this.wait();
        }
        // 干活
        number--;
        System.out.println(Thread.currentThread().getName() + "\t" + number);
        // 通知
        this.notifyAll();
    }

}

问题升级:防止虚假唤醒,4个线程,两个加,两个减

【重点】if 和 while

public class ProducerConsumerPlus {
    /**
     * 题目:现在四个线程,可以操作初始值为0的一个变量
     * 实现两个线程对该变量 + 1,两个线程对该变量 -1
     * 实现交替10次
     *
     * 诀窍:
     * 1. 高内聚低耦合的前提下,线程操作资源类
     * 2. 判断 、干活、通知
     * 3. 多线程交互中,必须要防止多线程的虚假唤醒,即(判断不能用if,只能用while)
     */

    public static void main(String[] args) {
        Data2 data = new Data2();

        //A线程增加
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();

        //B线程减少
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();

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

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

class Data2 {
    private int number = 0;

    public synchronized void increment() throws InterruptedException {
        // 判断该不该这个线程做
        while (number != 0) {
            this.wait();
        }
        // 干活
        number++;
        System.out.println(Thread.currentThread().getName() + "\t" + number);
        // 通知
        this.notifyAll();
    }

    public synchronized void decrement() throws InterruptedException {
        // 判断该不该这个线程做
        while (number == 0) {
            this.wait();
        }
        // 干活
        number--;
        System.out.println(Thread.currentThread().getName() + "\t" + number);
        // 通知
        this.notifyAll();
    }

}

lock

public class ProducerConsumerPlus {
    /**
     * 题目:现在四个线程,可以操作初始值为0的一个变量
     * 实现两个线程对该变量 + 1,两个线程对该变量 -1
     * 实现交替10次
     * <p>
     * 诀窍:
     * 1. 高内聚低耦合的前提下,线程操作资源类
     * 2. 判断 、干活、通知
     * 3. 多线程交互中,必须要防止多线程的虚假唤醒,即(判断不能用if,只能用while)
     */

    public static void main(String[] args) {
        Data2 data = new Data2();

        //A线程增加
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();

        //B线程减少
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();

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

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

class Data2 {
    private int number = 0;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void increment() throws InterruptedException {

        lock.lock();
        try {
            // 判断该不该这个线程做
            while (number != 0) {
                condition.await();
            }
            // 干活
            number++;
            System.out.println(Thread.currentThread().getName() + "\t" + number);
            // 通知
            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() + "\t" + number);
            // 通知
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

}

以上写的程序并不会按照ABCD线程顺序,只会按照 “生产” “消费”顺序

按照线程顺序执行

精确通知顺序访问

public class c {
    /**
     * 题目:多线程之间按顺序调用,实现 A->B->C
     * 重点:标志位
     */

    public static void main(String[] args) {
        Resources resources = new Resources();

        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                resources.a();
            }

        }, "A").start();

        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                resources.b();
            }

        }, "B").start();

        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                resources.c();
            }

        }, "C").start();

    }
}

class Resources {
    private int number = 1; // 1A 2B 3C
    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();

    public void a() {
        lock.lock();
        try {
            // 判断
            while (number != 1) {
                condition1.await();
            }
            // 干活
            System.out.println(Thread.currentThread().getName());
            // 通知,指定的干活!
            number = 2;
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void b() {
        lock.lock();
        try {
            // 判断
            while (number != 2) {
                condition2.await();
            }
            // 干活
            System.out.println(Thread.currentThread().getName() );

            // 通知,指定的干活!
            number = 3;
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void c() {
        lock.lock();
        try {
            // 判断
            while (number != 3) {
                condition3.await();
            }
            // 干活
            System.out.println(Thread.currentThread().getName());

            // 通知,指定的干活!
            number = 1;
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

4、8锁的现象

问题一

1、标准访问,请问先打印邮件还是短信?

package com.oddfar.lock8;

import java.util.concurrent.TimeUnit;

public class A {
    /**
     * 多线程的8锁
     * 1、标准访问,请问先打印邮件还是短信?
     */
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(() -> {
            try {
                phone.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "A").start();

        //休眠一秒
        //Thread.sleep(1000);
        TimeUnit.SECONDS.sleep(1);
        
        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "B").start();

    }
}

class Phone {
    public synchronized void sendEmail(){
        System.out.println("sendEmail");
    }

    public synchronized void sendSMS(){
        System.out.println("sendSMS");
    }
}

答案:sendEmail

结论:被synchronized修饰的方法,锁的对象是方法的调用者。因为两个方法的调用者是同一个,所以两个方法用的是同一个锁,先调用方法的先执行。

问题二

2、邮件方法暂停4秒钟,请问先打印邮件还是短信?

package com.oddfar.lock8;

import java.util.concurrent.TimeUnit;

public class B {
    /**
     * 多线程的8锁
     * 2、邮件方法暂停4秒钟,请问先打印邮件还是短信?
     */
    public static void main(String[] args) throws InterruptedException {
        Phone2 phone = new Phone2();
        new Thread(() -> {
            try {
                phone.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "A").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "B").start();

    }
}

class Phone2 {
    public synchronized void sendEmail() throws Exception {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("sendEmail");
    }

    public synchronized void sendSMS(){
        System.out.println("sendSMS");
    }
}

答案:sendEmail

结论:被synchronized修饰的方法,锁的对象是方法的调用者。因为两个方法的调用者是同一个,所以两个方法用的是同一个锁,先调用方法的先执行,第二个方法只有在第一个方法执行完释放锁之后才能执行。

问题三

3、新增一个普通方法hello()不加锁,请问先打印邮件还是hello?

package com.oddfar.lock8;

import java.util.concurrent.TimeUnit;

/**
 * @author zhiyuan
 */
public class C {
    /**
     * 多线程的8锁
     * 3、新增一个普通方法hello()不加锁,请问先打印邮件还是hello?
     */
    public static void main(String[] args) throws InterruptedException {
        Phone3 phone = new Phone3();
        new Thread(() -> {
            try {
                phone.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "A").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            try {
                phone.hello();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "B").start();

    }
}

class Phone3 {
    public synchronized void sendEmail() throws Exception {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("sendEmail");
    }
    
    // 没有 synchronized,没有 static 就是普通方式
    public void hello() {
        System.out.println("Hello");
    }
}

答案:Hello

结论:如果一个方法没有被synchronized修饰,不是同步方法,不受锁的影响,所以不需要等待。

问题四

4、两个手机,一个手机发邮件,另一个发短信,请问先执行sendEmail 还是 sendSMS

package com.oddfar.lock8;

import java.util.concurrent.TimeUnit;

public class D {
    /**
     * 多线程的8锁
     * 4、两个手机,请问先执行sendEmail 还是 sendSMS
     */
    public static void main(String[] args) throws InterruptedException {
        Phone4 phone = new Phone4();
        Phone4 phone2 = new Phone4();

        new Thread(() -> {
            try {
                phone.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "A").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            try {
                phone2.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "B").start();

    }
}

class Phone4 {
    public synchronized void sendEmail() throws Exception {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("sendEmail");
    }

    public synchronized void sendSMS() {
        System.out.println("sendSMS");
    }
}

答案:先执行“sendSMS”

结论:被synchronized修饰的方法,锁的对象是方法的调用者。用了两个对象调用各自的方法,所以两个方法的调用者不是同一个,于是两个方法用的不是同一个锁,后调用的方法不需要等待先调用的方法。

问题五

5、两个静态同步方法,同一部手机,请问先打印邮件还是短信?

package com.oddfar.lock8;

import java.util.concurrent.TimeUnit;

public class E {
    /**
     * 多线程的8锁
     * 5、两个静态同步方法,同一部手机,请问先打印邮件还是短信?
     */
    public static void main(String[] args) throws InterruptedException {
        Phone5 phone = new Phone5();

        new Thread(() -> {
            try {
                phone.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "A").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "B").start();

    }
}

class Phone5 {
    public static synchronized void sendEmail() throws Exception {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("sendEmail");
    }

    public static synchronized void sendSMS() {
        System.out.println("sendSMS");
    }
}

答案:先执行“sendEmail”

结论:被synchronized和static修饰的方法,锁的对象是类的class模板对象,这个则全局唯一!两个方法都被static修饰了,所以两个方法用的是同一个锁,后调用的方法需要等待先调用的方法。

问题六

6、两个静态同步方法,2部手机,一个手机发邮件,另一个发短信,请问先打印邮件还是短信?

package com.oddfar.lock8;

import java.util.concurrent.TimeUnit;

/**
 * @author zhiyuan
 */
public class F {
    /**
     * 多线程的8锁
     * 6、两个静态同步方法,2部手机,一个手机发邮件,另一个发短信,请问先打印邮件还是短信?
     */
    public static void main(String[] args) throws InterruptedException {
        Phone6 phone = new Phone6();

        Phone6 phone2 = new Phone6();

        new Thread(() -> {
            try {
                phone.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "A").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            try {
                phone2.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "B").start();

    }
}

class Phone6 {
    public static synchronized void sendEmail() throws Exception {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("sendEmail");
    }

    public static synchronized void sendSMS() {
        System.out.println("sendSMS");
    }
}

答案:先输出“sendEmail”

结论:被synchronized和static修饰的方法,锁的对象就是Class模板对象,这个则全局唯一!所以说这里是同一个

问题七

7、一个普通同步方法,一个静态同步方法,同一部手机,请问先打印邮件还是短信?

package com.oddfar.lock8;

import java.util.concurrent.TimeUnit;

public class G {
    /**
     * 多线程的8锁
     * 7、一个普通同步方法,一个静态同步方法,同一部手机,请问先打印邮件还是短信?
     */
    public static void main(String[] args) throws InterruptedException {
        Phone7 phone = new Phone7();

        new Thread(() -> {
            try {
                phone.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "A").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "B").start();

    }
}

class Phone7 {
    public static synchronized void sendEmail() throws Exception {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("sendEmail");
    }

    public synchronized void sendSMS() {
        System.out.println("sendSMS");
    }
}

答案:先执行“sendSMS”

结论:synchronized 锁的是这个调用的对象。被synchronized和static修饰的方法,锁的是这个类的Class模板 。这里是两个锁!

问题八

8、一个普通同步方法,一个静态同步方法,2部手机,一个发邮件,一个发短信,请问哪个先执行?

package com.oddfar.lock8;

import java.util.concurrent.TimeUnit;

/**
 * @author zhiyuan
 */
public class H {
    /**
     * 多线程的8锁
     * 8、一个普通同步方法,一个静态同步方法,2部手机,一个发邮件,一个发短信,请问先打印邮件还是短信?
     */
    public static void main(String[] args) throws InterruptedException {
        Phone8 phone = new Phone8();
        Phone8 phone2 = new Phone8();

        new Thread(() -> {
            try {
                phone.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "A").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            try {
                phone2.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "B").start();

    }
}

class Phone8 {
    public static synchronized void sendEmail() throws Exception {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("sendEmail");
    }

    public synchronized void sendSMS() {
        System.out.println("sendSMS");
    }
}

答案:sendSMS

结论:被synchronized和static修饰的方法,锁的对象是类的class对象。仅被synchronized修饰的方法,锁的对象是方法的调用者。即便是用同一个对象调用两个方法,锁的对象也不是同一个,所以两个方法用的不是同一个锁,后调用的方法不需要等待先调用的方法。

小结

1、new this 调用的这个对象,是一个具体的对象!

2、static class 唯一的一个模板!

一个对象里面如果有多个synchronized方法,某个时刻内,只要一个线程去调用其中一个synchronized 方法了,其他的线程都要等待,换句话说,在某个时刻内,只能有唯一一个线程去访问这些 synchronized方法,锁的是当前对象this,被锁定后,其他的线程都不能进入到当前对象的其他的 synchronized方法

加个普通方法后发现和同步锁无关,换成两个对象后,不是同一把锁,情况变化

都换成静态同步方法后,情况又变化了。所有的非静态的同步方法用的都是同一把锁(锁的class模板)

具体的表现为以下三种形式:

  • 对于普通同步方法,锁的是当前实例对象

  • 对于静态同步方法,锁的是当前的Class对象。

  • 对于同步方法块,锁是synchronized括号里面的配置对象

当一个线程试图访问同步代码块时,他首先必须得到锁,退出或者是抛出异常时必须释放锁,也就是说 如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,可以是别的实例对象非非静态同步方法因为跟该实例对象的非静态同步方法用 的是不同的锁,所以必须等待该实例对象已经获取锁的非静态同步方法释放锁就可以获取他们自己的 锁。

所有的静态同步方法用的也是同一把锁(类对象本身) ,这两把锁的是两个不同的对象,所以静态的同步方法与非静态的同步方法之间是不会有竞争条件的,但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要他们用一个的是同一个类的实例对象。

5、多线程下集合类的不安全

list不安全

多线程下

public class ListTest {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        // 对比3个线程 和 30个线程,看区别
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}

运行报错:java.util.ConcurrentModificationException

导致原因:add 方法没有加锁

解决方案:

/**
 * 换一个集合类
 *  1、List<String> list = new Vector<>(); JDK1.0 就存在了!
 *  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 = 1; i <= 30; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}

写入时复制(CopyOnWrite)思想

写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种优化策略。其核心思想是,如果有多个调用者(Callers)同时要求相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者视图修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本 (private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。

CopyOnWriteArrayList为什么并发安全且性能比Vector好

Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector,CopyOnWriteArrayList支持读多写少的并发情况。

set不安全

/**
 * 1、Set<String> set = Collections.synchronizedSet(new HashSet<>());
 * 2、Set<String> set = new CopyOnWriteArraySet();
 */
public class SetTest {
    public static void main(String[] args) {

        Set<String> set = new CopyOnWriteArraySet();

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

map不安全

hashMap底层是数组+链表+红黑树

Map<String,String> map = new HashMap<>();
// 等价于
Map<String,String> map = new HashMap<>(16,0.75);
// 工作中,常常会自己根据业务来写参数,提高效率

map不安全测试:

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

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

解决:

Map<String,String> map = new ConcurrentHashMap<>();

6、Callable

我们已经知道Java中常用的两种线程实现方式:分别是继承Thread类和实现Runnable接口。

image-20220817222701884

从上图中,我们可以看到,第三种实现Callable接口的线程,而且还带有返回值的。我们来对比下实现Runnable和实现Callable接口的两种方式不同点:

1:需要实现的方法名称不一样:一个run方法,一个call方法

2:返回值不同:一个void无返回值,一个带有返回值的。其中返回值的类型和泛型V是一致的。

3:异常:一个无需抛出异常,一个需要抛出异常。

基础入门

public class CallableDemo {
    public static void main(String[] args) throws Exception {
        MyThread myThread = new MyThread();
        FutureTask futureTask = new FutureTask(myThread); // 适配类
        Thread t1 = new Thread(futureTask, "A"); // 调用执行
        t1.start();
        Integer result = (Integer) futureTask.get(); // 获取返回值
        System.out.println(result);
    }
}

class MyThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("call 被调用");
        return 1024;
    }
}
image-20220817222744493

多个线程调用

public class CallableDemo {
    public static void main(String[] args) throws Exception {
        MyThread myThread = new MyThread();
        FutureTask futureTask = new FutureTask(myThread); // 适配类

        new Thread(futureTask, "A").start(); // 调用执行
        // 第二次调用执行,在同一个futureTask对象,不输出结果,可理解为“缓存”
        new Thread(futureTask, "B").start(); 

        //get 方法获得返回结果! 一般放在最后一行!否则可能会阻塞
        Integer result = (Integer) futureTask.get(); // 获取返回值
        System.out.println(result);
    }
}

class MyThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName() + "\tcall 被调用");
        TimeUnit.SECONDS.sleep(2);
        return 1024;
    }
}

参考资料

7、常用辅助类

CountDownLatch

“倒计时锁存器”

例如,执行完6个线程输出执行完毕

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        // 计数器
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\tStart");
                countDownLatch.countDown(); // 计数器-1
            }, String.valueOf(i)).start();
        }
        //阻塞等待计数器归零
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName() + "\tEnd");
    }
    
}

CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这些线程会阻塞

其他线程调用CountDown()方法会将计数器减1(调用CountDown方法的线程不会阻塞)

当计数器变为0时,await 方法阻塞的线程会被唤醒,继续执行

CyclicBarrier

翻译:CyclicBarrier 篱栅

作用:和上面的减法相反,这里是加法,好比集齐7个龙珠召唤神龙,或者人到齐了再开会!

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        // CyclicBarrier(int parties, Runnable barrierAction)
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
            System.out.println("召唤神龙成功");
        });

        for (int i = 1; i <= 7; i++) {
            final int tempInt = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + 
                        "收集了第" + tempInt + "颗龙珠");

                try {
                    cyclicBarrier.await(); // 等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }

            }).start();
        }
    }
}

Semaphore

翻译:Semaphore 信号量;信号灯;信号

举个“抢车位”的例子

public class SemaphoreDemo {
    public static void main(String[] args) {
        // 模拟资源类,有3个空车位
        Semaphore semaphore = new Semaphore(3);
        for (int i = 1; i <= 6; i++) { // 模拟6个车
            new Thread(() -> {
                try {
                    semaphore.acquire(); // acquire 得到
                    System.out.println(Thread.currentThread().getName() + " 抢到了车位");
                    TimeUnit.SECONDS.sleep(3); // 停3秒钟
                    System.out.println(Thread.currentThread().getName() + " 离开了车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release(); // 释放这个位置
                }
            }, String.valueOf(i)).start();
        }

    }
}

在信号量上我们定义两种操作:

  • acquire(获取)

    当一个线程调用 acquire 操作时,他要么通过成功获取信号量(信号量-1)

    要么一直等下去,直到有线程释放信号量,或超时

  • release (释放)

    会将信号量的值 + 1,然后唤醒等待的线程

信号量主要用于两个目的:一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。

8、读写锁

ReadWriteLock

独占锁(写锁):指该锁一次只能被一个线程锁持有。对于ReentranrLock和 Synchronized 而言都是独占锁。

共享锁(读锁):该锁可被多个线程所持有。

对于ReentrantReadWriteLock其读锁时共享锁,写锁是独占锁,读锁的共享锁可保证并发读是非常高效的。

public class ReadWriteLockDemo {
    /**
     * 多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。
     * 但是,如果有一个线程想去写共享资源,就不应该再有其他线程可以对该资源进行读或写。
     * 1. 读-读 可以共存
     * 2. 读-写 不能共存
     * 3. 写-写 不能共存
     */
    public static void main(String[] args) {
        MyCacheLock myCache = new MyCacheLock();
        // 写
        for (int i = 1; i <= 5; i++) {
            final int tempInt = i;
            new Thread(() -> {
                myCache.put(tempInt + "", tempInt + "");
            }, String.valueOf(i)).start();
        }

        // 读
        for (int i = 1; i <= 5; i++) {
            final int tempInt = i;
            new Thread(() -> {
                myCache.get(tempInt + "");
            }, 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() + " 写入成功!");
    }

    public void get(String key) {
        System.out.println(Thread.currentThread().getName() + " 读取" + key);
        Object result = map.get(key);
        System.out.println(Thread.currentThread().getName() + " 读取结果:" + result);
    }
}

// 加锁
class MyCacheLock {
    private volatile Map<String, Object> map = new HashMap<>();
    private 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() + " 写入成功!");
        } 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 result = map.get(key);
            System.out.println(Thread.currentThread().getName() + " 读取结果:" + result);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
}

9、阻塞队列

Interface  BlockingQueue<E>

阻塞队列简介

阻塞:必须要阻塞、不得不阻塞

阻塞队列是一个队列,在数据结构中起的作用如下图:

image-20220817222938698

当队列是空的,从队列中获取元素的操作将会被阻塞。

当队列是满的,从队列中添加元素的操作将会被阻塞。

试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素。

试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增。

阻塞队列的用处

在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自 动被唤起。

为什么需要 BlockingQueue?

好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue 都 给你一手包办了。

在 concurrent 包发布以前,在多线程环境下,我们每个程序员都必须自己去控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。

接口架构图

image-20220817222959042

  • ArrayBlockingQueue

    由数组结构组成的有界阻塞队列。

  • LinkedBlockingQueue

    由链表结构组成的有界(默认值为:integer.MAX_VALUE)阻塞队列。

  • PriorityBlockingQueue

    支持优先级排序的无界阻塞队列

  • DelayQueue

    使用优先级队列实现的延迟无界阻塞队列。

  • SynchronousQueue

    不存储元素的阻塞队列,也即单个元素的队列。

  • LinkedTransferQueue

    由链表组成的无界阻塞队列

  • LinkedBlockingDeque

    由链表组成的双向阻塞队列。

API的使用

方法\处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
插入方法 add(e) offer(e) put(e) offer(e,time,unit)
移除方法 remove() poll() take() poll(time,unit)
检查方法 element() peek() 不可用 不可用

解释:

  • 抛出异常

当阻塞队列满时,再往队列里add插入元素会抛出 IllegalStateException: Queue full

当阻塞队列空时,再往队列里remove移除元素会抛 NoSuchElementException`

  • 返回特殊值

插入方法,成功返回true,失败则false

移除方法,成功返回队列元素,队列里没有则返回null

  • 一直阻塞

当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产者线程直到put数据或响应中断退出

当阻塞队列空时,消费者线程从队列里take元素,队列会一直阻塞消费者线程直到队列可用

  • 超时退出

当阻塞队列满时,队列会阻塞生产者线程一定时间,超过限时后生产者线程会退出

抛出异常

package com.oddfar.bq;

import java.util.concurrent.ArrayBlockingQueue;

/**
 * @author zhiyuan
 */
public class BlockingQueueDemo {

    public static void main(String[] args) {
        // 队列大小
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        System.out.println(blockingQueue.add("c"));

        //java.lang.IllegalStateException: Queue full
//        System.out.println(blockingQueue.add("d"));

        System.out.println("首元素:" + blockingQueue.element()); // 检测队列队首元素!
        // public E remove() 返回值E,就是移除的值
        System.out.println(blockingQueue.remove()); //a
        System.out.println(blockingQueue.remove()); //b
        System.out.println(blockingQueue.remove()); //c
        // java.util.NoSuchElementException
//        System.out.println(blockingQueue.remove());

    }
}

返回特殊值

public class BlockingQueueDemo2 {
    public static void main(String[] args) {
        // 队列大小
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);

        System.out.println(blockingQueue.offer("a")); // true
        System.out.println(blockingQueue.offer("b")); // true
        System.out.println(blockingQueue.offer("c")); // true
        //System.out.println(blockingQueue.offer("d")); // false

        System.out.println("首元素:" + blockingQueue.peek()); // 检测队列队首元素!

        // public E poll()
        System.out.println(blockingQueue.poll()); // a
        System.out.println(blockingQueue.poll()); // b
        System.out.println(blockingQueue.poll()); // c
        System.out.println(blockingQueue.poll()); // null
    }
}

一直阻塞

public class BlockingQueueDemo3 {
    public static void main(String[] args) throws InterruptedException {
        // 队列大小
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);

        // 一直阻塞
        blockingQueue.put("a");
        blockingQueue.put("b");
        blockingQueue.put("c");
//         blockingQueue.put("d");
        System.out.println(blockingQueue.take()); // a
        System.out.println(blockingQueue.take()); // b
        System.out.println(blockingQueue.take()); // c
        System.out.println(blockingQueue.take()); // 阻塞不停止等待
    }
}

超时退出

public class BlockingQueueDemo4 {
    public static void main(String[] args) throws InterruptedException {
        // 队列大小
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);

        // 一直阻塞
        blockingQueue.put("a");
        blockingQueue.put("b");
        blockingQueue.put("c");
        blockingQueue.offer("d",2L, TimeUnit.SECONDS); // 等待2秒超时退出

        System.out.println(blockingQueue.take()); // a
        System.out.println(blockingQueue.take()); // b
        System.out.println(blockingQueue.take()); // c
        System.out.println(blockingQueue.take()); // 阻塞不停止等待
    }
}