给自己一个希望,不为昨天烦恼,不为明天迷惘,只为今天更美好;
1、什么是进程?
可以把进程理解成一个在内存中运行的应用程序。
每个进程都有自己独立的一块内存空间。
进程不依赖于线程而独立存在,一个进程中可以启动多个线程。
例如 当我们点击IDEA.exe可执行文件时,后台便会在内存中创建一个进程。
2、什么是线程?
线程 (thread) 是进程的一个子程序(模块)。
线程是进程的一条执行路径/执行单元。
线程也有独立的内存空间。
3、并行、并发
并行: 同一时刻,多个程序同时执行。
并发: 同一时间段,多个程序交替执行。
4、线程调度的原理
5、什么是多线程?
多进程是指操作系统能同时运行多个任务(应用程序)。
多线程是指一个进程中同时有多个执行路径正在执行(可以将耗时任务放后台执行,同时我们执行其他的操作)。
单线程:
多线程:
6、多线程用在哪里,有什么好处?
步骤如下:
1、首先定义一个类MyThread,去继承线程类java.lang.Thread
,然后重写run()方法。
2、创建MyThread类的对象。
3、调用线程对象的start()方法启动线程 (启动后还是执行run方法)
Thread类的构造器:
public class ThreadDemo1 {
public static void main(String[] args) {
// 3、new一个线程类对象
Thread t = new MyThread();
// 4、启动线程 (执行的还是run方法)
t.start();
// main方法代表主线程
for (int i = 0; i < 5 ; i++) {
System.out.println("主线程执行输出:"+i);
}
}
}
/**
* 1、定义一个线程类
*/
class MyThread extends Thread{
/**
* 2重写run方法,run方法里面定义线程要干什么
*/
@Override
public void run() {
for (int i = 0; i < 5 ; i++) {
System.out.println("子线程执行输出:"+i);
}
}
}
方式一的优缺点:
问题1、为什么不直接调用了run方法,而是调用start方法启动线程?
源码分析:
start()方法中有个native修饰的start0(),它是本地方法,是JVM调用,底层是c/c++实现。所以说:真正实现多线程的效果,是start0(),而不是 run()。
Java 是跨平台的,可以在不同系统上运行,每个系统的 CPU 调度算法不一样,所以就需要做不同的处理,这件事情就只能交给 JVM 来实现了,start0() 方法自然就表标记成了 native。
问题2、为什么不要把主线程任务放在子线程之前?
总结:
步骤如下:
1、定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法。
2、创建MyRunnable任务对象。
3、把MyRunnable任务对象交给Thread(线程)处理。
4、调用线程对象的start()方法启动线程。
//线程类
public class MyThread implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName()+"---------------"+i);
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
}
// 测试类
public class Demo01Runnable {
public static void main(String[] args) {
Thread thread1 = new Thread(new MyRunnable());
Thread thread2 = new Thread(new MyRunnable());
Thread thread3 = new Thread(new MyRunnable());
thread1.setName("我是线程一");
thread2.setName("我是线程二");
thread3.setName("我是线程小三");
thread1.start();
thread2.start();
thread3.start();
}
}
底层执行原理:
// Runnable接口源码
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface Runnable
is used
* to create a thread, starting the thread causes the object's
* run
method to be called in that separately executing
* thread.
*
* The general contract of the method run
is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
// Thread类部分源码
public class Thread implements Runnable {
/* What will be run. */
private Runnable target; //用于接收Runnable接口实现类对象
//无参构造器
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
// 有参构造器,参数为:Runnable 接口实现类
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
// 用当前的AccessControlContext初始化一个线程
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
init(g, target, name, stackSize, null, true);
}
// 初始化一个线程
private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,boolean inheritThreadLocals) {
...
this.target = target; // 将Runnable 接口实现类对象赋值给成员变量
...
}
// 启动线程的方法
public synchronized void start() {
start0();
...
}
// start0是本地方法,底层是C++实现的,然后调用系统资源,以多线程的方式调用run()方法
private native void start0();
// 在run()方法中指定线程要执行的任务
// 又因为Runnable接口实现类重写了这个方法,所以他走Runnable接口实现类的run()方法
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
方式二的优缺点:
实现Runnable接口(匿名内部类方式):
1、可以创建Runnable的匿名内部类对象。
2、交给Thread处理。
3、调用线程对象的start()方法启动线程。
public class ThreadDemo3 {
public static void main(String[] args) {
// 1、创建Runnable的匿名内部类对象
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("子线程启动1");
}
};
// 2、交给Thread处理
Thread t = new Thread(runnable);
// 3、启动线程
t.start();
/*
第二种写法:通过Thread构造器入参匿名内部类方式创建
*/
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子线程启动2");
}
}).start();
/*
第三种写法:使用lambda表达式简化内部类
*/
new Thread(() -> {
System.out.println("子线程启动3");
}).start();
// main方法代表主线程
for (int i = 0; i < 5; i++) {
System.out.println("主线程执行输出:" + i);
}
}
}
1、前两种线程创建方式都存在一个问题:
2、怎么解决这个问题?
3、实现Callable接口、结合FutureTask完成线程创建,步骤如下:
(1) 得到任务对象。
(2) 把线程任务对象交给Thread处理。
(3) 调用Thread的start方法启动线程,执行任务。
FutureTask的API:
方法 | 功能 |
---|---|
public FutureTask(Callable | 把Callable对象封装成 FutureTask 对象 |
public V get() | 获取线程执行call方法返回的结果 |
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
/**
* 实现Callable接口、结合FutureTask完成线程创建
*
* @author 白豆五
* @version 2022/11/13 19:26
* @since JDK8
*/
public class ThreadDemo4 {
public static void main(String[] args) {
// 3、创建Callable任务对象
Callable<String> call1 = new MyCallable(10);
// 4、把Callable任务对象交给FutureTask对象
/*
FutureTask的作用:
1、它是Runnable接口实现类对象,可以交给Thread对象
2、可以在线程执行完毕后,通过调用FutureTask的get方法得到线程执行完毕的结果
*/
FutureTask<String> f1 = new FutureTask<String>(call1);
// 5、交给线程处理
Thread t1 = new Thread(f1);
// 6、启动线程
t1.start();
/*
再次启动线程
*/
Callable<String> call2 = new MyCallable(100);
FutureTask<String> f2 = new FutureTask<String>(call2);
Thread t2 = new Thread(f2);
t2.start();
try {
// 如果f1任务没有执行完毕,这里的代码会等待,直到线程1跑完才提取结果。
String res1 = f1.get();
System.out.println("第一个结果:" + res1);
} catch (Exception e) {
e.printStackTrace();
}
try {
// 如果f2任务没有执行完毕,这里的代码会等待,直到线程2跑完才提取结果。
String res2 = f2.get();
System.out.println("第二个结果:" + res2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 1、定义一个任务类,实现Callable接口,应该声明线程任务执行完毕后返回结果的数据类型。
*/
class MyCallable implements Callable<String> {
private int n;
// 使用构造器,为变量n赋值
public MyCallable(int n) {
this.n = n;
}
/**
* 2、重写call方法(任务方法)
*
* @return
* @throws Exception
*/
@Override
public String call() throws Exception {
/*
需求:求1到n的和
*/
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return "子线程执行的结果是:" + sum;
}
}
方式三的优缺点:
三种方式的对比:
定义方式 | 优点 | 缺点 |
---|---|---|
继承Thread类 | 编程比较简单,可以直接使用Thread类中的方法 | 扩展性较差,不能再继承其他的类,不能返回线程执行的结果 |
实现Runnable接口 | 扩展性强,实现该接口的同时还可以继承其他的类 | 编程相对复杂,不能返回线程执行的结果 |
实现Callable接口 | 扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果 | 编程相对复杂 |
1、Thread类构造器
2、设置和获取线程的名字
String getName()
: 返回此线程的名称。void setName(String name)
: 修改此线程的名称。3、获取当前线程的对象
Thread.currentThread(); //获取当前线程的对象
4、线程休眠的方法
static void sleep(long millis)
: 让当前线程休眠指定的时间后再继续执行,单位为毫秒。
5、线程任务方法、线程启动方法
public void run()
:线程任务方法。public void start()
:线程启动方法。线程安全问题:当多个线程同时操作同一个共享资源的时候,可能会出现数据不一致问题,称为线程安全问题。
线程安全出现的原因: 存在多线程并发,同时访问共享资源,存在修改共享资源。
示例: 模拟三个窗口共卖100张票
//1.定义类实现Runnable接口
public class MySellTicketTask implements Runnable {
// 票数
private int num = 100;
// 2.Runnable接口的实现类,覆盖重写run方法,指定线程任务
@Override
public void run() {
while (true) {
//(1)判断是否有票
if (num > 0) {
//模拟出票的时间
try {Thread.sleep(100);}catch (Exception e){e.printStackTrace();}
//(2)有票: 打印输出一张票
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "卖出第 " + num + " 张票...........");
//(3)票的数量减少1张
num--;
}
}
}
}
public class Demo01Tickets {
public static void main(String[] args) {
//3.创建Runnable接口的实现类的对象,线程任务对象
MySellTicketTask task = new MySellTicketTask();
//4.创建3个线程对象,构造方法发传递线程任务对象
Thread t1 = new Thread(task,"手机App");
Thread t2 = new Thread(task,"网站");
Thread t3 = new Thread(task,"前台");
//5.这3个线程对象分别调用start方法,开启线程
t1.start();
t2.start();
t3.start();
}
}
线程同步:为了解决线程安全问题。
如何保证线程安全? 让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
线程同步的核心思想:
1、同步代码块的作用: 把出现线程安全问题的核心代码给上锁。
2、同步代码块的原理: 每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。
语法格式如下:
synchronized(同步锁对象){
操作共享资源的代码(核心代码)
}
3、锁对象用任意唯一的对象好不好呢?
@Override
public void run() {
while (true) {
//使用同步代码块,解决线程安全问题
synchronized (this) {
//(判断是否有票
if (num > 0) {
//延时模拟出票的时间
try {Thread.sleep(10);}catch (Exception e){e.printStackTrace();}
// 打印输出一张票
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "卖出第 " + num + " 张票...........");
//票的数量减少1张
num--;
}
}
}
}
4、锁对象的规范要求:
this
作为锁对象。类名.class
)对象作为锁对象。总结
1、同步代码块是如何实现线程安全的?
synchronized=
进行加锁2、同步代码块的同步锁对象有什么要求?
this
作为锁对象。类名.class
)对象作为锁对象。1、同步方法的作用: 把出现线程安全问题的核心方法给上锁。
2、同步方法的原理: 每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
格式如下:
修饰符 synchronized 返回值类型 方法名称(形参列表){
操作共享资源的代码
}
示例:
@Override
public synchronized void run() {
while (true) {
//(判断是否有票
if (num > 0) {
//延时模拟出票的时间
try {Thread.sleep(10);}catch (Exception e){e.printStackTrace();}
// 打印输出一张票
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "卖出第 " + num + " 张票...........");
//票的数量减少1张
num--;
}
}
}
3、同步方法底层原理:
this
作为的锁对象。但是代码要高度面向对象!类名.class
作为的锁对象。总结
1、同步方法是如何保证线程安全的?
synchronized
修饰加锁。2、同步方法的同步锁对象的原理?
this
作为锁对象。类名.class
对象作为锁对象。Lock锁概述
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便。
Lock接口在juc包(并发包: java.util.concurrent)里面。
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象。
ReentrantLock类的构造器
Lock接口的API
/*
使用Lock锁对象替换同步代码块
*/
@SuppressWarnings("All")
public class MySellTicketTask05 implements Runnable {
//多个线程共享的数据
private int num = 100;
//使用Lock锁对象替换同步代码块
//保证锁对象的唯一性
private Lock lock = new ReentrantLock();//多态
@Override
public void run() {
//死循环
while (true) {
//获取锁: 上锁 <================
lock.lock();
//(2)有票: 打印输出一张票
//模拟出票的时间
try {
//(1)判断是否有票
if (num > 0) {
Thread.sleep(10);
String threadName = Thread.currentThread().getName();
System.out.println(threadName+"卖出第 " + num + " 张票....");
//(3)票的数量减少1张
num--;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁 <================
lock.unlock();
}
}
}
}
//测试类
public class Demo05Lock {
public static void main(String[] args) {
//创建线程任务对象
MySellTicketTask05 task = new MySellTicketTask05();
//创建线程对象,传递线程任务对象,指定线程名称
Thread t1 = new Thread(task, "窗口");
Thread t2 = new Thread(task, "手机APP");
Thread t3 = new Thread(task, "网站");
//调用start方法开启线程
t1.start();
t2.start();
t3.start();
}
}
线程状态
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法。 |
Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。 |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
Timed Waiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
//TODO …