目录
一、相关概念
1.并行与并发
并行:同一个时间点,多个事件正在发生
并发:同一个微小的时间片段内,多个事件正在发生。
单核CPU只支持并发执行,不支持并行执行。
2.线程与进程
进程:程序运行过程的描述,系统以进程为单位为每个程序分配独立的系统资源,比如内存。
线程:是进程中的执行单元,一个进程中至少有一个线程,如果有多个线程就是多线程程序。
3.多线程的好处与应用场景
好处:充分利用CPU资源
应用场景:
多个线程做相同的任务,共同完成一个大的任务
多个线程分别做不同的任务,共同完成一个大的任务
4.线程调度方式
💡CPU以线程为单位分配资源
分时调度,平均分配CPU资源
抢占式调度方式,随机为每个线程分配CPU资源,Java支持的是抢占式调度方式
二、线程的创建与启动
1.方式一:继承Thread类
创建Thread类的子类并重写run方法
创建子类对象
调用start方法启动线程
⭕️自定义线程类:
public class MyThread extends Thread {
//定义指定线程名称的构造方法
public MyThread(String name) {
//调用父类的String参数的构造方法,指定线程的名称
super(name);
}
/**
* 重写run方法,完成该线程执行的逻辑
*/
@Override
public void run() {
for (int i = 0; i < 10; 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 < 10; i++) {
System.out.println("main线程!"+i);
}
}
}
2.方式二:实现Runnable接口
创建Runnable接口的实现类,并重写run方法
创建实现类对象(线程任务类)
创建Thread对象,通过构造器传入实现类对象
调用start方法启动线程
⭕️ 自定义线程任务类:
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("旺财 " + i);
}
}
}
3.线程的使用与启动注意事项:
调用线程对象的start方法启动线程,不要重复启动同一个线程
不要直接调用run方法,这不是启动线程,而是普通的方法调用
不要使用junit测试多线程程序
4.两种创建方式的比较:
Thread类本身也是实现了Runnable接口的,run方法都来自Runnable接口,run方法也是真正要执行的线程任务。
public class Thread implements Runnable {}
因为Java类是单继承的,所以继承Thread的方式有单继承的局限性,但是使用上更简单一些。
实现Runnable接口的方式,避免了单继承的局限性,并且可以使多个线程对象共享一个Runnable实现类(线程任务类)对象,从而方便在多线程任务执行时共享数据。
✒️总结:方式一有单继承限制 , 方式二方便共享数据
5.匿名内部类创建方式创建线程
//方式一的匿名内部类创建方式
new Thread(){
public void run(){
//线程任务
}
}.start();
//方式二的匿名内部类创建方式
new Thread(new Runnable(){
public void run(){
//线程任务
}
}).start();
三、Thread类
🚀构造方法
public Thread() :分配一个新的线程对象。
public Thread(String name) :分配一个指定名字的新的线程对象。
public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。
🚀 线程使用基础方法
public void run() :此线程要执行的任务在此处定义代码。
public String getName() :获取当前线程名称。
public static Thread currentThread() :返回对当前正在执行的线程对象的引用。
public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。
public final int getPriority() :返回线程优先级
public final void setPriority(int newPriority) :改变线程的优先级
每个线程都有一定的优先级,优先级高的线程将获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。Thread类提供了setPriority(int newPriority)和getPriority()方法类设置和获取线程的优先级,其中setPriority方法需要一个整数,并且范围在[1,10]之间,通常推荐设置Thread类的三个优先级常量:
MAX_PRIORITY(10):最高优先级
MIN _PRIORITY (1):最低优先级
NORM_PRIORITY (5):普通优先级,默认情况下main线程具有普通优先级。
🚀 线程控制常见方法
public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
public static void sleep(long millis) :线程睡眠,使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
public static void yield():线程礼让,yield只是让当前线程暂时失去执行权,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这个不能保证,完全有可能的情况是,当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。
void join() :加入线程,当前线程中加入一个新线程,等待加入的线程终止后再继续执行当前线程。
void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果millis时间到,将不再等待。
void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
public final void stop():强迫线程停止执行。 该方法具有不安全性,已被弃用,最好不要使用。
调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。
调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。
public void interrupt():中断线程,实际上是给线程打上一个中断的标记,并不会真正使线程停止执行。
public static boolean interrupted():检查线程的中断状态,调用此方法会清除中断状态(标记)。
public boolean isInterrupted():检查线程中断状态,不会清除中断状态(标记)
public void setDaemon(boolean on):将线程设置为守护线程。必须在线程启动之前设置,否则会报
IllegalThreadStateException
异常。
守护线程,主要为其他线程服务,当程序中没有非守护线程执行时,守护线程也将终止执行。JVM垃圾回收器也是守护线程。
public boolean isDaemon():检查当前线程是否为守护线程。
四、线程的生命周期状态
五、线程安全问题
1.线程安全问题的原因(条件)
使用多线程
多个线程共享数据
理解为多条语句操作共享数据
2.线程安全问题的解决
💡Java使用同步机制解决线程安全问题,使用关键字synchronized实现同步机制:
💡一个线程要执行同步代码,必须先获取同步锁对象,在执行同步代码时会持有锁对象,执行完后会自动释放锁对象,其他线程才有机会执行同步代码。
同步代码块
synchronized(锁对象){ //同步执行的代码 }
同步方法,由synchronized修饰的方法
public synchronized void sale(){ }
3.锁对象使用和选择的问题
锁对象可以是任意Java对象,但是必须保证多个线程使用同一个锁对象,否则解决不了线程安全问题。
非静态域中的同步代码块建议使用this做为锁对象,非静态方法默认使用this做为锁对象
静态域中的同步代码块建议使用当前类的Class实例作为锁对象。静态方法默认使用当前类的Class实例作为锁对象
4.同步代码的范围问题
💡多线程同步执行的代码范围越小越好,效率高。但是至少也要包含操作共享数据的所有关键代码,否则解决不了线程安全问题。
5.编写多线多线程程序的原则:
把线程任务类与共享资源类解耦合
6.单例模式之懒汉式的线程安全问题解决
public class Singleton{
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null)
synchronized(Singleton.class){
if(instance == null)
instance = new Singleton();
}
return instance;
}
}
六、等待唤醒机制
等待唤醒机制是指使用Object类中的wait和notify方法实现线程间协调通信的一种工作机制。
什么是等待唤醒机制
这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。
就是在一个线程满足某个条件时,就进入等待状态(wait()/wait(time)), 等待其他线程执行完他们的指定代码过后再将其唤醒(notify());或可以指定wait的时间,等时间到了自动唤醒;在有多个线程进行等待时,如果需要,可以使用 notifyAll()来唤醒所有的等待线程。wait/notify 就是线程间的一种协作机制。
-
wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING或TIMED_WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”或者等待时间到,在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中
-
notify:则选取所通知对象的 wait set 中的一个线程释放;
-
notifyAll:则释放所通知对象的 wait set 上的全部线程。
注意:
被通知线程被唤醒后也不一定能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
总结如下:
如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE(可运行) 状态;
否则,线程就从 WAITING 状态又变成 BLOCKED(等待锁) 状态
调用wait和notify方法需要注意的细节
-
wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
-
wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
-
wait方法与notify方法必须要在同步代码块或者是同步函数中使用,并且必须要通过锁对象调用这2个方法。
七、释放锁操作与死锁
任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?
1、释放锁的操作
当前线程的同步方法、同步代码块执行结束。
当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致当前线程异常结束。
当前线程在同步代码块、同步方法中执行了锁对象的wait()方法,当前线程被挂起,并释放锁。
2、不会释放锁的操作
线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。
线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该该线程挂起,该线程不会释放锁(同步监视器)。应尽量避免使用suspend()和resume()这样的过时来控制线程。
3、死锁
不同的线程分别锁住对方需要的同步监视器对象不释放,都在等待对方先放弃时就形成了线程的死锁。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
4、面试题:sleep()和wait()方法的区别
(1)sleep()不释放锁,wait()释放锁
(2)sleep()指定休眠的时间,wait()可以指定时间也可以无限等待直到notify或notifyAll
(3)sleep()在Thread类中声明的静态方法,wait方法在Object类中声明
因为我们调用wait()方法是由锁对象调用,而锁对象的类型是任意类型的对象。那么希望任意类型的对象都要有的方法,只能声明在Object类中。