【动力节点 Java进阶学习笔记】第七章 多线程
1、进程和线程的基本概念、区别以及多进程和多线程的作用
- 每个进程是一个应用程序,都有独立的内存空间;线程是程序执行的最小单位,是进程的一个执行流,一个进程是由多个线程组成的。
- 同一个进程中的线程共享其进程中的内存和资源
- 多进程的作用:提高CPU的使用率
- 多线程的作用:提高进程的使用率
- 线程和线程之间栈内存独立,堆内存和方法区内存共享
- 进程和线程的简单解释可参考:进程与线程的一个简单解释
2、创建线程的两种方式、启动线程的方法、run()和start()的区别
-
创建线程的方式有两种:
1、继承Thread类,重写run()方法
创建线程对象:new就行了。
2、实现Runnable接口(推荐使用Runnable接口),实现run方法
创建线程对象:Thread t = new Thread(new MyRunnable()); -
启动线程:调用线程对象的start()方法。
-
run()和start()的区别:
run()方法:不会启动线程,不会分配新的分支栈,多进程中JVM调用该方法
start()方法:启动一个分支线程,在JVM中开辟一个新的栈空间,任务完成之后,start()方法瞬间就结束了,线程进入就绪状态 -
两种创建线程方式的区别:
简要说的话就是接口方式的可以通过传入同一个实例来创建不同的线程,共享实例变量,因此接口方式可以实现多个线程同时完成一件任务的情况;而继承Thread方式则是通过创建不同的线程对象来实现多线程,即每个线程都在干同样的任务,但是线程之间没有交集。
详细解析可参考:多线程——Java中继承Thread类与实现Runnable接口的区别
3、线程的生命周期
- 新建:采用new语句创建完成进程对象
- 就绪:执行start ()方法后
- 运行:占用CPU 时间
- 阻塞:执行了wait 语句、执行了sleep 语句和等待某个对象锁,等待输入的场合
- 终止:退出run()方法
4、线程中常用的方法以及线程休眠、唤醒、终止的方法
- 获取当前线程对象:Thread.currentThread()
- 获取线程对象的名字:线程对象.getName()
- 修改线程对象的名字:线程对象.setName(“线程名字”)
- 线程休眠:Thread.sleep(1000)
- 线程唤醒:线程对象.interrupt() 依靠异常处理机制
- 线程终止:线程对象.stop()–已过时,容易丢失数据
推荐在线程对象中设置标志位,并且在run方法中增加对标志位的判断,通过修改标志位来终止线程
5、线程的调度:线程调度模型、与线程调度有关系的方法
- 抢占式调度模型(java采用这种)和均分式调度模型
- 设置线程的优先级:void setPriority(int newPriority)
- 获取线程优先级:int getPriority() 优先级1-10,默认为5
- 暂停线程:static void yield() 从运行状态回到就绪状态
- 合并线程:void join() 等待该线程终止
6、多线程安全:存在线程安全问题的三个条件、线程同步机制及语法、以及开发中解决线程安全问题的方法
-
存在线程安全问题的三个条件:
条件1:多线程并发。
条件2:有共享数据。
条件3:共享数据有修改的行为。 -
异步编程模型和同步编程模型:
异步编程模型:多线程并发,线程之间互不影响
同步编程模型:线程排队执行,线程执行需要等待另一个线程执行结束 -
如何解决多线程问题:线程同步机制
-
synchronized的三种写法:
第一种:同步代码块,优点是灵活
synchronized(线程共享对象){
同步代码块;
}
synchronized后面小括号中传的这个“数据”是相当关键的。
这个数据必须是多线程共享的数据,才能达到多线程排队的目的。
第二种:在实例方法上使用synchronized
表示共享对象一定是this,并且同步代码块是整个方法体。
优缺点:优点是代码写得少,缺点是共享对象只能是this,不灵活,而且扩大同步范围导致效率降低。
第三种:在静态方法上使用synchronized
表示找类锁,类锁永远只有1把,就算创建了100个对象,那类锁也只有一把。 -
采用synchronized同步最好只同步有线程安全的代码,可以优先考虑使用synchronized同步块,因为同步的代码越多,执行的时间就会越长,其他线程等待的时间就会越长,影响效率。
-
变量的线程安全:
局部变量+常量:不会有线程安全问题。 (局部变量在栈中,是每个线程独享的,常量是无法被修改)
成员变量:可能会有线程安全问题。 -
死锁:
概念:是两个或更多线程阻塞着等待其它处于死锁状态的线程所持有的锁。
通常发生在多个线程同时但以不同的顺序请求同一组锁的时候。 -
死锁的代码实现:
package com.javalearn.javase.review.thread;
public class DeadLock {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
MyThread11 mt1 = new MyThread11(o1, o2);
MyThread12 mt2 = new MyThread12(o1, o2);
mt1.start();
mt2.start();
}
}
class MyThread11 extends Thread {
private Object o1;
private Object o2;
public MyThread11 (Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o1) {
try {
Thread.sleep(1000 * 3);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println(111);
}
}
}
}
class MyThread12 extends Thread {
private Object o1;
private Object o2;
public MyThread12 (Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o2) {
try {
Thread.sleep(1000 * 3);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println(222);
}
}
}
}
- 开发中解决线程安全问题的方法:
第一种方案:尽量使用局部变量代替“实例变量和静态变量”。
第二种方案:如果必须是实例变量,那么可以考虑创建多个对象
第三种方案:如果不能使用局部变量,对象也不能创建多个,就只能选择synchronized了
7、守护线程的概念和设置守护线程的方法、定时器的概念和使用
7.1 守护线程
- java语言中线程分为两大类:用户线程(如主线程main方法)和守护线程(后台线程,如垃圾回收线程)
- 守护线程的特点:一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。
- 设置为守护线程:启动线程之前,将线程设置为守护线程 t.setDaemon(true); t.start();
- 守护线程的实现案例:
package com.javalearn.javase.review.thread;
public class DamenThreadTest {
public static void main(String[] args) {
BakDataThread bdt = new BakDataThread();
bdt.setName("数据备份线程");
bdt.setDaemon(true);
bdt.start();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class BakDataThread extends Thread {
@Override
public void run() {
int i = 0;
while (true) {
System.out.println(Thread.currentThread().getName() + "--->" + (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
7.2 定时器
- 定时器的作用:间隔特定的时间,执行特定的程序。
- 定时器的实现:可使用java.util.Timer,实际开发中使用框架来实现定时任务,如SpringTask框架
- Timer类:
构造方法:Timer() 创建一个新计时器。
方法:
安排指定的任务在指定的时间开始进行重复的固定延迟执行:
void schedule(TimerTask task, Date firstTime, long period)
安排指定的任务从指定的延迟后开始进行重复的固定延迟执行:
void schedule(TimerTask task, long delay, long period)
TimerTask为抽象类 - 定时器的实现案例:
package com.javalearn.javase.review.thread;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class TimerTest {
public static void main(String[] args) {
Timer timer = new Timer();
//安排指定的任务从10s后开始每隔10s执行一次
// timer.schedule(new LogTimerTask(),1000 * 10, 1000 * 10);
//安排指定的任务从2022年2月19日20点33分开始每隔10s执行一次
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
Date startDate = sdf.parse("2022-02-19 20:33:00");
timer.schedule(new LogTimerTask(), startDate, 1000 * 10);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
class LogTimerTask 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 + ":执行了一次日志备份操作");
}
}
8、实现线程的第三种方式:实现Callable接口来创建线程
- 实现Callable接口来创建线程的优缺点:
优点:这种方式实现的线程可以获取线程的返回值(即获取线程的执行结果)。
缺点:效率比较低,在获取线程执行结果的时候,当前线程受阻塞,效率较低。 - 创建线程的方法:
(1)创建一个FutureTask对象:构造方法参数传入Callable接口实现类对象(实现call()方法)
(2)创建线程对象:构造方法参数传入上述的FutureTask对象(本质也是Runnable接口实现类对象)
(3)获取线程返回结果:FutureTask的get()方法,get()方法的执行会导致“当前线程阻塞” - 实现Callable接口来创建线程的例子:
package com.javalearn.javase.review.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadTest11 {
public static void main(String[] args) {
FutureTask ft = new FutureTask(new MyThread13());
Thread thread = new Thread(ft);
thread.start();
Object obj = null;
try {
obj = ft.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("线程执行结果" + obj);
System.out.println("main method end");
}
}
class MyThread13 implements Callable {
@Override
public Object call() throws Exception {
System.out.println("call method begin");
Thread.sleep(1000 * 5);
System.out.println("call method end");
int a = 10;
int b = 20;
return a + b;
}
}
9、Object类中的wait和notify方法以及实现消费者和生产者模式
- wait和notify方法不是线程对象的方法,是普通java对象都有的方法(Object中方法)
- wait和notify方法的调用:<Object>.wait()、<Object>.noyify()
- wait方法作用:o.wait()让正在o对象上活动的线程t进入等待状态,并且释放掉t线程之前占有的o对象的锁。
- notify方法作用:o.notify()让正在o对象上等待的线程唤醒,只是通知,不会释放o对象上之前占有的锁。
- notifyAll方法:这个方法是唤醒o对象上处于等待的所有线程。
- 生产者和消费者模式:
生产线程负责生产,消费线程负责消费。
生产线程和消费线程要达到均衡。
这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法。 - 实现消费者和生产者模式的例子:
package com.javalearn.javase.review.thread;
import javafx.beans.binding.ObjectExpression;
import java.util.ArrayList;
import java.util.List;
public class ThreadTest12 {
//生产者模式和消费者模式,为简单化描述设定仓库中有1个元素就开始消费,消费完就开始生产
public static void main(String[] args) {
List list = new ArrayList();
Thread thread1 = new Thread(new Producer(list));
Thread thread2 = new Thread(new Customer(list));
thread1.setName("生产者线程");
thread2.setName("消费者线程");
thread1.start();
thread2.start();
}
}
class Producer implements Runnable{
List list;
public Producer (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 obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName() + "--->" + obj);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.notifyAll();
}
}
}
}
class Customer implements Runnable{
List list;
public Customer (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 obj = list.remove(0);
System.out.println(Thread.currentThread().getName() + "--->" + obj);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.notifyAll();
}
}
}
}