多线程
文章目录
1.多线程技术
1.1 并行与并发
- 并行:指两个或多个事件在同一时刻发生(同时执行)。
- 并发:指两个或多个事件在同一个时间段内发生(交替执行)。
从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度
1.2 线程与进程
-
进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间。
- 一个应用程序可以同时运行多个进程;
- 进程也是程序的一次执行过程,是系统运行程序的基本单位;
- 系统运行一个程序即是一个进程从创建、运行到消亡的过程。
-
线程:是进程中的一个执行单元,共享一个内存空间,负责当前进程中程序的执行
- 一个进程中至少有一个线程,可有多个线程
- 多线程的应用程序也可以称之为多线程程序。
-
进程与线程的区别
- 进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
- 线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。
- 一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
- 多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的
注意:
- 因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于 CPU 的调度,程序员是干涉不了的。而这也就造成的多线程的随机性。
- Java 程序的进程里面至少包含两个线程,主进程也就是 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中启动了一个线程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。
- 由于创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建多线程,而不是创建多进程。
多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高
1.3 同步与异步
- 同步:排队执行 , 效率低但是安全.
- 异步:同时执行 , 效率高但是数据不安全.
1.4 线程调度
-
分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
-
抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
2.Thread类
2.1 概述
-
继承关系
java.lang.Object java.lang.Thread
-
原型声明
public class Thread extends Object implements Runnable
-
概述:
java.lang.Thread
类代表线程,线程是程序中执行的线程。 Java虚拟机允许应用程序同时运行多个执行线程。
2.2 构造方法
public Thread()
:分配一个新的线程对象。public Thread(String name)
:分配一个指定名字的新的线程对象。public Thread(Runnable target)
:分配一个带有指定目标新的线程对象。public Thread(Runnable target,String name)
:分配一个带有指定目标新的线程对象并指定名字。
2.3 成员方法
public String getName()
:获取当前线程名称。public void start()
:导致此线程开始执行; Java虚拟机调用此线程的run方法。public void run()
:此线程要执行的任务在此处定义代码。public static void sleep(long millis)
:使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。public static Thread currentThread()
:返回对当前正在执行的线程对象的引用。
3.线程创建
- 创建线程的方式总共有两种,一种是继承Thread类方式,一种是实现Runnable接口方式
3.1 继承方式
- 简述:所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流
- 继承方式创建步骤:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
// 自定义线程类
public class MyThread extends Thread {
//定义指定线程名称的构造方法
public MyThread(String name) {
//调用父类的String参数的构造方法,指定线程的名称
super(name);
}
public MyThread() {
//不指定线程的名字,线程有默认的名字Thread-0
}
/**
* 重写run方法,完成该线程执行的逻辑
*/
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}
// 测试
public class Demo01 {
public static void main(String[] args) {
//创建自定义线程对象
MyThread mt = new MyThread("新的线程!");
//开启新线程
mt.start();
//在主方法中执行for循环
for (int i = 0; i < 200; i++) {
System.out.println("main线程!"+i);
}
}
}
3.2 实现方式
3.2.1 创建步骤
-
实现
java.lang.Runnable
接口,只需要重写run方法即可。 -
创建步骤:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动线程。
// 定义Runnable接口的实现类 public class MyRunnable implements Runnable{ @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println(Thread.currentThread().getName()+" "+i); } } } // 测试 public class Demo { public static void main(String[] args) { //创建自定义类对象 线程任务对象 MyRunnable mr = new MyRunnable(); //创建线程对象 Thread t = new Thread(mr, "聊天"); t.start(); for (int i = 0; i < 20; i++) { System.out.println("QQ " + i); } } }
3.2.2 Thread和Runnable区别
- 实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础
- 如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享
- 实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
3.3 匿名内部类方式
-
使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法
- 创建Thread线程对象,传入Runnable接口的匿名内部类
- 在匿名内部类中重写run()方法,把线程需要执行的任务放入run()方法中
- 使用Thread线程对象调用start()方法,启动线程,并执行任务
public class NoNameInnerClassThread { public static void main(String[] args) { // new Runnable(){ // public void run(){ // for (int i = 0; i < 20; i++) { // System.out.println("张宇:"+i); // } // } // }; //---这个整体 相当于new MyRunnable() Runnable r = new Runnable(){ public void run(){ for (int i = 0; i < 20; i++) { System.out.println("张宇:"+i); } } }; new Thread(r).start(); for (int i = 0; i < 20; i++) { System.out.println("费玉清:"+i); } } }
4.线程安全
4.1 线程安全
- 线程安全概念:多个线程同时访问一个共享变量的情况下,每次运行结果预期的是一样的,就是线程安全的。
4.2 线程同步
- 线程同步是为了解决线程安全问题
- 使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题
- 要解决上述多线程并发访问一个资源的安全性问题,Java中提供了同步机制(synchronized)来解决
4.2.1 同步代码块
-
同步代码块:
synchronized
关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。synchronized(同步锁){ //需要同步操作的代码 }
-
同步锁:对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁
- 锁对象可以是任意类型。
- 多个线程,锁对象要使用同一把锁。
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
4.2.2 同步方法
-
同步方法:使用
synchronized
修饰的方法,就叫做同步方法,保证当前线程执行该方法的时候,其他线程只能在方法外等着。public synchronized void method(){ //可能会产生线程安全问题的代码 }
同步锁是:
对于非static方法,同步锁就是this。
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
4.2.3 Lock锁
-
java.util.concurrent.locks.Lock
机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大 -
Lock锁也称同步锁,加锁与释放锁方法化了,如下:
public void lock()
:加同步锁。public void unlock()
:释放同步锁。
-
由于Lock锁进行加锁和释放锁是调用方法,所以需要创建Lock锁对象,而Lock是一个接口,所以只能创建其实现类对象来调用方法加锁和释放锁
- Lock锁的实现类:
ReentrantLock
- ReentrantLock类构造:
public ReentrantLock()
- Lock锁的实现类:
5.线程状态
5.1 线程状态
-
线程状态:线程由创建到被终止的过程
-
各个线程状态发生的条件,如下表
线程状态 导致状态发生条件 NEW(新建) 线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程对象,没有线程特征。 Runnable(可运行) 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。调用了t.start()方法 :就绪(经典教法) Blocked(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 Waiting(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 Timed Waiting(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 -
状态变迁:线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SWJ3GTAT-1605878264376)(img/image-20201120155506624.png)]
5.2 睡眠sleep方法
-
public static void sleep(long time)
:让当前线程进入到睡眠状态,到毫秒后自动醒来继续执行public class Test{ public static void main(String[] args){ for(int i = 1;i<=5;i++){ Thread.sleep(1000); System.out.println(i) } } }
5.3 等待和唤醒
-
public void wait()
:Object类的方法,让当前线程进入到等待状态 此方法必须由锁对象调用. -
public void notify()
:唤醒当前锁对象上等待状态的线程,此方法必须锁对象调用.- 唤醒的是该锁对象调用wait()方法进入无限等待或者计时等待的单个线程
-
public void notifyAll()
:需要使用锁对象调用,唤醒所有等待线程- 唤醒的是该锁对象调用wait()方法进入无限等待或者计时等待的所有线程
public class Demo { public static void main(String[] args) throws InterruptedException { // 步骤1 : 子线程开启,进入无限等待状态, 没有被唤醒,无法继续运行. new Thread(() -> { try { System.out.println("begin wait ...."); synchronized ("") { "".wait(); } System.out.println("over"); } catch (Exception e) { } }).start(); //步骤2: 加入如下代码后, 3秒后,会执行notify方法, 唤醒wait中线程. Thread.sleep(3000); new Thread(() -> { try { synchronized ("") { System.out.println("唤醒"); "".notify(); } } catch (Exception e) { } }).start(); } }
- 线程一旦进入无限等待,就不会争夺锁,也不会抢占系统资源(释放锁,不抢cpu)
- 在同步代码块或者同步方法中调用sleep()方法让线程进入计时等待,是不会释放锁!!
例如:synchronized(锁对象){
Thread.sleep(); 不会释放锁
锁对象.wait();释放锁,不会抢cpu
}
6.线程池创建
6.1 线程池概念
-
**线程池:**一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
-
合理利用线程池好处:
-
降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
-
提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
-
提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
-
6.2 线程池创建
-
java.util.concurrent.Executor
:Java里面线程池的顶级接口 -
java.util.concurrent.ExecutorService
:严格意义上讲Executor
并不是一个线程池,而只是一个执行线程的工具,ExecutorService才是真正的线程池接口 -
java.util.concurrent.Executors
:一个线程池的配置是比较复杂的,线程工厂类Executors
里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象,静态方法如下:- newSingleThreadExecutor:创建一个
单线程
化的Executor。 - newFixedThreadPool:创建一个
固定大小
的线程池。 - newCachedThreadPool:创建一个
可缓存
的线程池 - newScheduleThreadPool:创建一个定长的线程池,可以
周期性执行任务
。
public class ExcutorsTest { public void testExecutorService() { //初始化 ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3); ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3); //任务执行 MyTask task = new MyTask(); singleThreadExecutor.submit(task); cachedThreadPool.submit(task); fixedThreadPool.submit(task); scheduledExecutorService.scheduleAtFixedRate(task, 1000, 1000, TimeUnit.SECONDS); } class MyTask implements Runnable { @Override public void run() { System.out.println("耗时操作"); } } }
- newSingleThreadExecutor:创建一个
-
此外,还可以使用
ThreadPoolExecutor
自己定义线程池,弄懂它的构造参数即可- int corePoolSize,//核心池的大小
- int maximumPoolSize,//线程池最大线程数
- long keepAliveTime,//保持时间/额外线程的存活时间
- TimeUnit unit,//时间单位
- BlockingQueue workQueue,//任务队列
- ThreadFactory threadFactory,//线程工厂
- RejectedExecutionHandler handler //异常的捕捉器
public class TestThreadPoolExecutor {
private static ExecutorService pool;
public static void main(String[] args) {
pool = new ThreadPoolExecutor(
1,//核心大小
2,//最大线程数
1000,//1000
TimeUnit.MILLISECONDS,//ms
new SynchronousQueue<Runnable>(),//队列
Executors.defaultThreadFactory(),//线程工厂
new ThreadPoolExecutor.AbortPolicy()//决绝策略,直接抛异常
);
for (int i = 0; i < 3; i++) {
pool.execute(new ThreadTask());
}
}
}