Java多线程基础

Java多线程

文章目录

    • Java多线程
    • 一、线程介绍及相关概念
    • 二、创建和启动线程
      • 2.1 Thread类的常用结构
      • 2.2 创建线程法1:继承Thread类(分配线程对象)
      • 2.3 创建线程法2:实现Runnable接口(创建线程的目标对象)
      • 2.4 创建线程法3:实现Callable接口JDK5.0新特性
      • 2.4 创建线程法4:使用线程池
    • 三、多线程的生命周期
    • 四、线程安全问题及解决
      • 4.1 线程安全问题介绍(资源)
      • 4.2 解决方法:同步机制(同步代码块/同步方法)sychronized
      • 4.3 解决方法:同步机制Lock
      • 4.4 同步机制的应用(懒汉式单例模式-线程安全)
      • 4.5 同步机制引发的问题:死锁
    • 五、线程的通信
      • 5.1 线程通信机制
      • 5.2 场景题:交替打印&生产者消费者
    • 六、企业真题


一、线程介绍及相关概念

1、程序、进程、线程:

  • 程序program:静态,为了完成特定任务,用某种语言编写的一组指令的集合,即一段静态的代码,静态的对象。
  • 进程process:动态,与操作系统有关,程序的一次执行过程·,正在内存中运行的应用程序。
    • 进程作为操作系统调度和分配资源的最小单位(亦是系统运行程序的基本单位),系统在运行时会为每个进程分配不同的内存区域。
  • 线程(thread):与CPU有关,进程的进一步细化,是程序内部的一条执行路径。是CPU调度和执行的最小单位,在一个进程执行过程中CPU会来回切换执行不同的线程。
    • Java是支持多线程的:即一个进程同一时间若并行执行多个线程。
      • 多线程的优点:提高应用程序的响应;CPU利用率;改善程序结构,将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
    • 一个进程中的多个线程共享相同的内存单元,它们从同一个堆中分配对象,可以访问相同的变量和对象,便于线程之间通信,但多个线程操作共享的系统资源可能就会带来安全的隐患
      • 内存泄漏
      • 死锁
      • 线程不安全——数据混乱、错误或者丢失;而安全则是多线程情况下数据的正确性和一致性。解决:同步机制!!!
    • 各个进程独立,各个线程不是,同一进程中的线程会相互影响,线程开销小,但不利于资源的管理和保护。
    • 线程分类:IO密集型、CPU密集型

举例:运行360杀毒软件(一个进程),可以同时执行木马查杀、系统修复、电脑清理(多个线程)。不同进程之间不共享内存,因为数据交换和通信成本很高。进程之间通信举例:支付宝与饿了么之间。(socket)
如果想让多个线程共同完成一个任务,那么他们之间要可以通信(通过共享的系统资源:JVM的堆、方法区;电脑中的文件等等) JVM区域划分

从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。

  • 程序计数器是线程私有:其作用是①字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制(顺序、选择、循环、异常处理);②多线程时,记录当前线程执行的位置(下一条指令的地址),当线程切换回来继续执行。【注意执行native方法记录undefined地址】。因此为了线程切换后能恢复到正确的执行位置
  • 虚拟机栈私有: 每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
  • 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
  • 堆和方法区是所有线程共享的资源:堆主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

2、单核CPU&多核CPU&并行&并发&同步&异步

  • 单核CPU:在一个时间单元内,只能执行一个线程的任务。
  • 多核CPU:多几个CPU,但是效率未必就是单核的倍数,因为①多个核心的其他共用资源限制;②多核CPU之间的协调管理损耗
  • 并行(parallel):指两个或多个事件在同一时刻发生(同时发生)。指在同一时刻,有多条指令多个CPU同时执行。
  • 并发(concurrency):指两个或多个事件在同一个时间段内发生。即在一段时间内,有多条指令单个CPU快速轮换、交替执行,使得在宏观上具有多个进程同时执行的效果。——因此我们需要并发编程,JUC【即创建多个线程的任务同时执行】
  • 同步:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
  • 异步:调用在发出之后,不用等待返回结果,该调用直接返回。

总结:单核CPU对于多个线程任务只能并发,多核CPU则这些并发执行的程序便可以分配到多个CPU上,实现多任务并行执行,利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。
3、“并发时”,CPU调度线程的方式:

  • 分时调度:所有线程轮流使用 CPU 的使用权,并且平均分配每个线程占用 CPU 的时间。
  • 抢占式调度:让优先级高的线程以较大的概率优先使用 CPU。如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

二、创建和启动线程

创建多线程的方式:继承Thread类、实现Runnable接口、实现Callable接口、使用线程池、使用CompletableFuture类等等。
1、多线程:Java语言的JVM允许程序运行多个线程,使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。
2、Thread类的特性:Thread对象的run()定义线程需要完成的任务,start()启动线程。必须在主线程中创建新的线程对象,才能实现多线程
3、Thread类也是实现Runnable接口

  • Thread(Runnable target):无论是Runnable实现类的对象还是Thread子类对象,使用这个构造器本质都是调用target.run()
  • Thread():创建Thread子类对象,如new MyThread();
  • 无论是Thread类还是Thread子类都可以创建线程【Thread子类需要重载相关的构造器】,比如new MyThread(Runnable target)

2.1 Thread类的常用结构

1、构造器

  • public Thread() :分配一个新的线程对象。
  • public Thread(String name) :分配一个指定名字的新的线程对象。
  • public Thread(Runnable target) :指定创建线程的目标对象,它实现了Runnable接口中的run方法
  • public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。

2、常用方法系列1

  • public void run() :此线程要执行的任务在此处定义代码。
  • public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
  • public String getName() :获取当前线程名称。
  • public void setName(String name):设置该线程名称。
  • public static Thread currentThread() :返回对当前正在执行的线程对象的引用。在Thread子类中就是this,通常用于主线程和Runnable实现类。如:Thread.currentThread().setName(“主线程”);
  • public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
  • public static void yield():yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这个不能保证,完全有可能的情况是,当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。

3、常用方法系列2

  • public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。
  • void join() :等待该线程终止。
    void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果millis时间到,将不再等待。
    void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
  • public final void stop():已过时,不建议使用。强行结束一个线程的执行,直接进入死亡状态。run()即刻停止,可能会导致一些清理性的工作得不到完成,如文件,数据库等的关闭。同时,会立即释放该线程所持有的所有的锁,导致数据得不到同步的处理,出现数据不一致的问题。
  • void suspend() / void resume() : 这两个操作就好比播放器的暂停和恢复。二者必须成对出现,否则非常容易发生死锁。suspend()调用会导致线程暂停,但不会释放任何锁资源,导致其它线程都无法访问被它占用的锁,直到调用resume()。已过时,不建议使用。

4、常用方法系列3:每个线程都有优先级,分时调度策略:同级先到先服务;抢占式策略:让优先级高的线程以较大的概率优先使用CPU,如果优先级相同会随机选择一个。

  • Thread类的三个优先级常量:
    • MAX_PRIORITY(10):最高优先级
    • MIN _PRIORITY (1):最低优先级
    • NORM_PRIORITY (5):普通优先级,默认情况下main线程具有普通优先级。
  • public final int getPriority() :返回线程优先级
  • public final void setPriority(int newPriority) :改变线程的优先级,范围在[1,10]之间。

5、守护线程相关操作:在后台运行的,为其他线程提供服务的线程。如JVM垃圾回收线程。特点:其他非守护线程死亡,它会自动死亡。

  • setDaemon(true):将指定线程设置为守护线程。必须在线程启动之前设置,否则会报IllegalThreadStateException异常。
  • isDaemon():判断线程是否是守护线程。

2.2 创建线程法1:继承Thread类(分配线程对象)

1、实现步骤:创建并启动多线程

  • 定义线程需要完成的任务:定义Thread类的子类,并重写该类的run()方法——线程执行体。public void run()
  • 创建线程对象:创建Thread子类的实例
  • 启动线程:调用线程对象的start()方法来启动该线程。在主线程中创建新的线程对象!

定义线程代码,注意点如下

  • run()方法由JVM调用,由操作系统的CPU调度决定,如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
  • 启动多线程,必须调用start方法。一个线程对象只能调用一次start()方法启动,否则将抛出异常“IllegalThreadStateException”。
  • 为了让多个Thread子类对象可以共享一个资源对象,可以将这个对象作为构造器参数传入,全程只创建这个对象传入多个子类对象中
package com.atguigu.thread;
//自定义线程类
public class MyThread extends Thread {//private Person p;//比如这是需要共享的对象,可以定义static,可以作为构造器参数传入//定义指定线程名称的构造方法:默认时Thread-0依次增长!public MyThread(String name) {//调用父类的String参数的构造方法,指定线程的名称super(name);}public MyThread(Person p) {this.p = p;}public MyThread(Person p, String name) {//调用父类的String参数的构造方法,指定线程的名称super(name);}/*** 重写run方法,完成该线程执行的逻辑*/@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(getName()+":正在执行!"+i);}}
}
package com.atguigu.thread;
public class TestMyThread {public static void main(String[] args) {//创建自定义线程对象1MyThread mt1 = new MyThread("子线程1");//开启子线程1mt1.start();//创建自定义线程对象2MyThread mt2 = new MyThread("子线程2");//开启子线程2mt2.start();//在主方法中执行for循环for (int i = 0; i < 10; i++) {System.out.println("main线程!"+i);}}
}

执行步骤:主线程中存在两个线程,CPU会来回切换调度执行不同的线程——并行执行多线程
多线程的执行方式

2.3 创建线程法2:实现Runnable接口(创建线程的目标对象)

1、实现步骤:Java有单继承的限制,可以实现Runnable接口,重写run()方法,然后再通过Thread类的对象代理启动和执行线程体run()方法

  • 创建一个实现Runnable接口的类
  • 实现接口中的run()–>将此线程要执行的操作,声明在此方法体中
  • 创建当前实现类的对象
  • 将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例。即Thread t = new Thread(mr, “长江”);
  • Thread类的实例调用start():1.启动线程 2.调用当前线程的run()【Thread源码中又target.run()】

总结:如果使用这一个runnable接口实现类的实例来创建多个线程,那么这多个线程共享这一个实例(这个实例在堆中,是所有线程共享的),而单继承Thread类不一样,这个需要创建多个线程对象,但是可以使用静态变量来实现多个对象共享。
原理:多个线程共享一个实例:因为只创建了这一个实例在堆中

Thread这部分的源码:Thread类也是实现Runnable接口的

public class Thread implements Runnable {
/* What will be run. */private Runnable target;public Thread(Runnable target){this( group: null, target, name: "Thread-" + nextThreadNum(),stackSize:0);}
}

实现Runnable接口,使用Thread的target参数创建Thread线程。
通过Runnable接口的实现类的实例创建两个线程Thread,从始至终之创建了一个实例,因此这两个线程共享一个,该实例时new的因此在栈中,栈中都是共享的,多线程之间通过共享资源(JVM中共享是方法区、堆)来通信。

public class MyRunnable implements Runnable {int num = 0;//实现类的属性,如果只创建一个对象供多个线程使用,就是多个线程之间共享的@Overridepublic void run() {for (int i = 0; i < 20; i++) {System.out.println(Thread.currentThread().getName() + " " + i);}}
}
public class TestMyRunnable {public static void main(String[] args) {//创建自定义类对象  线程任务对象MyRunnable mr = new MyRunnable();//创建线程对象1Thread t1 = new Thread(mr, "长江");t1.start();//创建线程对象2Thread t2 = new Thread(mr, "大海");//t1、t2两个线程共享同一个实例t2.start();for (int i = 0; i < 20; i++) {System.out.println("黄河 " + i);}}
}

使用匿名内部类实现Thread类、或者Runnable接口

//继承Thread类
new Thread("新的线程!"){@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(getName()+":正在执行!"+i);}}
}.start();//实现Runnable接口
new Thread(new Runnable(){@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName()+":" + i);}}
}).start();

2、实现Runnable接口VS继承Thread类方式

  • 共同点:① 启动线程,使用的都是Thread类中定义的start()② 创建的线程对象,都是Thread类或其子类的实例。
  • 不同点:一个是类的继承,一个是接口的实现。
  • 建议:建议使用实现Runnable接口的方式。
    • Runnable方式的好处:① 实现的方式,避免的类的单继承的局限性 ② 更适合处理有共享数据的问题(只创建一个Runnable实现类的实例),实现多线程之间通信。③ 实现了代码和数据的分离。
      联系:public class Thread implements Runnable

2.4 创建线程法3:实现Callable接口JDK5.0新特性

1、Callable对于Runnable方式的好处

  • call():与run()方法对比可以有返回值,更灵活
  • call():可以使用throws的方式处理异常(抛出)
  • 支持泛型的返回值:Callable使用了泛型参数,可以指明具体的call()的返回值类型,更灵活。(需要借助FutureTask类,获取返回结果)

缺点:如果在主线程中需要获取分线程call()的返回值,则此时的主线程是阻塞状态的。
Future接口介绍:

  • 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
  • FutureTask是Futrue接口的唯一的实现类
  • FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值

2、具体实现方式:比Runnable接口的实现类对经历了FutureTask构造器中构造
创建Callable实现类并重写call()方法→创建Callable接口实现类的对象→将这个对象作为参数传递到FutureTask构造器中,创建FutureTask的对象→将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()

Callable接口创建线程的方式

//1.创建一个实现Callable的实现类
class NumThread implements Callable {//2.实现call方法,将此线程需要执行的操作声明在call()中@Overridepublic Object call() throws Exception {int sum = 0;for (int i = 1; i <= 100; i++) {if (i % 2 == 0) {System.out.println(i);sum += i;}}return sum;}
}
public class CallableTest {public static void main(String[] args) {//3.创建Callable接口实现类的对象NumThread numThread = new NumThread();//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象FutureTask futureTask = new FutureTask(numThread);//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()new Thread(futureTask).start();
//      接收返回值try {//6.获取Callable中call方法的返回值//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。Object sum = futureTask.get();//这里是主线程调用的,这是静态方法,和sleep()一样,在哪个线程中调用就是哪个System.out.println("总和为:" + sum);} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}}
}

2.4 创建线程法4:使用线程池

真正生产环境中使用的创建线程的方式,多个线程可以同时执行一个任务,也可以不同的线程执行各自的任务,也可以一个线程执行多个任务
1、为什么需要线程池?使得线程可以复用,即执行完一个任务,并不被销毁,而是可以继续执行其他的任务
并发的线程数量很多,且每个线程执行很短时间的任务就结束了,频繁创建线程会大大降低系统的效率(因为频繁创建线程和销毁线程需要时间)

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
线程池
2、好处:

  • 提高执行效率:因为线程已经提前创建好了
  • 提高资源的复用率:因为执行完的线程并未销毁,而是可以执行其他的任务。
  • 便于线程管理
    • corePoolSize:核心池的大小
    • maximumPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持多长时间后会终止

3、线程池相关API

  • JDK5.0之前,我们必须手动自定义线程池。从JDK5.0开始,Java内置线程池相关的API。在java.util.concurrent包下提供了线程池相关API:ExecutorServiceExecutors
  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
    • void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
    • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行Callable
    • void shutdown() :关闭连接池
  • Executors:一个线程池的工厂类,通过此类的静态工厂方法可以创建多种类型的线程池对象。
    • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
    • Executors.newFixedThreadPool(int nThreads); 创建一个可重用固定线程数的线程池
    • Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
    • Executors.newScheduledThreadPool(int corePoolSize):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

线程池创建线程

class NumberThread implements Runnable{@Overridepublic void run() {for(int i = 0;i <= 100;i++){if(i % 2 == 0){System.out.println(Thread.currentThread().getName() + ": " + i);}}}
}class NumberThread1 implements Runnable{@Overridepublic void run() {for(int i = 0;i <= 100;i++){if(i % 2 != 0){System.out.println(Thread.currentThread().getName() + ": " + i);}}}
}class NumberThread2 implements Callable {@Overridepublic Object call() throws Exception {int evenSum = 0;//记录偶数的和for(int i = 0;i <= 100;i++){if(i % 2 == 0){evenSum += i;}}return evenSum;}}public class ThreadPoolTest {public static void main(String[] args) {//1. 提供指定线程数量的线程池ExecutorService service = Executors.newFixedThreadPool(10);ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//        //设置线程池的属性
//        System.out.println(service.getClass());//ThreadPoolExecutorservice1.setMaximumPoolSize(50); //设置线程池中线程数的上限//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象service.execute(new NumberThread());//适合适用于Runnableservice.execute(new NumberThread1());//适合适用于Runnabletry {Future future = service.submit(new NumberThread2());//适合使用于CallableSystem.out.println("总和为:" + future.get());} catch (Exception e) {e.printStackTrace();}//3.关闭连接池service.shutdown();}}

三、多线程的生命周期

JDK1.5之前:新建、就绪、运行、阻塞、死亡
JDK1.5及之后:新建、可运行(准备、运行)、锁阻塞、计时等待、无限等待、死亡
对比:将阻塞细分为不同的情况
1、多线程生命周期在java.lang.Thread.State的枚举类中定义:

public enum State {NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED;
}

多线程的生命周期

锁、sleep(线程本身主动放弃)、wait(线程通信-共享中对象调用的)
加塞join、
2、不同的生命周期说明:阻塞状态分为三种:BLOCKED、WAITING、TIMED_WAITING

  • NEW(新建):线程刚被创建,但是并未启动。还没调用start方法。
  • RUNNABLE(可运行):(准备/运行)对于Java对象来说,只能标记为可运行,至于什么时候运行,不是JVM来控制的了,是OS来进行调度的,而且时间非常短暂,因此对于Java对象的状态来说,无法区分。
  • Teminated(被终止):表明此线程已经结束生命周期,终止运行。
  • BLOCKED(锁阻塞):一个正在阻塞、等待一个监视器锁(锁对象)的线程。只有获得锁对象的线程才能有执行机会。
    • 比如,线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。
  • TIMED_WAITING(计时等待):一个正在限时等待另一个线程执行一个(唤醒notify)动作的线程处于这一状态。
    • 当前线程执行过程中遇到Thread类的sleepjoin,Object类的wait,LockSupport类的park方法,并且在调用这些方法时,设置了时间,那么当前线程会进入TIMED_WAITING,直到时间到,或被中断。
  • WAITING(无限等待):一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。
    • 当前线程执行过程中遇到遇到Object类的wait,Thread类的join,LockSupport类的park方法,并且在调用这些方法时,没有指定时间,那么当前线程会进入WAITING状态,直到被唤醒。
      • 通过Object类的wait进入WAITING状态的要有Object的notify/notifyAll唤醒;
      • 通过Condition的await进入WAITING状态的要有Condition的signal方法唤醒;
      • 通过LockSupport类的park方法进入WAITING状态的要有LockSupport类的unpark方法唤醒
      • 通过Thread类的join进入WAITING状态,只有调用join方法的线程对象结束才能让当前线程恢复;

说明:当从WAITING或TIMED_WAITING恢复到Runnable状态时,如果发现当前线程没有得到监视器锁,那么会立刻转入BLOCKED状态。

四、线程安全问题及解决

4.1 线程安全问题介绍(资源)

JVM中多个线程共享的是:方法区:常量、静态变量等;堆:new对象。即多线程在访问这些共享的资源时就会出现问题。
1、线程不安全的原因:

  • 情形1:局部变量不能共享:run()中的执行代码定义的局部变量
  • 情形2:不同对象的实例变量不能共享:Thread子类定义的实例变量
  • 情形3:静态变量是共享的:Thread子类/Runnable实现类中定义的static变量(类属性)
  • 情形4:同一个对象的实例变量共享:Runnable实现类如果只创建一个实例对象,通过Thread(Runnable obj)创建多个线程的目标对象。
  • 情形5:抽取资源类,共享同一个资源对象:将共享的资源封装成一个对象

总结:Thread子类在创建多线程时,必须是创建多个Thread子类的线程对象才能实现;Runnable实现类创建多线程时只需创建一个实例对象,然后调用Thread类创建线程的目标对象即可实现。
实现方式:①抽取资源类,在主线程中定义内部类(继承Thread/实现Runnable接口);②单独创建Thread子类/Runnable实现接口,在主线程中重复使用。

卖票问题:100张票,由三个窗口同时开始售卖:

  • 法1使用Runnable实现类的方式(共享一个Runnbale实现类对象,多线程之间可以通信);
    存在的问题:这个会出现数据混乱的问题,多线程共享MyRunnable实例对象(包括其属性ticket),假如线程"窗口1"修改ticket资源还没来得及返回堆中,该资源又被线程"窗口2"读取修改,最后会导致这两个线程卖了同一张票!
  • 法2继承Thread类:创建多个线程对象,每个对象都有自己独立的资源,他们之间没办法通信。

解决:必须保证一个线程a在操作ticket(共享资源)的过程中,其它线程必须等待,直到线程a操作ticket结束以后,其它线程才可以进来继续操作ticket。

②单独创建Thread子类/Runnable实现接口,在主线程中重复使用。

public class OverrideTest {public static void main(String[] args){MyRunnable1 myrun = new MyRunnable1();Thread t1 = new Thread(myrun,"窗口1");Thread t2 = new Thread(myrun,"窗口2");Thread t3 = new Thread(myrun,"窗口3");t1.start();t2.start();t3.start();}
}
class MyRunnable1 implements Runnable{//static int ticket = 10;//情形3:静态变量由多个对象共享,适合Thread子类创建线程对象int ticket = 10;//情形2、4:当只创建一个对象放在堆中,是多个线程共享的属性@Overridepublic void run(){//放在方法内是局部变量,在栈中,不是多线程之间共享的区域!如果创建三个线程,那么就是每个线程都有一个独立的100//int ticket = 100;//情形1:局部变量不能共享while(true){try {//被阻塞。Thread.sleep(10);//加入这个,使得问题暴露的更明显} catch (InterruptedException e) {throw new RuntimeException(e);}if(ticket>0){System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket);//ticket.ticket是情形5ticket--;}else{break;//跳出循环}}}
}

①抽取资源类,在主线程中定义内部类(继承Thread/实现Runnable接口)

//1、编写资源类
class Ticket {private int ticket = 100;public void sale() {if (ticket > 0) {try {Thread.sleep(10);//加入这个,使得问题暴露的更明显} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);ticket--;} else {throw new RuntimeException("没有票了");}}public int getTicket() {return ticket;}
}
//在主线程中定义Thread内部类:因为资源已经被封装,无需写重复代码!
public class SaleTicketDemo5 {public static void main(String[] args) {//2、创建资源对象Ticket ticket = new Ticket();//3、启动多个线程操作资源类的对象Thread t1 = new Thread("窗口一") {public void run() {while (true) {ticket.sale();}}};Thread t2 = new Thread("窗口二") {public void run() {while (true) {ticket.sale();}}};Thread t3 = new Thread(new Runnable() {public void run() {ticket.sale();}}, "窗口三");t1.start();t2.start();t3.start();}
}

4.2 解决方法:同步机制(同步代码块/同步方法)sychronized

补充:

  • Java对象在堆中的数据部分:对象头、实例变量、空白的填充,其中对象头包含:Mark Word(和当前对象有关的GC、锁标记等信息)、指向类的指针(记录由哪个类创建的)、数组长度(只有数组才有)
  • 创建对象的过程:①分配堆内存空间②内存空间初始化③指向堆内存中对象的首地址。在不影响执行顺序的情况下是可以进行指令重排的比如①③②,这也会导致一系列问题,可以用volite(禁止指令重排)、synchronized、lock解决

1、Java是如何解决线程安全的问题?同步机制的原理
相当于给某段代码加“锁”,任何线程想要执行这段代码,都要先获得“锁”(同步锁)。哪个线程获得了“同步锁”对象之后,”同步锁“对象就会记录这个线程的ID,这样其他线程就只能等待了,除非这个线程”释放“了锁对象,其他线程才能重新获得/占用”同步锁“对象。
为什么可以给对象加锁?
因为Java对象在堆中的对象头会记录和当前对象有关的锁信息。
重点关注两个事:共享数据及操作共享数据的代码;同步监视器(保证唯一性)
线程同步机制的原理
2、具体实现方式:共享的资源、同步操作的代码(操作共享资源),同步锁必须唯一可以是任意一个对象(也可以用共享资源对象)

  • 同步代码块:synchronized 关键字可以用于某个区块前面,表示只对这个区块的资源实行互斥访问。
    • 需要被同步代码块:即为操作共享数据的代码。同一时刻只能有一个线程操作这些代码,其它线程必须等待。
    • 同步锁,也称同步监视器,哪个线程获取哪个执行,多个线程必须共用一个,可以是任何一个类的对象,必须是唯一的才能锁住。
    • 共享数据:即多个线程多需要操作的数据。比如:ticket
    • 注意点:同步锁可以是:实现Runnable接口方法中,this;继承Thread类中,“类名.Class”
  • 同步方法:如果操作共享数据的代码完整的声明在了一个方法中,那么我们就可以将此方法声明为同步方法即可。

3、何时释放锁?在当前线程的同步方法、同步代码块中

  • 执行结束
  • 遇到break、return终止了该代码块、该方法的继续执行。
  • 出现了未处理的Error或Exception,导致当前线程异常结束。
  • 执行了锁对象的wait()方法,当前线程被挂起,并释放锁。

4、不会释放锁的操作

  • 调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。
  • 其他线程调用了该线程的suspend()方法将该该线程挂起,该线程不会释放锁(同步监视器)。
  • 应尽量避免使用suspend()和resume()这样的过时来控制线程。
  • 注意:锁肯定都是加在重写的run()方法中的,同步方法要在run()中调用

同步机制的两种实现方式:同步代码块、同步方法。

//法1:同步代码块
synchronized(同步锁){//这个同步锁必须是唯一的,可以是this当前类的对象//需要同步操作的代码
}
//法2:同步方法:锁对象——非静态方法是当前类的对象,静态方法是一个类的Class对象(在内存中只有一个)
public synchronized void method(){//可能会产生线程安全问题的代码
}

实现三个窗口同时售卖火车票

法1:同步代码块:①Runnable实现类,this;②使用Thread子类把同步锁改为"类名.class"再创建线程对象即可!
法2:同步方法:①Runnable实现类,定义非静态方法加锁;②使用Thread子类,定义为静态方法加锁!

public class OverrideTest {public static void main(String[] args){MyRunnable1 myrun = new MyRunnable1();Thread t1 = new Thread(myrun,"窗口1");Thread t2 = new Thread(myrun,"窗口2");Thread t3 = new Thread(myrun,"窗口3");t1.start();t2.start();t3.start();}
}
//法1:同步代码块
class MyRunnable1 implements Runnable{static int ticket = 100;//当只创建一个对象放在堆中,是多个线程共享的属性Object obj = new Object();@Overridepublic void run(){while(true){//直接锁这里,肯定不行,会导致,只有一个窗口卖票synchronized (this){//同步监视器:唯一性。如果是Thread子类使用"类名.class"作为同步锁if(ticket>0){System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket);ticket--;}else{break;//跳出循环}}}}
}//法2:同步方法
class MyRunnable1 implements Runnable{static int ticket = 100;//当只创建一个对象放在堆中,是多个线程共享的属性@Overridepublic void run(){while(ticket>0){saleOneTicket();}}//锁对象是this,这里就是TicketSaleRunnable对象,因为上面3个线程使用同一个TicketSaleRunnable对象,所以可以public synchronized void saleOneTicket(){if(ticket>0){//不加条件,相当于条件判断没有进入锁管控,线程安全问题就没有解决System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket);ticket--;}}
}

练习题:银行有一个账户。有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
问题:该程序是否有安全问题,如果有,如何解决?
【提示】

  • 1,明确哪些代码是多线程运行代码,须写入run()方法
  • 2,明确什么是共享数据。
  • 3,明确多线程运行代码中哪些语句是操作共享数据的。

法1:继承Thread子类:采用同步代码块,抽取共享资源,封装共享操作代码

//继承Thread子类中的重写run()方法部分,创建线程对象方式Customer cus1 = new Customer(account,"甲");
class Customer extends Thread{Private Account account;//使用一个实例创建Thread多个对象,唯一的共享资源int sum = 3000;//创建三个线程,因此每个线程一份public Customer(Account account, String name){super(name);this.account = account;}@Overridepublic void run(){while(sum > 0){synchronized (Customer.class){//这里是唯一的account.deposit(1000);sum -= 1000;}}}
}
//封装共享资源和共享操作代码
class Account{private double balance;//余额public void deposit(double amt){if(amt>0){this.balance += amt;System.out.println(Thread.currentThread().getName()+"存钱"+amt+",余额为:"+ balance);}}
}

法2继承Thread子类:采用同步方法

//重写的run方法部分
@Override
public void run(){for(int i = 0; i < 3; i++){//放心地用,i是局部变量,每个线程私有account.deposit(1000);}}
//修改同步方法
class Account{private double balance;//余额public synchronized void deposit(double amt){//这里是唯一的,默认为Account.classif(amt>0){this.balance += amt;System.out.println(Thread.currentThread().getName()+"存钱"+amt+",余额为:"+ balance);}}
}

4.3 解决方法:同步机制Lock

1、定义:JDK5.0新特性,保证线程的安全,也称同步锁。通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。

  • java.util.concurrent.locks.Lock接口:控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。线程安全控制中常用ReentrantLock,可以显式加锁、释放锁。
  • ReentrantLock类:实现了 Lock 接口,拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。还提供了在激烈争用情况下更佳的性能。

步骤1,创建Lock的实例,需要确保多个线程共用同一个Lock实例!需要考虑将此对象声明为static final
步骤2.执行lock()方法,锁定对共享资源的调用
步骤3.unlock()的调用,释放对共享数据的锁定

Lock语法格式
注意:如果同步代码有异常,要将unlock()写入finally语句块。

class A{//1. 创建Lock的实例,必须确保多个线程共享同一个Lock实例private static final ReentrantLock lock = new ReenTrantLock();public void m(){//2. 调动lock(),实现需共享的代码的锁定lock.lock();try{//保证线程安全的代码;}finally{//放在这里面确保它一定会执行!//3. 调用unlock(),释放共享代码的锁定lock.unlock();  }}
}

2、Lock与synchronized对比
synchronized:隐式锁,出了作用域、遇到异常自动解锁。无论是同步代码块还是同步方法中,只有出了作用域才会释放锁。【调用监视器的wait()方法也可以释放,等notify()后在从这里执行】
Lock:显示锁,通过手动开启,手动关闭锁,释放对同步监视器的调用,更灵活。Lock有多种获取锁的方式(如从sleep线程中抢到锁),另外一个不行
Lock作为接口提供了多种实现类,适合更复杂的场景。JVM调用较少时间调度线程,性能更好,有很好的扩展性。
Lock可以对读锁不加锁,但是synchronized不行,
建议:Lock→同步代码块→同步方法

4.4 同步机制的应用(懒汉式单例模式-线程安全)

单例模式

饿汉式VS懒汉式:懒汉式可以理解为是饿汉式的一种延迟new对象的操作,其中懒汉式是线程不安全的,饿汉式是线程安全的!
懒汉式存在线程安全问题,需要同步机制来处理
枚举类的本质还是饿汉式,只不过饿汉式提供了方法调用,而枚举类是直接调用该对象。
法三:双重锁检验:为了避免指令重排需要将instance声明为volatile,再使用synchronized加同步锁。

//内部类方式
public class LazySingle {private LazySingle(){}public static LazySingle getInstance(){return Inner.INSTANCE;}private static class Inner{static final LazySingle INSTANCE = new LazySingle();}
}
//枚举类方式:本质是饿汉式
public enum HungryOne{INSTANCE;//这一句就相当于声明为class类的下面的两句//public static final HungryOne INSTANCE = new HungryOne();//private HungryOne(){};}
//饿汉式
public class Singleton{private Singleton(){}//私有化构造器private static Singleton instance = new Singleton();//对象是否声明为final 都可以public static Singleton getInstance(){return instance;}
}
//懒汉式
public class Singleton{private Singleton(){}//私有化构造器private static Singleton instance = null;//new延迟public static Singleton getInstance(){if(instance == null){return new Singleton();//延迟到这里就可能出现线程安全问题}return instance;}
}//法一:同步方法:解决懒汉式线程安全问题
public class Singleton{private Singleton(){}//私有化构造器private static Singleton instance = null;//new延迟public static synchronized Singleton getInstance(){//同步锁是唯一的:Singleton.classif(instance == null){instance = new Singleton();}return instance;}
}
//法二:同步代码块:解决懒汉式线程安全问题、及其优化点
public class Singleton{private Singleton(){}//私有化构造器private static Singleton instance = null;//new延迟public static Singleton getInstance(){//同步锁是唯一的:Singleton.classsynchronized (Singleton.class){if(instance == null){instance = new Singleton();}return instance;//可以放在同步代码块外也行}}
}
//法三:同步代码块的优化点:由于指令重排的问题会导致在new对象的过程中,线程1创建了对象但还没初始化完成,线程2判断发现不为null,直接出去了,就有问题!解决办法:volatile关键字
public class Singleton{private Singleton(){}//私有化构造器private static volatile Singleton instance = null;//new延迟,其中volatile 避免指令重排public static Singleton getInstance(){//同步锁是唯一的:Singleton.classif(instance == null){synchronized (Singleton.class){if(instance == null){instance = new Singleton();}}}return instance;}
}

4.5 同步机制引发的问题:死锁

1、死锁定义:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
发生在多线程并发时,对对方共享资源的争夺

2、死锁原因及解决方案

秀发死锁的原因解决死锁
占用互斥条件无法被破坏,因为线程需要通过互斥解决安全问题。
占用且等待一次性申请所有所需资源,就不存在等待的问题
不可抢夺(不可抢占)占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。
循环等待将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。

五、线程的通信

5.1 线程通信机制

1、线程间通信的理解:多线程共同完成同一个任务,那么多线程之间需要一些通信机制,协调它们的工作,以此实现多线程共同操作一份数据。
2、实现方式:声明在0bject类中的方法。notify()适用于交替工作

  • wait():线程进入等待状态。同时,会释放对同步监视器的调用。线程状态进入 WAITING 或 TIMED_WAITING
  • notify():唤醒被wait()的线程中优先级最高的那一个线程。(如果被wait()的多个线程的优先级相同随机唤醒一个)。被唤醒的线程从当初被wait的位置继续执行。如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE(可运行) 状态;否则,线程就从 WAITING 状态又变成 BLOCKED(等待锁) 状态
  • notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。

注意:以上是配合synchronized使用,而Lock需要配合Condition实现线程间的通信

  • wait方法与notify方法必须要由同一个锁对象(同步监视器)调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
  • wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
  • wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。否则会报java.lang.IllegalMonitorStateException异常。

被通知的线程被唤醒后也不一定能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以它需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。

5.2 场景题:交替打印&生产者消费者

问题1:两个线程交替打印1-10

public class PrintNum {public static void main(String[] args){PrintNumObj pri = new PrintNumObj();Thread th1 = new Thread(pri,"线程1");Thread th2 = new Thread(pri,"线程2");th1.start();th2.start();}
}
class PrintNumObj implements Runnable{int i = 1;@Overridepublic void run() {while(true){synchronized (this){//this可以省略,由同步监视器调用this.notify();//唤醒被wait()的线程中优先级最高的那一个线程if(i<=10){System.out.println(Thread.currentThread().getName()+"打印:"+i);i++;}else{break;}try {//由同步监视器调用this.wait();//线程进入等待状态} catch (InterruptedException e) {throw new RuntimeException(e);}}}}
}

问题2:生产者和消费者问题:生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。【店员可以理解为操作共享数据】
分析:多线程、共享数据、有线程安全问题【同步机制】、线程通信

  • 解决的问题:
    线程安全问题:共享的数据缓冲区,产生的安全问题→同步解决
    线程的协调工作问题:生产者缓冲区满时wait(),等到下次消费者消耗缓冲区数据时notify()正在等待的线程恢复到就绪状态,开始向缓冲区添加数据。同理,消费者在缓冲区空时wait(),进入阻塞状态,等到生产者添加数据时notify()正在等待的消费者恢复到就绪状态。
  • 原理:生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

生产者消费者:采用Thread子类,
但是如果采用Runnable的话如何区分不同的线程对象从而进行不同的消费/生产操作?

public class PrintNum {public static void main(String[] args){Clerk clerk = new Clerk();Thread th1 = new Customer(clerk);Thread th2 = new Producer(clerk);th1.start();th2.start();}
}
//封装共享资源对象,及对共享资源操作的业务代码
class Clerk{int productNum=0;public synchronized void addProductNum(){if(productNum >= 20){//共享内存区域满了try {this.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}else{this.notifyAll();productNum++;System.out.println(Thread.currentThread().getName()+"生产了了第"+productNum+"个产品");}}public synchronized void minusProductNum(){if(productNum <= 0){//共享内存区域空了try {this.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}else{this.notifyAll();System.out.println(Thread.currentThread().getName()+"消费了第"+productNum+"个产品");productNum--;}}
}
//消费者线程类
class Customer extends Thread{Clerk clerk;//体现共享区域public Customer(Clerk clerk){this.clerk = clerk;}@Overridepublic void run(){for (int i = 0; i < 20; i++) {//这里加个限制,就执行二十次,免得太多了clerk.minusProductNum();}}
}
//生产者线程类
class Producer extends Thread{Clerk clerk;//体现共享区域public Producer(Clerk clerk){this.clerk = clerk;}@Overridepublic void run(){for (int i = 0; i < 20; i++) {//这里加个限制,就执行二十次,免得太多了clerk.addProductNum();}}
}

六、企业真题

1、什么是程序、进程、线程?

  • 程序(program):为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码。
  • 进程(process):程序的一次执行过程,或是正在内存中运行的应用程序。程序是静态的,进程是动态的。进程作为操作系统调度和分配资源的最小单位
  • 线程(thread):进程可进一步细化为线程,是程序内部的一条执行路径。线程作为CPU调度和执行的最小单位

2、多线程使用场景

  • 手机app应用的图片的下载
  • 迅雷的下载
  • Tomcat服务器上web应用,多个客户端发起请求,Tomcat针对多个请求开辟多个线程处理

3、如何在Java中出实现多线程?
Thread类、Runnable接口都是重写run()方法
Callable接口是实现call()方法
线程池创建,关于线程池的优点

  • 提高了程序执行的效率。(因为线程已经提前创建好了)
  • 提高了资源的复用率。(因为执行完的线程并未销毁,而是可以继续执行其他的任务)
  • 可以设置相关的参数,对线程池中的线程的使用进行管理

4、Thread类中的start()和run()有什么区别?
start():① 开启线程 ② 调用线程的run()

5、Java中Runnable和Callable有什么不同?
Callable对于Runnable方式的好处

  • call():与run()方法对比可以有返回值,更灵活
  • call():可以使用throws的方式处理异常(抛出)
  • 支持泛型的返回值:Callable使用了泛型参数,可以指明具体的call()的返回值类型,更灵活。(需要借助FutureTask类,获取返回结果)

缺点:如果在主线程中需要获取分线程call()的返回值,则此时的主线程是阻塞状态的。

6、sleep() 和 yield()区别?(神*泰岳)
sleep():一旦调用,就进入“阻塞”(或TIMED_WAITING状态)
yield():释放cpu的执行权,处在RUNNABLE的状态

7、线程的生命周期?线程的基本状态以及状态之间的关系?
线程的几种状态

7、stop()和suspend()方法为何不推荐使用?
stop():一旦执行,线程就结束了,导致run()有未执行结束的代码。stop()会导致释放同步监视器,导致线程安全问题。
suspend():与resume()搭配使用,导致死锁。

8、Java 线程优先级是怎么定义的?
三个常量。[1,10]

9、你如何理解线程安全的?线程安全问题是如何造成的?
多线程并发在访问这些共享的资源时就会出现问题。

10、多线程共用一个数据变量需要注意什么?
线程安全问题

11、多线程保证线程安全一般有几种方式?

  • 同步机制:synchronized修饰同步方法、同步代码块
  • Lock接口

两种对比:synchronized和ReentrantLock有什么不同
synchronized不管是同步代码块还是同步方法,都需要在结束一对{}之后,释放对同步监视器的调用。
Lock是通过两个方法控制需要被同步的代码,更灵活一些。
Lock作为接口,提供了多种实现类,适合更多更复杂的场景,效率更高。

类似问题:
> 如何解决其线程安全问题,并且说明为什么这样子去解决?(北京联合**)
> 请说出你所知道的线程同步的方法。(天*伟业)
> 哪些方法实现线程安全?(阿*)   
> 同步有几种实现方法,都是什么? (锐*企业管理咨询)
> 你在实际编码过程中如何避免线程安全问题?(*软国际)
> 如何让线程同步?(*手)
> 多线程下有什么同步措施(阿*校招)
> 同步有几种实现方法,都是什么?(海*科)

12、synchronized加在静态方法和普通方法区别
同步监视器不同。静态:当前类本身 非静态:this

13、当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法?
需要看其他方法是否使用synchronized修饰,同步监视器的this是否是同一个。
只有当使用了synchronized,且this是同一个的情况下,就不能访问了。

14、线程同步与阻塞的关系?同步一定阻塞吗?阻塞一定同步吗?
同步一定阻塞;阻塞不一定同步。

15、什么是死锁,产生死锁的原因及必要条件
(1)如何看待死锁?
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
我们编写程序时,要避免出现死锁。

(2)诱发死锁的原因?

  • 互斥条件
  • 占用且等待
  • 不可抢夺(或不可抢占)
  • 循环等待

以上4个条件,同时出现就会触发死锁。
(3)如何避免死锁?
针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。
针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。
针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。

16、Java中notify()和notifyAll()有什么区别
notify():一旦执行此方法,就会唤醒被wait()的线程中优先级最高的那一个线程。(如果被wait()的多个线程的优先级相同,则
随机唤醒一个)。被唤醒的线程从当初被wait的位置继续执行。
notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。

17、为什么wait()和notify()方法要在同步块中调用
因为调用者必须是同步监视器。

18、多线程:生产者,消费者代码(同步、wait、notifly编程)
思路:封装共享资源clerk:产品数量productNum、对产品的add、minus
创建线程生产者、消费者,其中条件分别有共享空间满/为空时需wait(),执行后还需要notify()

19、wait()和sleep()有什么区别?调用这两个函数后,线程状态分别作何改变?
相同点:一旦执行,当前线程都会进入阻塞状态
不同点:

  • 声明的位置:wait():声明在Object类中sleep():声明在Thread类中,静态的
  • 使用的场景不同:wait():只能使用在同步代码块或同步方法中sleep():可以在任何需要使用的场景
  • 使用在同步代码块或同步方法中:wait():一旦执行,会释放同步监视器sleep():一旦执行,不会释放同步监视器
  • 结束阻塞的方式:wait(): 到达指定时间自动结束阻塞 或 通过被notify唤醒,结束阻塞sleep(): 到达指定时间自动结束阻塞

20、手写一个单例模式(Singleton),还要安全的.&手写一个懒汉式的单例模式&解决其线程安全问题,并且说明为什么这样子去解决
饿汉式;安全的懒汉式;内部类;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/4517.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

揭示C++设计模式中的实现结构及应用——行为型设计模式

简介 行为型模式&#xff08;Behavioral Pattern&#xff09;是对在不同的对象之间划分责任和算法的抽象化。 行为型模式不仅仅关注类和对象的结构&#xff0c;而且重点关注它们之间的相互作用。 通过行为型模式&#xff0c;可以更加清晰地划分类与对象的职责&#xff0c;并…

易错知识点(学习过程中不断记录)

快捷键专区&#xff1a; 注释&#xff1a;ctrl/ ctrlshift/ 保存&#xff1a;ctrls 调试&#xff1a; 知识点专区&#xff1a; 1基本数据类型 基本数据类型有四类&#xff1a;整型、浮点型、字符型、布尔型&#xff08;Boolean&#xff09;&#xff0c; 分为八种&#xff…

AI图书推荐:《企业AI转型:如何在企业中部署ChatGPT?》

Jay R. Enterprise AI in the Cloud. A Practical Guide...ChatGPT Solutions &#xff08;《企业AI转型&#xff1a;如何在企业中部署ChatGPT&#xff1f;》&#xff09;是一本由Rabi Jay撰写、于2024年由John Wiley & Sons出版的书籍&#xff0c;主要为企业提供实施AI转型…

2024.4.28

有以下类&#xff0c;完成特殊成员函数 #include <iostream>using namespace std; class Person{string name;int* age; public:Person():name("zhangsan"),age(new int(18)){}Person(string name,int* age):name(name),age(new int(*age)){}~Person(){delete…

接口测试-笔记

Date 2024年4月23日21:19:51 Author KarrySmile 1. 前言 因为想更加规范地开发接口&#xff0c;同时让自己测试接口的时候更加高效&#xff0c;更好地写好接口文档。所以学习黑马的《接口自动化测试》课程。链接&#xff1a;黑马程序员软件测试接口自动化测试全套视频教程&a…

Redis运维篇-快速面试笔记(速成版)

文章目录 1. Redis的持久化1.1 RDB&#xff08;快照模式&#xff09;1.2 AOF 模式 2. Redis主从模型&#xff08;高可用&#xff09;2.1 Redis的主从复制2.2 Redis拓扑结构 3. Redis集群模式&#xff08;高并发&#xff09;3.1 Redis的Slots3.2 集群模式的常用命令3.3 多主多从…

STL_List与萃取

List 参考文章: https://blog.csdn.net/weixin_45389639/article/details/121618243 List源码 List中节点的定义&#xff1a; list是双向列表&#xff0c;所以其中节点需要包含指向前一节点和后一节点的指针&#xff0c; data是节点中存储的数据类型 template <class _Tp&g…

北京车展“第一枪”:长安汽车发布全球首款量产可变新汽车

4月25日&#xff0c;万众瞩目的2024北京国际汽车展览会在中国国际展览中心如期而至。作为中国乃至全球汽车行业的盛宴&#xff0c;本次车展也吸引了无数业内人士的高度关注。 此次北京车展以“新时代 新汽车”为主题&#xff0c;汇聚了1500余家主流车企及零部件制造商&#xff…

【R语言】对EXCEL多行或多列数据合并成一行或一列

对于很多行或很多列数据合并成一行或一列数据&#xff0c;手动是非常麻烦的&#xff0c;尤其当行列数无穷大&#xff0c;根本无法手动处理&#xff0c;在这里价绍一种解决办法&#xff1a;运行R语言&#xff0c;对数据的快速合并。 这里一多列合并成一列为例&#xff08;如果是…

Linux基本指令(2)

目录 mv指令&#xff1a; cat&#xff1a; more指令&#xff1a; less指令&#xff1a; head指令&#xff1a; tail指令&#xff1a; mv指令&#xff1a; 说明&#xff1a; mv命令是move的缩写&#xff0c;可以用来移动文件或者文件改名(move(rename)files),是linux系统下…

(二十一)C++自制植物大战僵尸游戏僵尸游戏关卡结束数据处理

植物大战僵尸游戏开发教程专栏地址http://t.csdnimg.cn/8UFMs 文件位置 代码实现的文件在Class\Scenes\GameScene文件夹中,如下图所示。 GameEndLayer.h class GSGameEndLayer :public LayerColor { public:CREATE_FUNC(GSGameEndLayer);void successfullEntry();void brea…

必应bing广告推广开户时间需要多久?

企业选择合适的平台进行广告投放成为了企业获取竞争优势的关键一步&#xff0c;必应Bing作为全球第二大搜索引擎&#xff0c;凭借其庞大的用户基础和精准的广告定位能力&#xff0c;成为了众多企业海外及国内市场推广的优选渠道。云衔科技以专业、高效的服务&#xff0c;成为企…

科技感十足特效源码

源码介绍 科技感十足特效源码&#xff0c;源码由HTMLCSSJS组成&#xff0c;记事本打开源码文件可以进行内容文字之类的修改&#xff0c;双击html文件可以本地运行效果&#xff0c;也可以上传到服务器里面 源码截图 源码下载 科技感十足特效源码

Linux:冯诺依曼体系结构、操作系统、初识进程

文章目录 1.冯诺依曼体系结构总线与数据传输通路为什么有内存这个部分计算机存储结构 2.操作系统(Operator System)2.1 概念2.2 设计OS的目的2.3 理解“管理”先描述再组织 2.4 用户使用系统调用和库函数&#xff08;lib&#xff09;概念 总结 3.初识进程3.1 基本事实与引入3.2…

ZYNQ之嵌入式开发05——串口中断、定时器中断、QSPI和SD卡读写测试实验

文章目录 UART串口中断实验定时器中断实验PS-XADC实验QSPI Flash读写测试SD卡读写文本文档 UART串口中断实验 UART控制器是一个全双工异步收发器&#xff0c;支持可编程的波特率和IO信号格式&#xff0c;具有独立的TX和RX数据路径&#xff0c;每个路径有一个64字节的FIFO&…

59岁郑浩南罕见与索爆女儿合体,曾自曝婚变暗示妻子出轨人财两空

59岁的郑浩南&#xff0c;拍过不少脍炙人口的电影&#xff0c;尤其是在《古惑仔》中饰演奸角司徒浩南&#xff0c;近作有《黑社会》的「加钱哥」&#xff0c;虽然将近「登六」&#xff0c;却保养得宣&#xff0c;操出一身健硕肌肉。 跟前妻鲍爱玲离婚后&#xff0c;郑浩南独自在…

即插即用Mamba模块全新突破!无缝集成,无痛涨点

Mamba入局图像复原了&#xff01;基于Mamba的图像复原基准模型MambaIR性能超越SwinIR&#xff0c;达成新SOTA&#xff01; MambaIR是一种引入通道注意力和局部增强的即插即用Mamba模块。这类高效、创新的模块在写论文时可以帮助我们简化模型的构建过程&#xff0c;通过将这些模…

Android 11 bindService 流程分析

我们可以使用bindService来跨进程通信&#xff0c;其使用方法如下 Intent intent new Intent("xxx"); intent.setPackage("xxx"); boolean result bindService(intent,new ServiceConn(),BIND_AUTO_CREATE);private class ServiceConn implements Servi…

学习100个Unity Shader (15) ---透明+双面渲染

文章目录 效果shader理解参考 效果 shader Shader "Example/AlphaBlendBothSided" {Properties{_Color ("Main Tint", Color) (1, 1, 1, 1)_MainTex ("Texture", 2D) "white" {}_AlphaScale ("Alpha Scale", Range(0, 1)…

leetcode多个测试用例之间相互影响导致提交失败

背景 在做一道easy题&#xff0c;二叉树的中序遍历&#xff0c;我提交的代码如下 from typing import (Optional,List )# Definition for a binary tree node. class TreeNode:def __init__(self, val0, leftNone, rightNone):self.val valself.left leftself.right right…