Java进阶12 多线程
一、进程&线程
1、进程
1.1 定义
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位。通俗来讲,即程序的执行过程
1.2 特点
-
独立性
进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他的进程的地址空间。
-
动态性
进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的。
-
并发性
任何进程都可以同其他进程一起并发执行
⭐注意区别并发和并行!!!
-
并发(在同一时刻,有多个指令在单个CPU上【交替】执行)
-
并行(在同一时刻,有多个指令在多个CPU上【同时】执行)
-
1.3 多进程
对于一个CPU而言,它是在多个进程间轮换执行的,只是轮换很快,我们感知不到。
2、线程
2.1 定义
进程可以同时执行多个任务,每个任务就是线程。线程依赖进程,线程是进程执行的最小单元。一个进程至少应该存在一个线程
2.2 单线程&多线程
-
单线程:程序只有一条执行路径
-
多线程:程序有多条执行路径
Java程序默认就是多线程(主方法一进栈就意味着主线程(main)开启了,并且垃圾回收线程也随之开启,不定时去检测内存中的垃圾并回收)
2.3 多线程的意义
-
提高执行效率
-
“同时”处理多个任务
二、Java开启线程的方式(3种)
1、继承Thread类
1.1 方法介绍
方法 | 说明 |
---|---|
void run() | 在线程开启后,此方法将被调用执行 |
void start() | 使此线程开始执行,Java虚拟机会调用run方法() |
1.2 实现步骤
编写一个类,继承Thread类
重写run()方法(将线程任务代码写在run方法中)
创建线程对象
调用start方法开启线程
public class ThreadDemo1 { public static void main(String[] args) { //4、创建线程对象 MyThread mt1 = new MyThread(); MyThread mt2 = new MyThread(); //5、调用start方法开启线程 mt1.start(); mt2.start(); } } //1、编写一个类,继承Thread方法 class MyThread extends Thread{ //2、重写run方法 @Override public void run() { //3、将线程任务代码写在run方法种 for (int i = 1; i <=500 ; i++) { System.out.println("线程任务正在执行"+i); } } }
注意statrt方法和run方法的区别
调用start方法,内部会自动调用run方法,只有调用了start方法,才算开启了线程
调用run方法,仅仅是对方法的一次调用而已,并没有开启新的线程
2、实现Runable接口
2.1 Thread构造方法
方法名 | 说明 |
---|---|
Thread(Runnable target) | 分配一个新的Thread对象 |
Thread(Runnable target,String name) | 分配一个新的Thread对象 |
2.2 实现步骤
编写一个类,实现Runnable接口
重写run()方法(将线程任务代码写在run方法中)
创建线程任务对象
创建线程对象,将线程任务传入
使用线程对象调用start方法开启线程
public class ThreadDemo2 { public static void main(String[] args) { //4、创建线程任务对象 MyRunnable mr = new MyRunnable(); //5、创建线程对象,将线程任务传入 Thread t1 = new Thread(mr); Thread t2 = new Thread(mr); //6、使用线程对象,调用start方法开启线程 t1.start(); t2.start(); } } //1、编写一个类,实现Runnable接口 class MyRunnable implements Runnable{ //2、重写run方法 @Override public void run() { //3、将线程任务代码写在run方法中 for (int i = 1; i <=500 ; i++) { System.out.println("线程执行了:"+i); } } }
3、实现Callable接口
3.1 方法介绍
方法 | 说明 |
---|---|
V call() | 计算结果,如果无法计算结果,则抛出一个异常 |
FutureTask(Callable<V> callable) | 创建一个FutureTask,一旦运行就执行给定的Callable |
V get() | 如有必要,等待计算完成,然后获取其结果 |
3.2 实现步骤
编写一个类,实现Callable接口
重写call方法(将线程任务代码写在call()方法当中)
创建线程的资源对象
创建线程的任务对象(FutureTask<T>)来封装资源
创建线程对象,传入线程任务
使用线程对象调用start方法,开启线程
获取线程任务结束的返回值
public class ThreadDemo5 { public static void main(String[] args) throws ExecutionException, InterruptedException { //4、创建线程资源对象 MyCallable mc = new MyCallable(); //5、创建线程任务对象,封装资源; // 该对象可以作为参数传递给Thread对象,也可以获取线程执行完毕之后的结果 FutureTask<Integer> task1 = new FutureTask<>(mc); FutureTask<Integer> task2 = new FutureTask<>(mc); //6、创建线程对象,传入线程任务 Thread t1 = new Thread(task1); Thread t2 = new Thread(task2); //7、使用线程对象调用start方法,开启线程 t1.start(); t2.start(); //8、获取线程任务结束的返回值 Integer r1 = task1.get(); Integer r2 = task2.get(); System.out.println("task1获取返回值为"+r1); System.out.println("task2获取返回值为"+r2); } } //1、编写一个类实现Callable接口 class MyCallable implements Callable<Integer>{ //2、重写call方法 @Override public Integer call() throws Exception { //将线程任务代码写在call方法中 int sum =0; for (int i = 1; i <=100 ; i++) { sum+=i; System.out.println("sum的累加过程"+sum); } return sum; } }
⭐三种方式的对比
实现Runnable、Callable接口(重点掌握) | 继承Thread类 | |
---|---|---|
好处 | 扩展性强,实现该接口的同时还可以继承其他的类 | 编程比较简单,可以直接使用Thread类中的 |
缺点 | 编程相对复杂,不能直接使用Thread类中的方法 | 扩展性较差,不能再继承其他的类 |
推荐使用实现接口的两种方式,扩展性更好。当线程执行任务需要有返回值时,选择实现Callable接口来实现!
三、线程相关方法
1、线程名称相关方法
方法 | 说明 |
---|---|
String getName() | 返回此线程的名称 |
void setName(String name) | 设置线程名称(线程的构造方法也可以设置) |
Thread currentThread() | 获取当前正在执行的线程对象的引用 |
public class MethodDemo1 {
public static void main(String[] args) {
//线程的构造方法设置线程名称
MyThread mt1 = new MyThread("线程A");
MyThread mt2 = new MyThread("线程B");
/*//线程对象调用setName方法设置线程名
mt1.setName("线程A:");
mt1.setName("线程B:");*/
mt1.start();
mt2.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
for (int i = 1; i <=500 ; i++) {
//获取线程名称,此注的super.可以省略
System.out.println(super.getName()+"线程任务正在执行"+i);
//获取当前线程名,结果和上一行代码super.getName()相同
String name = Thread.currentThread().getName();
}
}
public MyThread() {
}
public MyThread(String name) {
super(name);
}
}
2、线程休眠
方法 | 说明 |
---|---|
static void sleep(long millis) | 使当前正在执行的线程休眠(暂停执行)指定的毫秒数 |
public class Test2 {
public static void main(String[] args) {
MyThread1 mt1 = new MyThread1();
Thread t1 = new Thread(mt1);
t1.start();
}
}
class MyThread1 implements Runnable{
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-M-d HH:mm:ss");
@Override
public void run() {
try {
for (int i = 1; i <=10 ; i++) {
System.out.println(Thread.currentThread().getName()+" "+formatter.format(LocalDateTime.now()));
//线程休眠1秒,效果就是每一秒打印一次当前时间
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3、线程优先级
方法 | 说明 |
---|---|
final int getPriority() | 返回此线程的优先级 |
final void setPriority(int newPriority) | 设置线程的优先级;线程默认优先级是5;更改范围是1-10 |
public class MethodDemo2 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <=20 ; i++) {
System.out.println(Thread.currentThread().getName()+"线程任务执行"+i);
}
}
},"线程A");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <=30 ; i++) {
System.out.println(Thread.currentThread().getName()+"线程任务执行"+i);
}
}
},"线程B");
//设置线程A的优先级为1
t1.setPriority(1);
//设置线程B的优先级为10
t2.setPriority(10);
//获取线程A的优先级
System.out.println(t1.getPriority());
//获取线程B的优先级
System.out.println(t2.getPriority());
t1.start();
t2.start();
}
}
4、守护线程
方法 | 说明 |
---|---|
final void setDeamon(boolean on) | 设置为守护线程 |
public class MethodDemo3 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <=20 ; i++) {
System.out.println(Thread.currentThread().getName()+"线程任务执行"+i);
}
}
},"线程A");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <=30 ; i++) {
System.out.println(Thread.currentThread().getName()+"线程任务执行"+i);
}
}
},"线程B");
//设置t2线程为守护线程,为其他线程“保驾护航”;
t2.setDaemon(true);
t1.start();
t2.start();
}
}
四、线程安全和同步
1、安全问题出现的条件
-
多线程环境
-
有数据共享
-
有多条语句操作共享数据
2、同步技术(解决数据安全问题)
解决数据安全问题的思路:将多条语句操作共享数据的代码给锁起来,让任意时刻只能由一个线程执行即可。
2.1 同步代码块
-
格式
synchronized(任意对象) { 多条语句操作共享数据的代码 }
synchronized(任意对象)就相当于给代码加锁了,任意对象就可以看成是一把锁
-
好处和弊端
-
好处:解决了多线程的数据安全问题
-
弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
-
-
Demo
public class CinemaTest1 { public static void main(String[] args) { TicketTask task = new TicketTask(); new Thread(task,"窗口1:").start(); new Thread(task,"窗口2:").start(); new Thread(task,"窗口3:").start(); } } class TicketTask implements Runnable{ int tickets = 1000; @Override public void run() { while (true) { //对以下代码加锁(多个线程为了保证共享数据安全需要加同一把锁!!!) synchronized (TicketTask.class) { if(tickets == 0){ break; }else{ System.out.println(Thread.currentThread().getName()+"卖出了第"+tickets+"号票"); tickets--; } } //锁释放 } } }
2.2 同步方法
-
格式
//非静态同步方法 锁对象为this 修饰符 synchronized 返回值类型 方法名(方法参数){ 方法体; } //静态同步方法 锁对象为 类名.class 修饰符 static synchronized 返回值类型 方法名(方法参数){ 方法体; }
-
注意事项:①非静态同步方法的锁对象为this;②静态同步方法的锁对象为 类名.class;
2.3 Lock锁(互斥锁)
使用Lock锁,能够更清晰的看到哪里加了锁,哪里释放了锁。Lock是接口,无法直接创建对象,创建的是其实现类对象ReentrantLock(可重入锁)
-
构造方法
方法名 说明 public ReentrantLock() 创建一个ReentrantLock的实例 -
加锁解锁方法(Object类的方法)
方法名 说明 void lock() 加锁 void unlock() 释放锁 -
Demo
public class CinemaTest4 { public static void main(String[] args) { TicketTask4 task = new TicketTask4(); new Thread(task,"窗口1:").start(); new Thread(task,"窗口2:").start(); new Thread(task,"窗口3:").start(); } } class TicketTask4 implements Runnable{ int tickets = 1000; //创建锁对象 ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (true) { try { //该线程获得锁 lock.lock(); if(tickets == 0){ break; }else{ System.out.println(Thread.currentThread().getName()+"卖出了第"+tickets+"号票"); tickets--; } //不管程序运行何时结束,锁一定要释放,所以使用finally } finally { //该线程释放锁 lock.unlock(); } } } }
* 死锁
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
五、线程通讯(等待唤醒机制)
1、线程通信
-
确保线程能够按照预定的顺序执行,并且能够安全地访问共享资源
-
使多线程更好地进行协同工作
2、等待唤醒机制
成员方法 | 说明 |
---|---|
void wait() | 使当前线程等待 |
void notify() | 随机唤醒单个等待地线程 |
void notifyAll() | 唤醒所有等待地线程 |
注意事项:
-
这些方法都需要使用锁对象调用
-
wait()方法在等待期间会释放锁对象
public class Test6 {
public static void main(String[] args) {
Printer p = new Printer();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (Printer.class) {
try {
p.Print1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}, "线程1").start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (Printer.class) {
try {
p.Print2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}, "线程2").start();
}
}
class Printer{
int flag = 1;
public void Print1() throws InterruptedException {
if (flag != 1) {
//线程1等待
Printer.class.wait();
}
System.out.print("黑");
System.out.print("马");
System.out.print("程");
System.out.print("序");
System.out.print("员");
System.out.println();
flag = 2;
//唤醒线程2
Printer.class.notify();
}
public void Print2() throws InterruptedException {
if(flag!=2){
//线程2等待
Printer.class.wait();
}
System.out.print("传");
System.out.print("智");
System.out.print("教");
System.out.print("育");
System.out.println();
flag = 1;
//唤醒线程1
Printer.class.notify();
}
}
也可以使用ReentrantLock实现同步,并获取Condition监视器对象(案例见3.3)Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是由Lock对象创建出来的,换句话说,Condition是依赖Lock对象的。Condition在调用方法之前必须获取锁。
成员方法 | 说明 |
---|---|
void await() | 指定线程等待 |
void signal() | 指定唤醒单个等待的线程 |
3、生产者消费者模式
生产者消费者模式是一个十分经典的多线程协作的模式。
3.1 包含线程两类
-
生产者线程:用于生产数据
-
消费者线程:用于消费数据
3.2 过程理解
-
为了解耦生产者和消费者的关系,通常会采用共享的数据区域(缓冲区),就像是一个仓库;
-
生产者生产数据之后直接放置在共享数据区中,并不需要关信消费者的行为;
-
消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为;
3.3 案例(吃包子)
public class WareHouse { /* 共享区域的数据,为了方便其他类调用,全都加上public 和 static修饰符 这样其他类调用就可以直接用类名.进行调用了 */ public static boolean mark; //选择可重入锁的锁对象,锁机制更灵活 public static ReentrantLock lock = new ReentrantLock(); //Condition监视器对象关联锁,Condition对象的创建依赖锁对象 public static Condition producer = lock.newCondition(); public static Condition consumer = lock.newCondition(); }
/** * 生产者类实现Runable接口 */ public class Producer implements Runnable{ //重写run方法 @Override public void run() { while(true){ WareHouse.lock.lock(); if(WareHouse.mark){ //mark为true,生产者休眠 try { WareHouse.producer.await(); } catch (InterruptedException e) { e.printStackTrace(); } }else{ //mark为false,生产者生产,改标记,唤醒消费者 System.out.println("生产者生产"); WareHouse.mark= true; WareHouse.consumer.signal(); } WareHouse.lock.unlock(); } } }
/** * 消费者类实现Runable接口 */ public class Consumer implements Runnable{ //重写run()方法 @Override public void run() { while(true){ //消费者线程上锁 WareHouse.lock.lock(); if(WareHouse.mark){ //mark为true说明有包子,吃包子 System.out.println("消费者消费"); //改标记 WareHouse.mark = false; //唤醒生产者线程 WareHouse.producer.signal(); }else{ //mark为false说明没有包子,消费者线程等待 try { WareHouse.consumer.await(); } catch (InterruptedException e) { e.printStackTrace(); } } //消费者线程解锁 WareHouse.lock.unlock(); } } }
/** * 测试类(主类) */ public class Test { public static void main(String[] args) { //开启生产者线程 new Thread(new Producer()).start(); //开启消费者线程 new Thread(new Consumer()).start(); } }