一 前述
程序:是为了完成特定任务,某种语言编写的一组指令的集合,指的是一组静态代码,静态对象 进程:是程序的一次执行过程,或者一个正在运行的程序,是一个动态的过程,具有自己的生命周期 线程:进程可以进一步分化为线程,是程序内部的一条执行路径
1)线程和进程之间的说明:
- 进程作为资源分配的基本单位,系统在运行时会为每个进程分配不同的内存区域
- 线程是调度和执行的单位,每个线程都拥有自己的运行栈和程序计数器,线程切换开支小
2)管程:
- monitor(监视器)其实就是平时说的锁,他的义务是保证在同一时刻上,只有一个线程可以访问到被保护的数据和代码。
- JVM的同步是基于进入和退出监视器对象(monitor,管程对象)来实现的,每个对象实例都会有一个monitor对象。
monitor对象和java对象一同被创建并销毁,底层是C++实现的
1.1 单核CPU和多核CPU
1)单核CPU,其实是因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是一种假的多线程,收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员。如果有某个人不想交钱,那么收费人员可以把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费)。但是因为CPU时间单元特别短,因此感觉不出来。
2)如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
3)一个Java应用程序java.exe,其实至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。1.2 并行和并发
1)并发:一个CPU在同时(采用时间片)执行多个任务
并发编程的本质:充分利用CPU资源
2)并行:多个CPU同时执行多个任务
// 获取cpu的核数
System.out.println(Runtime.getRuntime().availableProcessors());
1.3 线程分类
一般Java程序启动时,最少存在两个线程:main主线程 + GC守护线程
1)用户线程:工作线程,会完成特定的代码执行
2)守护线程:特殊线程,在后台默默完成一些系统性的服务,比如垃圾回收线程
- 两者的异同
- 两者几乎是一样的,唯一区别就是判断JVM在何时离开
- 守护线程是来服务用户线程的,在start()方法调用前通过setDaemon(true)可以将用户线程转为守护线程
- Java的垃圾回收器就是一个典型的守护线程
- 若jvm中都是守护线程,那当前jvm将退出
二 创建线程
Java允许程序运行多个线程,通过java.lang.Thread类实现。
Thread类:
- 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体
- 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
Thread类的构造器:
1)Thread():创建新的Thread对象
2)Thread(String threadName):创建线程并指定线程实例名
3)Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口的run()方法
4)Thread(Runnable target, String name):创建新的Thread对象
2.1 两种传统方式
在jdk1.5之前,创建线程只有传统的两种方式
方式一:继承Thread类
步骤:
- 创建子类继承于Thread类
- 在子类中重写Thread类中的run方法
- 创建子类的实例对象
使用子类对象调用父类中的start()方法开启子线程
public static void main(String[] args) {
new MyThread().start();
}
static class MyThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
方式二:实现Runnable接口
步骤:
创建Runnable接口的实现类
- 实现run方法
- 把Runnable接口实现类的实例作为参数传递进Thread实例中,启动Thread的start()方法启动子线程
new Thread(new Runnable() {
@Override
public void run() {
// 线程逻辑
}
}).start();
2.2 方式比较
1)相同点:都需要进行方法的重写;都需要调用Thread类的start方法来启动线程
2)不同点:类可以实现多接口,但只能继承一个父类
3)结论:实现Runnable的方式要好于继承Thread的方式
- 线程启动问题
1)能否通过直接用new Thread().run()的方式启动线程,并执行相关的逻辑?No!
2)再启动一个分线程,用于遍历100以内的偶数。能否让已经start()的线程再次执行start()? No!
3)每个线程只能被start()一次,一旦被多次调用start(),会报:illegalThreadStateException
2.3 JDK1.5新增的两种方式
方式三:实现callable接口
步骤:
①创建Callable接口的实现类
②实现call方法,线程的操作都存在此方法中
③将callable实现类对象传递进FutrueTask构造器中,创建FutrueTask对象
④将FutureTask对象传递进Thread构造器中,创建Thread对象,并调用start方法启动线程
⑤如果需要获取返回值的话,调用FutrueTask对象的get()方法返回call方法中返回call方法中返回的值
public class CallableTest {
public static void main(String[] args) throws Exception {
// 1 创建Callable接口的实现类,实现call()方法
Callable callable = () -> {
int sum = 0;
for (int i = 0; i < 50; i++) {
sum += i;
}
return sum;
};
// 2 需要FutureTask的支持,用于接收返回结果
FutureTask<Integer> futureTask = new FutureTask(callable);
new Thread(futureTask).start();
Integer sum = futureTask.get(); // 获取call()方法的返回值
System.out.println(sum);
}
}
方式四:使用线程池的方式创建线程
步骤:
public static void main(String[] args) {
// 1、提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
// 设置线程池的属性
// System.out.println(service.getClass());//ThreadPoolExecutor
service1.setMaximumPoolSize(20);
// 2、执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread()); // 适合适用于Runnable
service.execute(new NumberThread1()); // 适合适用于Runnable
// service.submit(Callable callable); // 适合使用于Callable
// 3、关闭连接池
service.shutdown();
}
使用线程池的好处
1) 降低资源的消耗,使用完的线程可以被复用
2) 提高了线程的响应速度
3) 便于管理
三 线程的生命周期
在JDK中,Thread.Status类定义了线程的几种状态。
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
1)新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
2)就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
3)运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
4)阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
5)死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
3.1 Thread.State内部枚举类
// Thread类中的枚举类源码,有6种状态
public enum State {
NEW, // 新建
RUNNABLE, // 运行
BLOCKED, // 阻塞
WAITING, // 等待
TIMED_WAITING, // 超市等待
TERMINATED; // 终止
}
四 常用方法
Thread类的相关方法
- start():启动线程;调用线程中的run()run() // 将线程要执行的操作声明在此方法中
- currentThread():获取执行当前代码的线程,静态方法
- getName():获取当前线程的名字
- setName():设置当前线程的名字
- sleep(long milisecond):一旦执行此方法,当前线程就阻塞指明的毫秒数,静态方法
- yield():每当执行此方法时,线程主动释放cpu的执行权,进入就绪状态
- join():在线程a中调用线程b的join(),此时线程a进入阻塞状态,直到线程b执行结束以后,a线程才结束阻塞状态进入就绪状态等待执行
-
五 线程优先级别
线程优先级别
MIN_PRIORITY:1
- NORM_PRIORITY:5
- MAX_PRIORITY:10
获取和设置优先级别
- getPriority():获取
- setPriortiy(int priority):设置
调整策略
- 高优先级的线程要抢占低优先级线程的策略。
高优先级的线程只是从概率上来讲,要更大概率的比低优先级线程先执行。并不是100%的一定优先于低优先级线程执行。
六 线程安全
当多线程共同操作共享数据时,容易出现安全问题,这时候需要使用线程同步机制来处理线程安全问题
问题:如火车站窗口的售票问题。6.1 synchronized机制
1)同步代码块
// 语法
synchronized (对象){
// 需要被同步的代码
}
2)同步方法
// 语法
public synchronized void show (String name){
// 同步代码
}
6.3 同步机制中的锁
在《Thinking in Java》中,是这么说的:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。
synchronized中的锁:
1)任意对象都可以作为同步锁,所有对象都自动的含有单一的锁(监视器)
2)同步方法的锁:不能显式定义,只能是默认的锁。
①对于非静态同步方法来说,默认同步监视器是:this
②对于静态同步方法来说,默认同步监视器是:当前类本身,也就是:类名.class
3)同步代码块:自己指定,很多时候,也可以定为this,或者类名.class
注意:无论是默认的锁还是自己指定的锁,都是必须遵循访问同一个资源的多个线程共用一把锁的原则,只有这样子才能确保共享资源的安全。6.4 释放锁的操作
1)当前线程的同步代码块,同步方法执行结束。
2)当前线程的同步代码块,同步方法在执行过程中遇到break,return语句终止了程序的同步代码继续执行。
3)当前线程的同步代码块,同步方法在执行过程中遇到未处理的Error,Exception,导致异常结束。
4)当前线程的同步代码块,同步方法在执行过程中执行了线程对象的wait()方法,当前线程暂停,并释放了锁。6.5 不会释放锁的操作
1)线程的同步代码块,同步方法在执行过程中调用了Thread.sleep(),Thread.yield()方法,暂停了当前线程的执行,此时锁不会被释放。
2)线程在执行同步代码块时,其他的线程调用当前线程的suspend()方法,将当前线程挂起,此时当前线程不会释放锁
(注意:应尽量避免使用suspend(),resume()方法在控制线程)6.6 死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续