8.1 基本概念
8.1.1 程序、进程、线程
- 程序(program):完成特定任务、用某种语言编写的指令集合。是一段静态的代码。
- 进程(process):程序的一次执行过程,或者正在运行的一个程序。
- 进程是系统分配资源的基本单位,根据进程执行的生命周期,系统会为不同时期的进程分配不同的内存空间。
- 线程(thread):程序内部的一条执行路径,一个进程可以含有多个进程。
- 如果一个进程可以并行执行多个线程,则进程支持多线程。
- 每个线程拥有独立的运行栈和程序计数器。
- 同一进程的多个线程共享相同的堆空间(对象、属性共享)、方法区,优点是线程间通信更便捷、高效,但多个线程同时操作公共资源会有安全隐患。
- Java中线程的分类(区别在于JVM何时离开):
- 守护线程:服务用户线程,通过在start()方法前调用
thread.setDaemon(true)
可以把一个用户线程变成守护线程。
- 垃圾回收是一个典型的守护线程
- JVM中都是守护线程时,JVM就会退出。
- 用户线程:
8.1.2 单核、多核
- 单核:CPU仅有一个核心数,同一时间内,只能执行一个线程任务。执行多个线程时,采取的是不断切换线程的方式。
- 由于CPU频率高、线程切换时间短,让人感觉”同时“执行了多个线程
- 多核:CPU有多个核心,每个核心可以执行一个线程。
- java.exe:一个Java运行程序至少有3个线程:
- main():主线程
- gc():垃圾回收线程
- 异常处理线程:发生异常时,会影响主线程。
8.1.3 并行、并发
- 并行:多个CPU执行多个任务。
- 并发:一个CPU同时执行多个任务。
8.2 创建多线程
8.1 方式一:继承Thread类
- 创建步骤:
- 定义子类继承Thread类
- 子类中重写Thread类中run()方法
- 创建Thread子类对象
- 创建一个对象即代表开启一个线程,要开启多个该线程,需要创建多个该对象。
- 调用子类对象的start()方法。
- 注意点:
- 使用Tread子类对象直接调用
run()
方法不会开启分支线程,它表示在main线程内,调用了Thread子类对象的方法。 - 使用Thread子类对象调用
start()
方法会开启一个线程,开启线程后run()
方法何时执行全由CPU调度决定,即main线程和分支线程中的语句执行具有随机性。 - 一个实例化的Thread子类对象只能调用一次
star()
方法,重复调用时,会抛出异常:IllegalThreadStateException。
- 常用方法:
start()
:启动当前线程;调用当前线程的run()方法run()
:通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中currentThread()
:静态方法,返回执行当前代码的线程getName()
:获取当前线程的名字setName("str")
:设置当前线程的名字yield()
:释放调用线程在cpu中的执行权,后续执行哪个线程由CPU确定,有可能还是这个线程。当前进程进入就绪状态。join()
:在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
- 有异常问题,可以根据使用位置进行throws或try-catch处理。
stop():已过时。当执行此方法时,强制结束当前线程。sleep(long millitime)
:让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。
- 有异常问题,由于该方法使用在
run()
方法中,而run()
方法是对父类Thread中run()
方法的重写,且Thread中run()
方法没有抛出异常,根据继承的特性(子类的异常不大于父类),所以子类的run()
方法不能抛出异常,只能由try-catch处置。
isAlive()
:判断当前线程是否存活。
- 线程优先级:
- 等级:
MAX_PRIORITY
:10MIN _PRIORITY
:1NORM_PRIORITY
:5 (默认等级)
- 方法:
getPriority()
:返回线程优先等级setPriority(int num)
:设置有限等级
- 注意点:设置了高等级的优先级,并不代表一定执行完该线程后执行其他线程,而是提高了CPU执行该线程的概率而已。
- Thread类构造器
Thread()
:Thread(String threadname
):创建指定名称的线程
- 搭配
super(threadname)
才能在getName()时获得名字
Thread(Runnable target)
:Thread(Runnable target, String name)
:
8.2.2 方式二:实现Runnable接口
- 创建步骤:
- 定义类,实现Runnable接口。
- 实现类中实现Runnable接口中的run方法
- 创建实现类对象
- 创建Thread类对象,将实现类对象作为参数传入。
- 使用Thread类的对象调用
start()
方法。
- 两种方式比较:
- 相同点:
- 实现类(继承类)都需要重写
run()
- 都具有线程安全问题
- 不同点:
- 方式二没有单继承的局限性
- 方式二更适合多个线程共享数据(数据只有一份)的情况
- 开发中优先选择方式二
8.2.3 方式三:实现Callable接口
- 创建步骤:
- 创建一个实现Callable的实现类
- 实现call方法,将此线程需要执行的操作声明在call()中
- 创建Callable接口实现类的对象
- 创建FutureTask的对象,将此Callable接口实现类的对象作为传递到FutureTask构造器中
- 创建Thread对象,将FutureTask的对象作为参数传递到Thread类的构造器中,Thread的对象调用start()
- FutureTask的实例对象调用get()方法,获取重写call方法的返回值。
- Callable的优点:
- call()可以返回值的。
- call()可以抛出异常,被外面的操作捕获,获取异常的信息。
- Callable是支持泛型
8.2.4 方式四:使用线程池(ThreadPool)
- 创建步骤:
- 提供指定线程数量的线程池:
ExecutorService service = Executors.newFixedThreadPool(10);
service1.setCorePoolSize(15)
service1.setKeepAliveTime()
- 执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(Runnable runable)
service.submit(Callable callable)
- 关闭连接池:
service.shutdown()
- 优点:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理:
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没任务时最多保持多长时间后会终止
8.3 线程的生命周期
- 新建:
- 继承方式(方式一):Thread类子类的对象被创建。
- 实现方式(方式二):Thread类声明并创建。
- 就绪:处于新建状态的线程调用start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态。
- 阻塞:线程被人为挂起或执行输入输出操作时,会让出 CPU资源,并临时中止自己的执行,即进入阻塞状态。
- 阻塞时临时状态,不可以作为最终状态。
- 死亡:线程完成了它的全部工作、线程被提前强制性地中止、出现异常导致结束。
- 死亡是线程的最终状态。
8.4 线程安全
5.4.1 线程安全问题——同步机制
- 线程安全问题:未处理的多线程任务在处理共享数据时,会造成数据破坏(重复数据、缺失数据、数据超范围等)。
- 原因:处理共享数据的情况时,一个线程多条语句只执行了一部分,未处理完时,另一个线程参与进来,也要处理共享数据,造成共享数据错误。
- 解决办法:单线程处理数据,执行完后再让其他线程参与——同步机制。
- 解决原理:给共享资源加锁,第一个访问资源的线程进行资源锁定,在解锁之前其他线程无法访问,解锁之后,其他线程可以锁定并使用。
8.4.2 Synchronized处理线程安全问题
- Synchronized(同步)语法:
- 同步代码块:
synchronized(同步监视器){}
- 同步方法:
public synchronized void show(){}
- Synchronized细节:
- 同步监视器必须唯一。
- 同步代码块:同步监视器可设置为
类名.class
、this
、任一对象(静态或非静态),取决于是否唯一。 - 同步方法:静态方法同步监视器默认为
类名.class
,非静态方法同步监视器默认为this
- 同步监视器一般情况:
- 在实现Runnable接口创建多线程的方式中,可以考虑使用this充当同步监视器。
- 在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
8.4.3 死锁及lock处理线程安全问题
- 死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源。
- 出现死锁后,不会出现异常、不会出现提示、程序也不会运行,处于阻塞状态,无法继续。
- Lock(JDK5.0新增):
- 引入
java.util.concurrent.locks.ReentrantLock;
包
java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的工具。ReentrantLock
类实现了 Lock。
- 创建
ReentrantLock
对象:private ReentrantLock lock = new ReenTrantLock();
- 根据对象是否唯一(lock是否唯一),可以在声明时使用static、或static final修饰。
- 在出现共享资源操作的代码前调用
lock()
方法 - 在结束共享资源操作的代码后调用
unlock()
方法
- 如果操作资源共享的代码需要使用try包裹,则必须把
unlock()
写入finally语句块,lock()
则不是必须要写入try中
- synchronized与Lock的异同:
- 相同:二者都可以解决线程安全问题
- 不同:
- synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器。
- Lock需要手动的启动同步,同时结束同步也需要手动的实现。
- 使用的优先顺序:Lock ---> 同步代码块(已经进入了方法体,分配了相应资源 ) --->同步方法(在方法体之外)
- 同步代码块包裹的共享资源操作代码可以更小。
8.4.4 同步的深入理解
- 同步的范围:
- 确定同步代码范围时,要将所有操作共享数据的语句包裹在内。
- 范围太大:操作数据的语句变为单线程的,没有发挥多线程的功能。
- 范围太小:操作共享数据的语句由遗漏,同步不起作用。
- 同步的问题:
- 优点:解决了线程安全的问题。
- 缺点:操作同步代码时,只有一个线程运行,其他线程等待,相当于单线程过程,效率低。
- 释放锁的操作:
- 同步方法、同步代码块执行结束
- 同步方法、同步代码块中遇到break、return
- 同步方法、同步代码块中出现未处理的Error或Exception
- 同步方法、同步代码块中执行了线程对象的
wait()
- 不会释放锁的操作:
- 同步方法、同步代码块中调用
Thread.sleep()
、Thread.yield()
方法暂停当前线程的执行 - 其他线程调用了当前执行线程的
suspend()
方法将该线程挂起。
- 应尽量避免使用
suspend()
和resume()
控制线程。
- 线程安全的懒汉式单例模式
classSingleton { privatestaticSingletoninstance=null; privateSingleton() { } // 1. 方式一publicstaticSingletongetInstance() { if (instance==null) { synchronized (Singleton.class) { if (instance==null) { instance=newSingleton(); } } } returninstance; } // 2. 方式二publicstaticSingletongetInstance() { synchronized (Singleton.class) { if (instance==null) { instance=newSingleton(); } } returninstance; } }
- 方式一效率优于方式二,假如有n个线程需要创建当前对象,多核CPU让k个线程运行到
getInstance()
:
- 方式一中共有k个线程判断对象是否等于null,1个线程执行同步代码块并创建对象,k-1个线程执行同步代码块结束判断,后续n-k个线程不会再进入同步代码块。
- 方式二中共有1个线程执行同步代码块并创建对象,k-1个线程执行同步代码块结束判断,后续n-k个线程还会执行同步代码块进行判断。
8.5 线程通信
wait()
:一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。notify()
:一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,由JVM决定执行哪个。notifyAll()
:一旦执行此方法,就会唤醒所有被wait的线程。- 说明:
- 三个方法必须使用在同步代码块或同步方法中。
- 三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException异常
- 三个方法是定义在java.lang.Object类中。
- sleep() 和 wait()的异同
- 相同点:
- 都可以使当前进程进入阻塞状态
- 不同点:
- 声明位置不同:
slee()
声明在Thread类中,wait()
声明在Object类中。 - 调用要求不同:
slee()
可以在任何需要的场景下调用,wait()
必须在同步方法、同步代码块中调用。 sleep()
不会释放同步监视器、wait()
会释放同步监视器。