概念
- 什么是进程?什么是线程?
- 进程是一个应用程序。
- 线程是一个进程中的执行场景/执行单元。
- 一个进程可以启动多个线程。
- 注意:进程之间内存独立资源不共享。
- 线程之间(在Java语言中)
- 堆内存和方法区内存共享。
- 但是栈内存独立,一个线程一个栈,每个栈之间互不干扰,各自执行各自的,这就是多线程并发。
- Java中之所以有多线程机制,目的就是为了提高程序的处理效率。
- 线程之间(在Java语言中)
- 思考:对于单核的CPU来说,真的可以做到真正的多线程并发吗?
- 不能,对于单核的CPU来说,在某一个时间节点上实际只能处理一件事情,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,给人类的感觉“多个线程同时在工作”。
- 不能,对于单核的CPU来说,在某一个时间节点上实际只能处理一件事情,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,给人类的感觉“多个线程同时在工作”。
线程的实现
第一种方式:
- 编写一个类,直接继承java.lang.Thread,重写run方法
- 怎么创建线程对象 ==> new
- 怎么启动线程 ==> 调用start()方法
// 创建一个可运行类
public class MyRunnable extends Thread {
@Override
public void run() {
}
}
// 创建线程对象
Thread t = new MyRunnable();
// 启动线程
t.start();
- start()的作用:启动一个分支线程,在JVM中开辟一个新的栈空间,只要新的栈空间开出来,start()方法就结束了,线程就启动成功了。
- 启动成功的线程会自动调用run()方法,并且run()方法位于栈底部(压栈)。
- run方法在分支栈的底部,main方法在主栈的底部,run和main是平级的。
- run方法与start方法区别:run方法并不会开辟分支栈
第二种方式:
- 编写一个类实现java.lang.Runnable接口。
// 创建一个可运行类
public class MyRunnable implements Runnable {
@Override
public void run() {
}
}
// 创建线程对象
Thread t = new Thread(new myRunnable());
// 启动线程
t.start();
建议使用:实现接口方式,面向接口编程;继承有局限性,只能继承一个类。
线程的生命周期
新建状态、就绪状态、运行状态、阻塞状态、死亡状态
实现线程的第三种方式
实现Callable 接口,JDK8新特性,这种方式实现的线程可以获取线程的返回值,之前讲解的两种线程无法获取返回值的,以为run方法返回viod ,此方式效率低(获取结果可能线程会阻塞)。
实现方式:
- 创建一个“未来对象类”对象,参数为 Callable 接口的实现类
- 重写call方法 相当于 run方法
注意:
- 获取线程结果
未来任务类对象.get()
- get方法的执行可能会造成“当前线程阻塞”,因为其拿另一个线程的结果,可能需要等待很久。
代码实现
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建“未来任务类”对象
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
// 模拟执行线程任务
System.out.println("call method begin");
Thread.sleep(1000 * 5);
System.out.println("call method over");
int a = 100;
int b = 200;
return a + b;
}
});
// 创建线程对象
Thread thread = new Thread(task);
thread.start();
// 获取线程执行结果
Object o = task.get();
System.out.println(o);
}
线程常用方法
- 获取当前线程对象
static Thread.currentThread()
- 获取/创建线程名称
线程对象.get/setName()
- 让当前线程休眠,进入“阻塞状态”,放弃占有时间片,让给其他线程使用
static void sleep(long millis)
- 使用场景:间隔特定的时间,去执行一段特定的代码。每隔多久就执行一次。
- 终止线程睡眠
线程对象.interrupt()
- 终止线程
线程对象.stop()
此方法已过时,建议使用 boolean 标记进行终止。
线程调度
- 抢占式调度模型(Java采用的模式):哪个线程的优先级高,抢到的CPU时间片的概率高一些/多一些。
- 均分式调度模型:平均分配CPU时间片,每个线程占有的时间片时间长度相等。一切平等。
- 实例方法(设置/获取线程优先级)【级别1-10】
void setPriority(int newPriority)
int getPriority()
- 默认级别为5
- 静态方法(让位方法,让给其他线程使用)
static void yield()
- 使得该线程从运行状态转换到就绪状态
- 实例方法(合并线程)
void join()
- 合并到当前线程中,当前线程会阻塞
线程安全
注意:在实际开发中,我们的项目运行在服务器中,而服务器已经将线程的定义、创建、启动都已经实现完了,这些代码不需要我们编写。我们要关注的是这些数据在多线程并发的环境下是否安全。
什么时候会发生安全问题:
- 多线程并发
- 有共享数据
- 共享数据有修改行为
同步和异步
异步编程模型:
- 线程t1和线程t2,各自执行各自的,互不相干,谁也不需要等谁,其实就是多线程并发(效率较高)。
同步编程模型
- 线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行完。两个线程之间发生等待关系,其实就是线程排队执行(效率较低)。
Java中哪些变量会产生线程安全问题
只有实例变量、静态变量会产生,而局部变量不会。
- 实例变量:存储在堆内存(数据共享)。
- 静态变量:存储在方法区(数据共享)。
- 局部变量:存储在栈内存(数据不共享)。
只有共享数据才可能出现线程安全问题。
如何解决线程安全问题
- 线程排队执行(不能并发),这种机制被称为“线程同步机制”。
- 线程同步就是线程排队,线程排队就会牺牲一些效率,安全第一位,只有数据安全的情况下,才可以谈效率。
详解如下
public class Account {
private String accountName;
private double balance;
public Account(String accountName, double balance) {
this.accountName = accountName;
this.balance = balance;
}
public Account() {
}
public String getAccountName() {
return accountName;
}
public void setAccountName(String accountName) {
this.accountName = accountName;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public void withdraw(double money) {
synchronized (this) {
// 获取当前余额
double balance = this.getBalance();
// 剩余余额
balance -= money;
// 模拟网络延迟,出现线程安全问题
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 更新余额
this.setBalance(balance);
}
}
}
public class AccountThread extends Thread{
// 两个线程共享一个账户
private Account account;
// 通过构造方法传递过来构造对象
public AccountThread(Account account) {
this.account = account;
}
@Override
public void run() {
// 执行取款操作
account.withdraw(5000);
System.out.println(Thread.currentThread().getName()+"对"+account.getAccountName()+"取款成功,余额为"+account.getBalance());
}
}
public class AccountTest {
public static void main(String[] args) {
// 创建账户
Account account = new Account("act-001", 10000);
// 创建两个线程
AccountThread t1 = new AccountThread(account);
AccountThread t2 = new AccountThread(account);
// 设置name
t1.setName("t1");
t2.setName("t2");
//启动线程取款
t1.start();
t2.start();
}
}
保证线程同步方式
使用 synchronized (){}
同步代码块,同步代码块的范围越小效率越高。
注意1:小括号() 中传入的数据是相当关键的,这个数据必须是多线程共享数据,才能达到线程排队。
注意2:在实例方法上使用synchronized,此时的锁一定是this,不能是其他对象,这种方式具有局限性,导致整个方法体都需要同步,程序的执行效率降低。
执行原理:
在Java语言中,任何一个对象都有“一把锁”,其实这个锁就是标记(只是它叫做锁)(100个对象100把锁)。
- 假设处t1t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。
- 假设t1先执行了,遇到了synchronized,这个时候自动找后面“共享对象”的对象锁,找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的。直到同步代码块代码结束,这把锁才会释放。
- 假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有后面共享对象的这把锁,结果这把锁被1占有,t2只能在同步代码块外面等待1的结束,直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后t2占有这把锁之后,进入同步代码块执行程序。
- 这样就达到了线程排队执行。这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队执行的这些线程对象所共享的。|
死锁
图解
线程t1与线程t2都在等待锁的释放,造成死锁现象
public class Test {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
Object1 t1 = new Object1(o1, o2);
Object2 t2 = new Object2(o1, o2);
t1.start();
t2.start();
}
}
class Object1 extends Thread {
Object o1;
Object o2;
public Object1(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("666");
}
}
}
}
class Object2 extends Thread {
Object o1;
Object o2;
public Object2(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("999");
}
}
}
}
注意:实际开发中,应避免synchronized(){} 同步代码块的嵌套,
死锁的必要条件:
1).互斥使用
线程1拿到了锁,线程2就只能阻塞等待,无法使用锁.
2).不可抢占
线程1拿到锁后,线程2不能强夺锁,只能阻塞等待,等待线程1释放锁
3).请求和保持
线程1拿到锁A后,再尝试获取锁B,此时锁A还是保持的(并不会因为要获取锁B就释放锁A)
4).循环等待
线程1尝试获取锁A和锁B 线程2尝试获取锁B和锁A
线程1在获取锁B的时候等待线程2释放锁B,线程2在获取锁A的时候等待线程1释放锁A
如何解除死锁
在死锁的四个必要条件中,前三个都是锁的基本特性,我们无法更改,所以说破除死锁的突破口就是:循环等待
如何破除循环等待呢?
最简单的办法:给锁编号,然后指定一个固定的顺序(比如从小到大)来加锁,任意线程加多把锁的时候,都让线程遵守上述顺序,此时循环等待自然破除.
实际开发解决安全问题
开发第一步,就是考虑使用synchronized同步代码块吗?
答案:不是,使用同步代码块,会使程序执行效率降低,用户体验不好。系统的吞吐量降低。在不得已的情况下才会使用。
解决方案一:
尽量使用局部变量代替“实例变量”和“静态变量”。
解决方案二:
如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了。)
第三种方案:
如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了。线程同步机制。
守护线程
Java语言中线程分为两大类
一类是:用户线程
一类是:守护线程(后台线程),最有代表性之一就是垃圾回收线程。
守护线程的特点
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程就会自动结束。
注意: 主线程main方法是一个用户线程。
守护线程创建方法:线程对象.setDaemon(true);
定时器
作用:间隔特定的时间,执行特定的程序。
实现方式
- 可以使用sleep方法,这种是最原始的方法,很少用。
- 在Java类库中已有定时器,java.util.Timer ,可以直接使用,不过在在实际开发中很少用,因为有很多高级框架都是支持定时任务的。
- 在实际开发中,目前使用较多的是spring框架所提供的SpringTask框架,只需要进行简单的配置,就可以完成定时器任务。
public class Test {
public static void main(String[] args) throws ParseException {
// 创建定时器对象
Timer timer = new Timer();
// 指定定时任务
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstTime = sdf.parse("2023-3-20 10:20:30");
timer.schedule(new timeTask(), firstTime, 1000 * 2);
}
}
class timeTask extends TimerTask {
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String strTime = sdf.format(new Date());
System.out.println(strTime + ":完成了一次数据备份");
}
}
关于object类中的wait和notify方法。(生产者和消费者模式!)
wait 和 notify 方法不是线程对象的方法,是Object类中自带的,是Java中任何一个对象都有的方法。
Object o = new Object; o.wait(); o.notify();
wait()方法作用:
- o.wait() 让正在o对象上活动的线程t进入等待状态,并释放掉t线程之前占有的o对象的锁。
- o.notify() 让正在o对象上等待的线程唤醒,只是通知,不会释放锁。
生产者和消费者模式:
模拟一个需求:
- 仓库我们采用List集合
- List集合中假设最多只能存储一个元素
- 保持 List仓库中元素个数在 0-1 保持动态平衡
代码实现:
/**
* 消费线程
*/
class Consume implements Runnable {
/**
* 共享仓库 List
*/
private List list;
public Consume(List list) {
this.list = list;
}
@Override
public void run() {
// 一直消费
while (true) {
synchronized (list) {
if (list.size() == 0) {
// 此时仓库为空,不可消费
try {
// 暂停消费线程,释放锁,一直等待
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 执行到此处,可消费
Object o = list.remove(0);
System.out.println(Thread.currentThread().getName() + "--->" + o);
// 唤醒生产线程
list.notifyAll();
}
}
}
}
class Product implements Runnable {
/**
* 共享仓库 List
*/
private List list;
public Product(List list) {
this.list = list;
}
@Override
public void run() {
// 一直生产
while (true) {
synchronized (list) {
if (list.size() > 0) {
// 此时仓库为满,不可生产
try {
// 暂停生产线程,并释放锁,一直等待,
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 执行到此处,可生产
Object o = new Object();
list.add(o);
System.out.println(Thread.currentThread().getName() + "--->" + o);
// 唤醒消费者进行消费
list.notifyAll();
}
}
}
}