JAVA 多线
1、了解多线程
- 多线程(multithreading)是指从软件或者硬件上实现多个线程并发执行的技术。
- 具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。
- 具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能
2、线程相关的概念
-
并行:在同一时刻,有多个指令在多个CPU 上运行执行
-
并发:在同一时刻,有多个指令在单个CPU上交替执行1
- 进程和线程‘
-
进程:是正在运行的软件
- 独立性:进程是一个能够独立运行的基本单位,同时也是系统分配资源和调度的独立单位。
- 动态性:进程的实质是程序的一次执行过程,进程是动态产线,动态消亡的。
- 并发性:任何进程都可以同其他进程一起并发执行。
-
线程:是进程中的单个顺序控制流,是一条执行路径
- 单线程:一个进程如果只有一条执行路径,则称为单线程程序。
- 多线程:一个进程如果有多条执行路径,则称为多线程程序。
-
说明:
- 系统中正在运行的一个应用程序。
- 线程:就是应用程序中做的事情,比如:360软件中的杀毒、扫描木马、清理垃圾
-
3、多线程实现的方式
3.1、多线程实现的方案
3.1.1、继承Thread类的方式机型实现
-
继承Thread类线程实现的步骤
- 定义一个MyThread基础Thread的
- 在MyThread类中重写Run 方法
- 创建MyThread类的对象
- 启动线程
package org.example;
/**
* 编码小王子 😊😊 😊😊😊
*
* @version V1.0
* @date 2023/7/4 14:00
*/
public class MyThread extends Thread{
@Override
public void run() {
//代码就是线程开始启动之后执行的代码
for (int i = 0; i < 100; i++) {
System.out.println("线程开启了"+i);
}
}
}
package org.example;
/**
* 编码小王子 😊😊 😊😊😊
*
* @author 谷凤宇
* @version V1.0
* @date 2023/7/4 14:03
*/
public class Demo {
public static void main(String[] args) {
// 创建了一个线程对象
MyThread myThread1= new MyThread();
// 创建了一个线程对象
MyThread myThread2 = new MyThread();
// 开启了第一个线程
myThread1.start();
// 开启了第二个线程
myThread2.start();
}
}
问题:
- 为啥要重写run方法
- 因为run是用来封装线程的执行方法。
- run() 和start方法的区别?
- run():封装线程的执行代码,直接调用,相当于普通方法的调用,并没有开启线程。
- start():启动线程,然后由JVM调用次线程的Run() 方法。
3.1.2、实现Runnable接口的方式进行实现
-
实现Runnable 接口
-
定义一个MyRunnable实现Runnable接口的类
-
在MyRunnable类中重新Run方法
-
创建Myrunnable类的对象
-
创建Thread类的对象,把Runnable对象作为构造方法的参数
-
启动线程
package org.example2; /** * 编码小王子 😊😊 😊😊😊 * * @version V1.0 * @date 2023/7/4 14:24 */ public class MyRunnable implements Runnable{ @Override public void run() { // 线程启动后执行的代码 for (int i = 0; i <100; i++) { System.out.println("第二种方式实现多线程" + i); } } } package org.example2; /** * 编码小王子 😊😊 😊😊😊 * * @version V1.0 * @date 2023/7/4 14:25 */ public class Demo { public static void main(String[] args) { //创建一个参数的对象 MyRunnable myRunnable = new MyRunnable(); // 创建一个线程对象,并把参数传递给这个线程 // 在线程启动之后,执行的就是参数里面的run方法 Thread thread = new Thread(myRunnable); // 开启线程 thread.start(); MyRunnable myRunnable1 = new MyRunnable(); Thread thread1 = new Thread(myRunnable1); thread1.start(); } }
-
3.1.3、利用Calable和Future接口方式实现
-
Callable 和Future 实现线程
1. 定义一个MyCallable并实现Callable接口的类 2. 在MyCallable类中重写call() 方法 3. 创建MyCallable类的对象 4. 创建Future的实现类FutureTask对象,把MyCallable对象作为方法的构造参数 5. 创建Thread类的对象,把FutureTask对象作为构造方法的参数 6. 启动线程 7. 在调用get,就可以获取线程结束后的结果
package org.example3;
import java.util.concurrent.Callable;
/**
* 编码小王子 😊😊 😊😊😊
*
* @version V1.0
* @date 2023/7/4 15:58
*/
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println("跟女孩表白:"+ i);
}
// 返回值就标示线程运行完毕之后的结果
return "答应了";
}
}
package org.example3;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 编码小王子 😊😊 😊😊😊
*
* @version V1.0
* @date 2023/7/4 16:01
*/
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 线程开启之后需要执行里面的call 方法
MyCallable myCallable = new MyCallable();
// 可以获取线程执行完毕之后的结果,也可以作为参数传递给Thread对象
FutureTask<String> futureTask = new FutureTask<>(myCallable);
// 创建线程
Thread thread = new Thread(futureTask);
thread.start();
// 获取线程运行之后的结果
//如果线程没有运行结束,那么get就会死等
String str = futureTask.get();
System.out.println(str);
}
}
3.1.4、三种线程方式对比
优点 | 缺点 | |
---|---|---|
实现Runnable、Callable接口 | 扩展性强,实现该接口的同时还可以继承其他的类。 | 编程相对复杂,不能直接使用thread类中1的方法 |
继承Thread | 编程比较简单,可以直接使用thread中的方法 | 可以扩展性较差,不能在继承其他的类 |
4、线程类的常见方法
4.1 、获取和设置线程名称
-
获取线程名称
- String getName():返回此线程的名称
4.2、Thread类中设置线程的名称
-
void setName(String name):将此线程的名称更改为等于参数name
-
通过构造方法也可以设置线程名称
package org.example4; /** * 编码小王子 😊😊 😊😊😊 * * @version V1.0 * @date 2023/7/4 16:54 */ public class MyThread extends Thread{ public MyThread() { } public MyThread(String name) { super(name); } @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(getName() + ":@@@ " + i); } } } package org.example4; /** * 编码小王子 😊😊 😊😊😊 * * @version V1.0 * @date 2023/7/4 16:55 */ public class Demo { // 1、 线程是有默认名字的,格式:Thread-编号 public static void main(String[] args) { MyThread myThread = new MyThread("小微"); MyThread myThread2 = new MyThread("小红"); // 设置线程名称 //myThread.setName("小微"); //myThread2.setName("小红"); myThread.start(); myThread2.start(); } }
4.3、获取当前线程的对象
// 此方法返回对当前正在执行的线程对象的引用
public static native Thread currentThread();
public static void main(String[] args) {
Thread.currentThread();
}
4.4 、线程休眠
// 此方法让线程休眠指定时间,单位为毫秒。
public static native void sleep(long millis) throws InterruptedException
// 使用
public static void main(String[] args) {
Thread.sleep(1000);
}
4.5、后台线程/守护
守护线程:当普通线程执行完毕后,那么守护线程也没有继续运行下去的必要了。
// 此方法可以设置为守护线程
public final void setDaemon(boolean on):
public static void main(String[] args) {
Thread.setDaemon(true);
}
4.6、线程调度
线程的调度模型
-
分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间段。
-
抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程,获取的CPU时间片相对多一些。
注意:Java使用的是抢占式调度模型
demo 案例
package org.example7; import java.util.concurrent.Callable; /** * 编码小王子 😊😊 😊😊😊 * * @version V1.0 * @date 2023/7/5 9:16 */ public class MyCallable implements Callable<String> { @Override public String call() throws Exception { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName()+"----"+ i); } return "线程执行完毕了"; } } package org.example7; import java.util.concurrent.FutureTask; /** * 编码小王子 😊😊 😊😊😊 * * @version V1.0 * @date 2023/7/5 9:18 */ public class Demo { public static void main(String[] args) { MyCallable myCallable = new MyCallable(); FutureTask<String> futureTask = new FutureTask<>(myCallable); Thread thread = new Thread( futureTask); thread.setName("飞机"); thread.setPriority(10); System.out.println(thread.getPriority()); thread.start(); MyCallable myCallable1 = new MyCallable(); FutureTask<String> futureTask1 = new FutureTask<>(myCallable1); Thread thread1 = new Thread(futureTask1); thread1.setName("坦克"); thread1.setPriority(1); System.out.println(thread1.getPriority()); thread1.start(); } }
4.7 线程的生命周期
线程的生命周期分为5个阶段,分别为:新建、就绪、运行、阻塞、死亡。
线程生命周期图如下:
1、新建状态(new)
当程序通过new关键字创建出来的线程,该线程就处于新建状态。
2、就绪状态(runnable)
当线程调用start()方法以后,该线程就处于就绪状态。但这并不代表该线程就可以执行了,而是需要去争夺时间片,谁争夺到了时间片就可以执行。
3、运行状态(running)
当处在就绪状态的线程获取到了CPU资源时,随后就会自动执行run()方法,该线程就进入了运行状态。
4、阻塞状态(blocked)
处在运行状态的线程,可能会因为某些原因而导致处在运行状态的线程就会变成阻塞状态。当线程到了就绪状态,线程才有机会转化成运行状态。阻塞的情况有如下三种:
- 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
- 对象锁阻塞:运行的线程当获取到对象锁时,若该对象锁被别的线程占用,则JVM会把该线程放入锁池中。
- 其他阻塞:运行的线程执行了sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程变成阻塞状态。当sleep()方法时间片到了或者阻塞方式结束时,线程就会重新转入就绪状态。(注意:sleep是不会释放持有的锁)
5、死亡状态(terminated)
线程会通过以下三种方式的一种结束,结束后就会处于死亡状态:
- run()方法执行完成,线程正常结束;
- 线程抛出一个未捕获的Exception或Error;
- 直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用;
5、线程的安全问题
5.1、案例:卖票
需求:
- 某电影院目前正在上映国产大片,共有100张票,而他有3个窗口在买票,请设计一个程序模拟该电影院买票
思路:
-
定义一个Ticket类实现Runnable接口,里面定义一个成员变量,
private int ticket = 100;
-
在Ticket类中重写Run() 方法实现卖票,代码步骤如下:
- 判断票数大于0,就卖票,并告知是那个窗口卖的票
- 卖的票数要减1
- 卖光之后,线程停止
-
定义一个测试类TicketDemo,里面有main方法,代码步骤如下
-
创建Ticket 类的对象。
-
创建三个Thread类的对象,把ticket对象作为构造方法的参数,并更改线程对应窗口的名称。
-
启动线程
-
package org.example9;
/**
* 编码小王子 😊😊 😊😊😊
*
* @version V1.0
* @date 2023/7/5 13:36
*/
public class Ticket implements Runnable{
// 票的数量
private int ticket = 100;
@Override
public void run() {
while (true){
if (ticket <= 0){
// 买完了
break;
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket --;
System.out.println(Thread.currentThread().getName() +"在买票,还剩下"+ ticket+"张票");
}
}
}
}
package org.example9;
/**
* 编码小王子 😊😊 😊😊😊
*
* @version V1.0
* @date 2023/7/5 13:40
*/
public class Demo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread thread1 = new Thread(ticket);
Thread thread2 = new Thread(ticket);
Thread thread3 = new Thread(ticket);
thread1.setName("窗口一");
thread2.setName("窗口二");
thread3.setName("窗口三");
thread1.start();
thread2.start();
thread3.start();
}
}
5.2、案例思考:
执行上面的demo 出现了如下问题,为什么,正常卖票情况都会有延迟,每次出票的时间为100毫秒,用sleep() 实现睡眠100毫秒
-
问题
-
相同的票,重复售卖,出现多次
-
卖完了,居然还在出票,出现了负票数
-
5.3、解决卖票数据安全问题
-
为啥会出现卖票数据安全问题
- 多线程共享数据
-
解决多线程安全问题
-
把多条语句共享数据的代码给=锁起来,让任意时刻只能有一个线程执行即可
-
JAVA提供了同步代码快的方式来解决
-
-
锁多条语句操作共享数据,可以使用同步代码块实现
-
格式:
synchronized(任意对象){
多条语句操作共享数据的代码
}
-
默认情况是打开的,只要有一个线程进去执行代码了,锁就会关闭
-
当线程执行完出来了,锁才会自动打开
-
-
同步的好处和弊端
-
好处:解决了多线程数据安全问题
-
弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这个是很耗费资源的,无形中会降低程序的运行效率
public class Ticket implements Runnable{ // 票的数量 private int ticket = 100; private final Object object = new Object(); @Override public void run() { while (true){ synchronized (object) { if (ticket <= 0){ // 买完了 break; }else { try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } ticket --; System.out.println(Thread.currentThread().getName() +"在买票,还剩下"+ ticket+"张票"); } } } } }
注意:synchronized 中锁的对象必须是唯一的
-
5.4、synchronized 同步方法
-
同步方法:就是把synchronized 关键字添加到方法上面
-
格式:
public synchronized void test(int num){}
-
-
同步代码快和同步方法的去吧
- 同步代码快可以锁住指定代码,同步方法是锁住方法中所有代码。
-
同步代码快,可以指定锁对象,同步方法不能指定锁对象。
-
同步方法的锁对象是this
5.5、同步方法
同步静态方法:就是把synchronized关键字加到静态方法上
-
格式:
修饰符static synchronized返回值类型方法名(方法参数){
同步静态方法的锁对象是什么呢?
- 类名.class
5.6、Lock锁
虽然我们可以理解同步代码快和同步方法的锁对象问题,但是我们并没有直接看到在哪里添加了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁操作
Lock中提供了获得锁和释放锁的方法
-
void Lock():获得锁
-
void unlock:释放锁
Lock是接口不能直接实例化,这里采用他的实现类ReentrantLock来实例化
ReentrantLock的构造方法
- RenntrantLock():创建一个ReentrantLock的实例
package org.example12;
import java.util.concurrent.locks.ReentrantLock;
/**
* 编码小王子 😊😊 😊😊😊
*
* @version V1.0
* @date 2023/7/8 10:43
*/
public class Ticket implements Runnable {
// 票的数量
private int ticket = 100;
private Object obj = new Object();
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// synchronized (obj){ // 多个线程必须使用同一把锁
try {
lock.lock();
if (ticket <= 0) {
// 卖完了
break;
} else {
Thread.sleep(100);
ticket--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩余:" + ticket + "张票");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
// }
}
}
}
package org.example12;
/**
* 编码小王子 😊😊 😊😊😊
*
* @version V1.0
* @date 2023/7/8 10:52
*/
public class Demo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread thread1 = new Thread(ticket);
Thread thread2 = new Thread(ticket);
Thread thread3 = new Thread(ticket);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
6、死锁
线程死锁是指由于两个或者多个线程相互持有对方锁需要的资源,导致这些线程处于等待状态,无法前往执行
发生死锁的四个必要条件
- 互斥使用,线程1拿到了锁,线程2就得等着。(锁的基本特性)
- 不可抢占,线程1拿到锁之后,必须是线程1主动释放,不能说是线程2就把锁给强行获取到。
- 请求和保持,线程1拿到锁A之后,再尝试获取锁B,A这把锁还是保持的。(不会因为获取锁B就把A给释放了)
- 循环等待,线程1尝试获取到锁A和锁B,线程2尝试获取到锁B和锁A。线程1在获取B的时候等待线程2释放B;同时线程2在获取A的时候等待线程1释放A
package org.example13;
/**
* 编码小王子 😊😊 😊😊😊
*
* @version V1.0
* @date 2023/7/8 11:06
*/
public class Demo {
public static void main(String[] args) {
Object obeA = new Object();
Object obeB = new Object();
new Thread(()->{
while (true) {
synchronized (obeA){
// 线程一
synchronized (obeB){
System.out.println("小明同学正在学走路......");
}
}
}
}).start();
new Thread(() -> {
while (true) {
synchronized (obeB){
// 线程二
synchronized (obeA){
System.out.println("小强同学正在学走路。。。。。。。");
}
}
}
}).start();
}
}
注意:死锁是因为锁的嵌套导致的,破除死锁的办法就是给锁编号,然后指定一个固定的顺序(比如从小到大)来加锁。任意线程加多把锁的时候,都让线程遵守上述顺序,此时循环等待就自然破除了。
7、生产者与消费者
7.1 等待和唤醒
等待和唤醒的方法
为了体现生产和消费过程中的等待和唤醒,JAVA提供了几个方法供我们使用,这几个方法在Object类中,Object类中等待和唤醒方法:
方法名 | 说明 |
---|---|
void wail() | 导致当前线程等待,直到另一个线程调用该对象的notify() 方法或notifyAll()方法 |
void notify() | 唤醒正在等待对象监视器的单个线程 |
void notifyAll() | 唤醒正在等待对象监视器的所有线程 |