【Java】多线程笔记-尚硅谷JUC视频

153 阅读7分钟

Java 多线程——JUC

1、相关基础概念

  • 进程
  • 线程
  • 管程:又叫 monitorJava 中就是所说的。是一种同步机制,保证同一个时间,只有一个线程能够访问或者代码。
  • JVM 同步基于进入合退出,使用管程对象实现
  • 用户线程:平时所用到的线程基本上都是的,比如说 new Thread() 出来的。自定义线程。
  • 守护线程:用于后台中的一种特殊的线程,比如说垃圾回收
public class Main {
    public static void main(String[] args) {
        Thread aa = new Thread(() ->  {
            // isDaemon 表明是用户线程还是守护线程
            System.out.println(Thread.currentThread().getName() + "::" + Thread.currentThread().isDaemon());
            while (true) {

            }
        }, "aa");
        // 设置守护线程
        aa.setDaemon(true); // 代码A
        aa.start();
        System.out.println(Thread.currentThread().getName() + "over");
    }
}

上面的代码的现象是:主线程结束了,但是用户线程还没有结束,jvm 存活。如果增加上代码A,则没有用户线程,都是守护线程,jvm 结束.

2、 Synchronized 关键字

synchronized 是 Java 中的关键字,是一种同步锁。可以修饰的对象有以下几种:

  1. 修饰一个代码块,被修饰的代码块成为同步语句块,其作用范围是大括号 {} 括起来的代码,作用的对象是调用这个代码块的对象;
  2. 修饰一个方法,被修饰的方法成为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
    • 虽然可以使用 synchronized 来定义方法,但是其并不属于方法定义的一部分,因此,synchronized 关键字不能被继承。
  3. 修饰一个静态方法,其作用范围是整个静态方法,作用的对象是这个类的所有对象
  4. 修饰一个类,其作用范围是**synchronized后面括号括起来的部分**,作用对象是这个类的所有对象
// 3 个售票员,卖出 30 张票
class Ticket {
    // 票数
    private int number = 30;

    // 操作方法:卖票
    public synchronized void sale() {
        // 判断:是否有票
        if (number > 0) {
            System.out.println(Thread.currentThread().getName() + ": 卖出了: " + (number--) + " 剩下: " + number);
        }
    }
}

public class SaleTicket {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        }, "AA").start();

        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        }, "BB").start();

        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        }, "CC").start();
    }
}

2.1 多线程编程的步骤

  • 第一步:创建资源类,在资源类创建属性和操作方法;
  • 第二步:在资源类的操作方法中:
    • 判断
    • 干活
    • 通知
  • 第三步:创建多个线程,调用资源类的操作方法;
  • 第四步:防止虚假唤醒问题

3. Lock 接口

3.1 可重入锁:ReentrantLock

reentrantLock 与 synchronized 区别

  • Lock 不是 java 语言内置的,synchronized 是内置的;
  • Lock 是一个类,通过这个类可以实现同步访问;

4、 创建多线程的多种方式

  • 继承 Thread 类
  • 实现 Runnable 接口
  • 使用 Callable 接口
  • 使用线程池

5、线程间的定制通信


// 第一步:创建资源类
class ShareResource {
   // 定义标志位
   private int flag = 1; // 1 AA; 2 BB; 3 CC
   private final ReentrantLock lock = new ReentrantLock();
   private Condition c1 = lock.newCondition();
   private Condition c2 = lock.newCondition();
   private Condition c3 = lock.newCondition();

   // 打印 5 次,参数第几轮
   public void printFive(int loop) throws InterruptedException {
       lock.lock();

       try {
           // 判断
           while (flag != 1) {
               c1.await();
           }
           for (int i = 1; i <= 5; i++) {
               System.out.println(Thread.currentThread().getName() + " :: " + i + " :: " + loop);
           }
           // 通知
           flag = 2; // 修改标志位
           c2.signal(); // 通知 BB
       } finally {
           lock.unlock();
       }
   }

   // 打印 10 次,参数第几轮
   public void printTen(int loop) throws InterruptedException {
       lock.lock();

       try {
           // 判断
           while (flag != 2) {
               c2.await();
           }
           for (int i = 1; i <= 10; i++) {
               System.out.println(Thread.currentThread().getName() + " :: " + i + " :: " + loop);
           }
           // 通知
           flag = 3; // 修改标志位
           c3.signal(); // 通知 BB
       } finally {
           lock.unlock();
       }
   }

   // 打印 15 次,参数第几轮
   public void printFifty(int loop) throws InterruptedException {
       lock.lock();

       try {
           // 判断
           while (flag != 3) {
               c3.await();
           }
           for (int i = 1; i <= 15; i++) {
               System.out.println(Thread.currentThread().getName() + " :: " + i + " :: " + loop);
           }
           // 通知
           flag = 1; // 修改标志位
           c1.signal(); // 通知 BB
       } finally {
           lock.unlock();
       }
   }
}

public class ThreadDemo3 {
   public static void main(String[] args) {
       ShareResource sResource = new ShareResource();

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

       new Thread(() -> {

           for (int i = 0; i <= 10; i++) {
               try {
                   sResource.printTen(i);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       }, "BB").start();

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

6、集合的线程安全

6.1 ArrayList 线程不安全

  • Vector
  • Collections.synchronizedList()
  • CopyOnWriteArrayList, 写时复制技术

6.2 HashSet 线程不安全

  • CopyOnWriteArraySet

6.3 HashMap 线程不安全

  • ConcurrentHashMap

7. 八种多线程锁

synchronized 实现同步的基础:Java 中的每一个对象都可以作为锁。 具体表现为以下 3 种形式:

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的 Class 对象。
  • 对于同步方法块,锁是 synchronized 括号里面配置的对象。

class Phone {
    public synchronized void sendSMS() throws Exception {
        // 停留 4 秒
        TimeUnit.SECONDS.sleep(4);
        System.out.println("------- sendSMS");
    }

    public synchronized void sendEmail() throws Exception {
        System.out.println("------- sendEmail");
    }

    public void getHello() {
        System.out.println("------- getHello");
    }
}

public class ThreadDemo5 {

    public static void main(String[] args) throws Exception {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();

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

        Thread.sleep(100); // 确保两个线程都创建

        new Thread(() -> {
            try {
//                phone1.sendEmail();
//                phone1.getHello();
                phone2.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "BB").start();
    }
}

  • 标准访问,先打印短信还是邮件
------- sendSMS
------- sendEmail

synchronized 修饰实例方法的时候,锁的是当前的对象,即是 this

  • 停 4 秒在短信方法内,先打印短信还是邮件
------- sendSMS
------- sendEmail
  • 新增普通的 hello 方法,先打印短信还是 hello
------- getHello
------- sendSMS

getHello 与锁无关,所以先执行。

  • 现在有两部手机,先打印短信还是邮件
------- sendEmail
------- sendSMS

两个 phone 对象用的不是同一把锁。

  • 两个静态同步方法,1 部手机,先打印短信还是邮件
------- sendSMS
------- sendEmail

synchronized 的锁对象是当前类的 Class 对象。

  • 两个静态同步方法,2 部手机,先打印短信还是邮件
------- sendSMS
------- sendEmail
  • 1 个静态同步方法,1 个普通同步方法,1 部手机,先打印短信还是邮件
------- sendEmail
------- sendSMS

此时一个的锁是对象实例 this,一个是类的 Class 对象。下同

  • 1 个静态同步方法,1个普通同步方法,2 部手机,先打印短信还是邮件
------- sendEmail
------- sendSMS

8. 公平锁与非公平锁

  • 非公平锁可能出现线程空闲的情况;但是效率高
  • 公平锁:效率相对较低;

9. 可重入锁(递归锁)

  • synchronized(隐式)和Lock(显式)都是可重入锁

示例:

public class SyncLockDemo {
     public synchronized void add() {
        add();
    }

    public static void main(String[] args) {
        /* 会导致栈溢出,因为循环递归引用了 */
        //new SyncLockDemo().add(); // 可重入锁也叫递归锁
        Object o = new Object();
        new Thread(() -> {
            synchronized (o) {
                System.out.println(Thread.currentThread().getName() + " 外层");
                synchronized (o) {
                    System.out.println(Thread.currentThread().getName() + " 中层");
                    synchronized (o) {
                        System.out.println(Thread.currentThread().getName() + " 内层");
                    }
                }
            }
        }, "t1").start();

        // Lock 演示
        Lock lock = new ReentrantLock();
        new Thread(() -> {
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName() + " 外层");

                try {
                    lock.lock();
                    System.out.println(Thread.currentThread().getName() + " 内层");
                } finally {
                    lock.unlock();
                }

            } finally {
                lock.unlock();
            }
        }, "t1").start();
    }
}

10. 死锁

public class DeadLock {
    static Object a = new Object();
    static Object b = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (a) {
                System.out.println(Thread.currentThread().getName() + ":: 持有锁a,试图获取锁b");

                synchronized (b) {
                    System.out.println(Thread.currentThread().getName() + " 获取到锁b");
                }
            }
        }, "AA").start();

        new Thread(() -> {
            synchronized (b) {
                System.out.println(Thread.currentThread().getName() + ":: 持有锁b,试图获取锁a");

                synchronized (a) {
                    System.out.println(Thread.currentThread().getName() + " 获取到锁a");
                }
            }
        }, "BB").start();
    }
}

10.1 什么是死锁?

  • 两个或者两个以上进程在执行过程中,因为争夺资源而造成的一种互相等待的现象,如果没有外力干涉,他们无法在执行下去。

10.2 产生死锁的原因

  • 系统资源不足
  • 进程运行推进顺序不合适
  • 资源分配不当

10.3 验证是否是死锁

  • jps: 类似于 Linux 中的 ps -ef,能查看当前运行中的进程
  • jstack:能查看 jvm 中的堆栈跟踪工具

11. Callable 接口

11.1 Runnable 与 Callable

  • 是否有返回值
  • 是否抛出异常
  • 实现方法名称不同,一个是 run 方法,一个是 call 方法
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class MyThread1 implements Runnable {

    @Override
    public void run() {

    }
}

class MyThread2 implements Callable {
    @Override
    public Integer call() throws Exception {
        return 200;
    }
}

public class Demo1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
//        new Thread(new MyThread1(), "t1").start();

        FutureTask<Integer> task = new FutureTask<Integer>(new MyThread2());

        FutureTask<Integer> futureTask2 = new FutureTask<>(() -> {
            System.out.println(Thread.currentThread().getName() + " come in callable");
           return 1024;
        });
        new Thread(futureTask2, "lucy").start();
        while (!futureTask2.isDone()) {
            System.out.println("wait......");
        }
        System.out.println(futureTask2.get());
        System.out.println(futureTask2.get());

        System.out.println(Thread.currentThread().getName() + " come over");

        // FutureTask 原理,未来任务
        /**
         * 1、老师上课,口渴了,去买水不合适,讲课线程继续。
         *      但开启线程,找班上班长帮我买水
         *          把水买回来,需要的时候直接 get
         * 2、4 个同学分别做计算, 1 同学 1+2+...+5 ;2 同学 10 + 11+12+...+50;
         *      3 同学 60+61+62; 4 同学 100 + 200
         */
    }
}

12. JUC 强大的辅助类

12.1 减少计数 CountDownLatch

CountDownLatch 类可以设置一个计数器,然后通过 countDown() 方法来进行减 1 的操作,使用 await() 方法等待计数器不大于 0,然后继续执行 await() 方法之后的语句。

  • CountDownLatch 主要有两个方法,当一个或者多个线程调用 await() 方法时,这些线程会阻塞;
  • 其它线程调用 countDown() 方法会将计数器见 1(调用 countDown 方法的线程不会阻塞);
  • 当计数器的值变为 0 时,因 await 方法阻塞的线程会被唤醒,继续执行。
public class CountDownLatchDemo {
    // 六个同学陆续离开教室之后,班长锁门
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch downLatch = new CountDownLatch(6);
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " 号同学离开了教室");
                downLatch.countDown();
            }, String.valueOf(i)).start();
        }

        downLatch.await();
        System.out.println(Thread.currentThread().getName() + ":: 班长锁门走人了");
    }
}

12.2 循环栅栏 CyclicBarrier

一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。

CyclicBarrier 支持一个可选的 Runnable 命令,在一组线程中的最后一个线程到达之后(但在释放所有线程之前),该命令只在每个屏障点运行一次。若在继续所有参与线程之前更新共享状态,此屏障操作 很有用。

示例代码:

public class CyclicBarrierDemo {
    // 创建固定值
    private static final int NUMBER = 7;

    // 集齐七颗龙珠召唤神龙
    public static void main(String[] args) {
        // 创建 CyclicBarrier
        CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, () -> {
            System.out.println("集齐七颗龙珠召唤神龙");
        });
        // 集齐七颗龙珠的过程
        for (int i = 1; i <= 7; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 星龙被收集到了");
                    cyclicBarrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }
}

12.3 信号灯 Semaphore

一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。

Semaphore 通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。

示例代码:

public class SemaphoreDemo {
    // 6 辆汽车,停 3 个车位
    public static void main(String[] args) {
        // 创建 Semaphore,设置许可数量
        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(new Random().nextInt(5));

                    System.out.println(Thread.currentThread().getName() + " ------ 离开了车位");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放
                    semaphore.release();
                }
            }, String.valueOf(i)).start();
        }
    }
}

13. 读写锁

13.1 悲观锁

image.png

13.2 乐观锁