一、线程的2种实现方式
- 继承 Thread 类
- 实现 Runnable 接口
二、线程的5种状态
- 创建状态
在程序中用构造方法创建了一个线程对象后,新的线程对象便处于新建状态,此时它已经有了相应的内存空间和其他资源,但还处于不可运行状态。新建一个线程对象可采用Thread 类的构造方法来实现,例如 “Thread thread=new Thread()”。
- 就绪状态
新建线程对象后,调用该线程的 start() 方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待 CPU 服务,这表明它已经具备了运行条件。
- 运行状态
当就绪状态被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的 run() 方法。run() 方法定义该线程的操作和功能。
- 阻塞状态
一个正在执行的线程在某些特殊情况下,如被人为挂起或需要执行耗时的输入/输出操作,会让 CPU 暂时中止自己的执行,进入阻塞状态。在可执行状态下,如果调用sleep(),suspend(),wait() 等方法,线程都将进入阻塞状态,发生阻塞时线程不能进入排队队列,只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。
- 死亡状态
线程调用 stop() 方法时或 run() 方法执行结束后,即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。
三、线程相关的方法
- 取得线程的名称
Thread.currentThread().getName()
- join()
在线程操作中,可以使用 join() 方法让一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等待此线程完成之后才可以继续执行。
- Thread.sleep()
在程序中允许一个线程进行暂时的休眠,直接使用 Thread.sleep() 即可实现休眠。
- interrupt()
当一个线程运行时,另外一个线程可以直接通过interrupt()方法中断其运行状态。
- setDaemon()
守护线程,在 Java 程序中,只要前台有一个线程在运行,则整个 Java 进程都不会消失,所以此时可以设置一个后台线程,这样即使 Java 线程结束了,此后台线程依然会继续执行,要想实现这样的操作,直接使用 t.setDaemon(true) 方法即可。
- setPriority()
线程的优先级,哪个线程的优先级高,哪个线程就有可能会先被执行。t1.setPriority(Thread.MIN_PRIORITY) ;
- yield()
线程的礼让,在线程操作中,也可以使用 yield() 方法将一个线程的操作暂时让给其他线程执行。
四、线程池
线程池ThreadPoolExecutor的主要参数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
- corePoolSize: 线程池中核心线程的数量。
- maximumPoolSize:线程池中最大线程数量。
- keepAliveTime:非核心线程的超时时长,当系统中非核心线程闲置时间超过keepAliveTime之后,则会被回收。如果ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,则该参数也表示核心线程的超时时长。
- unit:keepAliveTime这个参数的单位,有纳秒、微秒、毫秒、秒、分、时、天等。
- workQueue:线程池中的任务队列,该队列主要用来存储已经被提交但是尚未执行的任务。存储在这里的任务是由ThreadPoolExecutor的execute方法提交来的。
- threadFactory:为线程池提供创建新线程的功能,这个我们一般使用默认即可。
- **handler: 拒绝策略,当线程无法执行新任务时(一般是由于线程池中的线程数量已经达到最大数或者线程池关闭导致的),默认情况下,当线程池无法处理新线程时,会抛出一个RejectedExecutionException。
Executors类提供的线程池
- newCachedThreadPool:用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)
- newFixedThreadPool:创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
- newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务。
- newScheduledThreadPool:适用于执行延时或者周期性任务。
线程池的优势
- 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
- 提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
- 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换。
- 提供更强大的功能,延时定时线程池。
五、线程安全
使用场景
遇到多个线程访问同一个共享资源,这时就需要考虑维护数据的一致性,也就是要用到锁。
synchronized
synchronized的作用范围
- 同步代码块
- 同步方法
- 静态方法加锁,称它为“类锁”
注意,接口方法和构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。
volatile
volatile 可以看作是一个轻量级锁,所以使用 volatile 的成本更低,因为它不会引起线程上下文的切换和调度。
volatile 保证可见性和禁止指令重排序。
volatile 修饰的变量,当修改后的变量写回主内存时,其他线程能立即看到最新值。
volatile 的原子性,在多线程环境下,volatile 关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性。也就是说,多线程环境下,使用 volatile 修饰的变量是线程不安全的。
对任意单个使用 volatile 修饰的变量的读 / 写是具有原子性,但类似于 flag = !flag 这种复合操作不具有原子性。简单地说就是,单纯的赋值操作是原子性的。
六、并发
多线程是实现并发机制的一种有效手段。
并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
参考链接
https://blog.youkuaiyun.com/qq_41648631/article/details/103045252
https://www.cnblogs.com/java1024/archive/2019/11/28/11950129.html
https://zhuanlan.zhihu.com/p/138819184