Java多线程基础

Java多线程


一、线程介绍及相关概念

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方法,完成该线程执行的逻辑
     */
    @Override
    public 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) {
        //创建自定义线程对象1
        MyThread mt1 = new MyThread("子线程1");
        //开启子线程1
        mt1.start();
        
        //创建自定义线程对象2
        MyThread mt2 = new MyThread("子线程2");
        //开启子线程2
        mt2.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;//实现类的属性,如果只创建一个对象供多个线程使用,就是多个线程之间共享的
    @Override
    public 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();
        //创建线程对象1
        Thread t1 = new Thread(mr, "长江");
        t1.start();
        //创建线程对象2
        Thread t2 = new Thread(mr, "大海");//t1、t2两个线程共享同一个实例
        t2.start();
        for (int i = 0; i < 20; i++) {
            System.out.println("黄河 " + i);
        }
    }
}

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

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

//实现Runnable接口
new Thread(new Runnable(){
	@Override
	public 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()中
    @Override
    public 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:使用线程池

真正生产环境中使用的创建线程的方式,多个线程可以同时执行一个任务,也可以不同的线程执行各自的任务,也可以一个线程执行多个任务。注意:线程池中最好不要放耗时任务,例如网络请求、文件读写,这些可以采用异步请求操作来处理,伊庇民阻塞线程池中得线程。
前面线程只能处理特定的任务,线程池中的线程可以处理不同的任务–由线程池统一管理其生命周期。
步骤:创建线程(Runnable-Thread继承它、Callable)worker --> 新建线程池(如ThreadPoolExecutor)–> 使用线程池的方法execute(Runnable worker )表示提交一个任务到线程池中去(该方法执行的逻辑就是线程池实现原理)
补充知识点:
1、线程池创建线程的两种方式:

  • 通过ThreadPoolExecutor构造函数【推荐】
  • 通过Executor框架的工具类Executors:可以创建多种类型的线程池
    • FixedThreadPool:固定线程数量的线程池。核心线程数=最大线程数。当新任务过来时,如果线程池中有空闲线程立即执行,如果没有,新的任务暂存在任务队列(阻塞队列)–LinkedBlockingQueue无界队列,容量为Integer.MAX_VALUE。
    • SingleThreadExecutor:只有一个线程的线程池。核心线程数=最大线程数=1。若>1个任务被提交到该线程池,那么该任务会被保存在阻塞队列中–LinkedBlockingQueue无界队列。
    • CachedThreadPool: 可根据实际情况调整线程数量的线程池。最大线程数是Integer.MAX_VALUE,可以理解为线程数可以无限扩展。使用的是SynchronousQueue同步队列(作用就是没有空闲线程时新建线程处理任务)。但是若有空闲线程可以复用,会优先使用可以复用的线程。若果没有空闲线程,且又有新任务提交,就会创建新的线程来处理任务。
    • ScheduledThreadPool:给定的延迟后运行任务或者定期执行任务的线程池。使用的阻塞队列是DelayedWorkQueue延迟阻塞队列。

2、推荐第一种方式:更加明确线程池的运行规则,避免资源耗尽风险。必须手动通过它的构造函数来声明。也提供了一些检测线程池运行状态–获取线程池当前得线程数/活跃数、已经执行完成得线程数、正在排队中得线程数。
第二种方式:会有OOM风险。使用工具类Executors创建的内置线程池:

  • FixedThreadPool、SingleThreadExecutor使用无界队列,可能会堆积大量的任务请求,导致OOM。
  • SingleThreadExecutor使用同步队列,会创建大量的线程,导致OOM
  • ScheduledThreadPool使用无界的延迟阻塞队列,任务队列最大长度为Integer.MAX_VALUE,

注:规避OOM风险–使用有界队列,控制线程创建数量。

3、提交任务到线程池中的方法
注:使用ThreadPoolExecutor创建的线程池可以使用execute(Runnable 线程)方法提交任务;工具类Executor框架的工具类Executors可以使用execute()、submit()两种方式。
Executors工具类可以将Runnable对象转换成Callable对象。

  • execute(Runnable):提交无需返回值的任务,无法判断是否被线程池成功执行
  • submit(Runnable/Callable):提交需要返回值的任务,线程池会返回一个Future类型的对象,通过它可以判断任务是否执行成功。其中的get()方法获取返回值–会阻塞当前线程直到任务完成。

4、线程池关闭操作:shutdown() VS shutdownNow();判定

  • shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。
  • shutdownNow() :关闭线程池,线程池的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。
  • isShutDown 当调用 shutdown() 方法后返回为 true。
  • isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true几种常见的内置线程池

5、如何设计?既保证任务不丢丢弃且在服务器有余力时及时处理?–持久化思路

  • 设计一张表间任务存储到MySQL数据库中:实现RejectedExecutionHandler接口自定义拒绝策略(将线程池暂时无法处理(此时阻塞队列已满)的任务入库)–> 继承BlockingQueue实现一个混合式阻塞队列,重写take()方法,取任务时优先从数据库中读取最早的任务,数据库中无任务时再从 ArrayBlockingQueue中去取任务。
  • Redis缓存任务
  • 将任务提交到消息中心

ThreadPoolExecutor 类中提供了四种构造函数,下面为参数最全的一种。其余构造函数在此基础上产生–给给定某些默认参数的构造方法,例如默认制定拒绝策略是什么。
几种参数解释:注意–任务队列存放任务,线程池中运行线程,空闲线程处理队列中的任务

  • corePoolSize:任务队列未达到最大值时,最大可以同时运行的线程数量。
  • maximumPoolSize:任务队列的任务达到最大容量时,当前可以同时运行线程数量变为最大线程数
  • workQueue:如果核心线程数已满,新任务会被存放到队列中。
    阻塞队列如下:】
    • LinkedBlockingQueue(无界队列):队列大小为Integer.MAX_VALUE,任务队列永远不会被放满。理解为任务队列可以无限扩展。
    • SynchronousQueue(同步队列):没有容量,不存储元素,保证对于提交的任务,如果有空闲线程,则处理;否则新建一个线程来处理任务。可以理解为线程数可以无限扩展。
    • DelayedWorkQueue(延迟阻塞队列):按照延迟的时间长短对任务进行排序,内部采用堆结构,保证每次出队的任务都是当前队列中执行时间最靠前的。
  • keepAliveTime:线程数量大于corePoolSize时,如果还没有新任务提交,核心线程外的线程不会被立即销毁,而是会等待。
  • unit:keepAliveTime的单位
  • threadFactory:executor创建新线程用,线程工厂,用来创建线程
  • handler:拒绝策略:同时运行的线程达到最大线程数量且队列已经被放满任务时,定义再过来新任务的处理方式。
    • AbortPolicy(默认):抛出RejectedExecutionException异常拒绝新任务的处理
    • CallerRunsPolicy:调用自己的线程运行任务,在调用execute(线程)方法的线程中运行run被拒绝的任务,如果执行程序关闭则丢弃该任务。–> 缺点:降低对于新任务提交速度(如主线程运行run被拒绝的任务,会导致主线程阻塞,影响程序正常运行)
    • DiscardPolicy:不处理新任务直接丢弃。
    • DiscardOldestPolicy:丢弃最早的未处理的任务请求。
	/**
     * 用给定的初始参数创建一个新的ThreadPoolExecutor。
     */
    public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                              int maximumPoolSize,//线程池的最大线程数
                              //当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                              long keepAliveTime,
                              TimeUnit unit,//时间单位
                              //任务队列,用来储存等待执行任务的队列
                              BlockingQueue<Runnable> workQueue,
                              //线程工厂,用来创建线程,一般默认即可
                              ThreadFactory threadFactory,
                //拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
                              RejectedExecutionHandler handler
                               ) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

线程池创建示例–使用ThreadPoolExecutor创建

import java.util.Date;

/**
 * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。
 * @author shuang.kou
 */
public class MyRunnable implements Runnable {

    private String command;

    public MyRunnable(String s) {
        this.command = s;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
        processCommand();
        System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
    }

    private void processCommand() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String toString() {
        return this.command;
    }
}
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorDemo {

    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;
    public static void main(String[] args) {

        //使用阿里巴巴推荐的创建线程池的方式
        //通过ThreadPoolExecutor构造函数自定义参数创建
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 0; i < 10; i++) {
            //创建WorkerThread对象(WorkerThread类实现了Runnable 接口)
            Runnable worker = new MyRunnable("" + i);
            //执行Runnable
            executor.execute(worker);
        }
        //终止线程池
        executor.shutdown();
        while (!executor.isTerminated()) {
        }
        System.out.println("Finished all threads");
    }
}

Executor框架结构

Future异步思想:当我们执行某一耗时的任务时,可以将这个耗时任务交给一个子线程去异步执行,同时我们可以干点其他事情,不用傻傻等待耗时任务执行完成。等我们的事情干完后,我们再通过 Future 类获取到耗时任务的执行结果。
使用方式:把线程池放进来。

private ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>());
CompletableFuture.runAsync(() -> {
     //...
}, executor);

Future异步思想

Executor框架的使用示意图
Executor框架使用
线程池实现原理
线程池实现原理

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{

    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

class NumberThread1 implements Runnable{

    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

class NumberThread2 implements Callable {
    @Override
    public 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());//ThreadPoolExecutor
        service1.setMaximumPoolSize(50); //设置线程池中线程数的上限

        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于Runnable
        service.execute(new NumberThread1());//适合适用于Runnable

        try {
            Future future = service.submit(new NumberThread2());//适合使用于Callable
            System.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:当只创建一个对象放在堆中,是多个线程共享的属性
    
    @Override
    public 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是情形5
                ticket--;
            }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();
    @Override
    public 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;//当只创建一个对象放在堆中,是多个线程共享的属性
    @Override
    public 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;
	}
	@Override
	public 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.class
        if(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.class
		if(instance == null){
			instance = new Singleton();
		}
		return instance;
	}
}
//法二:同步代码块:解决懒汉式线程安全问题、及其优化点
public class Singleton{
	private Singleton(){}//私有化构造器
	private static Singleton instance = null;//new延迟
	public static Singleton getInstance(){//同步锁是唯一的:Singleton.class
		synchronized (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.class
		if(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;
    @Override
    public 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;
    }
    @Override
    public void run(){
        for (int i = 0; i < 20; i++) {//这里加个限制,就执行二十次,免得太多了
            clerk.minusProductNum();
        }
    }
}
//生产者线程类
class Producer extends Thread{
    Clerk clerk;//体现共享区域
    public Producer(Clerk clerk){
        this.clerk = clerk;
    }
    @Override
    public 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),还要安全的.&手写一个懒汉式的单例模式&解决其线程安全问题,并且说明为什么这样子去解决
饿汉式;安全的懒汉式;内部类;

  • 9
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值