JavaSE之第十三章多线程

第十三章 多线程

13.1 相关概念

13.1.1 程序

在计算机中,一系列的指令的集合

进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器
等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)

13.1.2 进程

操作系统为程序运行分配资源的基本单位
一个程序运行时,可以同时存在多个进程,至少有一个进程
是具有一定独立功能的程序、它是系统进行资源(内存)分配和调度的最小单位,重点在系统调度和单独的单位,也就是说进程是可以独 立运行的一段程序。

13.1.3 线程

CPU调度的最小单位
一个进程可以有多个线程,至少有一个线程
主线程:main方法运行在主线程中
在主线程中还可以创建新的线程,叫子线程
线程进程的一个实体,是CPU调度和分派的基本单位,他是比进程更小的能独立运行的基本单位,线程自己基本上不拥有系统资源。

线程与进程的关系
1、一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程(通常说的主线程)
2、资源分配给进程,同一进程的所有线程共享该进程的所有资源
3、线程在执行过程中,需要协作同步(生产者消费者)。不同进程的线程间要利用消息通信的办法实现同步。
4、CPU是分给线程,即真正在CPU上运行的是线程。
Image.png
进程作为拥有资源的基本单位,线程是作为CPU调度和分配的基本单位。
进程是拥有资源的独立单位,线程是不拥有系统资源,但是可以访问隶属于进程的资源。
Image.png

13.1.4 并行与并发

单核 cpu 下,线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows下时间片最小约为 15 毫秒,人能感知到的是0.1秒)分给不同的线程使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。总结为一句话就是: 微观串行,宏观并行 。
一般会将这种线程轮流使用 CPU 的做法称为并发, concurrent。
多核 cpu下,每个核(core) 都可以调度运行线程,这时候线程可以是并行的。
多个东西同时运行,并发执行
单任务操作系统->多任务操作系统->多用户多任务操作系统
多线程支持
好处:充分利用计算机的资源
坏处:线程过多,操作系统调度负担过重
Image.png
大部分时候是既有并行又有并发,虽然有多个核,但是来的线程数多。

13.1.5 同步和异步

需要等待结果返回,才能继续运行就是同步
不需要等待结果返回,就能继续运行就是异步
同步synchronized:A叫B去干活,A等着B干完之后才去干活
异步asynchronized:A叫B去干活之后,A还可以继续干自己活,B干完了要通知A

13.1.6 JUC

什么是JUC:
image.png
Java真的可以开启线程吗? 开不了的,底层是用native关键词修饰.调用本地操作系统
并发编程的本质: 充分利用CPU的资源
concurent:并发,高并发像秒杀一样,多个线程去访问同一个资源
并行:多个事情一路并行去做,比如说我正在泡方便面,一边用热水器去烧热水,一边并行的动作拆方便面的调料包。

13.2 Java中的多线程

创建对象的方式

  1. New
  2. Clone
  3. 反序列化
  4. 反射

多线程创建方式:

  1. 继承Thread类
  2. 实现Runnable接口

两者最大区别就是,Java里面是单继承的,继承Thread类方式将单继承这个位置给占了,只能去实现接口,不能再去继承别的类了,而实现Runnable接口这种方式不影响继承类也不影响实现其他接口。

  1. 实现Callable接口 JDK 1.8
  2. 线程池

13.2.1 继承Thread类

1)编写一个类继承Thread类

  1. 重写run方法


3)创建线程对象并执行start方法

public class MyThread extends Thread{
   @Override
   public void run() {
     for (int i = 0; i < 100; i++) {
        System.out.println(i);
     }
   }
}

public static void main(String[] args) {
   MyThread myThread = new MyThread();
   //这不是启动一个线程,这是调用对象里面一个普通方法run()方法
   //myThread.run();
   //真正启动一个线程调用start()方法,执行的代码就是run()方法里面代码
   myThread.start();
}

13.2.2 实现Runnable接口

1)编写一个类实现Runnable接口
2)重写run()方法
image.png
3)创建Runnable对象,并绑定到一个线程对象(Thread对象)上,并调用线程的start()方法
image.png

13.2.3实现Callable接口 JDK 1.8

image.png
JDK1.5之后才出现的。
普通的线程代码, 之前都是用的thread或者runnable接口
但是相比于callable来说,thread没有返回值,且效率没有callable高
**与Runnable区别: **
1、可以有返回值
2、可以抛出异常
3、方法不同,分别是call()和run()
提供了线程结束时的返回值,还可以抛出异常

实现类实现接口
image.png
image.png
步骤:
1)编写一个类,实现Callable接口
2)重写call()方法
创建Callable接口对象,交给FutureTask对象,再将FutureTask对象绑定给线程对象(Thread对象),并调用线程的start()方法

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

public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable();
        // 需要适配类和Thread建立关联
        FutureTask<String> futureTask = new FutureTask<>(myCallable);
        Thread thread = new Thread(futureTask,"CallableDemo");
        thread.start();
        String str = futureTask.get();
        System.out.println(str);
        System.out.println("CallableDemo.main");
    }

}

class MyCallable implements Callable {
    @Override
    public Object call() throws Exception {
        System.out.println("MyCallable.call");
        Thread.sleep(5000);
        return "abc";
    }
}

13.2.4 线程池(ThreadPoolExecutor)

image.png
为什么要使用线程池?
如果一个程序中,需要频繁的创建线程,销毁线程,因此会浪费大量的系统资源的时间

线程池的思想:重复利用

初始线程数:线程池创建时,包含的线程数量
最大线程数:当线程池中的想成不够用,创建新的线程,最多只允许线程池中拥有的最大线程数
最大空闲时间:线程池中线程闲置的时间,如果达到这个时间,就会考虑销毁线程
最小线程数:线程池中保留的最少的线程数量
最大等待时间:等待队列中的任务,等待的最长时间,如果还没有等到,就会超时,返回

线程池根据需求创建线程,可扩容,遇强则强线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
创建销毁线程是一个非常消耗性能的。
image.png

我们详细的解释一下为什么要使用线程池?
在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。

数据库连接池(原理和线程池是一样的):C3P0
传统JDBC的操作,每次创建和销毁连接都是非常消耗系统资源的两个过程,影响程序的运行效率!
连接管理: 为了解决性能问题,可以使用连接池优化的程序,来共享链接Connection
预先创建一组连接,放入到连接池中,用的时候每次取出一个; 用完后,放回;


image.png
image.png
Java里面线程池的顶级接口是 Executor,不过真正的线程池接口是ExecutorService, ExecutorService 的默认实现是 ThreadPoolExecutor;
普通类 Executors 里面调用的就是 ThreadPoolExecutor。Executors:线程池创建工厂类(生产什么的工厂)
Executors 类提供工厂方法用来创建不同类型的线程池。比如: newSingleThreadExecutor() 创建一个只有一个线程的线程池,newFixedThreadPool(int numOfThreads)来创建固定线程数的线程池,newCachedThreadPool()可以根据需要创建新的线程,但如果已有线程是空闲的会重用已有线程。

看一下各个接口的源码:
image.png
Executors 提供四种线程池:
1)newCachedThreadPool 线程池根据需求创建线程,可扩容,遇强则强(银行一共7个窗口,只开放了3个窗口,其他4个窗口没有开放,如果人很多就要开放其余的窗口,高峰结束再恢复到3个窗口状态)
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程.
特点:
1、线程池中数量没有固定,可达到最大值(Interger. MAX_VALUE)
2、线程池中的线程可进行缓存重复利用和回收(回收默认时间为 1 分钟)
3、当线程池中,没有可用线程,会重新创建一个线程
场景: 适用于创建一个可无限扩大的线程池,服务器负载压力较轻,执行时间较短,任务多的场景
2)newSingleThreadExecutor 一个任务一个任务执行,一池一线程(例如窗口只能服务一个人,后面来的只能等待)
创建是一个单线程池,也就是该线程池只有一个线程在工作,所有的任务是串行执行的,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它,
此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
特点: 线程池中最多执行 1 个线程,之后提交的线程活动将会排在队列中以此执行
场景: 适用于需要保证顺序执行各个任务,并且在任意时间点,不会同时有多个线程的场景
3)newFixedThreadPool (int) 一池N线程
创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小,线程池的大小一旦达到最大值就会保持不变。
特征:
1、线程池中的线程处于一定的量,可以很好的控制线程的并发量
2、线程可以重复被使用,在显示关闭之前,都将一直存在
3、超出一定量的线程被提交时候需在队列中等待
场景: 适用于可以预测线程数量的业务中,或者服务器负载较重,对线程数有严格限制的场景
** 4)newScheduledThreadPool (了解)**
创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。
场景: 适用于需要多个后台线程执行周期任务的场景

都是通过new ThreadPoolExecutor来构造线程池,线程池相关参数的概念:
image.png
1)corePoolSize:3 线程池的核心线程数(常驻线程数)
线程池的核心线程数(常驻线程数),一般情况下不管有没有任务都会一直在线程池中一直存活
2)maximumPoolSize: 7 线程池所能容纳的最大线程数
线程池所能容纳的最大线程数,当活动的线程数达到这个值后,后续的新任务将会被阻塞。
3)keepAliveTime:4 线程闲置时的超时时长
控制线程闲置时的超时时长,超过则终止该线程。一般情况下用于非核心线程
4)unit: 时间单位
用于指定 keepAliveTime 参数的时间单位,TimeUnit 是个 enum 枚举类型,常用的有:TimeUnit.HOURS(小时)、TimeUnit.MINUTES(分钟)、TimeUnit.SECONDS(秒) 和 TimeUnit.MILLISECONDS(毫秒)等。
5)workQueue:任务队列(阻塞队列)
当核心线程数达到最大时,新任务会放在任务队列中排队等待执行。
6)threadFactory:线程工厂
线程工厂,它是一个接口,用来为线程池创建新线程的。
7)RejectedExecutionHandler handler: 拒绝策略(银行有7个窗口,核心是3个窗口,所有窗口都开放,等待的座位也坐满了,银行再来新的顾客,银行没有能力接受新的顾客,银行就要做一个拒绝策略,建议去别的银行)

线程池的工作流程图:
image.png
image.png
1、在创建了线程池后,线程池中的线程数为零
2、当调用 execute()方法添加一个请求任务时,线程池会做出如下判断:
2.1 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
2.2 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
2.3 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
2.4 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
当提交的任务数大于(workQueue.size() +maximumPoolSize ),就会触发线程池的拒绝策略。
3、当一个线程完成任务时,它会从队列中取下一个任务来执行
4、当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
4.1 如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。
4.2 所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
image.png
实际项目开发中也是推荐使用手动创建线程池的方式,而不用默认方式,关于这点在《阿里巴巴开发规范》中是这样描述的:
image.png
OOM:out of memory 内存溢出

import java.util.concurrent.*;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        //一池5线程
        //ExecutorService threadPool = Executors.newFixedThreadPool(5);
        //一池1线程
        //ExecutorService threadPool = Executors.newSingleThreadExecutor();
        //可扩容的线程
        //ExecutorService threadPool = Executors.newCachedThreadPool();
        //自定义线程池创建
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                2L,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );

        //10个顾客请求
        try {
            for (int i = 1; i <=10; i++) {
                final int index = i;
                //线程池执行调用execute方法
                threadPool.execute(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println(Thread.currentThread().getName()+" 为第"+index+"个客户办理业务");
                    }
                });
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            //线程池的关闭调用shutdown方法
            threadPool.shutdown();
        }
    }
}

线程池的关闭
ThreadPoolExecutor 提供了两个方法,用于线程池的关闭,分别是 shutdown() 和 shutdownNow()。
shutdown()不会立即的终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。
shutdownNow()立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。

13.2.5 Lock 锁

image.png
Lock接口实现类
image.png
entrance:进入
public class ReentrantReadWriteLock implements ReadWriteLock

ReentrantLock构造器
image.png
true是公平锁,false是非公平锁
为什么默认是非公平锁,如果有两个线程一个3s一个3h,3s在3h后面,那样3s的虽然执行时间很短也要等3h才能执行。

Lock使用:
// 1、创建锁
private Lock lock = new ReentrantLock();
// 2、加锁
lock.lock();
try {
// 3、access the resource protected by the lock
// 业务代码
} finally {
// 4、解锁
lock.unlock();
}

1.案例一:卖票案例
//synchronized锁
public class SyncSaleTicket {
    public static void main(String[] args) {
        Ticket1 ticket1 = new Ticket1();
        //多个线程同时访问同一资源,把资源放入线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 40; i++) {
                    ticket1.sale();
                }
            }
        },"a").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 40; i++) {
                    ticket1.sale();
                }
            }
        },"b").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 40; i++) {
                    ticket1.sale();
                }
            }
        },"c").start();
    }
}

//资源类
class Ticket1 {
    private int ticketNum = 30;

    //卖票 synchronized 加锁
    public synchronized void sale() {
        if (this.ticketNum > 0) {
            System.out.println(Thread.currentThread().getName()
                    + "购得第" + ticketNum-- + "张票,剩余" + ticketNum+ "张票");
            try {
                //增加错误发生的机率
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
//Lock锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockSaleTicket {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        // 多个线程同时访问同一个资源,把资源放入线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }
        }, "a").start();

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

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


// 资源类
class Ticket {
    private int ticketNum = 30;
    //1.创建锁
    private Lock lock = new ReentrantLock();
    /*lock.lock();
    try {

    } finaliy {
        lock.unlock();
    }*/

    public void sale() {
        //2.加锁
        lock.lock();
        //3.access the resource protected by the lock
        //业务代码
        try {
            if (ticketNum > 0) {
                System.out.println(Thread.currentThread().getName()
                + "购得第 : " + ticketNum-- + "张票,还剩:" + ticketNum + "张票");
                // 让线程慢下来,增大出错的概率
                Thread.sleep(10);
            }
        } catch (InterruptedException e) {
                e.printStackTrace();
        } finally {
            //4.解锁
            lock.unlock();
        }
    }
}

}

synchronized和lock(ReentrantLock)锁的区别
1、synchronized内置的java关键字,Lock是一个java类
2、synchronized无法判断获取锁的状态, Lock可以判断是否获取到了锁 boolean isLocked()
3、synchronized会自动释放锁,Lock必须要手动释放锁!如果不是释放锁,会产生死锁
4、synchronized 线程1(获得锁),线程2(等待); Lock锁就不一定会等待下去,lock.tryLock()可以尝试去获取锁,不会一直等待,等不到就结束。
5、synchronized 和Lock锁都是 可重入锁,非公平的; Lock默认是非公平锁,可以设置为公平锁, synchronized不可以中断(interrupt和stop都不可中断)。
6、synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码

2.生产者消费者问题
案例:对一个数字不停进行 +1 -1操作,加完了减,减完了加
生产者消费者synchronized版本:

public class PCWithSynchronized {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        data.increment();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "Inc1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        data.increment();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "Inc2").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        data.decrement();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "Dec1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        data.decrement();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "Dec2").start();

    }
}
//判断等待-->业务代码-->通知
//数字:资源类
class Data{
    //属性  只有0和1两个值
    private int number = 0;
    //+1方法  0->1
    // number:1  Inc1 Inc2
    public synchronized  void increment() throws InterruptedException {
        while (number == 1){
            //等待
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName()+"-->"+number);
        //加完了通知其他线程
        this.notifyAll();
    }

    //-1   1->0
    public synchronized void decrement() throws InterruptedException {
        while (number == 0){
            //等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+"-->"+number);
        //减完了通知其他线程
        this.notifyAll();
    }
}

问题来了,现在只有"Inc"和"Dec"两个线程执行操作,如果增加多两个线程会怎样呢?

原因如下:
以两个加法线程Inc1、Inc2举例:
1、比如Inc1先执行,执行时调用了wait方法,那它会等待,此时会释放锁。
那么线程Inc2获得锁并且也会执行wait()方法,且释放锁,同时两个加线程一起进入等待状态,等待被唤醒。
2、此时减线程中的某一个线程执行完毕并且唤醒了这俩加线程(notifyAll),那么这俩加线程不会一起执行,其中Inc1获取了锁并且加1,执行完毕之后Inc2再执行。
如果是if的话,那么Inc1修改完num后,Inc2不会再去判断num的值,直接会给num+1,如果是while的话,Inc1执行完之后,Inc2还会去判断num的值,因此就不会执行。
上述情况称为:虚假唤醒

问题解决:将 if 改为 while
形象的例子:当一个条件满足时,很多线程都被唤醒了,但是只有其中部分是有用的唤醒,其它的唤醒都是无用功
比如说买货,如果商品本来没有货物,突然进了一件商品,这是所有的线程都被唤醒了,但是只能一个人买,所以其他人都是假唤醒,获取不到对象的锁
使用Lock和Condition来实现生产者消费者问题:

Lock替代synchronized,Condition替代Object里面的wait和notify/notifyAll

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

public class PCWithLock {
    public static void main(String[] args) {
        DataLock data = new DataLock();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        data.increment();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "Inc1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        data.increment();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "Inc2").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        data.decrement();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "Dec1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        data.decrement();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "Dec2").start();

    }
}

class DataLock{
    //属性
    private int number = 0;

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    //+1方法
    public void increment() throws InterruptedException {
        lock.lock();
        try {
            while (number == 1){
                // 等待
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "--->" + number);
            // 加完之后通知其他线程
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    //-1
    public void decrement() throws InterruptedException {
        lock.lock();
        try {
            while (number == 0){
                // 等待
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + "--->" + number);
            // 减完之后通知其他线程
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
}

3.读写锁 ReadWriteLock

ReadWriteLock 接口
所有已知实现类: ReentrantReadWriteLock
1、读:可多条线程同时获取数据
2、写:只能单条线程写入

独占锁(写锁) 一次只能被一个线程占用
共享锁(读锁) 多个线程可以同时占用
ReadWriteLock:
读-读 可以共存
读-写 不能共存
写-写 不能共存

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

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        //未上锁:
        // MyCache myCache = new MyCache();
        //myCache.write("cn", "中国");
        //myCache.write("us", "美国");
        //myCache.read("cn");
        //上了读写锁:
        MyCacheWithLock myCache = new MyCacheWithLock();
        //写入:
        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            // new Thread(new , i + "").start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    myCache.write(temp + "", temp + "");
                }
            }, i + "").start();

        }

        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    myCache.read(temp + "");
                }
            }, String.valueOf(i)).start();
        }
    }
}

class MyCacheWithLock {
    private volatile Map<String, Object> map = new HashMap<>();

    //读写锁:对数据更精准控制
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    //写数据:只希望有一个线程在执行
    public void write(String key, Object value) {
        Lock lock = readWriteLock.writeLock();
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "写入" + key);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写入完成!");
        } finally {
            lock.unlock();
        }
    }

    //读数据:可一条或者多条同时执行
    public void read(String key) {
        // Lock lock = readWriteLock.readLock();
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "读取数据:" + key);
            Object o = map.get(key);
            System.out.println(Thread.currentThread().getName() + "读取数据完成-->" + o);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
}

/**
 * 未上锁:
 * 写入线程会被 读取线程中断,造成脏读,对数据不安全
 */
class MyCache {
    private volatile Map<String, Object> map = new HashMap<>();

    //写数据:
    public synchronized void  write(String key, Object value) {
        System.out.println(Thread.currentThread().getName() + "线程写入" + key);
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "线程写入完成!");
    }

    //读数据:
    public synchronized Object read(String key) {
        System.out.println(Thread.currentThread().getName() + "线程读取数据:" + key);
        Object o = map.get(key);
        System.out.println(Thread.currentThread().getName() + "线程读取数据完成-->" + o);
        return o;
    }
}

Image.png
多个写的时候出现问题

4.Volatile

Volatile是Java虚拟机提供的轻量级同步机制(在单线程是用不到的)
1 保证可见性
2 不保证原子性
3 禁止指令重排

保证可见性:
可见性就是当一个线程 修改一个共享变量时,另外一个线程能读到这个修改的值。
image.png

不保证原子性:
什么叫原子性: 不可分割
线程A在执行任务的时候,是不能被打扰的,要么同时成功,要么同时失败
不保证原子性是指:
private volatile static int num=0;
public static void add() {
num++;
}
num++操作不是原子性(++不是原子操作),可能多个线程同时操作
在Java中自增i++操作不是原子操作,实际上包含三个独立的操作:(1)读取i值;(2)加1;(3)将新值写回i
因此,如果并发执行自增操作,可能导致计算结果的不准确
解决方法:我们可以加synchronized
public synchronized static void add() {
num++;
}

禁止指令重排序
指令重排是指在程序执行过程中,为了提高性能, 编译器和CPU可能会对指令进行重新排序。
单例模式

5.理解CAS

CAS: 全称Compare and swap,字面意思:”比较并交换“
CAS 是现代操作系统,解决并发问题的一个重要手段

CAS操作包括了3个操作数:
1、需要读写的内存位置(V)
2、进行比较的预期值(A)
3、拟写入的新值(B)
CAS操作逻辑如下:如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。
许多CAS的操作是自旋的:如果操作不成功,会一直重试,直到操作成功为止。
这里引出一个新的问题,既然CAS包含了Compare和Swap两个操作,它又如何保证原子性呢?
答案是:CAS是由CPU支持的原子操作,其原子性是在硬件层面进行保证的。

为什么需要AtomicInteger原子操作类?
Java中自增i++操作不是原子操作,实际上包含三个独立的操作:(1)读取i值;(2)加1;(3)将新值写回i
因此,如果并发执行自增操作,可能导致计算结果的不准确
对于Java中的运算操作,例如自增或自减,若没有进行额外的同步操作,在多线程环境下就是线程不安全的。
明显,这个操作不具备原子性,多线程并发共享这个变量时必然会出现问题
J.U.C 并发包提供了:
1、AtomicBoolean
2、AtomicInteger
3、AtomicLong

AtomicInteger i = new AtomicInteger(0);
// 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
System.out.println(i.getAndIncrement());
// 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
System.out.println(i.incrementAndGet());

import org.junit.Test;

import java.util.concurrent.atomic.AtomicInteger;

public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);
        //如果是2020就改成2021
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());
        //如果和期望的值相同,就更新这个值,否则就不更新
        System.out.println(atomicInteger.compareAndSet(2020, 2022));
        System.out.println(atomicInteger.get());
    }

    @Test
    public void test1() {
        int num = 0;
        num++;
        // CAS
        AtomicInteger i = new AtomicInteger(0);
        int i1 = i.getAndIncrement();// ++i  i++
        System.out.println(i1);
    }
}

总结:CAS:比较当前工作内存(线程)中的值和主内存中的值,如果这个值是期望的,那么则执行操作,如果不是,就一直循环
优点:好处是不用切换线程状态,因为切换线程状态性能消耗比较大
缺点:
1:由于底层是自旋锁,循环会浪费时间
2:因为是底层的cpu操作,一次只能保证一个共享变量的原子性

13.2.6 总结各种锁

各种锁的理解:

1.公平锁、非公平锁

公平锁:不能插队,先来后到
非公平锁:可以插队,比如有些线程执行时间很短,但是也需要等待拿到锁的线程执行完(3h,3s),synchronized和lock默认都是非公平锁
Lock lock=new ReentrantLock();
对应的构造方法
public ReentrantLock() {
sync = new NonfairSync();
}
当给构造函数传参为true的时候,是公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

2.可重入锁

可重入锁,也叫做递归锁,是指在一个线程中可以多次获取同一把锁
比如:一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法【即可重入】,而无需重新获得锁;ReentrantLock 和synchronized 都是 可重入锁

class Phone {
 
    public synchronized void sms(){
        System.out.println(Thread.currentThread().getName() + "sms");
        call(); // 这里也有锁
    }
 
    public synchronized void call(){
        System.out.println(Thread.currentThread().getName() + "call");
    }
}

3.自旋锁

不断地尝试,直到成功为止
image.png

4.死锁

死锁是一个互相争抢的过程,互相拥有对方需要的资源又不释放
image.png
**死锁的发生必须具备以下四个必要条件。 **
1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
银行家算法
只要打破四个必要条件之一就能有效预防死锁的发生

5.乐观锁和悲观锁

乐观锁和悲观锁是两种思想,用于解决并发场景下的数据竞争问题。
乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展。
这两种人各有优缺点,不能不以场景而说一种人好于另外一种人。
1)悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
2)乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。
乐观锁适用于多读的应用类型,这样可以提高吞吐量。
在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
两种锁的使用场景
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种
乐观锁适用于比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
乐观锁常见的两种实现方式
1、版本号机制
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
image.png
2、CAS算法
即compare and swap(比较与交换),是一种有名的无锁算法
一般情况下是一个自旋操作,即不断的重试。
synchronized
Lock:1、ReentrantLock 2、ReentrantReadWriteLock.ReadLock 3、ReentrantReadWriteLock.WriteLock
ReadWriteLock->ReentrantReadWriteLock
Cas
volatile

Condition

13.3 线程的状态

创建线程->死亡(终止)
1)新建(new):当一个线程对象创建,还没有调用start方法前
2)就绪/可运行( runnable ):准备好运行,尚未取得CPU的使用权
线程对象创建后,其他线程(比如 main 线程)调用了该对象 的 start ()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获 取 cpu 的使用权 。
3)运行:线程获取CPU使用权,再CPU执行代码
可运行状态( runnable )的线程获得了 cpu 时间片( timeslice ) ,执行程序代码。
4)阻塞( block ):阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice ,暂时停止运行。直到线程进入可运行( runnable )状态,才有 机会再次获得 cpu timeslice 转到运行( running )状态。阻塞的情况分三种:
Blocked(堵塞)状态:
同步锁;调用了sleep()和join()方法进入Sleeping状态;执行wait()方法进入Waiting状态,等待其他线程notify通知唤醒);
(一). 等待阻塞:运行( running )的线程执行 o . wait ()方法, JVM 会把该线程放 入等待队列( waitting queue )中。
(二). 同步阻塞:运行( running )的线程在获取对象的同步锁时(synchronized),若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池( lock pool )中。
(三). 其他阻塞: 运行( running )的线程执行 Thread . sleep ( long ms )或 t . join ()方法,或者发出了 I / O 请求时, JVM 会把该线程置为阻塞状态。当 sleep ()状态超时、 join ()等待线程终止或者超时、或者 I / O 处理完毕时,线程重新转入可运行( runnable )状态。
5)终止/死亡( dead ):线程 run ()、 main () 方法执行结束,或者因异常退出了 run ()方法,则该线程结束生命周期。死亡的线程不可再次复生。
线程状态转换

1294022-20171206070245644-1496567738.png
Image.png

13.4 线程的优先级

线程执行时,优先分配到CPU等级
优先级越高,越容易被分配到CPU
线程的优先级分为1-10,默认优先级为5
image.png
只有多个线程同时运行在同一块CPU核心上时,优先级才会起作用
设置优先级的方法

public static void main(String[] args) {
    Thread thread1 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("线程1:" + i);
            }
        }
    });
    
    Thread thread2 = new Thread(new Runnable() {  
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("线程2:" + i);
            }	
        }
    });
    
    thread1.setPriority(Thread.MAX_PRIORITY);
    thread2.setPriority(Thread.MIN_PRIORITY);
    
    thread2.start();
    thread1.start();
}

13.5 Thread类的常用方法

GUI应用程序
** **几乎所有的GUI应用程序都会用多线程。使用word来编辑一个非常大的文件,一个线程用来编辑,另一个线程用来做搜索。
多个客户端
基本上网络上的服务器必须同时处理一个以上的客户端,不过,一定要在服务器这边的程序设计加入一个以上客户端的概念的话,程序会变得更复杂。此时,不妨准备一个当有客户端连接到服务器的时候,会自动出来迎接这个客户点的线程,这样一来,服务器的程序就可以设计成好像只服务一个客户端
Image.png
currentThread() 返回对当前正在执行的线程对象的引用。
getId() 返回该线程的标识符。
getName() 返回该线程的名称。
getPriority() 返回线程的优先级。

public static void main(String[] args) {
    Thread thread1 = new Thread(new Runnable() {        
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("线程1:" + i);
            }
        }
    });
    
    Thread thread2 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("线程2:" + i);
            }	
        }
    });
    
    thread1.setPriority(Thread.MAX_PRIORITY);
    thread2.setPriority(Thread.MIN_PRIORITY);
    
    thread2.start();
    thread1.start();
}

join() 等待该线程终止。
在某些情况下,如果子线程里要进行大量的耗时的运算,主线程可能会在子线程执行完之前结束,但是如果主线程又需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()。
阻塞所在线程,等调用它的线程执行完毕,再向下执行
a.join,在API中的解释是,在B线程中调用a.join(),堵塞当前线程B,直到A执行完毕并死掉,再执行B。
线程实例的方法join()方法可以使得一个线程在另一个线程结束后再执行。如果join()方法在一个线程实例上调用,当前运行着的线程将阻塞直到这个线程实例完成了执行
//Waits for this thread to die.
**public final void join() throws InterruptedException
在join()方法内设定超时,使得join()方法的影响在特定超时后无效。当超时时,主方法和任务线程申请运行的时候是平等的。然而,当涉及sleep时,join()方法依靠操作系统计时,所以你不应该假定join()方法将会等待你指定的时间。
线程实例的方法join()方法可以使得一个线程在另一个线程结束后再执行。如果join()方法在一个线程实例上调用,当前运行着的线程将阻塞直到这个线程实例完成了执行
像sleep,join通过抛出InterruptedException对中断做出回应。
sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
sleep方法的调用被放在try-catch里面,这是因为sleep方法可能会抛出一个称为InterruptedException的异常。InterruptedException是用来取消线程处理时的异常。
“半路唤醒”被Thread sleep暂停的线程,则可利用interrupt方法

让线程暂时停止可以选择sleep方法。比如Thread.sleep(1000),当前线程睡眠1秒。需要知道的是,1秒后,线程是
回到可执行状态,并不是执行状态,什么时候执行那是由虚拟机来决定的。**所以sleep(1000)并不是在睡眠1秒后立即执行。

//join(),sleep()方法示例
public class JoinExample {
   public static void main(String[] args) throws InterruptedException {
      Thread t1 = new Thread(new Runnable() {
         public void run() {
            System.out.println("First task started");
            System.out.println("Sleeping for 2 seconds");
            try {
               Thread.sleep(2000);
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
            System.out.println("First task completed");
         }
      });
      Thread t2 = new Thread(new Runnable() {
         public void run() {
            System.out.println("Second task completed");
         }
      });
      t1.start(); 
      t1.join(); Main线程等待t1线程执行完毕,在执行下面的操作
      t2.start();
   }
}

Output:
First task started
Sleeping for 2 seconds
First task completed
Second task completed

start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。

public static void main(String[] args) {
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName()+":" + i);
                if (i == 50) {
                    try {
                        //是当前正在执行的线程以指定的毫秒数暂停休眠(暂时停止执行)
                        //具体取决于系统定时器和调度程序的精度和准确性
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    });
    thread.setName("子线程");
    thread.start();
    for (int i = 0; i < 100; i++) {
        if (i == 10) {
            try {
                //等待thread线程执行结束后,当前线程再继续执行
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("主线程:" + i);
    }
}

yield()
解释它之前,先简述下,多线程的执行流程:多个线程并发请求执行时,由cpu决定优先执行哪一个,即使通过thread.setPriority(),设置了线程的优先级,也不一定就是每次都先执行它
Thread.yield();,表示暂停当前线程,执行其他线程(包括执行yield这个线程), 执行谁由cpu决定
yield这个方法是让当前线程回到可执行状态,以便让具有相同优先级的线程进入执行状态**(包括这个执行yield的线程,因为其也在可执行状态)
public static native void yield();
- Yield是一个静态的原生(native)方法
- Yield告诉当前正在执行的线程把运行机会交给线程池中拥有相同优先级的线程。
- Yield不能保证使得当前
正在运行的线程迅速转换到可运行的状态**
- 它仅能使一个线程从运行状态转到可运行状态,而不是等待或阻塞状态

13.6 线程同步

有一个电影院,100个座位
两个窗口,同时买票

线程并发执行时,访问同一资源可能产生冲突
线程同步,就是为了解决资源访问冲突
资源的独占
解决资源冲突的方法
同步代码块
同步方法

13.6.1 同步代码块

将访问资源代码放到一个块中,需要获取到锁才能执行,如果获取不到,只能等到
窗口类方法

public class Window extends Thread {
    //座位号是1-100
    static int num = 1;
    //lock:锁
    static Object lock = new Object();
    @Override
    public void run() {
        while (num <=100) {
            //1.尝试获取lock对象的锁
            //2.如果能够获取,进入后面的代码块
            //3.当代码块执行完成后,lock对象的锁就会被释放
            //4.如果获取不到lock对象的锁,就要等待,直到lock对象的锁被释放
            //5.重复上述操作
            /*
			 * 注意:同步代码块必须是引用类型,不能是基本类型
			 * 包装类也不行,在自增是会创建新的对象
			 * 同步时,必须要求锁的是同一个对象
			 */
            synchronized(lock) {
                if (num <= 100) {
                    System.out.println(Thread.currentThread().getName() + "卖出一张票,座号是:" + num);
                    num++;
                }

            }

        }
    }
//测试类
public static void main(String[] args) {
    Window win1 = new Window();
    win1.setName("窗口一");

    Window win2 = new Window();
    win2.setName("窗口二");

    //启动两个窗口
    win1.start();
    win2.start();
}

13.6.2 同步方法

同步方法在多线程中被调用时,会先尝试获取执行当前方法的对象的锁,能过获取到锁,则执行方法,否则就等待

无论是同步方法还是同步代码块,必须保证的是锁的是同一个对象

//票类
public class Ticket {
	int num = 1;
	//卖
	synchronized public boolean sell() {
		if (num <= 100) {
			System.out.println(Thread.currentThread().getName() + "卖出一张票,座号是:" + num);
			num++;
			return true;
		} else {
			return false;
		}
	}
}
//窗口类
public class Window extends Thread {
	static Ticket ticket = new Ticket();
	
	@Override
	public void run() {
		while(ticket.sell()) {
		}
	}
}
//测试类
public class Demo01 {
	public static void main(String[] args) {
		Window win1 = new Window();
		win1.setName("窗口一");
		
		Window win2 = new Window();
		win2.setName("窗口一");
		
		//启动两个窗口
		win1.start();
		win2.start();
	}
}

1.Synchronized实例方法和Synchronized阻挡

假设现在有一个类型如下的synchronized实例方法
synchronized void method(){

}
在功能上和下面以synchronized阻挡为主的方法有异曲同工之妙。
void method(){
synchronized(this){

}
}
换句话说synchronized方法是使用
this锁
去做线程的共享互斥。
this 指的是当前对象实例本身,所以,所有使用 synchronized(this) 方式的方法都共享同一把锁。

2.类方法和Synchronized阻挡

假设现在有一个类型如下的synchronized的类方法,synchronized类方法有限制同时只能让一个线程执行。这部分和synchronized实例方法一样,但是两者是有不同的。
Class Something{
static synchronized void method(){

}
static synchronized void method2() {
}
}
在功能上和下面以synchronized阻挡为主的方法有异曲同工之妙。
Class Something{
static void method(){
synchronized(Something.class){

}
}

}
换句话说synchronized的类方法是使用该类的
类对象
的锁去做线程的共享互斥。Something.class是对应
Something类的java.lang.Class类的实例。

public class Something {
    private Object lock1 = new Object();
    private Object lock2 = new Object();
    private Object lock3 = new Object();

    public void lockMethod1() {
        synchronized (lock1) {
        }
    }

    public void lockMethod2() {
        synchronized (lock1) {
        }
    }

    public void lockMethod3() {
        synchronized (lock2) {
        }
    }

    // this
    public synchronized void method1() {
    }

    public void method2() {
        synchronized (this) {
        }
        // ...
    }

    // Something.class
    public static synchronized void staticMethod1() {
    }

    public static synchronized void staticMethod2() {
        synchronized (Something.class) {
        }
    }
}

这里synchronized关键字拿到的锁是对象object的锁,所有需要这个对象的锁的方法都不能同时执行。这是最常用的高并发场景下要锁住某个方法所用的操作。锁住同一个变量的方法块共享同一把锁。

13.6.3 哲学家就餐问题

image.png
死锁:
线程的死锁:
1)并发
2)资源共享
3)资源独占
4)环路等待

避免死锁:破坏环路等待
规定好资源访问的顺序

13.6.4 生产者和消费者问题

1.问题一

生产者:生产问题
仓库:保存产品,容量
仓库满了,停止生产
仓库空了,停止消费
消费者:消耗产品

1)wait,notify,notifyAll操作的对象,必须是同步对象
2)Wait方法让当前线程暂停
3)Notify方法,再wait暂停的线程中找一个执行
4)notifyAll方法,让wait暂停的所有线程全部都唤醒

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

public class Demo01 {
	public static void main(String[] args) {
		//仓库
		ArrayList<String> store = new ArrayList<String>();
		//生产者
		Thread producer = new Thread(new Runnable() {
			@Override
			public void run() {
				while(true) {
					//生产商品			//随机生成字符串
					String product = UUID.randomUUID().toString();
					System.out.println("生产者生产:" + product);
					//放到仓库
					synchronized (store) {
						store.add(product);
						//产品生产好了,消费者可以消费了
						store.notify();
						if(store.size() >= 10) {
							//仓库满了,停止生产
							System.out.println("仓库满了,停止生产");
							try {
								store.wait();
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
						}
					}
				}
			}
		});
		//消费者
		Thread customer = new Thread(new Runnable() {
			@Override
			public void run() {
				while(true) {
					//从仓库中取出一个产品
					String product = null;
					synchronized (store) {
						if(store.size() == 0) {
							//仓库已经空了
							try {
								//让仓库等待
								System.out.println("仓库空了,停止消费");
								store.wait();
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
						}
						product = store.remove(0);
						//仓库空了,通知生产者生产
						store.notify();
					}
					System.out.println("消费者消费:" + product);
				}
			}
		});
		producer.start();
		customer.start();
	}
}

2.问题二

生产者和消费者问题:

名字说明
Cake蛋糕类
Panzi蛋糕的队列
ProducerThread生产蛋糕的线程
ConsumerThread消费蛋糕的线程
Main测试类

Sequence序列图:
image.png
对于同步和异步的控制都是在Panzi这个类里面进行的,是最核心的类,生产者和消费者只是不停的
生产蛋糕和吃蛋糕,不需要考虑线程的同步和异步,同步和异步的操作都是在Panzi这个类里面完成。
调用wait和notify/notifyAll都必须要加上synchronized。
notify和notifyAll都可以完成通知,notify控制比较复杂,我们直接使用notifyAll就可以。
什么时候使用wait和notifyAll:
这两个是成对出现的,我们在等待的是生产的Cake,没有Cake就wait,生产者生产了Cake之后就调用notifyAll。

image.png

//蛋糕类
public class Cake {
    //蛋糕的编号
    private String name;

    public Cake() {
    }

    public Cake(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

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

    @Override
    public String toString() {
        return "Cake{" +
                "name='" + name + '\'' +
                '}';
    }
}
//盘子类--资源和心类
import java.util.LinkedList;

/* 资源类(核心类),
 * 生产者线程ProducerThread生产了蛋糕调用putCake放到盘子里,
 * 消费者线程ConsumerThread吃蛋糕就调用getCake从盘子里拿蛋糕
*/
public class Panzi {
    //使用LinkedList来模拟队列的操作,队列尾部添加,队列头部删除
    private LinkedList<Cake> list = new LinkedList<>();

    /*
     * 生产了蛋糕放到盘子里面
     */
    public synchronized void putCake(Cake cake) {
        //限制盘子大小
        if (list.size() >= 2) {
            try {
                System.out.println("生产者线程 putCake wait");
                this.wait();//盘子里的蛋糕满了,需要等待消费者吃蛋糕
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //在队列尾部添加蛋糕
        list.addLast(cake);
        notifyAll();//生产了蛋糕之后要通知正在wait的ConsumerThread这些消费者
        System.out.println("生产者线程 putCake notifyAll");
    }

    /*
     * 从盘子里面拿出蛋糕吃
     */
    public synchronized Cake getCake() {
        if (list.size() <= 0) {//盘子里面没有蛋糕
            try {
                System.out.println("消费者线程 getCake  wait");
                wait();//盘子里面没有蛋糕,需要等待生产者生产蛋糕
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //队列头部删除
        Cake cake = list.removeFirst();
        return cake;
    }
}
//生产者类
import java.util.Random;

public class ProducerThread extends Thread{
    private Panzi panzi;

    public ProducerThread(String name, Panzi panzi) {
        super(name);
        this.panzi = panzi;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 1000; i++) {
            Cake cake = new Cake("no:" + i);
            panzi.putCake(cake);
            System.out.println(Thread.currentThread().getName() + " putCake: " + cake);

            // 等待的时间是随机的,模拟生产蛋糕时间
            try {
                Thread.sleep(new Random().nextInt(5000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
//消费者类
import java.util.Random;

public class ConsumerThread extends Thread{
    private Panzi panzi;

    public ConsumerThread(String name, Panzi panzi) {
        super(name);
        this.panzi = panzi;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 1000; i++) {
            Cake cake = panzi.getCake();
            System.out.println(Thread.currentThread().getName() + " getCake: " + cake);

            // 等待的时间是随机的,模拟吃蛋糕时间
            try {
                Thread.sleep(new Random().nextInt(5000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
//测试类
public class Main {
    public static void main(String[] args) {
        //生产者和消费者操作的是同一个盘子
        Panzi panzi = new Panzi();
        //启动生产者线程去生产蛋糕
        ProducerThread producerThread = new ProducerThread("生产者线程", panzi);
        producerThread.start();
        //启动消费者线程去吃蛋糕
        ConsumerThread consumerThread = new ConsumerThread("消费者线程", panzi);
        consumerThread.start();
    }
}

生产者线程 putCake notifyAll
生产者线程 putCake: Cake{name=‘no:1’}
消费者线程 getCake: Cake{name=‘no:1’}
消费者线程 getCake wait
生产者线程 putCake notifyAll
生产者线程 putCake: Cake{name=‘no:2’}
消费者线程 getCake: Cake{name=‘no:2’}
生产者线程 putCake notifyAll
生产者线程 putCake: Cake{name=‘no:3’}
生产者线程 putCake notifyAll
生产者线程 putCake: Cake{name=‘no:4’}
生产者线程 putCake wait
消费者线程 getCake: Cake{name=‘no:3’}
消费者线程 getCake: Cake{name=‘no:4’}
消费者线程 getCake wait

13.7多线程的编程步骤

1、第一步:创建资源类,在资源类创建属性和操作方法(在这类Panzi就是资源类)
2、第二步:在资源类中操作方法 (Panzi里面有getCake()方法)
1、判断
2、业务代码(干活)
3、通知
3、第三步:创建多个线程,调用资源类的操作方法。 (生产者线程和消费者线程生产了蛋糕放到盘子panzi.putCake(),吃蛋糕panzi.getCake())

13.8 如何正确的停止线程

java中有三种停止线程方法
1)使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
2)使用stop方法方法强行终止线程,但是不推荐使用这个方法,应为stop不安全而且已经被废弃的方法,还有suspend和resume都是废弃的方法。
3)使用interrupt方法中断线程。
interrupt()方法 仅仅使线程中打了一个停止的标记,并不是真的停止线程。
this.interrupted() 测试当前线程是否已经中断。
this.isInterrupted()测试线程是否已经中断。

中断线程
线程的thread.interrupt()方法是中断线程,将会设置该线程的中断状态位,即设置为true,之后的结果:线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身,并不是一定中断这个线程。线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)。
image.png
// 下载
boolean keepRunning = true;
// run方法执行结束,线程也就结束
public void run() {
while((lenght = input.read(buffer)) != -1 && keepRunning) {
}
}

13.9 Object类中的几个与线程相关的方法

wait,notify,notifyAll是Object类的方法
obj.wait()是把现在的线程放到obj的wait set
obj.notify()是从obj的wait set里唤醒一个线程
obj.notifyAll()是唤醒所有在obj的wait set里的线程
换句话说,把wait、notify、notifyAll三者均解释为对实例对象的wait set的操作,会比说他们是对线程的操作更贴切,由于所有实例都会有wait set,所以wait、notify、notifyAll才会是Object类的方法。
虽然三者不是Thread类固有的方法,不过,因为Object类是Java所有类的祖先类,所以wait、notify、notifyAll也是Thread的方法

image.png
每个实例都有个线程的休息室wait set。
Wait方法——把线程放入wait set
使用wait方法时,线程便进入wait set,假设现在已经执行如下语句:obj.wait();
则目前的线程停止执行,进入实例obj的的wait set.这个操作成为:线程在obj上wait().(这个obj不是线程而是对象)
如果实例方法还有如下的语句时:wait();
则其意义同:this.wait();
故执行wait的线程就会进入this的wait set.此时就变成了在this上wait.
如欲执行wait()方法,线程需获取锁定(这是规则)。但是当线程进入wait set时,已经释放了该实例的锁定。
image.png
image.png
Notify方法——从wait set拿出线程
使用notify()(通知)方法时,可以从wait set拿出一个线程。
obj.notify();(这个obj也是对象,不是线程)则从wait set里的线程中挑出一个,唤醒这个线程。被唤醒的线程便退wait set
image.png
image.png
线程A想紧接着wait之后的代码执行,但是现在线程B拿到了锁。
image.png
Notify后的线程
被notify唤醒的线程不是在notify后立即执行,因为在notify的那一刻,执行notify 的线程还握着锁定不放,所以其他线程无法获取该实例的锁定。

Notify如何选择线程
假设执行notify方法时,wait set里面正在执行的线程不止一个。规格并没有注明此时该选择哪一个线程。究竟是选择等待线程里面的第一个,随机选择或是另以其他方式选择,则以java处理系统而异。
notifyAll()方法——从wait set 拿出所有线程
使用notifyAll(通知全体)方法时,会将所有在waitset苦等的线程都拿出来。
obj.notifyAll()则会唤醒所有留在实例obj的wait set里的线程。
而notifyAll();则其意义同this.notifyAll();故这个语句所在方法的实例(this)的wait set里的线程会全部放出来。
跟wait方法和notify方法一样,线程必须要获取要调用实例的锁定,才能调用notifyAll方法。
被唤醒的线程便开始去获取刚才wait时释放掉的锁定,那么现在这个锁定现在是在谁的手中呢?没错,锁定就是在刚才执行notifyAll方法的程序手里,因此即使所有线程都退出了wait set,但他们仍然在去获得锁定的状态下,还是有阻挡。要等到刚才执行notifyAll方法的线程释放出锁定后,其中一名幸运儿才会实际执行。
要是没有锁定呢
若没有锁定的线程去调用wait,notify或notifyAll时,便会抛出异常java.lang.IllegalMonitorStateException.
调用notify方法还是notifyAll方法
Notify方法和notifyAll方法两者非常相似,到底该用哪一个?老实说,这个选择有点难。选择notify的话,因为要唤醒的线程比较少,程序处理速度当然要比notifyAll略胜一筹。但是选择notify时,若这部分程序处理的不好,可能会有程序挂掉的危险性,一般说来,选择notifyAll所写出来的程序代码要比选择notify可靠。除非你能确定程序员对程序代码的意义和能力限度一清二楚,否则选择notifyAll应该比较稳扎稳打

13.10 面试题

1、java中有几种方法可以实现一个线程?用什么关键字修饰同步方法?
答:显示一个线程有4种方式:
1、继承Thread类
2、实现Runnable接口
3、实现Callable接口,重写call方法(有返回值)
4、线程池
用synchronized关键字修饰同步方法

2、sleep() 和 wait() 有什么区别?
答:sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。

3、同步和异步有何异同,在什么情况下分别使用他们?举例说明。
答:如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。
当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。

4、启动一个线程是用run()还是start()?
答:启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。这并不意味着线程就会立即运行。

5、当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它synchronized方法?
**答:**不能,一个对象的一个synchronized方法只能由一个线程访问。

6、请说出你所知道的线程同步的方法。
答:wait():使一个线程处于等待状态,并且释放所持有的对象的lock。
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
notityAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。

8、简述synchronized和java.util.concurrent.locks.Lock的异同 ?
答:主要相同点:Lock能完成synchronized所实现的所有功能主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。
synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。

9.1 什么是 Executor 框架?
Executor框架在Java 5中被引入,Executor 框架是一个根据一组执行策略调用、调度、执行和控制的异步任务的框架。
无限制的创建线程会引起应用程序内存溢出,所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用 Executor 框架可以非常方便的创建一个线程池。
9.2 Executors 类是什么?
Executors为Executor、ExecutorService、ScheduledExecutorService、ThreadFactory 和 Callable 类提供了一些工具方法。Executors 可以用于方便的创建线程池。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值