Java多线程与并发编程

课程地址:
https://www.itlaoqi.com/chapter.html?sid=98&cid=1425
源码文档:
链接:https://pan.baidu.com/s/1WMvM3j6qhyjIeAT87kIcxg
提取码:5g56

Java多线程与并发编程

      • 1-并发背后的故事
          • 什么是并发
      • 2-你必须知道线程的概念
          • 程序、进程与线程
          • 并发和并行
          • 同步和异步
          • 临界区
          • 线程活跃度(死锁,饥饿,活锁)
          • 线程安全
      • 3-Java内存模型(JMM)
          • Java内存模型全称JMM(Java Memory Model)
      • 4-创建多线程-继承Thread
      • 5-创建多线程-实现Runnable接口
      • 6-创建多线程-实现Callable接口
      • 7-Synchronized线程同步机制
          • Synchronize的使用场景
      • 8-面试题-线程的五种状态
      • 9-死锁的产生
      • 10-重新认识线程安全ThreadSafe
      • 11-JAVA并发包与线程池
          • 什么是线程池
          • new Thread的弊端
          • ThreadPool - 线程池
          • 线程池的种类
      • 12-JUC之CountDownLatch倒计时锁
      • 13-JUC之Semaphore信号量
      • 14-JUC之CyclicBarrier循环屏障
          • CyclicBarrier的应用场景
      • 15-JUC之ReentrantLock重入锁
          • ReentrantLock与synchronized的区别
      • 15.1-JUC之Condition线程等待与唤醒
      • 16-JUC之Callable_Future
      • 17-JUC之同步容器
          • 请写出线程安全的类
          • 线程安全-并发容器
      • 18-JUC之Atomic与CAS算法(乐观锁)
          • 回顾原子性
          • Atomic包
          • Atomic的应用场景
      • 19-课程总结

1-并发背后的故事

什么是并发

并发就是指程序同时处理多个任务的能力。
并发编程的根源在于对多任务情况下对访问资源的有效控制

例如多人同时操作在线文档等
Java多线程与并发编程_第1张图片

2-你必须知道线程的概念

程序、进程与线程

程序是静态的概念,windows下通常指exe文件。

进程是动态的概念,是程序在运行状态,进程说明程序在内存中的边界。
线程是进程内的一个”基本任务”,每个线程都有自己的功能,是CPU分配与调度的基本单位。

并发和并行

并行是基于多核cpu进行,多个线程同时调度任务
并发是单个cpu处理

Java多线程与并发编程_第2张图片

同步和异步

同步,前面的事情不做完后面的事情干不了
异步,异步是指在程序执行过程中,不需要等待某个任务的完成,而是在发出该任务后继续执行后面的代码,等任务完成后再回来处理该任务的结果

Java多线程与并发编程_第3张图片

临界区

临界区用来表示一种公共资源与共享数据,可以被多个线程使用。
同一时间只能有一个线程访问临界区(阻塞状态),其他资源必须等待。

举例:数据库在执行某条数据更新操作,数据库为了保证数据访问有效,所以会在更新前对这条数据开启一个锁,这个锁只能被其中某个用户访问,这个用户对这条数据更新的时候,其他用户对这条数据更新都得等着,直到更新的的用户更新完把锁释放掉,其他等待的用户才能够更新。

线程活跃度(死锁,饥饿,活锁)

死锁,大家对公共资源彼此争执,并且都不愿意释放
饥饿,线程一直获取不到资源
活锁,线程调度不够智能,资源没有被占用,线程还一直处于等待状态
三种情况都会形成系统阻塞
Java多线程与并发编程_第4张图片

线程安全

在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
线程安全和不安全,取决于在多线程的情况下,在对比单线程所造成的结果是否相同,如果是相同则是线程安全的,结果不一致则可以定义为线程不安全

3-Java内存模型(JMM)

Java内存模型全称JMM(Java Memory Model)

内存主要有堆和栈组成
在这里插入图片描述
下面来一段demo代码详细讲解堆栈的作用,以及流程

public class Employee {
    private String name;
    private Integer age;
    private Department department;
    public Employee(){

    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Department getDepartment() {
        return department;
    }

    public void setDepartment(Department department) {
        this.department = department;
    }

    public void sayJoke(String content){
        System.out.println(this.getName() + "说" + content);
    }

    public static void main(String[] args) {
        Employee emp = new Employee();
        emp.setName("老齐");
        emp.setAge(13);
        Department department = new Department();
        department.setDname("小卖部");
        emp.setDepartment(department);
        emp.sayJoke("一言不合就开车");
    }
}

class Department{
    private String dname;

    public String getDname() {
        return dname;
    }

    public void setDname(String dname) {
        this.dname = dname;
    }
}

在这里插入图片描述
1.运行方法前进行类的加载,加载到employee和department这两个类,把这两个类的结构,成员变量和成员方法加载到方法区(方法区静态还会存储静态的方法和变量
2.启动main方法,创建一个线程,会开辟一个栈空间,压入mian方法的栈(栈帧:每执行一个方法就会有一个对应的栈帧
3.emp = new Empolyee(); 创建对象,存储在堆空间,将栈地址指向开辟的堆空间(也可以叫做引用
4.emp.setName(“老齐”);先在方法区创建字符串"老齐",然后将字符串引用到name属性上,setName()方法的栈帧执行完,就会出栈
5.emp.setAge(13);数字按值引用,直接赋值,不需要在静态区开辟新空间
6.dept = new Department();同上3
7.dept.setDname(“小卖铺”);同上4
8.emp.setDepartment(dept);Employee对象的department属性直接指向Department对象的地址(也可以叫做地址被属性所引用)
9.emp.sayJoke(“一言不合就开车”)
10.方法执行完成,站内栈帧全部弹出,线程销毁

4-创建多线程-继承Thread

/**
 * 使用集成Thread的方式实现多线程
 */
public class Match1 {
    public static void main(String[] args) {
        Runner liuxiang = new Runner();//创建一个新的线程
        liuxiang.setName("刘翔");//设置线程名称
        Runner laoqi = new Runner();
        laoqi.setName("老齐");
        Runner op = new Runner();
        op.setName("路飞");

        liuxiang.start();//启动线程
        laoqi.start();
        op.start();

    }
}
class Runner extends Thread{
    @Override
    public void run() {
        Integer speed = new Random().nextInt(100);
        for(int i = 1 ; i <= 100 ; i++){
            try {
                Thread.sleep(1000); //当前线程休眠1秒
            }catch (Exception e){
                e.printStackTrace();
            }
            //this.getName()打印当前线程的名字
            System.out.println(this.getName() + "已前进" + (i * speed) + "米(" + speed + "米/秒)");
        }
    }
}

5-创建多线程-实现Runnable接口

public class Match2 {
    public static void main(String[] args) {
        Runner2 liuxiang = new Runner2();
        Thread thread1 = new Thread(liuxiang);
        thread1.setName("刘翔");

        Thread laoqi = new Thread(new Runner2());
        laoqi.setName("老齐");

        Thread op = new Thread(new Runner2());
        op.setName("路飞");

        thread1.start();
        laoqi.start();
        op.start();
    }
}

class Runner2 implements Runnable {
    @Override
    public void run() {
        Integer speed = new Random().nextInt(100);
        for(int i = 1 ; i <= 100 ; i++){
            try {
                Thread.sleep(1000); //当前线程休眠1秒
            }catch (Exception e){
                e.printStackTrace();
            }
            //Thread.currentThread()用于获取当前执行的线程对象
            //在Runnable中是无法使用this获取到当前线程对象的
            System.out.println(Thread.currentThread().getName() + "已前进" + (i * speed) + "米(" + speed + "米/秒)");
        }
    }
}

6-创建多线程-实现Callable接口

并发工具包-Concurrent

JDK1.5以后为我们专门提供了一个并发工具包java.util.concurrent。
java.util.concurrent 包含许多线程安全、测试良好、高性能的并发构建块。创建 concurrent 的目的就是要实现 Collection 框架对数据结构所执行的并发操作。通过提供一组可靠的、高性能并发构建块,开发人员可以提高并发类的线程安全、可伸缩性、性能、可读性和可靠性

public class Match3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建一个线程池。里面天生有3个“空”线程。Executors是调度器,对线程池进行管理
        ExecutorService executorService =  Executors.newFixedThreadPool(3);
        Runner3 liuxiang = new Runner3();//实例化Callable对象
        liuxiang.setName("刘翔");
        Runner3 laoqi = new Runner3();
        laoqi.setName("老齐");
        Runner3 op = new Runner3();
        op.setName("路飞");

        //将这个对象扔到线程池中,线程池自动分配一个线程来运行liuxiang这个对象的call方法
        //Future用于接受线程内部call方法的返回值
        Future<Integer> result1 =  executorService.submit(liuxiang);
        Future<Integer> result2 =  executorService.submit(laoqi);
        Future<Integer> result3 =  executorService.submit(op);

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        executorService.shutdown();//关闭线程池释放所有资源
        System.out.println("刘翔累计跑了" + result1.get() + "米" );
        System.out.println("老齐累计跑了" + result2.get() + "米" );
        System.out.println("路飞累计跑了" + result3.get() + "米" );
    }
}
class Runner3 implements Callable<Integer>{
    private String name ;
    public void setName(String name){
        this.name = name;
    }
    //实现Callable接口可以允许我们的线程返回值或抛出异常
    @Override
    public Integer call() throws Exception {
        Integer speed = new Random().nextInt(100);
        Integer distince = 0; //总共奔跑的距离
        for(int i = 1 ; i <= 100 ; i++){
            Thread.sleep(10);
            distince = i * speed;
            System.out.println(this.name + "已前进" + distince + "米(" + speed + "米/秒)");
        }
        return distince;
    }
}

三种线程创建方式的对比
Java多线程与并发编程_第5张图片

7-Synchronized线程同步机制

同步代码样例

public class SyncSample {
    public static void main(String[] args) {
        Couplet c = new Couplet();
        for(int i = 0 ; i < 10000 ; i++){
            new Thread(){
                public void run(){
                    int r = new Random().nextInt(2);
                    if(r % 2 == 0){
                        Couplet.first();
                    }else{
                        Couplet.second();
                    }
                }
            }.start();
        }
    }
}
class Couplet{
    Object lock = new Object(); //锁对象
    public synchronized static void first(){
//        synchronized (lock) { //同步代码块,在同一时间只允许有一个线程执行访问这个方法
            System.out.printf("琴");
            System.out.printf("瑟");
            System.out.printf("琵");
            System.out.printf("琶");
            System.out.println();
//        }
    }
    public static void second(){
        synchronized (Couplet.class) { //因为两个同步代码指向了同一把锁lock,所以在同一个时间内只允许有一个代码块执行,其他等待
            System.out.printf("魑");
            System.out.printf("魅");
            System.out.printf("魍");
            System.out.printf("魉");
            System.out.println();
        }

    }
}

现实中同步的列子
Java多线程与并发编程_第6张图片

代码中同步的列子
synchronized(同步锁)关键字的作用就是利用一个特定的对象设置一个锁lock(绣球),在多线程(游客)并发访问的时候,同时只允许一个线程(游客)可以获得这个锁,执行特定的代码(迎娶新娘)。执行后释放锁,继续由其他线程争抢。

Synchronize的使用场景

Synchronize可以使用在以下三种场景,对应不同锁对象:
synchronized代码块 - 任意对象即可
synchronized方法 - this当前对象
synchronized静态方法 - 该类的字节码对象

8-面试题-线程的五种状态

新建,当线程被new出来时,处于新建状态
就绪,等待状态,等待cpu分配资源(时间片),当分配到资源,就会自动执行run()
运行,程序运行状态
阻塞,当锁被解除或者休眠时间到了,线程就处于就绪状态,等待cpu分配资源
》I/O(下载一个很大数据的文件需要很长时间),
》sleep()(线程休眠),
》lock锁(比如synchronized关键字,其中就有等待的机制),
》yield()(主动把当前cpu的时间让出去,让给优先级更高的线程来去执行),
死亡,当程序所有都运行完,当前线程就会被jvm自动销毁,进入死亡状态,被垃圾回收
Java多线程与并发编程_第7张图片

9-死锁的产生

举例两个线程运用不同的顺序操作AB文件:

public class DeadLock {
    private static String fileA = "A文件";
    private static String fileB = "B文件";

    public static void main(String[] args) {
        new Thread(){ //线程1
            public void run(){
                while(true) {
                    synchronized (fileA) {//打开文件A,线程独占
                        System.out.println(this.getName() + ":文件A写入");
                        synchronized (fileB) {
                            System.out.println(this.getName() + ":文件B写入");
                        }
                        System.out.println(this.getName() + ":所有文件保存");
                    }
                }
            }
        }.start();

        new Thread(){ //线程2
            public void run(){
                while(true) {
                    synchronized (fileB) {//打开文件A,线程独占
                        System.out.println(this.getName() + ":文件B写入");
                        synchronized (fileA) {
                            System.out.println(this.getName() + ":文件A写入");
                        }
                        System.out.println(this.getName() + ":所有文件保存");
                    }
                }
            }
        }.start();
    }
}

Java多线程与并发编程_第8张图片

10-重新认识线程安全ThreadSafe

线程安全,在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。

举例多线程情况下的计数器

public class DownloadsSample {
    public static int users = 100;//同时模拟的并发访问用户数量
    public static int downTotal = 50000; //用户下载的真实总数
    public static int count = 0 ;//计数器

    public static void main(String[] args) {
        //调度器,JDK1.5后提供的concurrent包对于并发的支持
        ExecutorService executorService  = Executors.newCachedThreadPool();
        //信号量,用于模拟并发的人数
        final Semaphore semaphore = new Semaphore(users);
        for(int i = 0 ; i < downTotal ; i++){
            executorService.execute(()->{
                //通过多线程模拟N个用户并发访问并下载
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        executorService.shutdown();//关闭调度服务
        System.out.println("下载总数:" + count);
    }
    //线程不安全
    public static void add(){
        count++;
    }
    /*线程安全
    public synchronized static void add(){
        count++;
    }*/

}

Java多线程与并发编程_第9张图片
Java多线程与并发编程_第10张图片

11-JAVA并发包与线程池

什么是线程池

线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。

new Thread的弊端

new Thread()新建对象,性能差
线程缺乏统一管理,可能无限制的新建线程,相互竞争,严重时会占用过多系统资源导致死机或OOM

ThreadPool - 线程池

》 重用存在的线程,减少对象对象、消亡的开销
》 线程总数可控,提高资源的利用率
》 避免过多资源竞争,避免阻塞
》提供额外功能,定时执行、定期执行、监控等。

线程池的种类

在java.util.concurrent中,提供了工具类Executors(调度器)对象来创建线程池,可创建的线程池有四种:

  1. CachedThreadPool - 可缓存线程池
  2. FixedThreadPool - 定长线程池
  3. SingleThreadExecutor - 单线程池
  4. ScheduledThreadPool - 调度线程池

CachedThreadPool

public class ThreadPoolSample1 {
    public static void main(String[] args) {
        //调度器对象
        //ExecutorService用于管理线程池
        ExecutorService threadPool = Executors.newCachedThreadPool();//创建一个可缓存线程池
        //可缓存线程池的特点是,无限大,如果线程池中没有可用的线程则创建,有空闲线程则利用起来
        for(int i = 1 ; i <= 1000 ; i++) {
            final  int index = i;
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + ":" + index);
                }
            });
        }
        try {
            Thread.sleep(1000); //跟线程足够的运行时间
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //shutdown() 代表关闭线程池(等待所有线程完成)
        //shutdownNow() 代表立即终止线程池的运行,不等待线程,不推荐使用
        threadPool.shutdown();
    }
}

FixedThreadPool

public class ThreadPoolSample2 {
    public static void main(String[] args) {
        //调度器对象
        //ExecutorService用于管理线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);//创建一个可创建一个定长线程池
        //定长线程池的特点是固定线程总数,空间线程用于执行任务,如果线程都在使用后续任务则处于等待状态,在线程池中的线程
        //如果任务处于等待的状态,备选的等待算法默认为FIFO(先进先出) LIFO(后进先出)
        //执行任务后再执行后续的任务。
        for(int i = 1 ; i <= 1000 ; i++) {
            final  int index = i;
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + ":" + index);
                }
            });
        }
        try {
            Thread.sleep(1000); //跟线程足够的运行时间
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //shutdown() 代表关闭线程池(等待所有线程完成)
        //shutdownNow() 代表立即终止线程池的运行,不等待线程,不推荐使用
        threadPool.shutdown();
    }
}

SingleThreadExecutor

public class ThreadPoolSample3 {
    public static void main(String[] args) {
        //调度器对象
        //ExecutorService用于管理线程池
        ExecutorService threadPool = Executors.newSingleThreadExecutor();//单线程线程池
        for(int i = 1 ; i <= 1000 ; i++) {
            final  int index = i;
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + ":" + index);
                }
            });
        }
        try {
            Thread.sleep(1000); //跟线程足够的运行时间
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //shutdown() 代表关闭线程池(等待所有线程完成)
        //shutdownNow() 代表立即终止线程池的运行,不等待线程,不推荐使用
        threadPool.shutdown();
    }
}

ScheduledThreadPool

public class ThreadPoolSample4 {
    public static void main(String[] args) {
        ScheduledExecutorService scheduledThreadPool =  Executors.newScheduledThreadPool(5);//可调度线程池
        /*//延迟三秒执行一次Run方法
        scheduledThreadPool.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("延迟3秒执行");
            }
        } , 3 , TimeUnit.SECONDS);*/
        //Timer , 项目实际开发中scheduledThreadPool与Timer都不会用到,应为有成熟的调度框架Quartz,或者Spring自带调度,
        //程序的调度框架支持一种表达式叫做Cron表达式,有兴趣的童鞋可以了解一下。
        scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println(new Date() + "延迟1秒执行,每三秒执行一次");
            }
        }, 1, 3, TimeUnit.SECONDS);
    }
}

12-JUC之CountDownLatch倒计时锁

CountDownLatch倒计时锁特别适合”总-分任务”,例如多线程计算后的数据汇总
CountDownLatch类位于java.util.concurrent(J.U.C)包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他3个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。

举例:我们需要计算三个子线程完成计算的结果汇总,设置countDownLatch等于3,子线程执行完一次countdownLatch就会减一,当三个线程都执行完成,countDownLatch等于0时,就会执行汇总任务
Java多线程与并发编程_第11张图片
案例:多线程的情况下计算10000

public class CountDownSample {
    private static int count = 0;
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(100);
        CountDownLatch cdl = new CountDownLatch(10000); //CDL总数和操作数保持一致
        for(int i = 1 ; i <= 10000 ; i++) {
            final int index = i;
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    synchronized (CountDownSample.class) {
                        try {
                            count = count + index;
                            //计数器减一
                        }catch(Exception e){
                            e.printStackTrace();
                        }finally {
                            cdl.countDown();
                        }
                    }
                }
            });
        }
/*        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }*/
        try {
            cdl.await(); //堵塞当前线程,直到cdl=0的时候再继续往下走
            //为了避免程序一致挂起,我们可以设置一个timeout时间
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count);
        threadPool.shutdown();
    }
}

13-JUC之Semaphore信号量

Semaphore信号量经常用于限制获取某种资源的线程数量,例如限制游戏服务器在线人数,降低服务器运行压力,避免系统扛不住导致宕机
Java多线程与并发编程_第12张图片
案例:假设服务器只能容纳5人游戏,20人应该怎么处理(当5个信号量被占用时,其他的线程必须等到5人中有人释放信号量,才能执行)

public class SemaphoreSample1 {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newCachedThreadPool();
        Semaphore semaphore = new Semaphore(5);//定义5个信号量,也就是说服务器只允许5个人在里面玩
        for(int i = 1 ; i <= 20 ; i++) {
            final int index = i;
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        semaphore.acquire();//获取一个信号量,“占用一个跑到”
                        play();
                        semaphore.release();//执行完成后释放这个信号量,“从跑道出去”
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            });
        }
        threadPool.shutdown();
    }

    public static void play(){

        try {
            System.out.println(new Date() + " " + Thread.currentThread().getName() + ":获得紫禁之巅服务器进入资格");
            Thread.sleep(2000);
            System.out.println(new Date() + " " + Thread.currentThread().getName() + ":退出服务器");
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

14-JUC之CyclicBarrier循环屏障

可以让所有的线程同时执行,线程执行到barrier时会被拦住,知道所有的线程都准备就绪,然后同时执行
Java多线程与并发编程_第13张图片

public class CyclicBarrierSample {
    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for(int i = 1 ; i<=20 ; i++) {
            final int index = i;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    go();
                }
            });

        }
        executorService.shutdown();
    }

    private static void go(){
        System.out.println(Thread.currentThread().getName() + ":准备就绪" );
        try {
            cyclicBarrier.await();//设置屏障点,当累计5个线程都准备好后,才运行后面的代码
            System.out.println(Thread.currentThread().getName() + ":开始运行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }

    }
}
CyclicBarrier的应用场景

cpu性能测试
双11秒杀活动
抢票软件
Java多线程与并发编程_第14张图片

15-JUC之ReentrantLock重入锁

什么是重入锁

重入锁是指任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞
ReentrantLock设计的目标是用来替代synchronized关键字

ReentrantLock与synchronized的区别

Java多线程与并发编程_第15张图片

public class ReentrantLockSample {
    public static int users = 100;//同时模拟的并发访问用户数量
    public static int downTotal = 50000; //用户下载的真实总数
    public static int count = 0 ;//计数器
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        //调度器,JDK1.5后提供的concurrent包对于并发的支持
        ExecutorService executorService  = Executors.newCachedThreadPool();
        //信号量,用于模拟并发的人数
        final Semaphore semaphore = new Semaphore(users);
        for(int i = 0 ; i < downTotal ; i++){
            executorService.execute(()->{
                //通过多线程模拟N个用户并发访问并下载
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        executorService.shutdown();//关闭调度服务
        System.out.println("下载总数:" + count);
    }
    //线程不安全
    public static void add(){
        lock.lock();//上锁
        try {
            count++;
        }finally {
            lock.unlock(); //解锁,一定要放在finally里面否则会出现死锁
        }


    }


}

15.1-JUC之Condition线程等待与唤醒

condition条件唤醒

我们在并行程序中,避免不了某些线程要预先规定好的顺序执行,例如:先新增再修改,先买后卖,>先进后出…,对于这类场景,使用JUC的Condition对象再合适不过了。
JUC中提供了Condition对象,用于让指定线程等待与唤醒,按预期顺序执行。它必须和ReentrantLock重入锁配合使用。
Condition用于替代wait()/notify()方法

notify只能随机唤醒等待的线程,而Condition可以唤醒指定的线程,这有利于更好
的控制并发程序。

Condition核心方法

await() - 阻塞当前线程,直到singal唤醒
signal() - 唤醒被await的线程,从中断处继续执行
signalAll() - 唤醒所有被await()阻塞的线程

16-JUC之Callable_Future

Callable和Runnable一样代表着任务,区别在于Callable有返回值并且可以抛出异常。
Future 是一个接口。它用于表示异步计算的结果。提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。

案例:打印出10000以内的质数

public class FutureSample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for(int i = 2 ; i <= 10000 ; i++){
            Computor c = new Computor();
            c.setNum(i);
            //Future是对用于计算的线程进行监听,因为计算是在其他线程中执行的,所以这个返回结果的过程是异步的
            Future<Boolean> result = executorService.submit(c);//将c对象提交给线程池,如有空闲线程立即执行里面的call方法
            try {
                Boolean r = result.get(); //用于获取返回值,如果线程内部的call没有执行完成,则进入等待状态,直到计算完成
                if(r == true){
                    System.out.println(c.getNum());
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
        executorService.shutdown();
    }
}
class Computor implements Callable<Boolean>{
    private Integer num;

    public Integer getNum() {
        return num;
    }

    public void setNum(Integer num) {
        this.num = num;
    }

    @Override
    public Boolean call() throws Exception {
        boolean isprime = true;
        for(int i = 2 ; i < num ; i++) {
            if (num % i == 0) {
                isprime = false;
                break;
            }
        }

        return isprime;
    }
}

17-JUC之同步容器

请写出线程安全的类

Vector是线程安全的,ArrayList、LinkedList是线程不安全的
Properties是线程安全的,HashSet、TreeSet是不安全的
StringBuffer是线程安全的,StringBuilder是线程不安全的
HashTable是线程安全的,HashMap是线程不安全的

线程安全-并发容器

ArrayList -> CopyOnWriteArrayList - 写复制列表
HashSet -> CopyOnWriteArraySet - 写复制集合
HashMap -> ConcurrentHashMap - 分段锁映射

CopyOnWriteArrayList案例

public class CopyOnWriteArrayListSample {
    public static void main(String[] args) {
        //写复制列表
        List<Integer> list = new CopyOnWriteArrayList<>();
        for(int i = 0 ; i < 1000 ; i++){
            list.add(i);
        }
        Iterator<Integer> itr = list.iterator();
        while (itr.hasNext()) {
            Integer i = itr.next();
            list.remove(i);
        }
        System.out.println(list);
    }
}

List和Set底层数据结构都是数组,只不过一个是有序一个是无序,在多线程的情况下,进行add()和remove()时,会报ConcurrentModificationException并发修改异常,推荐使用CopyOnWriteArrayList和CopyOnWriteArraySet,原理就是在新增一条数据的时候,新创建一个副本,对副本进行操作,然后讲原来的指针指向新的副本地址,源码
Java多线程与并发编程_第16张图片

public boolean add(E e) {
		// 重入锁
        final ReentrantLock lock = this.lock;
        // 加锁
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            // 复制原来的list,并且长度加一
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            // 引用新的副本
            setArray(newElements);
            return true;
        } finally {
        	// 释放锁
            lock.unlock();
        }
    }

hashMap在多线程的情况下也是线程不安全的,线程安全的有hashTable,但是为什么实际不适用hashTable,而是用ConcurrentHashMap呢

  • hashTable底层时使用synchronized关键字实现,是多个线程操作同一个map,效率低
  • ConcurrentHashMap使用的是分段锁,将map以2*n次方分为多端长度,每个分段之间可以同时进行操作,但是单个分段还是单个操作,对于hashTable效率高
    Java多线程与并发编程_第17张图片

18-JUC之Atomic与CAS算法(乐观锁)

回顾原子性

原子性:是指一个操作或多个操作要么全部执行,且执行的过程不会被任何因素打断,要么就都不执行。

Atomic包

Atomic包是java.util.concurrent下的另一个专门为线程安全设计的Java包,包含多个原子操作类。

Atomic常用类
– AtomicInteger
– AtomicIntegerArray
– AtomicBoolean
– AtomicLong
– AtomicLongArray

将Atomic之前,先说下乐观锁和悲观锁的概念,其实意如其名
Java多线程与并发编程_第18张图片

Atomic的应用场景

虽然基于CAS的线程安全机制很好很高效,但要说的是,并非所有线程安全都可以用这样的方法来实现,这只适合一些粒度比较小型,如计数器这样的需求用起来才有效,否则也不会有锁的存在了。

19-课程总结

Java多线程与并发编程_第19张图片

你可能感兴趣的:(IT老齐的私房菜,java,开发语言)