• JavaSE高级-多线程


    一、概念:
    1.进程:
    在系统中独立运行的程序,直接分配占用系统的CPU、磁盘、网络等资源。
    进程也是程序执行的一次过程,是系统运行程序的基本单位。
    进程中至少要有一个主线程。
    进程类比“车间”
    2.线程:
    线程是进程中的一个执行单元
    线程会分配及占用进程的磁盘,CPU的使用权等。
    一个进程可以包含多个线程,这就是多线程,线程也支持并发性。
    线程类比“车间工人”, 主线程类比“车间主任”。
    二、多线程的好处:
    为了保证程序的并发性,提高程序的执行效率,可以让进程有更多机会得到CPU。
    创建一个线程的开销比一个进程的开销小的多。
    多线程可以解决很多业务模型
    大型高并发技术的核心技术
    三、多线程的弊端:
    若无限制的开辟多条线程,势必会在成程序卡顿,维护困难
    引发线程安全的问题。

    串行:一个一个的去完成的事情。
    并行: 指两个或多个事件在"同一时刻"发生。(同时执行)。
    并发:指两个或多个事件在"同一时间段内发生"(交替执行)。

    主线程:
    程序运行时,至少会有一条主线程。
    程序正常时,至少会一条主线程,会按照程序的执行顺序依次执行。
    程序出现运行期异常:除了主线程,还会有一条线程来控制异常信息的输出。此时,发生了多线程执行情况,抢占“时间片”的使用权,执行出现的结果产生了随机性。

    Thread 线程类:
    创建线程的几种方式:
    创建线程的方式一 --- 继承extends Thread
    自定义一个类继承 Thread 类
    重写run方法,run() 定义的就是线程需要执行的任务。
    创建线程对象
    真正启动线程,调用线程的start(),底层其实是给CPU注册当前线程,并触发run()方法执行。进入可运行状态,开始争抢时间片,一旦争抢到时间片,就会进入到运行状态,执行run()中的任务。
    优点:代码很简洁
    缺点:Java单继承,一旦继承了一个类,想要再拓展再继承别的类是不被允许的。
    创建线程的方式二 -- 实现 implements Runnable 接口
    自定义一个类实现Runnable接口
    重写run()方法,run()定义执行的任务
    创建一个任务对象Runnable
    创建一个线程对象,将任务对象包装进去。
    启动线程对象。start()

    优点:
    拓展性好,实现Runnable 接口,还可以再实现接口,再继承类
    同一个线程任务对象可以被包装成多个线程对象。
    适合多个线程去共享同一个资源
    实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立。
    线程池可以放入实现Runnable和Callable线程任务对象。
    缺点:
    相对于继承Thread 麻烦

    创建线程的方式三 --- 实现 implements Callable 接口
    java.util.concurrent 简称 JUC
    Callable 提供一个方法 V call() throws Exception
    返回一个结果,此时结果可能是执行任务返回的值,也有可能是抛出的异常

    执行步骤:
    定义一个类实现Callable接口
    重写Callable方法,返回值与泛型一致,定义执行的任务
    创建Callable 带有返回的任务对象
    创建FutureTask (间接实现Runnable接口)任务对象,包装进Callable 对象,返回Runnable 对象
    创建Thread 对象,放入任务对象
    启动线程 start()
    获得当前线程执行的结果
    优点:
    拓展性良好,实现Runnable接口,还可以再实现接口,再继承类
    同一个线程任务对象可以被包装成多个线程对象
    适合多个线程去共享一个资源
    实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立
    线程池可以放入实现Runnable和Callable 线程任务对象
    Callable中的call() 是带有返回值的任务方法
    缺点:
    步骤过于繁琐。
    线程的方法:
    sleep() 或 yield()
    static void sleep(long millis) 当前正在执行的线程休眠(暂时执行)为指定的毫秒数,根据精度和系统定时器和调度的准确性。
    静态方法 Thread.sleep(1000) ;
    调用sleep(),线程会进入到阻塞状态 “真的退让”

             sleep()                    睡眠时间结束
    

    运行状态 ----------------> 阻塞状态 ----------------> 可运行状态(继续争抢时间片)

    static void yield() 给调度程序一个提示,当前线程愿意得到它当前的处理器的使用。
    静态方法 Thread.yield();
    将当前线程的时间片让度给优先级高的线程,会进入到可运行状态,拥有再次争抢时间片的机会 “假意退让”。
    yield()
    运行状态 -----------------> 可运行状态(继续争抢时间片)

    void join() 等待该线程死亡
    非静态方法 t.join();
    调用join(),线程会进入到阻塞状态,直到等待线程结束。
    join() 等待线程结束
    运行状态 -------------------> 阻塞状态 --------------------------------> 可运行状态(继续争抢时间片)

    Priority 优先级:
    优先级:1~10
    MAX_PRIORITY 10
    MIN_PRIORITY 1
    NORM_PRIORITY 5 默认优先级
    结论:优先级越高,抢占到的时间片的概率越高,但是是否真的去运行,还是要看是否抢占了时间片。

    Thread.currentThread() 静态方法,获得当前正在执行的线程的状态信息
    Thread[Thread - 0 ,10,mian]
    Thread[线程的名称,优先级,线程组的名称]
    第一个值:线程的名称,若没有指定名字,则用默认的Thread-阿拉伯数字
    第二个值:优先级
    第三个值:当前线程所在的线程组,例如main线程组

    Strat()
    真正启动线程的方法
    group.add(this);将当前线程对象添加到线程组中。

    线程安全:
    线程同步:解决线程安全的问题,当多条线程,访问共享资源,为了让资源不发生争抢的现象,能够有序的进行
    第一种方式:同步代码块
    synchronized 关键字可用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
    synchronized(对象锁){
    //会引发线程安全的逻辑代码块
    }

    对象锁:
    引用数据类型 数组byte[0] 字符串“A” this 类名.class
    多个线程对象 要使用同一把锁
    在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着

    当多条线程访问共同资源时,发生了“争抢”现象,若要解决此现象,加上“同步锁”,产生“互斥效果”
    对象锁是 this who 执行此方法,who就是对象锁
    若此时是同一个对象执行方法,则会发生“等待”现象
    若此时不是同一个对象执行方法,则会发生“争抢”现象
    对象锁是 “A”
    若此时是同一个对象执行方法,则会发生“等待”现象
    若此时不是同一个对象执行方法,则会发生“等待”现象
    3.对象锁是 开销最小 byte [] bytes = new byte[0];
    若此时是同一个对象执行方法,则会发生“等待”现象
    若此时不是同一个对象执行方法,则会发生“争抢”现象
    4.对象锁是 类名.class 类类型
    若此时是同一个对象执行方法,则会发生“等待”现象
    若此时不是同一个对象执行方法,则会发生“等待”现象
    第二种方式:同步方法
    同步方法 synchronized
    public synchronized void 方法名(){
    //会引发线程安全的逻辑代码块
    }
    对象锁:
    synchronized 修饰非静态方法,此时对象锁是 this
    若此时是同一个对象执行方法,则会发生“等待”现象
    若此时不是同一个对象执行方法,则会发生“争抢”现象
    2.synchronized 修饰静态方法,此时对象锁 类名.class
    此时无论创建同一个对象,还是不同对象,都会发生“等待”现象

    第三种方式:LOCK锁
    Lock锁 也称同步锁
    1.java.util.concurrent.locks.Lock
    2.public void lock():加同步锁
    public void unlock():释放同步锁
    3.注意,无论代码运行是否出现异常,最终都要释放同步锁,finally完成释放同步锁。

    线程状态:

    NEW (尚未启动的线程的线程状态)
    RUNNABLE(一个可运行的线程的线程状态)
    BLOCKED(受阻塞并等待某个监视器锁的线程处于这种状态)
    WAITING(无限期地等待另一个线程来执行某一特定操作的线程处于这种状态)
    TIMED_WAITTING(等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态)
    TERMINATED(已退出的线程处于这种状态)

    NEW 代表是新建状态,是尚未启动的线程状态,现在new Thread 线程,start()真正启动线程的方法,若有CPU的使用权,会进入到RUNNABLE状态,即可运行状态,反之,会进入BLOCKED阻塞状态,进入运行状态时,若时间片用完就会进入阻塞状态,若阻塞状态得到时间片,则会进入运行状态。运行状态时调用 Object wait() 时会进入无限等待状态(WAITING),此时,若调用 Object notify()或Object notifyAll() ,且获得对象锁,就会从无限等待状态变成运行状态。若在无限等待状态,调用Object notify()未获得对象锁,则会进入阻塞状态。 运行状态,调用 Thread sleep(long)或 Object wait(long)时,则会进入计时等待状态(TIMED_WAITING),若sleep()时间结束,wait时间结束,获得对象锁,notify唤醒,获得对象锁,进入运行状态。若当前是计时等待状态,且wait时间结束,未获得对象锁,wait提前被唤醒,未获得对象锁,则会进入阻塞状态。死亡状态是在运行状态时 run()执行结束或出现异常时转换。

    wait() 和 sleep() 两者的区别:
    Object 类中的wait() 非静态的方法,属于对象的
    Thread 类中的sleep() 静态方法,属于当前线程的
    wait() 释放对象锁,sleep() 不会释放对象锁
    sleep() 是可以不使用synchronized 关键字
    wait()必须使用synchronized 关键字的,否则出现java.lang.IllegalMonitorStateException 异常
    4.sleep(long time) 不需要被唤醒,时间结束了自动醒。
    wait(long time ) 时间结束了自动醒;但是wait()一直等待,直到被唤醒。

    线程池:
    1.单线程的线程池:
    newSingleThreadExecutor
    创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。
    如果这个唯一的线程因异常结束,那么会有一个新的线程来替代它
    此线程保证所有任务的执行顺序按照任务的提交顺序执行。

    2.创建一个大小无限的线程池:
    创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。
    newScheduleThreadPool
    第一个参数 :Runnable 定期执行任务
    第二个参数:initialDelay 首次执行任务的延迟时间
    第三个参数:period 执行任务的周期
    第四个参数:unit 前两个时间的单位。

    3.创建大小固定的线程池:
    newFixedThreadPool
    创建固定大小的线程池
    每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
    线程池的大小一旦达到最大值就会保持不变,如果某个线程因执行异常而结束,那么线程池会补充一个新的线程。

    4.创建一个可缓存的线程池:
    newCachedThreadPool
    创建一个可缓存的线程池
    如果线程池的大小超过了处理任务所需要的线程,那么会回收部分空闲(60秒不执行任务)
    的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池的大小做出限制
    线程池的大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
    并发三特性:可见性、原子性、重排性
    一、可见性:
    论题:多个线程访问共享变量,会出现一个线程修改变量的值后,其他线程看不到变量最新值的情况。
    总结:
    并发编程下,多线程修改变量,会出现线程间变量的不可见性。
    不可见性的原因:
    每个线程都有自己的工作内存,操作的共享变量不是主存中的,而是读过来的共享变量的副本。
    解决方式:
    方式一:加锁
    解决原因:清空线程的工作内存,从主内存拷贝共享变量最新的值到工作内存成为副本。
    方式二:volatile 关键字
    解决原因:一旦一个线程中的变量,添加了volatile修饰符,有线程修改了此变量,
    其他线程会立即失效,当其他线程使用时可以立即读取主内存中的最新值至工作内存中使用。

    volatile 和 synchronized 的区别:

    volatile 关键字修饰变量(实例变量或类变量),synchronized关键字修饰方法(同步方法)或代码块(同步代码块)
    从性能上来说,volatile更好点,仅仅是对现实线程间变量的可见性上
    volatile 保证数据的可见性,但是不保证原子性(即:多线程进行写操作,不保证线程安全);
    而synchronized 是一种排他(互斥)的机制,实现线程安全。

    volatile总结:
    volatiles 是Java 虚拟机中提供的轻量级的同步机制
    保证可见性
    不保证原子性
    禁止指令重排
    volatile是可以保持可见性,不能保证原子性,由于内存屏障,可以保障避免指令重排的现象产生。

    二、原子性:
    原子性 - 方式一:使用锁机制synchronized保证原子性:

    public class AtomicDemo2 {
    public static void main(String[] args) {
    MyRunnable2 my = new MyRunnable2();
    for (int i = 1; i <= 100; i++) {
    new Thread(my).start();
    }
    }
    }
    class MyRunnable2 implements Runnable{
    private int count; //0

    @Override
    public void run() {
        for (int i = 1; i <=100 ; i++) {
            synchronized (MyRunnable2.class){
         //读取主内存中的count,count+1,将工作内存的count写回到主内存
                count++;
                System.out.println("count ====> "+count);
            }
        }
    }
    

    }

    原子性-方式二:JUC包下的原子类:

    public class AtomicDemo3 {
    public static void main(String[] args) {
    MyRunnable3 my = new MyRunnable3();
    for (int i = 1; i <= 100; i++) {
    new Thread(my).start();
    }
    }
    }
    class MyRunnable3 implements Runnable{
    //原子类
    private AtomicInteger atomicInteger = new AtomicInteger(); //0
    @Override
    public void run() {
    for (int i = 1; i <=100 ; i++) {
    int count = atomicInteger.incrementAndGet(); //等价于++count
    System.out.println("count ====> "+count);
    }
    }
    }

    原子类依据的原理CAS Compare And Swap
    CAS原理:比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作,如果不是就一直循环。
    缺点:
    循环会耗时
    一次性只能保证一个共享变量的原子性
    会出现ABA问题(此时建议引入原子引用,使用带版本号的原子操作,例如:AtomicStampedReference)中的getStamp()方法)
    CAS 中存在的ABA问题
    解决方案:AtomicStampedReference

    compareAnset()
    第一个参数:期望值
    第二个参数:更新值
    第三个参数:期望标志
    第四个参数:更新标志
    返回值 true 代表交换成功,flase交换失败

    CAS 与 Synchronized:乐观锁和悲观锁
    共同点:
    CAS和Synchronized 都可以保证多线程环境下共享数据的安全性。
    不同点:
    Synchronized 是从悲观的角度出发(悲观锁)
    总是假设最坏的情况,每次去拿数据的时候都认为别人都会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。(共享资源每次只给一个线程使用,其他线程阻塞,用完后再把资源转让给其他线程)。因此,Synchronized 我们也称之为悲观锁。JDK中的ReentrantLock也是一种悲观锁。性能较差!
    CAS是从乐观的角度出发(乐观锁)
    总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。CAS这种机制我们也可以将其称为乐观锁。总和性能较好。
    指令重排:
    什么是重排序?
    重排序就是编译器或者CPU的代码的结构重排序,已达到最佳的执行效果。重排大概分为编译器重排,处理器重排。

    内存屏障:
    内存屏障指令:
    写内存屏障(Store Memory Barrier):处理器将当前存储缓存的值写回主存,以阻塞的方式。
    读内存屏障(Load Memory Barrier): 处理器处理失效队列,以阻塞的方式。

    通过加入内存屏障,保证了两个操作之间数据的可见性。volatile关键字通过“内存屏障”来防止指令被重排序,volatitle会在读取数据前插入一个读屏障,写数据之后加入一个写屏障,所以,他可以避免CPU重排序导致的问题,实现多线程之间数据的可见性。

    内存屏障的主要类型
    为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。然而,对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,Java内存模型采取保守策略。

    下面是基于保守策略的JMM内存屏障插入策略:
    在每个volatile写操作的前面插入一个StoreStore屏障。
    在每个volatile写操作的后面插入一个StoreLoad屏障。
    在每个volatile读操作的后面插入一个LoadLoad屏障。
    在每个volatile读操作的前面插入一个LoadStore屏障。

    内存屏障的主要类型(了解即可):
    不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的平台商城相应的机器码。

    Java内存屏障主要分为Load和Store两类。
    对Load Barrier 来说,在读指令前插入读屏障,可以告诉缓存中的数据失效,重新从主存中加载数据。
    对Store Barrier来说,在写指令之后插入写屏障,能让写入缓存的最新数据写会到主内存。
    对于Load 和 Store,在实际使用中,又分为一下四种:

    LoadLoad 屏障
    序列:Load1,Loadload,Load2
    确保Load1所要读入的数据能够在被Load2和后续的load指令访问前读入。通常能执行预加载指令或/和支持乱序处理的处理器中需要显式声明Loadload屏障,因为在这些处理器中正在等待的加载指令能够绕过正在等待存储的指令。 而对于总是能保证处理顺序的处理器上,设置该屏障相当于无操作。
    StoreStore 屏障
    序列:Store1,StoreStore,Store2
    确保Store1的数据在Store2以及后续Store指令操作相关数据之前对其它处理器可见(例如向主存刷新数据)。通常情况下,如果处理器不能保证从写缓冲或/和缓存向其它处理器和主存中按顺序刷新数据,那么它需要使用StoreStore屏障。
    LoadStore 屏障
    序列: Load1; LoadStore; Store2
    确保Load1的数据在Store2和后续Store指令被刷新之前读取。在等待Store指令可以越过loads指令的乱序处理器上需要使用LoadStore屏障。
    StoreLoad 屏障
    序列: Store1; StoreLoad; Load2
    确保Store1的数据在被Load2和后续的Load指令读取之前对其他处理器可见。StoreLoad屏障可以防止一个后续的load指令 不正确的使用了Store1的数据,而不是另一个处理器在相同内存位置写入一个新数据。正因为如此,所以在下面所讨论的处理器为了在屏障前读取同样内存位置存过的数据,必须使用一个StoreLoad屏障将存储指令和后续的加载指令分开。Storeload屏障在几乎所有的现代多处理器中都需要使用,但通常它的开销也是最昂贵的。它们昂贵的部分原因是它们必须关闭通常的略过缓存直接从写缓冲区读取数据的机制。这可能通过让一个缓冲区进行充分刷新(flush),以及其他延迟的方式来实现。

    并发包:
    JDK中有用的并发容器和并发工具类:
    ConcurrentHashMap
    为什么要使用ConcurrentHashMap:
    hashMap线程不安全,会导致数据错乱
    使用线程安全的Hashtable 效率低下

    HashTable效率低下的原因:
    public synchronized V put(K key, V value)
    public synchronized V get(Object key)

    HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下,HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争激烈效率越低。

    补充:
    多并发访问HashMap集合时,出现了线程安全的情况

    解决方案:
    方式一:Map<String,String> map = new Hashtable<>();
    HashTable 线程安全类,在方法上添加syncronaized 关键字,效率较差,被淘汰。
    HashTable 效率低的原因:锁定整张hash表,只要有一个线程进入操作,其他线程进入阻塞。
    方式二: Map<String,String> map = Collections.synchronizedMap(new Hashmap<>());
    方式三:ConcurrentHashMap<String,String> map = new ConcurrentHashMap<>();
    综合性能更高,ConcurrentHashMap 更高效的原因:CAS 和 局部(synchronized)锁定 “分段式锁”;
    CountDownLatch
    CountDownLatch 允许一个或多个线程等待其他线程完成操作,在执行自己。
    CountDownLatch构造方法:
    public void await() throws InterruptedException// 让当前线程等待
    public void countDown()// 计数器进行减1

    CountDownLatch 是通过一个计数器来实现的,每当一个线程完成了自己的任务后,可以调用countDown() 方法让计数器-1,当计数器到达0时,调用CountDownLatch的await()方法的线程阻塞状态解除,继续执行。

    CyclicBarrier:
    CyclicBarrier 的构造方法:
    // 用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景
    public CyclicBarrier(int parties, Runnable barrierAction)

    CyclicBarrier重要方法:
    // 每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞
    public int await()

    使用场景:
    使用场景:CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的场景。
    需求:使用两个线程读取2个文件中的数据,当两个文件中的数据都读完毕以后,进行数据汇总操作。

    Semaphore
    Semaphore(发信号) 的主要作用是控制线程的并发数量。
    synchronized 可以起到“锁”的作用,但某个时间段内,只能有一个线程允许被执行。
    Semaphore 可以设置同时允许几个线程执行。
    Semaphore 字面意思是信号量的意思,它的作用是控制访问特定资源的线程数目。
    Semaphore构造方法:
    //permits 表示许可线程的数量
    public Semaphore(int permits)
    //fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程
    public Semaphore(int permits, boolean fair)

    Semaphore 重要方法:
    //表示获取许可
    public void acquire() throws InterruptedException
    //release() 表示释放许可
    public void release()

    Exchanger
    Exchanger(交换者) 是一个用于线程间协作的工具类。Exchanger 用于进行线程间数据交换。
    这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange() 方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
    Exchanger构造方法:
    Exchanger() 创建一个新的 Exchanger。

    Exchanger重要方法:
    //等待另一个线程到达此交换点(除非当前线程被中断),
    然后将给定的对象传递给该线程,并接收该线程的对象。
    public V exchange(V x)
    //等待另一个线程到达此交换点(除非当前线程被中断,或者超出了指定的等待时间)
    然后将给定的对象传送给该线程,同时接收该线程的对象。
    V exchange(V x,long timeout,TimeUnit unit)

    Exchanger类 交换者,是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换

    使用场景:
    使用场景:
    需求:比如我们需要将纸质银行流水通过人工的方式录入成电子银行流水。为了避免错误,采用AB岗两个进行录入,录入到两个文件中,系统需要加载这两个文件,并对文件进行校对,看看是否录入一致。
    案例:
    public class ExchangerDemo {
    public static void main(String[] args) {
    Exchanger exchanger = new Exchanger<>();

        new BoyThread("傅总",exchanger).start();
        new GirlThread("桂太",exchanger).start();
    
    }
    

    }
    //BoyThread 男孩线程
    class BoyThread extends Thread{
    private Exchanger exchanger;

    public BoyThread(String name, Exchanger<String> exchanger){
        super(name);
        this.exchanger = exchanger;
    }
    
    @Override
    public void run() {
        //交换零食
        try {
            Thread.sleep(6000);
            String str = exchanger.exchange(Thread.currentThread().getName() + "给出零食A");
            System.out.println(Thread.currentThread().getName()+"收到--->"+str);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    

    }
    //GirlThread 女孩线程
    class GirlThread extends Thread{
    private Exchanger exchanger;

    public GirlThread(String name, Exchanger<String> exchanger){
        super(name);
        this.exchanger = exchanger;
    }
    
    @Override
    public void run() {
        //交换零食
        try {
            //String str = exchanger.exchange(Thread.currentThread().getName() + "给出零食B");
            //指定交换时间5s,若5s内没有交换零食,则抛出异常java.util.concurrent.TimeoutException
            String str = exchanger.exchange(Thread.currentThread().getName() + "给出零食B",5, TimeUnit.SECONDS);
            System.out.println(Thread.currentThread().getName()+"收到--->"+str);
        } catch (InterruptedException | TimeoutException e) {
            e.printStackTrace();
        }
    }
    

    }

    线程通讯:生产者和消费者
    线程通讯是一种等待唤醒机制
    线程通讯一定是多个线程在操作同一个资源才需要进行通信
    线程通信必须先保证线程安全,否则毫无意义,代码也会报错(java.lang.IllegalMonitorStateException)
    线程同步使用的对象锁必须保证唯一
    只有锁对象才能调用notify()和 wait()方法。

    生产者和消费者案例:

    消费者:
    //消费者(线程)
    public class Customer implements Runnable{
    private WareHouse house;
    public Customer(WareHouse house){
    this.house = house;
    }
    @Override
    public void run() {
    while (true){
    try {
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    char c = house.pop();
    }
    }
    }

    生产者:
    public class Product implements Runnable{

    private WareHouse house;
    
    public Product(WareHouse house){
        this.house = house;
    }
    
    @Override
    public void run() {
        //死循环
        for (int i = 0; ;i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            char c = (char)('A'+i);
            house.push(c);
        }
    }
    

    }

    仓库(共同资源):

    public class WareHouse {

    private char[] chs = new char[5];
    private int index;
    
    //生产(入栈)
    public synchronized void push(char c){
        if(index>=chs.length){
            System.out.println("仓库已满...");
            try {
                //生产者进入等待状态
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        chs[index++] = c;
        System.out.println("仓库添加元素 "+c+" 了,可以消费了....");
        //一旦生产者进行生产了,通知消费者你可以消费了
        this.notify();
    }
    
    
    //消费(出栈)
    public synchronized char pop(){
        if(index==0){
            System.out.println("仓库已空");
            try {
                //消费者等待
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        char c = chs[--index];
        System.out.println("仓库消费元素 "+c+" 了,可以生产了....");
        //通知生产者可以进行生产了
        this.notify();
        return c;
    }
    

    }

    测试类:
    public class Test {

    public static void main(String[] args) {
    
        WareHouse house = new WareHouse();
    
        //生产者
        new Thread(new Product(house)).start();
        //消费者
        new Thread(new Customer(house)).start();
    }
    

    }

    银行账户存取款案例:
    账户类:
    public class Account {

    private String id;
    private int balance;
    
    /**
     * 取款方法
     * @param money  取的金额
     * @return  账户信息
     */
    public synchronized Account withdraw(int money){
        String name = Thread.currentThread().getName();
    
        if(balance>=money){
            balance-=money;
            System.out.println(name+"正在取钱,余额充足,取出"+money+",当前账户剩余"+balance);
            //取钱完毕,唤醒其他(存款)线程,可以进行存钱
            this.notifyAll();
            //当前自己的线程,进入等待状态
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            System.out.println(name+"正在取钱,余额不足");
            //唤醒其他(存款)线程,可以进行存钱
            this.notifyAll();
            //当前自己的线程,进入等待状态
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return this;
    }
    
    
    /**
     * 存款方法
     * @param money  存的金额
     * @return  账户信息
     */
    public synchronized Account deposit(int money){
        String name = Thread.currentThread().getName();
    
        if(balance>0){
            System.out.println(name+"正在存钱,但是当前账户有余额");
            //唤醒其他(取钱的)线程,进行取钱操作
            this.notifyAll();
            //本线程进入等待状态
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            balance+=money;
            System.out.println(name+"正在存钱,存进"+money+",当前账户剩余"+balance);
            //唤醒其他(取钱的)线程,进行取钱操作
            this.notifyAll();
            //本线程进入等待状态
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return this;
    }
    
    public Account() {
    }
    
    public Account(String id, int balance) {
        this.id = id;
        this.balance = balance;
    }
    
    /**
     * 获取
     * @return id
     */
    public String getId() {
        return id;
    }
    
    /**
     * 设置
     * @param id
     */
    public void setId(String id) {
        this.id = id;
    }
    
    /**
     * 获取
     * @return balance
     */
    public int getBalance() {
        return balance;
    }
    
    /**
     * 设置
     * @param balance
     */
    public void setBalance(int balance) {
        this.balance = balance;
    }
    
    public String toString() {
        return "Account{id = " + id + ", balance = " + balance + "}";
    }
    

    }

    存款线程:
    public class DepositThread implements Runnable{

    private Account acc;
    
    public DepositThread(Account acc){
        this.acc = acc;
    }
    
    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            acc.deposit(10000);
        }
    }
    

    }

    取款线程:
    public class WithdrawThread implements Runnable{

    private Account acc;
    
    public WithdrawThread(Account acc){
        this.acc = acc;
    }
    
    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            acc.withdraw(10000);
        }
    }
    

    }

    测试类:
    public class Test {

    public static void main(String[] args) {
        //共同资源
        Account acc = new Account("002",10000);
    
        //创建了一条张三线程,进行访问共同资源的账户,完成取款操作
        new Thread(new WithdrawThread(acc),"张三").start();
    
        //创建了一条李四线程,进行访问共同资源的账户,完成取款操作
        new Thread(new WithdrawThread(acc),"李四").start();
    
        //创建了一条粑粑线程,进行访问共同资源的账户,完成存款操作
        new Thread(new DepositThread(acc),"粑粑").start();
    
        //创建了一条麻麻线程,进行访问共同资源的账户,完成存款操作
        new Thread(new DepositThread(acc),"麻麻").start();
    }
    

    }

    懒汉式单例模式(线程安全)-双重锁(Double Check Lock)

    public class Singleton {
    private static Singleton instance;
    private Singleton (){
    }
    public static Singleton getInstance(){ //对获取实例的方法进行同步
    if (instance == null){
    synchronized(Singleton.class){
    if (instance == null)
    instance = new Singleton();
    }
    }
    return instance;
    }
    }

    为什么要使用双重锁?
    就单例本身来说,synchronized(LazyAgainSingletonPattern.class)已经能够保证了,那为什么还要在synchronized(LazyAgainSingletonPattern.class)这句话的外层加一层singleton是否为空的判断,目的是:懒汉加载的双重校验,外面一层是否为空的判断,是为了减少并发访问时线程阻塞所带来的的问题,如果不为空则不再竞争锁。

  • 相关阅读:
    Blue:贪心,单调队列
    建设城市(city):组合数,容斥原理
    [考试反思]0809NOIP模拟测试15:解剖
    [考试反思]0807NOIP模拟测试14:承认
    [考试反思]阶段性总结:NOIP模拟测试7~13
    [考试反思]0805NOIP模拟测试13:窒息
    [考试反思]0803NOIP模拟测试12:偿还
    [态度]关于博客
    差异:后缀数组(wzz模板理解),决策单调性
    [考试反思]0801NOIP模拟测试11
  • 原文地址:https://www.cnblogs.com/monkeycode/p/14070966.html
Copyright © 2020-2023  润新知