1501 - JUC高并发

须知少许凌云志,曾许人间第一流

看的是尚硅谷的视频做的学习总结,感恩老师,下面是视频的地址

传送门icon-default.png?t=N7T8https://www.bilibili.com/video/BV1Kw411Z7dF

0.思维导图

1.JUC简介

1.1 什么是JUC

JUC, java.util.concurrent工具包的简称,一个处理线程的工具包。

1.2 进程和线程的概念

1.2.1 进程与线程

  • 进程

指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程是资源分配的最小单位。

  • 线程

系统分配处理器时间资源的基本单元;进程之内独立执行的一个单元执行流;线程是程序执行的最小单位。

一个进程包含多个线程。

1.2.2 线程的状态

查看jdk源码

1.2.3 wait 和 sleep

区别

  1. sleep是Thread的静态方法;wait是Object的方法,任何对象实例都能调用。
  2. sleep不会释放锁,它也不需要占用锁;wait会释放锁。(wait会释放锁去睡觉,sleep会抓住锁去睡觉,在哪里谁就会在哪里醒)
  3. 它们都可以被interrupt方法中断。

1.2.4 并发和并行

  • 并发

同一时间间隔内多个线程交替执行,实际上是宏观上并行,微观上串行。(春运抢票,电商秒杀,抢同一个资源

  • 并行

同一时刻多个线程正在执行,多核并行。(一边看书,一边听音乐)

1.2.5 管程

叫 Monitor 监视器,就是锁。

是一种同步机制,保证同一个时间,只有一个线程访问被保护的数据或者代码。

1.2.6 用户线程和守护线程

  • 用户线程

自定义的线程,不随主线程的结束而结束。主线程结束了,用户线程还会运行,jvm还是存活状态。

  • 守护线程

随着主线程的结束而结束,如垃圾回收线程。主线程结束,jvm结束。

2.Lock接口

2.1 Synchronized

2.1.1 Synchronized作用范围

synchronized是Java的关键字,是一种同步锁。

synchronized的作用范围可以根据使用方式的不同而有所区别,主要有以下几种情况:

  • 同步方法(实例方法)
public class SynchronizedExample {
    public synchronized void syncMethod() {
        // 同步代码块
    }
}

synchronized修饰一个实例方法时,它作用于整个方法体。当一个线程进入一个对象的同步方法时,其他线程在该对象上调用同步方法时会被阻塞,直到第一个线程退出该方法。这种同步方式是基于对象的,也就是说,不同的对象实例的同步方法是互不干扰的。

  • 同步静态方法
public class SynchronizedExample {
    public static synchronized void syncStaticMethod() {
        // 同步代码块
    }
}

synchronized修饰一个静态方法时,它作用于整个静态方法体。由于静态方法是属于类的,而不是类的实例,因此这种同步是基于类的。当一个线程进入一个类的同步静态方法时,其他线程在该类上调用同步静态方法时会被阻塞。

  • 同步代码块
public class SynchronizedExample {
    private final Object lock = new Object();

    public void someMethod() {
        synchronized (lock) {
            // 同步代码块
        }
    }
}

synchronized也可以用来修饰一个代码块,此时需要指定一个对象作为锁对象。当线程进入同步代码块时,它会获取指定对象的锁,如果其他线程已经持有该对象的锁,则进入阻塞状态。这种同步方式允许更细粒度的控制,只同步需要同步的代码部分。 

2.1.2 多线程编程步骤

第一步:创建资源类,在资源类创建属性和操作方法。

第二步:创建多个线程,去调用资源类的操作方法。

2.1.3 Synchronized实现买票示例

需求:3个售票员卖一百张门票。

分析:资源是一百张门票,操作方法是买票,创建多个线程是3个售货员。

代码示例

// 第一步:定义资源类
class Ticket {
    // 第二步:定义资源
    public int number = 100;

    // 第三步:定义操作方法
    public synchronized void sale() {
        if (number > 0) {
            System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - number + 1) + "张票,剩余" + --number + "张票");
        }
    }
}

public class SaleTicket {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        // 使用匿名内部类创建线程
        Runnable runnable = () -> {
            for (int i = 0; i < 150; i++) {
                ticket.sale();
            }
        };

        // 创建多个线程进行卖票
        new Thread(runnable,"售票员1").start();
        new Thread(runnable,"售票员2").start();
        new Thread(runnable,"售票员3").start();
    }
}

输出结果

2.2 Lock

2.2.1 Lock接口的介绍

Lock 实现提供比使用 synchronized 方法和语句可以获得的更广泛的锁定操作。

2.2.2 使用Lock实现卖票例子

// 第一步:定义资源类
class Ticket {
    // 第二步:定义资源
    public int number = 100;

    // 创建可重入锁
    private final ReentrantLock lock = new ReentrantLock();

    // 第三步:定义操作方法
    public synchronized void sale() {
        // 手动上锁
        lock.lock();
        try{
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - number + 1) + "张票,剩余" + --number + "张票");
            }
        }finally {
            // 手动解锁
            lock.unlock();
        }
    }
}

public class SaleTicket {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        // 使用匿名内部类创建线程
        Runnable runnable = () -> {
            for (int i = 0; i < 150; i++) {
                ticket.sale();
            }
        };

        // 创建多个线程进行卖票
        new Thread(runnable,"售票员1").start();
        new Thread(runnable,"售票员2").start();
        new Thread(runnable,"售票员3").start();
    }
}

2.2.3 synchronized和Lock两者差异

  • synchronized是java内置关键字。Lock不是内置,可以实现同步访问且比 synchronized中的方法更加丰富。
  • synchronized自动释放锁,而lock需手动释放锁(不解锁会出现死锁,需要在 finally 块中释放锁)。
  • Lock 可以让等待锁的线程响应中断,而等待synchronized锁的线程不能响应中断,会一直等待。
  • 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
  • Lock 可以提高多个线程进行读操作的效率(当多个线程竞争的时候,Lock 性能远远好于synchronized)。

2.2.4 创建线程的四种方式

  1. 继承Thread类
  2. 实现Runnable接口
  3. 使用Callable接口
  4. 使用线程池

3.线程间通信

3.1 多线程编程步骤

第一步:创建资源类,在资源类创建属性和操作方法。

第二步:在资源类操作方法,判断、干活、通知。

第三步:创建多个线程,去调用资源类的操作方法。

第四步:防止虚假唤醒。

3.2 synchronized 实现线程通信案例

关键字 synchronized 与 wait()/notify() 这两个方法一起使用可以实现等待/通知模式。

代码示例

// 第一步:创建资源类,定义属性和操作方法
class Share {
    private int number = 0;

    // +1的方法
    public synchronized void incr() throws InterruptedException {
        // 第二步:判断
        if (number != 0) {
           this.wait();
        }
        // 干活
        number++;
        System.out.println(Thread.currentThread().getName() + ":" + number);
        // 通知其他线程
        this.notifyAll();
    }

    // -1的方法
    public synchronized void decr() throws InterruptedException {
        // 判断
        if (number != 1) {
            this.wait();
        }
        // 干活
        number--;
        System.out.println(Thread.currentThread().getName() + ":" + number);
        // 通知其他线程
        this.notifyAll();
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        // 第三步:创建多个线程,调用资源类中的操作方法
        Share share = new Share();

        new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "生产").start();

        new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "消费").start();

    }

}

3.3 虚假唤醒问题(if改while)

当多个线程都处于等待集合中,一旦收到通知,可以直接操作而不再判断,这叫做虚假唤醒问题。 将this.wait()放在while循环中可以解决该问题。

代码示例

class Share {
    int number = 0;

    public synchronized void incr() throws InterruptedException {
        //判断
        if (number != 0) {
            this.wait();//这里会释放锁
        }
        //执行
        number++;
        System.out.print(Thread.currentThread().getName() + " : " + number + "-->");
        // 通知
        this.notifyAll();
    }

    public synchronized void decr() throws InterruptedException {
        //判断
        if (number != 1) {
            this.wait();
        }
        //执行
        number--;
        System.out.println(Thread.currentThread().getName() + " : " + number);
        //通知
        this.notifyAll();
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        Share share = new Share();
        new Thread(() -> {
            for (int i = 1; i <= 100; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 1; i <= 100; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 1; i <= 100; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "C").start();

        new Thread(() -> {
            for (int i = 1; i <= 100; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "D").start();
    }
}

输出结果

原因

由于 wait() 方法使线程在哪里睡就在哪里醒,B和D在wait后被唤醒,执行操作时不会再通过 if 判断,从而导致出现异常结果。
为了保证线程“醒了”之后再次判断,需要将wait() 方法放入while循环中。 

class Share {
    int number = 0;

    public synchronized void incr() throws InterruptedException {
        //判断
        while (number != 0) {
            this.wait();
        }
        //执行
        number++;
        System.out.print(Thread.currentThread().getName() + " : " + number + "-->");
        // 通知
        this.notifyAll();
    }

    public synchronized void decr() throws InterruptedException {
        //判断
        while (number != 1) {
            this.wait();
        }
        //执行
        number--;
        System.out.println(Thread.currentThread().getName() + " : " + number);
        //通知
        this.notifyAll();
    }
}

3.4 Lock实现线程间通信案例

在 Lock 接口中,有一个 newCondition() 方法,返回一个新 Condition 绑定到该实例 Lock 实例。

Condition 类中有 await() signalAll() 等方法,和 synchronized 实现案例中的 wait() 和 notifyAll() 方法相同。

代码示例

class Share {
    private int number = 0;
    //创建Lock
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();

    public void incr() throws InterruptedException {
        lock.lock();
        try {
            while (number != 0) {
                condition.await();
            }
            number++;
            System.out.print(Thread.currentThread().getName() + " : " + number + "-->");
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void decr() throws InterruptedException {
        lock.lock();
        try {
            //判断
            while (number != 1) {
                condition.await();
            }
            //执行
            number--;
            System.out.println(Thread.currentThread().getName() + " : " + number);
            //通知
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        Share share = new Share();
        new Thread(() -> {
            for (int i = 1; i <= 100; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 1; i <= 100; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 1; i <= 100; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "C").start();

        new Thread(() -> {
            for (int i = 1; i <= 100; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "D").start();
    }
}

4.线程间定制化通信

4.1 Condition 类选择性通知

案例: 启动三个线程,按照如下要求执行,AA打印5此,BB打印10次,CC打印15次,一共进行10轮
具体思路: 每个线程添加一个标志位,是该标志位则执行操作,并且修改为下一个标志位,通知下一个标志位的线程。

代码示例

class ShareResource {
    // 标志位 1:AA 2:BB 3:CC
    private int flag = 1;
    private ReentrantLock lock = new ReentrantLock();
    // 创建三个Condition对象,实现定向唤醒
    Condition c1 = lock.newCondition();
    Condition c2 = lock.newCondition();
    Condition c3 = lock.newCondition();

    public void print5(int loop) throws InterruptedException {
        lock.lock();
        try {
            while (flag != 1) {
                c1.await();
            }
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " :: " + i + ", loop=" + loop);
            }
            flag = 2;
            c2.signal();
        } finally {
            lock.unlock();
        }
    }

    public void print10(int loop) throws InterruptedException {
        lock.lock();
        try {
            while (flag != 2) {
                c2.await();
            }
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + " :: " + i + ", loop=" + loop);
            }
            flag = 3;
            c3.signal();
        } finally {
            lock.unlock();
        }
    }

    public void print15(int loop) throws InterruptedException {
        lock.lock();
        try {
            while (flag != 3) {
                c3.await();
            }
            for (int i = 0; i < 15; i++) {
                System.out.println(Thread.currentThread().getName() + " :: " + i + ", loop=" + loop);
            }
            flag = 1;
            c1.signal();
        } finally {
            lock.unlock();
        }
    }
}

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

上面的代码采用单标志法,设置一个公用整型变量flag,用于指示被允许进入临界区的进程编号。

若 flag =1,则允许 AA 进程进入临界区;

若 flag =2,则允许 BB 进程进入临界区;

若 flag =3,则允许 CC 进程进入临界区。

该算法可确保每次只允许一个进程进入临界区。但两个进程必须交替进入临界区,若某个进程不再进入临界区,则另一个进程也无法进入临界区。在线程的run()方法调用中设置不同的loop次数,在后期会有部分线程不能访问 Share 资源了,违背了"空闲让进"原则,让资源利用不充分。

4.2 进程/线程同步四个原则

  • 空闲让进:临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区。
  • 忙则等待:当已经有进程进入临界区的时候,其他试图进入临界区的进程必须等待。
  • 有限等待:对请求访问的进程,应保证能在有限时间内进入临界区。
  • 让权等待:当进程不能进入临界区的时候,应立即释放处理机,防止进程忙等待。

5.集合的线程安全

集合线程不安全,简单来说就是底层的方法没有使用同步安全锁。

5.1 ArrayList 不安全

jdk源码

没有使用synchronized关键字,所以在多线程并发时,会出现线程安全问题。

代码示例

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

运行报错

5.2 解决方案 Vector

jdk源码

Vector类中的方法加了synchronized关键字,因此可以保证线程安全。

代码改造

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

 运行结果

5.3 解决方案 synchronizedList(List list) 

Collections 接口中的 synchronizedList(List list) 方法,可以将传入的 List列表对象转为同步(线程安全的)列表并返回。

语法

List<String> list = Collections.synchronizedList(new ArrayList<>());

jdk源码

代码示例

public class ThreadDemo {
    public static void main(String[] args) {
        List<String> list = Collections.synchronizedList(new ArrayList<>());
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}

5.4 解决方案 CopyOnWriteArrayList

语法

List<String> list = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList 采用读写分离的思想,读操作不加锁,写操作加锁。(redis也使用)

  • 读的时候并发读取旧数据(多个线程操作)
  • 写的时候独立,先复制一份比旧数据长 1 的数据出来,在最后添加数据,旧新合并,完成写操作,之后就可以读所有数据(每次加新内容都写到新区域,合并之前旧区域,读取新区域添加的内容)
  • 11
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值