JUC基础学习笔记

参考文献01.尚硅谷_JUC-课程介绍_哔哩哔哩_bilibili

1.JUC概述

1.1JUC简介

juc就是java.util.concurrent工具类包的简称,这是一个处理线程的工具包,JDK1.5开始出现的。

1.2进程和线程的概念

进程:指在系统内运行的一个应用程序,程序一旦运行就是进程。进程是操作系统调度和分配的基本单位。系统会每一个进程分配独立的操作空间。

线程:线程是程序的一条执行路径,一个进程最少有一个线程。线程是cpu调度和执行的最小单位。一个进程中的多个线程共享一个单独的内存空间。

1.3并发和并行的概念

并发:指两个或多个任务在同一个时间段内发生。即在同一时间段,多个指令在一个cpu中快速轮换、交替执行,使得在宏观上多个进程在同时执行。(多个线程使用同一资源执行)

并行:指两个或多个任务在同一时刻发生(同时发生)。指同一时刻,有多个指令在cpu中同时执行。如多个人在同时做不同的事情。

1.4管程(Monitor监视器)

管程:java在称为锁,是一种同步机制,保证同一时间,只有一个线程能够访问被保护的数据或者代码。

1.5用户线程和守护线程

用户线程:自定义线程都是用户线程。

守护线程:

  • 守护线程是运行在后台的一种特殊进程。
  • 它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
  • 在 Java 中垃圾回收线程就是特殊的守护线程。

如果主线程已经结束,用户线程还在执行,jvm仍然为存活状态

/**
 * @author yzh
 * @date 2023-12-25
 */

public class Dome0001 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "::" + Thread.currentThread().isDaemon());
            while (true) {

            }
        }, "aa");
        t.start();

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

如果没有用户线程,都为守护线程,jvm就会结束

/**
 * @author yzh
 * @date 2023-12-25
 */

public class Dome0001 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "::" + Thread.currentThread().isDaemon());
            while (true) {

            }
        }, "aa");
        //设置为守护线程
        t.setDaemon(true);
        t.start();

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

2.Lock接口与synchronized关键字

2.1synchronized的作用范围

  1. 修饰普通方法:作用于当前对象实例,进入同步代码前要获得当前对象实例的锁
  2. 修饰静态方法:作用于当前类,进入同步代码前要获得当前类对象的锁,synchronized关键字加到static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁
  3. 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁

2.2synchronized实现卖票例子

/**
 * @author yzh
 * @date 2023-12-25
 */

/**
 * 例子:50张票,三个售票员卖票
 * 第一步:创建资源类,在资源类中创建属性和操作方法
 * 第二步:创建多个线程,调用资源类的操作方法
 *
 */
public class Dome0002 {
    public static void main(String[] args) {
        //第二步:创建多个线程,调用资源类的操作方法
        //创建Sale对象
        Sale sale = new Sale();
        //创建三个线程
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //调用资源类的操作方法
                for (int i = 0; i < 50; i++) {
                    sale.saleTicket();
                }
            }
        }, "aa");

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                //调用资源类的操作方法
                for (int i = 0; i < 50; i++) {
                    sale.saleTicket();
                }
            }
        }, "bb");

        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                //调用资源类的操作方法
                for (int i = 0; i < 50; i++) {
                    sale.saleTicket();
                }
            }
        }, "cc");

        t1.start();
        t2.start();
        t3.start();

    }
}


//第一步:创建资源类,在资源类中创建属性和操作方法
class Sale{
    //属性:票数
    private int num =50;
    //操作方法:卖票
    public synchronized void saleTicket(){
        //判断是否有票
        if (num >0){
            System.out.println(Thread.currentThread().getName()+"卖出票"+(num--)+"剩下票数:"+num);
        }
    }
}

2.3Lock接口简介

Lock 锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。Lock 提供了比 synchronized 更多的功能。

2.4使用可重入锁(ReentrantLock)实现卖票例子

/**
 * @author yzh
 * @date 2023-12-25
 */

import java.util.concurrent.locks.ReentrantLock;

/**
 * 例子:50张票,三个售票员卖票
 * 第一步:创建资源类,在资源类中创建属性和操作方法
 * 第二步:创建多个线程,调用资源类的操作方法
 *  Lock接口实现
 */
public class Dome0003 {
    public static void main(String[] args) {
        //创建Ticket对象
        Ticket ticket = new Ticket();
        //创建三个线程
       new Thread(()->{
           for (int i = 0; i < 50; i++) {
               ticket.saleTicket();
           }
       },"aa").start();
       new Thread(()->{
           for (int i = 0; i < 50; i++) {
               ticket.saleTicket();
           }
       },"bb").start();
        new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                ticket.saleTicket();
            }
        }, "cc").start();
    }

}


class Ticket{
    //票数量
    private int num = 50;
    //创建可重入锁(ReentrantLock)
    private final ReentrantLock lock = new ReentrantLock();
    public void saleTicket(){
        //上锁
        lock.lock();
        try {
            //判断是否有票可卖
            if (num >0){
                System.out.println(Thread.currentThread().getName()+"卖出票"+(num--)+"剩下票数:"+num);
            }
        } finally {
            //解锁
            lock.unlock();
        }
    }
}

2.4.Lock接口与synchronized的区别

1. Lock 是一个接口,而 synchronized 是Java 中的关键字,synchronized 是内置的语言实现。

2.synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()主动去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁 。 

3.Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用synchronized 时,等待的线程会一直等待下去,不能够响应中断。

4.通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

5.Lock 可以提高多个线程进行读操作的效率。在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时( 即有大量线程同时竞争 ),此时 Lock 的性能要远远优于synchronized。

3.线程间的通信

3.1synchronized实现创建两个线程对同一资源进行操作 一个加+1,一个-1

/**
 * @author yzh
 * @date 2023-12-25
 */

/**
 * 例子:创建两个线程对同一资源进行操作  一个加+1,一个-1
 * 第一步:创建资源类创建属性和操作方法
 * 第二步:判断、干活、通知
 * 第三步:创建多个线程调用资源类的方法
 */
public class Dome0004 {
    public static void main(String[] args) {
        Share share = new Share();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    share.incr();//+1
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "aa").start();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    share.decr();//-1
                } catch (InterruptedException e) {

                }
            }
        }, "bb").start();
    }

}

class Share {
    //初始值
    public int num = 0;

    //+1方法
    public synchronized void incr() throws InterruptedException {
        //判断 num 值是否为0,不为0进行等待,为0 +1
        if (num != 0) {
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName() + "---" + num);
        //通知其他线程
        this.notifyAll();

    }

    //-1方法
    public synchronized void decr() throws InterruptedException {
        //判断 num 值是否为0,不为0 -1,为0 进行等待
        if (num == 0) {
            this.wait();
        }
        num--;
        System.out.println(Thread.currentThread().getName() + "---" + num);
        //通知其他线程
        this.notifyAll();

    }
}

注意wait()可能存在虚假唤醒问题

wait在哪里睡,就会在哪里醒。

/**
 * @author yzh
 * @date 2023-12-25
 */

/**
 * 例子:创建两个线程对同一资源进行操作  一个加+1,一个-1
 * 第一步:创建资源类创建属性和操作方法
 * 第二步:判断、干活、通知
 * 第三步:创建多个线程调用资源类的方法
 */
public class Dome0004 {
    public static void main(String[] args) {
        Share share = new Share();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    share.incr();//+1
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "aa").start();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    share.decr();//-1
                } catch (InterruptedException e) {

                }
            }
        }, "bb").start();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    share.incr();//-1
                } catch (InterruptedException e) {

                }
            }
        }, "cc").start();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    share.decr();//-1
                } catch (InterruptedException e) {

                }
            }
        }, "dd").start();
    }

}

class Share {
    //初始值
    public int num = 0;

    //+1方法
    public synchronized void incr() throws InterruptedException {
        //判断 num 值是否为0,不为0进行等待,为0 +1
        if (num != 0) {
            //wait在哪里睡,就会在哪里醒。
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName() + "---" + num);
        //通知其他线程
        this.notifyAll();

    }

    //-1方法
    public synchronized void decr() throws InterruptedException {
        //判断 num 值是否为0,不为0 -1,为0 进行等待
        if (num == 0) {
            //wait在哪里睡,就会在哪里醒。
            this.wait();
        }
        num--;
        System.out.println(Thread.currentThread().getName() + "---" + num);
        //通知其他线程
        this.notifyAll();

    }
}

大于1和大于2的值出现的原因就是因为wait()的虚假唤醒问题

因为当多个线程执行时,

notifyAll()是随机唤醒所有线程

如:第一次aa抢到锁,应该加1,  此时值为1

        第二次cc抢到锁,值为1进入等待状态,并且释放锁,唤醒其他线程

        第三次aa抢到锁,值为1进入等待状态,并且释放锁,唤醒其他线程

        第四次cc抢到锁,值虽然为1,但是此时cc唤醒时,是已经执行if后的位置,所以值会被再次+1变为2.

        。。。。

其他情况同理所得。

此题解决方法:

将if换为while


/**
 * @author yzh
 * @date 2023-12-25
 */

/**
 * 例子:创建两个线程对同一资源进行操作  一个加+1,一个-1
 * 第一步:创建资源类创建属性和操作方法
 * 第二步:判断、干活、通知
 * 第三步:创建多个线程调用资源类的方法
 */
public class Dome0004 {
    public static void main(String[] args) {
        Share share = new Share();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    share.incr();//+1
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "aa").start();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    share.decr();//-1
                } catch (InterruptedException e) {

                }
            }
        }, "bb").start();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    share.incr();//-1
                } catch (InterruptedException e) {

                }
            }
        }, "cc").start();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    share.decr();//-1
                } catch (InterruptedException e) {

                }
            }
        }, "dd").start();
    }

}

class Share {
    //初始值
    public int num = 0;

    //+1方法
    public synchronized void incr() throws InterruptedException {
        //判断 num 值是否为0,不为0进行等待,为0 +1
        while (num != 0) {
            //wait在哪里睡,就会在哪里醒。
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName() + "::" + num);
        //通知其他线程
        this.notifyAll();

    }

    //-1方法
    public synchronized void decr() throws InterruptedException {
        //判断 num 值是否为0,不为0 -1,为0 进行等待
        while (num == 0) {
            //wait在哪里睡,就会在哪里醒。
            this.wait();
        }
        num--;
        System.out.println(Thread.currentThread().getName() + "::" + num);
        //通知其他线程
        this.notifyAll();

    }
}

3.2Lock实现创建两个线程对同一资源进行操作 一个加+1,一个-1


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

/**
 * @author yzh
 * @date 2023-12-25
 */

public class Dome0005 {
    public static void main(String[] args) {
        Share1 share1 = new Share1();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    share1.incr();//+1
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "aa").start();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    share1.decr();//-1
                } catch (InterruptedException e) {

                }
            }
        }, "bb").start();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    share1.incr();//-1
                } catch (InterruptedException e) {

                }
            }
        }, "cc").start();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    share1.decr();//-1
                } catch (InterruptedException e) {

                }
            }
        }, "dd").start();
    }
    }


class Share1 {
    //初始值
    public int num = 0;
    //创建lock
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();


    //+1方法
    public void incr() throws InterruptedException {
        lock.lock();
        try {
            //判断 num 值是否为0,不为0进行等待,为0 +1
            while (num != 0) {
                //wait在哪里睡,就会在哪里醒。
                condition.await();
            }
            num++;
            System.out.println(Thread.currentThread().getName() + "::" + num);
            //通知其他线程
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    //-1方法
    public synchronized void decr() throws InterruptedException {
        lock.lock();
        try {
            //判断 num 值是否为0,不为0 -1,为0 进行等待
            while (num == 0) {
                //wait在哪里睡,就会在哪里醒。
                condition.await();
            }
            num--;
            System.out.println(Thread.currentThread().getName() + "::" + num);
            //通知其他线程
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

3.3线程间定制化通信

启动三个线程,按照如下要求实现

aa打印5次,bb打印10次,cc打印15次   按照顺序进行10轮

实现思路:利用标志位进行判断实现:

/**
 * @author yzh
 * @date 2023-12-25
 */

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

/**
 * 启动三个线程,按照如下要求实现
 * aa打印5次,bb打印10次,cc打印15次  按照顺序进行10轮
 */

public class Dome0006 {
    public static void main(String[] args) {
        ShareResource shareResource = new ShareResource();
        new Thread(()->{
            for (int i = 1; i <=10 ; i++) {
                try {
                    shareResource.print5(i);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"aa").start();
        new Thread(()->{
            for (int i = 1; i <=10 ; i++) {
                try {
                    shareResource.print10(i);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"bb").start();
        new Thread(()->{
            for (int i = 1; i <=10 ; i++) {
                try {
                    shareResource.print15(i);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"cc").start();
    }
}


//创建资源类
class ShareResource{
    //定义标志位 1代表aa线程  2代表bb线程  3代表cc线程
    private int flag = 1;
    //创建lock锁
    private Lock lock =new ReentrantLock();

    //创建三个condition
    private Condition c1 =lock.newCondition();
    private Condition c2 =lock.newCondition();
    private Condition c3 =lock.newCondition();

    //打印5次,参数第几轮
    public void print5(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+"轮");
            }
            //修改标志位为2
            flag=2;
            //通知bb线程
            c2.signal();
        }finally {
            lock.unlock();
        }
    }
    //打印10次,参数第几轮
    public void print10(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+"轮");
            }
            //修改标志位为2
            flag=3;
            //通知bb线程
            c3.signal();
        }finally {
            lock.unlock();
        }
    }
    //打印15次,参数第几轮
    public void print15(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+"轮");
            }
            //修改标志位为2
            flag=1;
            //通知bb线程
            c1.signal();
        }finally {
            lock.unlock();
        }
    }
}

4.ArrayList集合间的线程安全问题

4.1ArrayList集合线程不安全问题演示

package myjuc;

/**
 * @author yzh
 * @date 2024-01-08
 */

import java.util.ArrayList;
import java.util.UUID;

/**
 * list 集合线程不安全问题
 */
public class ThreadDome001 {
    public static void main(String[] args) {
        //创建ArrayList集合
        ArrayList<String> list = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                //向集合中添加内容
                list.add(UUID.randomUUID().toString().substring(0,8));
                //从集合中取出内容
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

出现问题:

原因:

ArrayList中add方法没有被synchronized等修饰,存在线程安全问题。

多个线程并发地向ArrayList集合中添加元素,并在添加后打印出集合的内容。然而,ArrayList不是线程安全的集合类,这可能会导致ConcurrentModificationException异常

4.2解决方案 Vector

package myjuc;

import java.util.ArrayList;
import java.util.UUID;
import java.util.Vector;

/**
 * @author yzh
 * @date 2024-01-08
 */
/*
Vector 集合解决线程安全问题
 */
public class ThreadDome002 {
    public static void main(String[] args) {
        //创建Vector集合
        Vector<String> list = new Vector<>();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                //向集合中添加内容
                list.add(UUID.randomUUID().toString().substring(0,8));
                //从集合中取出内容
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

解决原因:

Vector中add方法被synchronized等修饰,解决线程安全问题。

4.3解决方案

package myjuc;

import java.util.*;

/**
 * @author yzh
 * @date 2024-01-08
 */

/**
 * Conllections解决线程安全问题
 */
public class ThreadDome003 {
    public static void main(String[] args) {
        //通过工具类Conllections
        List<String> list =  Collections.synchronizedList(new ArrayList<String>());
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                //向集合中添加内容
                list.add(UUID.randomUUID().toString().substring(0,8));
                //从集合中取出内容
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

4.4解决方案 CopyOnWriteArrayList

package myjuc;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * @author yzh
 * @date 2024-01-08
 */

public class ThreadDome004 {
    public static void main(String[] args) {
        //创建CopyOnWriteArrayList集合
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                //向集合中添加内容
                list.add(UUID.randomUUID().toString().substring(0,8));
                //从集合中取出内容
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

CopyOnWriteArrayList的主要思想:写时复制技术

它会先创建该列表的副本,并在副本上进行修改操作,而原始列表保持不变。只有在修改操作完成后,才会将副本替换回原始列表,从而保证了并发安全性。

写时复制的主要思想是牺牲一部分空间来换取并发安全性。当有多个线程同时读取CopyOnWriteArrayList时,它们可以共享同一个原始列表而无需加锁,因为原始列表是不可变的。只有在有写操作时,才会进行复制和修改操作,以确保线程安全。

写时复制的优势在于读操作不需要加锁,从而提高了读取性能。然而,由于每次写操作都需要进行数组的复制,写操作的性能相对较低。因此,如果应用程序有频繁的写操作,而读操作较少,可能会影响整体性能。

CopyOnWriteArrayList的add方法加锁,来保证线程安全问题

5.HashSet线程不安全

5.1HashSet线程不安全演示

package myjuc;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.UUID;

/**
 * @author yzh
 * @date 2024-01-08
 */

public class ThreadDome005 {
    public static void main(String[] args) {
        //创建HashSet集合
        HashSet<String> list = new HashSet<>();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                //向集合中添加内容
                list.add(UUID.randomUUID().toString().substring(0,8));
                //从集合中取出内容
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

HashSet的add方法没有被synchronized等修饰,存在线程安全问题。

5.2解决方案CopyOnWriteArraySet

package myjuc;


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

/**
 * @author yzh
 * @date 2024-01-08
 */

public class ThreadDome005 {
    public static void main(String[] args) {
        //创建CopyOnWriteArraySet集合
        Set<String> list = new CopyOnWriteArraySet<>();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                //向集合中添加内容
                list.add(UUID.randomUUID().toString().substring(0,8));
                //从集合中取出内容
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

6.HashMap线程不安全

6.1HashMap线程不安全演示

package myjuc;


import java.util.HashMap;
import java.util.UUID;

/**
 * @author yzh
 * @date 2024-01-08
 */

public class ThreadDome005 {
    public static void main(String[] args) {
        //创建HashMap集合
        HashMap<String, String> map = new HashMap<>();
        for (int i = 0; i < 10; i++) {
            String key = String.valueOf(i);
            new Thread(()->{
                //向集合中添加内容
                map.put(key,UUID.randomUUID().toString().substring(0,8));
                //从集合中取出内容
                System.out.println(map);
            },String.valueOf(i)).start();
        }
    }
}

6.2解决法案ConcurrentHashMap

package myjuc;


import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author yzh
 * @date 2024-01-08
 */

public class ThreadDome005 {
    public static void main(String[] args) {
        //创建HashMap集合
        ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
        for (int i = 0; i < 10; i++) {
            String key = String.valueOf(i);
            new Thread(()->{
                //向集合中添加内容
                map.put(key,UUID.randomUUID().toString().substring(0,8));
                //从集合中取出内容
                System.out.println(map);
            },String.valueOf(i)).start();
        }
    }
}

7.多线程锁

7.1演示synchronized锁的八种情况

package myjuc;

import java.util.concurrent.TimeUnit;

/**
 * @author yzh
 * @date 2024-01-08
 */

/**
 * 1 标准访问,先打印短信还是邮件
 * ------sendSMS
 * ------sendEmail
 * 2 停4秒在短信方法内,先打印短信还是邮件
 * ------sendSMS
 * ------sendEmail
 * 3 新增普通的hello方法,是先打短信还是hello
 * ------getHello
 * ------sendSMS
 * 4 现在有两部手机,先打印短信还是邮件
 * ------sendEmail
 * ------sendSMS
 * 5 两个静态同步方法,1部手机,先打印短信还是邮件
 * ------sendSMS
 * ------sendEmail
 * 6 两个静态同步方法,2部手机,先打印短信还是邮件
 * ------sendSMS
 * ------sendEmail
 * 7 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
 * ------sendEmail
 * ------sendSMS
 * 8 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
 * ------sendEmail
 * ------sendSMS
 */
public class Dome01 {
    public static void main(String[] args) throws Exception {
        Phone phone = new Phone();
//        Phone phone2 = new Phone();
        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "A").start();
        Thread.sleep(100);
        new Thread(() -> {
            try {
//                 phone.sendEmail();
                 phone.getHello();
//                phone2.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "B").start();
    }

}

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

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

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

}

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

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

对于静态同步方法,锁是当前类的class对象

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

7.2公平锁和非公平锁

非公平锁时:

package myjuc;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author yzh
 * @date 2024-01-08
 */

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

 class LTicket {
        //票数
        private int num = 30;
        //创建可重入锁
        private final ReentrantLock lock = new ReentrantLock();

        //售票方法
        public void sale() {
            //上锁
            lock.lock();
            try {
                //判断是否有票
                if (num > 0) {
                    System.out.println(Thread.currentThread().getName() + ": 卖出" + (num--) + "剩余:" + num);
                }
            } finally {
                //解锁
                lock.unlock();
            }
        }
    }

此时B和C未抢到一次锁,被饿死

公平锁:

package myjuc;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author yzh
 * @date 2024-01-08
 */

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

 class LTicket {
        //票数
        private int num = 30;
        //创建可重入锁
        private final ReentrantLock lock = new ReentrantLock(true);

        //售票方法
        public void sale() {
            //上锁
            lock.lock();
            try {
                //判断是否有票
                if (num > 0) {
                    System.out.println(Thread.currentThread().getName() + ": 卖出" + (num--) + "剩余:" + num);
                }
            } finally {
                //解锁
                lock.unlock();
            }
        }
    }

ReentrantLock无参构造时默认为非公平锁
  1. 公平锁(Fair Lock):公平锁是指多个线程按照申请锁的顺序来获取锁,即先到先得的原则。当一个线程尝试获取锁时,如果发现锁已经被其他线程占用,则该线程会被放入等待队列中,等待其他线程释放锁后再次尝试获取锁。公平锁保证了所有线程获取锁的顺序是公平的,避免了饥饿现象,但由于需要维护等待队列,因此可能会降低系统的整体性能。

  2. 非公平锁(Nonfair Lock):非公平锁是指多个线程获取锁的顺序是不确定的,不按照申请锁的顺序来进行。当一个线程尝试获取锁时,如果锁还没有被其他线程占用,则该线程可以直接获取到锁;如果锁已经被其他线程占用,则该线程会一直尝试获取锁,直到获取成功。非公平锁允许新到来的线程优先获取锁,这样可以提高系统的吞吐量,但可能会导致某些线程长时间等待,产生饥饿现象。

综上所述,公平锁和非公平锁的主要区别在于线程获取锁的顺序。公平锁按照申请锁的顺序来获取锁,保证了线程获取锁的公平性,但可能降低系统性能;非公平锁允许新到来的线程优先获取锁,提高了系统的吞吐量,但可能导致某些线程长时间等待。

7.3可重入锁

可重入锁(Reentrant Lock)是一种支持同一个线程多次获取锁的锁机制。也就是说,当一个线程获得了某个锁之后,可以再次获得该锁而不会被阻塞。可重入锁主要用于解决线程在递归调用或多层级调用中对同一个锁的重复获取的情况。

可重入锁通过一个计数器来记录锁的持有次数,每次成功获取锁时计数器加1,每次释放锁时计数器减1。只有当计数器归零时,其他线程才能获取到该锁。

可重入锁的优点:

  1. 避免死锁:在递归调用或多层级调用中,如果使用普通的互斥锁,线程在第二次获取锁时会被自己阻塞,导致死锁。而可重入锁可以让同一个线程多次获取锁,避免了死锁的发生。
  2. 灵活性:可重入锁提供了更灵活的锁获取和释放方式,可以根据具体的业务需求灵活控制锁的获取和释放操作。

synchronized和lock都是可重入锁。

  • sychronized是隐式锁,不用手工上锁与解锁,而lock为显示锁,需要手工上锁与解锁
package myjuc;

/**
 * @author yzh
 * @date 2024-01-08
 */

public class Dome03 {
    public synchronized void add(){
        add();
    }
    public static void main(String[] args) {
        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();
        new Dome03().add();
    }
}

此时出现栈溢出异常,就形象的说明了synchronized为可重入锁,因为如果synchronized为不可重入锁,那么add方法就只能被调用一次然后发生死锁,而此时的add方法被无限循环调用导致栈溢出。

package myjuc;

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

/**
 * @author yzh
 * @date 2024-01-08
 */

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

    public static void main(String[] args) {
        //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() + "中层");
                try {
                    //上锁
                    lock.lock();
                    System.out.println(Thread.currentThread().getName() + "内层");

                } finally {
                    lock.unlock();
                }
            } finally {
                lock.unlock();
            }

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

        new Thread(()->{
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName()+"bb");
            }finally {
                lock.unlock();
            }
        },"B").start();
    }
}

如果上面线程有一个锁未释放第一个线程不会被影响执行,第二个线程会一直获取不到锁

7.4死锁

死锁(Deadlock)是指两个或多个线程在互相等待对方释放资源而无法继续执行的状态,从而导致程序无法继续运行下去。当发生死锁时,线程将永久地阻塞在这个状态上,无法进行任何进一步的操作。

一个典型的死锁场景包括以下几个条件:

  1. 互斥条件(Mutual Exclusion):至少有一个资源同时只能被一个线程持有。
  2. 请求与保持条件(Hold and Wait):一个线程在持有一个资源的同时,又请求其他线程持有的资源。
  3. 不可剥夺条件(No Preemption):已经分配给一个线程的资源不能被强制性地剥夺。
  4. 循环等待条件(Circular Wait):存在一个进程资源的循环链,每个进程都在等待下一个进程所持有的资源。

package myjuc;

/**
 * @author yzh
 * @date 2024-01-08
 */

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");
                }
            }
        },"A").start();

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

互相等待,发生死锁

验证死锁:

1.jps -l

2.jstack port

此时展示信息:

8.Callable接口

  • Callable接口方法是call(),Runnable的方法是run()
  • Callable接口call方法有返回值,支持泛型,Runnable接口run方法无返回值。
  • Callable接口call()方法允许抛出异常;而Runnable接口run()方法不能继续上抛异常。

Callable创建线程:

package myjuc;

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

/**
 * @author yzh
 * @date 2024-01-09
 */
//比较Runnable和Callable的区别
public class Dome05 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //Runnable创建线程
        MyTest1 t1 = new MyTest1();
        new Thread(t1,"A").start();

        //Callable创建线程
        //FutureTask
        FutureTask<Integer> t2 = new FutureTask<Integer>(new MyTest2());
        //lamd表达式
        FutureTask<Integer> t3 = new FutureTask<>(() -> {
            System.out.println(Thread.currentThread().getName()+"t3");
            return 1024;
        });
        new Thread(t2,"B").start();
        new Thread(t3,"c").start();
        //调用FutureTask的isDone方法判断t3线程是否完成
        while (!t3.isDone()){
            System.out.println("wait.......");
        }
        //调用FutureTask的get方法获取线程的执行结果
        System.out.println(t2.get());
        System.out.println(t3.get());

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

        //FutureTask原理      未来任务
        /**
         * 1.老师上课,口渴了,去买票不合适,讲课线程继续单开启线程找班上班长帮我买水,把水买回来,需要时候直接get
         *
         * 2.现在有4个同学,1同学 1+2...52同学 10+11+12....50 ,3同学 60+61+62 ,4同学 100+200第2个同学计算量比较大
         *      FutureTask单开启线程给2同学计算,先汇总 1 3 4,最后等2同学计算位完成,统一汇总
         *
         * 3.考试,做会做的题目,最后看不会做的题目
         *
         *
         * 最终汇总的值只需要汇总一次即可
         */
    }
}

class  MyTest1 implements Runnable{
    @Override
    public void run() {
        System.out.println("Runnable启动");

    }
}

class MyTest2 implements Callable{
    @Override
    public Object call() throws Exception {
        System.out.println(Thread.currentThread().getName()+"t2");
        return 100;
    }
}

9.JUC中的辅助类

9.1减少计数CountDownLatch

CountDownLatch 是 Java 中的一个同步辅助类,可以用于控制多个线程之间的执行顺序。

CountDownLatch 类提供了一个计数器,通过调用 countDown() 方法来减少计数器的值,并通过调用 await() 方法来阻塞线程,直到计数器的值变为零。当计数器的值变为零时,所有在调用 await() 方法阻塞的线程将被释放,可以继续执行。

CountDownLatch 主要用于一组线程等待其他线程完成操作后再继续执行。它的典型应用场景包括:

  1. 主线程等待所有工作线程完成任务后再继续执行。
  2. 多个工作线程等待某个共享资源就绪后再同时执行。

例子:

没有用CountDownLatch时可能出现同学还未全部离开老师就锁门

package myjuc;

/**
 * @author yzh
 * @date 2024-01-09
 */
//演示 CountDownLatch
public class Dome06 {
    //6个同学离开教室老师才能锁门,
    public static void main(String[] args) {
        //6个同学陆续离开教室
        for (int i = 1; i <= 6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"同学离开教室");
            },String.valueOf(i)).start();
        }
        System.out.println(Thread.currentThread().getName()+"老师锁门");
    }
}

使用CountDownLatch:

package myjuc;

import java.util.concurrent.CountDownLatch;

/**
 * @author yzh
 * @date 2024-01-09
 */
//演示 CountDownLatch
public class Dome06 {
    //6个同学离开教室老师才能锁门,
    public static void main(String[] args) throws InterruptedException {
        //创建CountDownLatch对象,设置初始值
        CountDownLatch countDownLatch = new CountDownLatch(6);
        //6个同学陆续离开教室
        for (int i = 1; i <=6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"同学离开教室");
                //让计数器每次-1
                countDownLatch.countDown();
            },String.valueOf(i)).start();
        }
        //等待
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName()+"老师锁门");
    }
}

9.2循环栅栏CyclicBarrier

CyclicBarrier 是 Java 中的一个同步辅助类,可以用于控制多个线程之间的执行顺序。

CyclicBarrier 类提供了一个等待点,当多个线程到达这个等待点时,CyclicBarrier 将会把它们阻塞在这里,直到所有线程都到达这个等待点。当所有线程都到达这个等待点时,CyclicBarrier 将会解除阻塞,所有线程继续执行。

CyclicBarrier 主要用于多个线程之间需要相互等待的场景,例如多个线程需要等待某个共享资源就绪后再同时执行。

例子:

集齐7龙珠召唤神龙
package myjuc;

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

/**
 * @author yzh
 * @date 2024-01-09
 */
//集齐7龙珠召唤神龙
public class Dome07 {
    //创建固定值
    private static final int num =7;
    public static void main(String[] args) {
    //创建CyclicBarrier对象
        CyclicBarrier barrier = new CyclicBarrier(num, () -> {
            System.out.println("**集齐7龙珠召唤神龙**");
        });
        //集齐七颗龙珠的过程
        for (int i = 1; i <=7 ; i++) {
            new Thread(()->{
                try {
                    System.out.println(Thread.currentThread().getName()+"被收集");
                    //等待
                    barrier.await();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            },String.valueOf(i)).start();
        }
    }
}

9.3信号灯Semaphore

Semaphore 是 Java 中的一个同步辅助类,用于控制同时访问某个资源的线程数量。

Semaphore 维护了一组许可证(permits),线程可以通过调用 acquire() 方法获取许可证,或者通过调用 release() 方法释放许可证。每个 acquire() 方法调用都会阻塞线程,直到获得一个许可证为止。而每个 release() 方法调用都会释放一个许可证。

Semaphore 主要用于控制同时执行某个特定操作的线程数量,例如限制同时访问某个共享资源的线程数量。它可以解决并发访问问题,确保多个线程之间的安全性。

例子:

现在有6辆汽车,停在3个停车位

package myjuc;

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

/**
 * @author yzh
 * @date 2024-01-09
 */
//现在有6辆汽车,停在3个停车位
public class Dome8 {
    public static void main(String[] args) {
        //创建Semaphore,设置许可量
        Semaphore semaphore = new Semaphore(3);
        //模拟6辆汽车
        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();
        }
    }
}

10.ReentrantReadWriteLock读写锁

10.1悲观锁和乐观锁

悲观锁,每次访问资源都会加锁,执行完同步代码释放锁,synchronizedReentrantLock属于悲观锁。

乐观锁,不会锁定资源,所有的线程都能访问并修改同一个资源,如果没有冲突就修改成功并退出,否则就会继续循环尝试。乐观锁最常见的实现就是CAS

适用场景:

  • 悲观锁适合写操作多的场景。
  • 乐观锁适合读操作多的场景,不加锁可以提升读操作的性能。

10.2表锁和行锁

表锁(Table Lock)和行锁(Row Lock)是数据库中常见的锁机制,用于控制对数据库对象(如表、行)的并发访问。

  1. 表锁(Table Lock):

    • 表锁是在操作整个表时加锁,对整个表进行加锁操作,可以阻塞其他事务对该表的读写操作。
    • 表锁具有较低的粒度,锁定了整个表,因此会对并发性能产生影响。
    • 当一个事务获取表锁后,其他事务无法同时对同一表进行写操作或者部分读操作,这可能导致并发性能下降。
  2. 行锁(Row Lock):

    • 行锁是在操作表中的行数据时加锁,只锁定需要操作的行,而不是整个表。
    • 行锁具有较高的粒度,只影响到涉及到的行数据,对其他行的读写操作不受影响。
    • 当一个事务获取某一行的行锁后,其他事务可以同时对其他行进行读写操作,不会被阻塞。

行锁可能发生死锁

10.3读锁和写锁

  1. 读锁(Shared Lock):

    • 也称为共享锁,多个事务可以同时获取读锁,允许并发地进行读操作。
    • 若一个事务持有了读锁,则其他事务也可以获取读锁,而不会被阻塞。
    • 读锁适用于读多写少的场景,可以提高并发性能。
    • 读锁之间不会产生冲突,即多个事务可以同时持有读锁。
  2. 写锁(Exclusive Lock):

    • 也称为排它锁(独占式锁),只允许一个事务持有写锁,其他事务无法同时获取读锁或写锁。
    • 若一个事务已经持有写锁,则其他事务无法同时获取写锁或读锁,会被阻塞等待。
    • 写锁适用于写多读少的场景,确保数据的一致性和完整性。
    • 写锁之间和读锁之间都会产生冲突,即写锁与任何其他锁都不能同时存在。

共享式与独占式的最主要区别在于:同一时刻独占式只能有一个线程获取同步状态,而共享式在同一时刻可以有多个线程获取同步状态。例如读操作可以有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其他操作都会被阻塞。

读锁发生死锁场景:

写锁发生死锁场景

未上锁时:

package myjuc;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @author yzh
 * @date 2024-01-09
 */
//资源类
class MyCache {
    //创建map集合
    private volatile Map<String, Object> map = new HashMap<>();

    //向map集合中放数据
    public void add(String key, Object value) {
        System.out.println(Thread.currentThread().getName() + "正在写操作" + key);
        //暂停一下
        try {
            TimeUnit.MICROSECONDS.sleep(300);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //放数据
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "写完了" + key);
    }

    //向map集合中读数据
    public Object get(String key) {
        Object result = null;
        System.out.println(Thread.currentThread().getName() + "正在读取操作" + key);
        //暂停一下
        try {
            TimeUnit.MICROSECONDS.sleep(300);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //放数据
        result = map.get(key);
        System.out.println(Thread.currentThread().getName() + "读取完了" + key);
        return result;
    }

}

public class Dome08 {

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

        //创建5个线程放数据
        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                myCache.add(String.valueOf(num), num);
            }, String.valueOf(i)).start();
        }
        //创建5个线程读数据
        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                myCache.get(String.valueOf(num));
            }, String.valueOf(i)).start();
        }

    }
}

发现写操作还未完成时,读操作就已经正在进行。

使用ReentrantReadWriteLock解决这一问题

package myjuc;

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

/**
 * @author yzh
 * @date 2024-01-09
 */
//资源类
class MyCache {
    //创建map集合
    private volatile Map<String, Object> map = new HashMap<>();

    //创建ReentrantReadWriteLock对象
    private ReadWriteLock readWriteLock =new ReentrantReadWriteLock();

    //向map集合中放数据
    public void add(String key, Object value) {
        //添加写锁
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "正在写操作" + key);
            //暂停一下
            TimeUnit.MICROSECONDS.sleep(300);
            //放数据
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写完了" + key);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //释放写锁
            readWriteLock.writeLock().unlock();
        }
    }

    //向map集合中读数据
    public Object get(String key) {
        //添加读锁
        readWriteLock.readLock().lock();
        Object result = null;
        try {
            System.out.println(Thread.currentThread().getName() + "正在读取操作" + key);
            //暂停一下
            TimeUnit.MICROSECONDS.sleep(300);
            //放数据
            result = map.get(key);
            System.out.println(Thread.currentThread().getName() + "读取完了" + key);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //释放读锁
            readWriteLock.readLock().unlock();
        }

        return result;
    }

}

public class Dome08 {

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

        //创建5个线程放数据
        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                myCache.add(String.valueOf(num), num);
            }, String.valueOf(i)).start();
        }
        //创建5个线程读数据
        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                myCache.get(String.valueOf(num));
            }, String.valueOf(i)).start();
        }

    }
}

运行效果也说明了读锁为共享锁,写锁为独占锁。

注意:读写锁:一个资源可以被多个读线程访问,或者可以被一个写线程访问,但是不能同时存在读写线程,读写互斥,读读共享的

10.4锁降级

锁降级是指在持有一个较高级别的锁的情况下,获取一个较低级别的锁,并释放掉较高级别的锁,以降低锁的粒度和提高并发性能。

具体来说,锁降级包括以下步骤:

  1. 首先,获取一个较高级别的锁,例如写锁。
  2. 然后,在持有较高级别锁的情况下,获取一个较低级别的锁,例如读锁。
  3. 接着,释放掉较高级别的锁,只保留较低级别的锁。
  4. 最后,完成对锁的操作,释放掉较低级别的锁。

通过锁降级,可以减少锁的争用范围,提高并发性能。因为在降级后,其他线程可以同时获取较低级别的锁,并行地执行读操作,而不需要等待较高级别的锁。

需要注意的是,锁降级需要按照特定的顺序获取和释放锁,以避免发生死锁或数据不一致的情况。具体的顺序规则取决于具体的应用场景和锁的类型。

锁降级在某些并发场景中非常有用,例如在数据库事务中的读写操作,可以先获取写锁进行写操作,然后降级为读锁进行读操作,以保证数据的一致性和并发性能。

需要注意的是,并非所有的锁都支持降级操作,具体取决于使用的锁的类型和实现。在使用锁降级时,需要仔细阅读相关文档并确保所使用的锁支持降级操作。

注意:读锁不能升级为写锁

演示代码:

package myjuc;

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @author yzh
 * @date 2024-01-09
 */
//演示读锁降级为写锁
public class Dome09 {
    public static void main(String[] args) {
        //可重入读写锁对象
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        //读锁
        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        //写锁
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

        //锁降级
        //1.获取写锁
        writeLock.lock();
        System.out.println("锁降级");
        //2.获取读锁
        readLock.lock();
        System.out.println("**read");

        //3.释放写锁
        writeLock.unlock();
        //4.释放读锁
        readLock.unlock();
    }
}

反向操作代码:

package myjuc;

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @author yzh
 * @date 2024-01-09
 */
//演示读锁降级为写锁
public class Dome09 {
    public static void main(String[] args) {
        //可重入读写锁对象
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        //读锁
        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        //写锁
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

        //锁降级
        //2.获取读锁
        readLock.lock();
        System.out.println("**read");
        //1.获取写锁
        writeLock.lock();
        System.out.println("锁降级");


        //3.释放写锁
        writeLock.unlock();
        //4.释放读锁
        readLock.unlock();
    }
}

此时运行结果说明:读锁不能升级为写锁

11.BlockingQueue阻塞队列

11.1阻塞队列概述

阻塞队列顾名思义,首先它是一个队列,通过一个共享队列,可以使数据从一端输入,另一端输出。是一种支持在多线程环境下使用的数据结构,它的特点是当队列为空或已满时,插入和删除操作会被阻塞。

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

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

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

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

11.2阻塞队列的基本架构

BlockingQueue接口定义了阻塞队列的基本操作,包括put()、take()、offer()、poll()等方法。常用的实现类有:

  1. ArrayBlockingQueue:基于数组实现的阻塞队列,大小固定,读写锁分离,支持公平和非公平两种模式。

  2. LinkedBlockingQueue:基于链表实现的阻塞队列,大小可以选择是否限制(如果没有限制,则容量为Integer.MAX_VALUE),读写锁分离,支持公平和非公平两种模式。

  3. SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等待另一个线程的相应删除操作,反之亦然。

  4. PriorityBlockingQueue:支持优先级排序的阻塞队列,内部使用平衡二叉树实现。

11.3核心方法

演示:

add和remove

队列长度为3时,若向其中放入4个元素时的异常结果

package myjuc;

import java.util.concurrent.ArrayBlockingQueue;

/**
 * @author yzh
 * @date 2024-01-10
 */

public class Dome10 {
    public static void main(String[] args) {
        //创建阻塞队列
        ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<String>(3);

        //第一组
        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        System.out.println(blockingQueue.add("c"));
        System.out.println(blockingQueue.element());
        System.out.println(blockingQueue.add("f"));
    }
}

队列长度为3时,若向其中取出4个元素时的异常结果

package myjuc;

import java.util.concurrent.ArrayBlockingQueue;

/**
 * @author yzh
 * @date 2024-01-10
 */

public class Dome10 {
    public static void main(String[] args) {
        //创建阻塞队列
        ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<String>(3);

        //第一组
        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        System.out.println(blockingQueue.add("c"));
        System.out.println(blockingQueue.element());
        for (int i = 1; i <=4 ; i++) {
            System.out.println(blockingQueue.remove());
        }
    }
}

poll和offer方法

package myjuc;

import java.util.concurrent.ArrayBlockingQueue;

/**
 * @author yzh
 * @date 2024-01-10
 */

public class Dome10 {
    public static void main(String[] args) {
        //创建阻塞队列
        ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<String>(3);

        //第二组
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));
        System.out.println(blockingQueue.offer("d"));

        for (int i = 1; i <=4 ; i++) {
            System.out.println(blockingQueue.poll());
        }
    }
}

put和take

put方法放入超过队列长度的元素时会发生阻塞

package myjuc;

import java.util.concurrent.ArrayBlockingQueue;

/**
 * @author yzh
 * @date 2024-01-10
 */

public class Dome10 {
    public static void main(String[] args) throws InterruptedException {
        //创建阻塞队列
        ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<String>(3);

        //第三组
        blockingQueue.put("a");
        blockingQueue.put("b");
        blockingQueue.put("c");
        blockingQueue.put("d");


    }
}

put方法取出超过队列长度的元素时会发生阻塞

package myjuc;

import java.util.concurrent.ArrayBlockingQueue;

/**
 * @author yzh
 * @date 2024-01-10
 */

public class Dome10 {
    public static void main(String[] args) throws InterruptedException {
        //创建阻塞队列
        ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<String>(3);

        //第三组
        blockingQueue.put("a");
        blockingQueue.put("b");
        blockingQueue.put("c");

        for (int i = 1; i <=4 ; i++) {
            System.out.println(blockingQueue.take());
        }



    }
}

offer(E e, long timeout, TimeUnit unit)和poll(long timeout, TimeUnit unit) throws InterruptedException 

package myjuc;

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

/**
 * @author yzh
 * @date 2024-01-10
 */

public class Dome10 {
    public static void main(String[] args) throws InterruptedException {
        //创建阻塞队列
        ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<String>(3);

        //第四组
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));
        //offer("d",3L, TimeUnit.SECONDS)   3L为超时时间    TimeUnit.SECONDS为时间单位   此时阻塞时间超过3s就会停止
        System.out.println(blockingQueue.offer("d",3L, TimeUnit.SECONDS));


    }
}

package myjuc;

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

/**
 * @author yzh
 * @date 2024-01-10
 */

public class Dome10 {
    public static void main(String[] args) throws InterruptedException {
        //创建阻塞队列
        ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<String>(3);

        //第四组
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));


        for (int i = 1; i <=3 ; i++) {
            System.out.println(blockingQueue.poll());
        }
        //poll(3L, TimeUnit.SECONDS)   3L为超时时间    TimeUnit.SECONDS为时间单位   此时阻塞时间超过3s就会停止
        System.out.println(blockingQueue.poll(3L, TimeUnit.SECONDS));


    }
}

12.ThreadPool线程池

12.1线程池概述

线程池( 英语 : thread pool ) :一种线程使用模式。线程过多会带来调度开销进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。

线程池的优势: 线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。

线程池的特点:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。统一管理线程,避免系统创建大量同类线程而导致消耗完内存。

12.2线程池的使用方法

一池N线程

特点:

线程池中的线程处于一定的量,可以很好的控制线程的并发量

线程可以重复被使用,在显示关闭之前,都将一直存在

超出一定量的线程被提交时候需在队列中等待.

package myjuc;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author yzh
 * @date 2024-01-10
 */

//演示线程池三种常见分类
public class Dome11 {
    public static void main(String[] args) {
        //一池N线程
        //5个窗口
        ExecutorService pool = Executors.newFixedThreadPool(5);
        //100个顾客
        try {
            for (int i = 1; i <=100 ; i++) {
                final int num =i;
                //执行
                pool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"正在办理业务"+num);
                });
            }

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //关闭
            pool.shutdown();
        }


    }
}

一个任务一个任务执行,一池一线程

package myjuc;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author yzh
 * @date 2024-01-10
 */
//演示线程池三种常见分类
public class Dome12 {
    public static void main(String[] args) {
        //一池一线程
        //一个窗口
        ExecutorService pool = Executors.newSingleThreadExecutor();

        //100个顾客
        try {
            for (int i = 1; i <=100 ; i++) {
                final int num =i;
                pool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"正在办理业务"+num);
                });
            }

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //关闭
            pool.shutdown();
        }

    }
}

线程池根据需求创建线程,可以扩容,遇强则强

package myjuc;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author yzh
 * @date 2024-01-10
 */
//演示线程池三种常见分类
public class Dome13 {
    public static void main(String[] args) {
        //一池可扩容线程池
        ExecutorService pool = Executors.newCachedThreadPool();


        //100个顾客
        try {
            for (int i = 1; i <=100 ; i++) {
                final int num =i;
                pool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"正在办理业务"+num);
                });
            }

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //关闭
            pool.shutdown();
        }
    }
}

12.3线程池的底层原理及七大参数

线程池执行原理

  • 当线程池里存活的线程数小于核心线程数corePoolSize时,这时对于一个新提交的任务,线程池会创建一个线程去处理任务。当线程池里面存活的线程数小于等于核心线程数corePoolSize时,线程池里面的线程会一直存活着,就算空闲时间超过了keepAliveTime,线程也不会被销毁,而是一直阻塞在那里一直等待任务队列的任务来执行。
  • 当线程池里面存活的线程数已经等于corePoolSize了,这是对于一个新提交的任务,会被放进任务队列workQueue排队等待执行。
  • 当线程池里面存活的线程数已经等于corePoolSize了,并且任务队列也满了,假设maximumPoolSize>corePoolSize,这时如果再来新的任务,线程池就会继续创建新的线程来处理新的任务,知道线程数达到maximumPoolSize,就不会再创建了。
  • 如果当前的线程数达到了maximumPoolSize,并且任务队列也满了,如果还有新的任务过来,那就直接采用拒绝策略进行处理。默认的拒绝策略是抛出一个RejectedExecutionException异常。

七大参数

1、corePoolSize:当有新任务时,如果线程池中线程数没有达到线程池的基本大小,则会创建新的线程执行任务,否则将任务放入阻塞队列。当线程池中存活的线程数总是大于 corePoolSize 时,应该考虑调大 corePoolSize。

2、maximumPoolSize:当阻塞队列填满时,如果线程池中线程数没有超过最大线程数,则会创建新的线程运行任务。否则根据拒绝策略处理新任务。非核心线程类似于临时借来的资源,这些线程在空闲时间超过 keepAliveTime 之后,就应该退出,避免资源浪费。

3、BlockingQueue:存储等待运行的任务。

4、keepAliveTime非核心线程空闲后,保持存活的时间,此参数只对非核心线程有效。设置为0,表示多余的空闲线程会被立即终止。

5、TimeUnit:时间单位

TimeUnit.DAYS
TimeUnit.HOURS
TimeUnit.MINUTES
TimeUnit.SECONDS
TimeUnit.MILLISECONDS
TimeUnit.MICROSECONDS
TimeUnit.NANOSECONDS

6、ThreadFactory:每当线程池创建一个新的线程时,都是通过线程工厂方法来完成的。在 ThreadFactory 中只定义了一个方法 newThread,每当线程池需要创建新线程就会调用它。

public class MyThreadFactory implements ThreadFactory {
    private final String poolName;
    
    public MyThreadFactory(String poolName) {
        this.poolName = poolName;
    }
    
    public Thread newThread(Runnable runnable) {
        return new MyAppThread(runnable, poolName);//将线程池名字传递给构造函数,用于区分不同线程池的线程
    }
}

7、RejectedExecutionHandler:当队列和线程池都满了的时候,根据拒绝策略处理新任务。

AbortPolicy:默认的策略,直接抛出RejectedExecutionException
DiscardPolicy:不处理,直接丢弃
DiscardOldestPolicy:将等待队列队首的任务丢弃,并执行当前任务
CallerRunsPolicy:由调用线程处理该任务

12.4自定义线程池

package myjuc;

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

/**
 * @author yzh
 * @date 2024-01-10
 */
//自定义线程池
public class Dome14 {

    public static void main(String[] args) {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                2,
                5,
                2L,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );

        //100个顾客
        try {
            for (int i = 1; i <=100 ; i++) {
                final int num =i;
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"正在办理业务"+num);
                });
            }

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //关闭
            threadPool.shutdown();
        }
    }
}

出现此异常的原因:最大线程数量都被占用,等待队列也满了,拒绝策略为

AbortPolicy:默认的策略,直接抛出RejectedExecutionException

13.Fork/Join框架

Fork/Join框架是Java中用于并行计算的一种编程模型和框架。它基于"分而治之"(divide and conquer)的思想,将一个大任务划分为若干个小任务,然后将这些小任务分配给不同的线程进行执行,最后将各个子任务的结果合并得到最终结果。

Fork/Join框架主要由以下几个组成部分构成:

  1. ForkJoinPool(分支合并池):是整个框架的核心,用于管理和调度任务的执行。它通过工作窃取(work-stealing)算法实现任务的负载均衡,即空闲的线程会主动去窃取其他线程的任务执行,以提高线程的利用率。

  2. ForkJoinTask(分支合并任务):是任务的抽象类,可以有两种具体的子类:

    • RecursiveAction:表示没有返回值的任务。
    • RecursiveTask:表示有返回值的任务。
  3. Fork(分支):指将一个大任务拆分成多个小任务的过程。在拆分过程中,框架会根据一定的策略将任务分配给不同的线程去执行。

  4. Join(合并):指将多个小任务的结果合并成最终结果的过程。在合并过程中,框架会等待所有子任务完成,并将它们的结果进行合并。

使用Fork/Join框架的步骤通常包括:

  1. 继承RecursiveAction或RecursiveTask类,实现compute()方法来定义具体的任务逻辑。
  2. 在compute()方法中判断任务是否足够小,如果足够小则直接执行任务,否则将任务拆分成更小的子任务并调用fork()方法提交给线程池。
  3. 在compute()方法中使用join()方法等待子任务完成,并将子任务的结果合并得到最终结果。

Fork/Join框架可以方便地实现任务的并行执行和结果的合并,适用于处理一些需要递归拆分的问题,例如归并排序、求和等。它能够充分利用多核处理器的优势,提高程序的执行效率。

package myjuc;

import java.util.concurrent.*;

/**
 * @author yzh
 * @date 2024-01-10
 */

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

        //创建MyTask对象
        MyTask task = new MyTask(0, 100);
        //创建分支合并池对象
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(task);
        //获取最终合并之后的结果
        Integer result = forkJoinTask.get();
        System.out.println(result);
        //关闭
        forkJoinPool.shutdown();
    }

}

class MyTask extends RecursiveTask<Integer>{

    //拆分差值不能超过10,计算10以内运算
    private static final Integer VALUE=10;

    //开始值
    private int begin;
    //结束值
    private int end;
    //结果值
    private int result;

    //有参构造
    public MyTask(int begin, int end) {
        this.begin = begin;
        this.end = end;
    }

    //拆分与合并过程
    @Override
    protected Integer compute() {
        //判断相加的两个数差值是否大于10
        if (end - begin <VALUE){
            //相加
            for (int i = begin; i <=end ; i++) {
                result = result+i;
            }
        }else {
            //进一步拆分
            //获取数据中间值
            int mid =(begin+end)/2;
            //左边拆分
            MyTask myTask1 = new MyTask(begin, mid);
            //右边拆分
            MyTask myTask2 = new MyTask(mid+1, end);
            //调用方法拆分
            myTask1.fork();
            myTask2.fork();
            //合并结果
            result=myTask1.join()+myTask2.join();
        }
        return result;
    }
}

14.CompletableFuture异步回调

CompletableFuture是Java8中新增的一个类,它提供了一种异步编程模型,可以方便地处理异步任务并使用回调函数处理任务完成后的结果。

CompletableFuture可以看作是一个可编排的异步回调机制,它的主要特点包括:

  1. 异步执行:CompletableFuture会在后台执行异步任务,不会阻塞当前线程。

  2. 可组合性:可以通过链式调用的方式组合多个CompletableFuture,形成一个任务流水线。

  3. 回调函数:可以通过回调函数的方式处理任务完成后的结果。

  4. 异常处理:提供了异常处理的机制,可以捕获异步任务执行过程中的异常,并进行处理。

使用CompletableFuture的基本流程如下:

  1. 创建CompletableFuture对象,指定需要执行的异步任务。

  2. 注册回调函数,处理异步任务完成后的结果。

  3. 组合多个CompletableFuture,形成一个任务流水线。

  4. 获取异步任务的执行结果,或者等待所有任务执行完成后再获取结果。

package myjuc;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

/**
 * @author yzh
 * @date 2024-01-10
 */

public class Dome16 {

    public static void main(String[] args) throws Exception {
        //异步调用没有返回值
        CompletableFuture<Void> voidCompletableFuture = CompletableFuture.runAsync(()->{
            System.out.println(Thread.currentThread().getName()+"voidCompletableFuture");
        });
        voidCompletableFuture.get();
        //异步调用有返回值
        CompletableFuture<Integer> integerCompletableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName()+"integerCompletableFuture");
            //模拟异常
            int i =1/0;
            return 1024;
        });
        integerCompletableFuture.whenComplete((t,u)->{
            //方法的返回值
            System.out.println("****t"+t);
            //得到方法中的异常信息
            System.out.println("****u"+u);
        }).get();
    }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值