一, 前言
今天总结一些关于线程方面的知识, 说到线程可谓是无人不知, 毕竟这东西不管是在工作开发中, 还是实际生活中都时时存在着. 关于线程方面的内容非常多, 从简单的单线程, 多线程, 线程安全以及到高并发等等, 当然也包括信息通信.
当然这次从线程的基本开始, 后面也会慢慢的补充线程的高级使用, 这也算是让自己再复习一次了(哈哈).
以下内容包括:
二, 线程介绍
三, 线程的创建
四, 线程安全
五, 线程池
二, 线程介绍
先来介绍几个关于线程方面的概念.
2.1, 并行与并发
并发: 指两个或多个事件在同一个时间段内发生.
并行: 指两个或多个事件在同一时刻发生(同时发生).
在操作系统中, 安装了多个程序, 并发指的是在一段时间内宏观上有多个程序同时运行, 这在单 CPU 系统中, 每一时刻只能有一道程序执行, 即微观上这些程序是分时的交替运行, 只不过是给人的感觉是同时运行, 那是因为分时交替运行的时间是非常短的, CPU 在多个程序之间高速切换.
而在多个 CPU 系统中, 则这些可以并发执行的程序便可以分配到多个处理器上(CPU), 实现多任务并行执行, 即利用每个处理器来处理一个可以并发执行的程序, 这样多个程序便可以同时执行. 目前电脑市场上说的多核 CPU, 便是多核处理器, 核越多, 并行处理的程序越多, 能大大的提高电脑运行的效率.
注意:
单核处理器的计算机肯定是不能并行的处理多个任务的, 只能是多个任务在单个 CPU 上并发运行. 同理, 线程也是一样的, 从宏观角度上理解线程是并行运行的, 但是从微观角度上分析却是串行运行的, 即一个线程一个线程的去运行, 当系统只有一个 CPU 时, 线程会以某种顺序执行多个线程, 我们把这种情况称之为线程调度.
2.2 , 线程与进程
进程: 是指一个内存中运行的应用程序, 每个进程都有一个独立的内存空间, 一个应用程序可以同时运行多个进程; 进程也是程序的一次执行过程, 是系统运行程序的基本单位; 系统运行一个程序即是一个进程从创建, 运行到消亡的过程.
线程: 线程是进程中的一个执行单元, 负责当前进程中程序的执行, 一个进程中至少有一个线程. 一个进程中是可以有多个线程的, 这个应用程序也可以称之为多线程程序.
一个程序运行后至少有一个进程, 一个进程中可以包含多个线程 .
我们可以再电脑底部任务栏, 右键 ----->打开任务管理器, 可以查看当前任务的进程:
线程调度:
分时调度
所有线程轮流使用 CPU 的使用权, 平均分配每个线程占用 CPU 的时间.
抢占式调度
优先让优先级高的线程使用 CPU, 如果线程的优先级相同, 那么会随机选择一个(线程随机性),Java 使用的为抢占式调度.
三, 线程的创建
创建线程有两种方式:
继承 java.lang.Thread 类, 重写 run 方法实现线程创建.
实现 java.lang.Runnable 接口, 实例化其实现类对象创建线程.
3.1,Thread
先来看看 API 文档的说明:
Thread 是一个类, 但同时也实现了 Runnable 接口.
接着使用 Thread 创建线程.
- public class ThreadMain {
- public static void main(String[] args) {
- // 1, 实例化 ThreadMode 对象
- ThreadMode thread = new ThreadMode();
- // 2, 调用 start()方法启动线程
- thread.start();
- }
- }
- /**
- * 继承 Thread 类
- */
- class ThreadMode extends Thread {
- // 1, 重写父类的 run 方法.
- @Override
- public void run() {
- System.out.println("使用 Thread 创建线程!");
- }
- }
3.2, 构造方法
public Thread(): 分配新的 Thread 对象.
public Thread(Runnable target): 分配一个带有指定目标的新线程.
public Thread(String name): 分配一个带有名字的新线程.
public Thread(Runnable target, String name): 分配一个带有指定目标的新线程, 并指定线程的名字.
3.3,Runnable
java.lang.Runnable:Runnable 接口该由那些打算通过某一线程其实例的类来实现. 类必须定义一个称为 run 的无参方法.
实现步骤:
1, 创建一个 Runnable 接口的实现类.
2, 在实现类中重写 Runnable 接口中的 run 方法, 设置线程的任务.
3, 创建一个 Runnable 实现类的对象.
4, 创建 Thread 类对象, 构造方法中传递 Runnable 接口的实现类对象.
5, 调用 Thread 类中 start 方法, 开启线程执行 run 方法.
- public class ThreadInterface implements Runnable{
- @Override
- public void run() {
- System.out.println("使用 Runnable 创建线程!");
- }
- }
- // 在 main 方法中, 创建实现类对象并开启线程
- ThreadInterface thread = new ThreadInterface();
- new Thread(thread,"runnable").start();
说到这里 Thread 类和 Runnable 接口都可以创建新的线程, 那么它们之间又有什么区别呢?
使用 Runnable 接口的好处:
避免了单继承的局限性一个类只能继承一个父类, 如果创建线程选择继承 Thread 类, 那么就不能再继承别的父类. 而采用实现 Runnable 接口, 则还可以再继承, 且再实现别的接口.
增强了程序的扩展性, 降低了程序的耦合性 (解耦) 实现 Runnable 接口的方式, 把设置线程任务和开启线程的任务进行了分离.
3.4, 匿名内部类方式创建线程
作用:
1, 简化代码
2, 把子类继承父类, 重写父类的方法, 创建子类对象一步合成.
3, 把实现类接口, 重写接口中的方法, 创建实现类对象一步合成.
格式:
new 父类 / 接口(){}
- Thread
- new Thread(){
- @Override
- public void run() {
- super.run();
- }
- }.start();
使用 lambda 表达式写法(JDK8 特性, 后面会分享该特性):
- new Thread(() -> System.out.println("Thread 匿名内部类")).start();
- Runnable
- Runnable runnable = new Runnable() {
- @Override
- public void run() {
- System.out.println("Runnable 匿名内部类");
- }
- };
- // 开启线程
- new Thread(runnable).start();
使用 lambda 表达式:
- Runnable runnable = () -> System.out.println("Runnable 匿名内部类");
- // 开启线程的第一种方式
- new Thread(runnable).start();
四, 线程安全
线程安全通常有 3 种解决方式:
1, 同步代码块
2, 同步方法
3, 锁机制(lock)
我们以卖车票为案例, 用三种方式去解决车票的重复售卖, 超卖情况.
4.1, 同步代码块
格式:
synchronized(锁对象){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
注意:
1. 通过代码块中的锁对象, 可以使用任意的对象.
2. 但是必须保证多个线程使用的锁对象是同一个.
3. 锁对象作用: 把同步代码块锁住, 只让一个线程在同步代码块中执行.
- public class RunnableImpl implements Runnable{
- // 定义一个多个线程共享的票源
- private int ticket = 100;
- // 创建一个锁对象
- Object obj = new Object();
- // 设置线程任务: 卖票
- @Override
- public void run() {
- // 使用死循环, 让卖票操作重复执行
- while(true){
- // 同步代码块
- synchronized (obj){
- // 先判断票是否存在
- if(ticket>0){
- // 提高安全问题出现的概率, 让程序睡眠
- try {
- Thread.sleep(10);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // 票存在, 卖票 ticket--
- System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
- ticket--;
- }
- }
- }
- }
- }
在 main 方法中调用线程, 并模拟三个窗口同时售票.
- public class Demo01Ticket {
- public static void main(String[] args) {
- // 创建 Runnable 接口的实现类对象
- RunnableImpl run = new RunnableImpl();
- // 创建 Thread 类对象, 构造方法中传递 Runnable 接口的实现类对象
- Thread t0 = new Thread(run);
- Thread t1 = new Thread(run);
- Thread t2 = new Thread(run);
- // 调用 start 方法开启多线程
- t0.start();
- t1.start();
- t2.start();
- }
- }
4.2, 同步方法
使用步骤:
1. 把访问了共享数据的代码抽取出来, 放到一个方法中
2. 在方法上添加 synchronized 修饰符
格式: 定义方法的格式
修饰符 synchronized 返回值类型 方法名(参数列表){
可能会出现线程安全问题的代码(访问了共享数据的代码)
- }
- public class RunnableImpl implements Runnable{
- // 定义一个多个线程共享的票源
- private static int ticket = 100;
- // 设置线程任务: 卖票
- @Override
- public void run() {
- System.out.println("this:"+this);
- // 使用死循环, 让卖票操作重复执行
- while(true){
- payTicketStatic();
- }
- }
- /*
- 静态的同步方法
- 锁对象应该是谁?
- 不能是 this
- this 是创建对象之后产生的, 静态方法优先于对象
- 静态方法的锁对象是本类的 class 属性 -->class 文件对象(反射)
- */
- public static /*synchronized*/ void payTicketStatic(){
- synchronized (RunnableImpl.class){
- // 先判断票是否存在
- if(ticket>0){
- // 提高安全问题出现的概率, 让程序睡眠
- try {
- Thread.sleep(10);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // 票存在, 卖票 ticket--
- System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
- ticket--;
- }
- }
- }
- /*
- 定义一个同步方法
- 同步方法也会把方法内部的代码锁住
- 只让一个线程执行
- 同步方法的锁对象是谁?
- 就是实现类对象 new RunnableImpl()
- 也是就是 this
- */
- public /*synchronized*/ void payTicket(){
- synchronized (this){
- // 先判断票是否存在
- if(ticket>0){
- // 提高安全问题出现的概率, 让程序睡眠
- try {
- Thread.sleep(10);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // 票存在, 卖票 ticket--
- System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
- ticket--;
- }
- }
- }
- }
在 main 函数中调用该线程, 其代码与上述一样.
4.3, 同步锁(Lock)
java.util.concurrent.locks.Lock 接口
Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作.
Lock 接口中的方法:
void lock()获取锁.
void unlock() 释放锁.
使用步骤:
1. 在成员位置创建一个 ReentrantLock 对象.
2. 在可能会出现安全问题的代码前调用 Lock 接口中的方法 lock 获取锁.
3. 在可能会出现安全问题的代码后调用 Lock 接口中的方法 unlock 释放锁 .
请看如下 API 说明:
- public class RunnableImpl implements Runnable{
- // 定义一个多个线程共享的票源
- private int ticket = 100;
- //1. 在成员位置创建一个 ReentrantLock 对象
- Lock l = new ReentrantLock();
- // 设置线程任务: 卖票
- @Override
- public void run() {
- // 使用死循环, 让卖票操作重复执行
- while(true){
- //2. 在可能会出现安全问题的代码前调用 Lock 接口中的方法 lock 获取锁
- l.lock();
- // 先判断票是否存在
- if(ticket>0){
- try {
- // 票存在, 卖票 ticket--
- System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
- ticket--;
- } catch (InterruptedException e) {
- e.printStackTrace();
- }finally {
- //3. 在可能会出现安全问题的代码后调用 Lock 接口中的方法 unlock 释放锁
- l.unlock();// 无论程序是否异常, 都会把锁释放
- }
- }
- }
- }
- }
五, 线程池
如果并发的线程数量很多, 并且每个线程都是执行一个时间很短的任务就结束了, 这样频繁创建线程就会大大降低系统的效率, 因为频繁创建线程和销毁线程是需要时间的.
在 JDK5 之前, 对于线程池的使用是需要程序员用集合来自己进行创建. 在 JDK5 之后就不再需要手动去创建, JDK 已经帮我们封装好了.
5.1, 线程池概念
线程池: 其实就是一个容纳多个线程的容器, 其中的线程可以反复使用, 省去了频繁创建线程对象的操作, 无需反复创建线程而消耗过多资源.
用一张简单的图来理解下线程池工作的原理.
合理利用线程池能够带来三个好处:
降低资源消耗. 减少了创建和销毁线程的次数, 每个工作线程都可以被重复利用, 可执行多个任务.
提高响应速度. 当任务到达时, 任务可以不需要的等到线程创建就能立即执行.
提高线程的可管理性. 可以根据系统的承受能力, 调整线程池中工作线线程的数目, 防止因为消耗过多的内存, 而把服务器累趴下(每个线程需要大约 1MB 内存, 线程开的越多, 消耗的内存也就越大, 可能最后死机).
5.2, 使用方式
Java 里面线程池的顶级接口是 java.util.concurrent.Executor, 但是严格意义上讲 Executor 并不是一个线程池, 而只是一个执行线程的工具. 真正的线程池接口是 java.util.concurrent.ExecutorService.
要配置一个线程池是比较复杂的, 尤其是对于线程池的原理不是很清楚的情况下, 很有可能配置的线程池不是较优的, 因此在 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂, 生成一些常用的线程池. 官方建议使用 Executors 工程类来创建线程池对象.
Executors 类中有个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads)
: 返回线程池对象.(创建的是有界线程池, 也就是池中的线程个数可以指定最大数量)
获取到了一个线程池 ExecutorService 对象, 那么怎么使用呢, 在这里定义了一个使用线程池对象的方法如下:
public Future<?> submit(Runnable task): 获取线程池中的某一个线程对象, 并执行
Future 接口: 用来记录线程任务执行完毕后产生的结果. 线程池创建与使用.
使用线程池中线程对象的步骤:
创建线程池对象.
创建 Runnable 接口子类对象.(task)
提交 Runnable 接口子类对象.(take task)
关闭线程池(一般不做, 因为再次使用的时候线程池中就没有线程了).
- public class MyRunnable implements Runnable {
- @Override
- public void run() {
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("线程:" + Thread.currentThread().getName());
- }
- }
- public class ThreadPoolDemo {
- public static void main(String[] args) {
- // 创建线程池对象
- ExecutorService service = Executors.newFixedThreadPool(2);// 包含 2 个线程对象
- // 创建 Runnable 实例对象
- MyRunnable r = new MyRunnable();
- // 从线程池中获取线程对象, 然后调用 MyRunnable 中的 run()
- service.submit(r);
- // 再获取个线程对象, 调用 MyRunnable 中的 run()
- service.submit(r);
- service.submit(r);
- // 注意: submit 方法调用结束后, 程序并不终止, 是因为线程池控制了线程的关闭.
- // 将使用完的线程又归还到了线程池中
- }
- }
六, sleep()与 wait()
1, 所属分类不同, sleep 属于 Thread 类中, 而 wait 属于 Object 类中.
2, 锁控制不同, sleep 不会释放锁, 而 wait 会释放锁且不会影响其他线程进入同步代码块或同步方法中. 也就是说 sleep 会占用资源, wait 不会占用资源.
3,sleep 可以在任意地方使用, 而 wait 需在同步代码块或者同步方法中使用.
七, volatile 与 synchronized
1,volatile 性能比 synchronized 要好, 因为 volatile 是线程同步的轻量级实现.
2,volatile 只能修饰变量, synchronized 可以修饰方法以及代码块.
3, 多线程访问 volatile 不会发生阻塞, synchronized 会出现阻塞.
4,volatile 能保证数据的可见性, 但不能保证原子性. synchronized 可以保证原子性, 也可以间接的保证可见性.
5,volatile 解决的是变量在多线程之间的可见性. synchronized 解决的是多线程之间访问资源的同步性.
6,volatile 防止指令重排.
八, 总结
似乎觉得本人的每一篇博客的篇幅都好长, 可能是因为都是一些很基础的知识点吧, 所以就会涉及到很多方方面面, 写着写着就很多了. 不过这样记下来时间久了还可以再回来看看, 多少也算有点印象(哈哈).
如果你阅读到此, 很感谢您的耐心. 以上总结的内容, 如有不适之处, 欢迎留言指正.
来源: https://www.cnblogs.com/fenjyang/p/11493015.html