一、进程与线程的概念
进程:进程是程序的基本执行实体。【线程是进程里面的】
线程:线程是操作系统能够进行运算调度的最小单位。它被包含在进程中,是进程中的实际运作单位。(简单理解:应用软件中互相独立,可以同时运行的功能)
程序的执行是需要时间的,多线程就是让cpu像牛马一样不间断的工作,在等待的时间也要去做其他事情,充分利用cpu来达到提高效率的目的,一句话:不许歇着,起来干活!
image.png
二、并发与并行的概念
并发:在同一时刻,有多个指令在单个cpu上交替执行
并行:在同一时刻,有多个指令在多个cpu上同时执行
image.png
三、多线程的三种实现方式
方式1.继承 Thread 类并重写run方法
myThread 类
package com.进程和多线程;
//多线程的第一种实现方式:继承 Thread 类并重写run方法
public class myThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+ " hello world");
}
}
}
测试类
package com.进程和多线程;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class demo1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 方式1
method1();
}
// 方式一:多线程的第一种实现方式:继承 Thread 类并重写run方法
private static void method1() {
System.out.println("--------------------------------- 方式一:多线程的第一种实现方式:继承 Thread 类并重写run方法 ---------------------------------");
myThread t1 = new myThread();
myThread t2 = new myThread();
// 给线程取名字
t1.setName("线程1");
t2.setName("线程2");
// 开启线程 - 可以看到控制台是交替执行的两个线程 - “并发”
t1.start();
t2.start();
}
}
方式2.实现 Runnable 接口重写run方法
myThread2 类
package com.进程和多线程;
// 多线程的第二种实现方式:实现 Runnable 接口重写run方法
public class myThread2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// Thread.currentThread() 获取当前执行的线程
System.out.println(Thread.currentThread().getName() + " hello world");
}
}
}
测试类
package com.进程和多线程;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class demo1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 方式2
method2();
}
// 方式二:多线程的第二种实现方式:实现 Runnable 接口重写run方法
private static void method2(){
System.out.println("--------------------------------- 方式二:多线程的第二种实现方式:实现 Runnable 接口重写run方法 ---------------------------------");
// 定义一个任务
myThread2 m = new myThread2();
// 将任务传递给线程,表示执行这个任务一次
Thread t1 = new Thread(m);
// 将任务传递给线程,表示执行这个任务两次
Thread t2 = new Thread(m);
// 给线程取名字
t1.setName("线程1");
t2.setName("线程2");
// 开启线程 - 可以看到控制台是交替执行的两个线程 - “并发”
t1.start();
t2.start();
}
}
方式3.利用Callable接口和Future接口实现 - 注意:这种方式可以获取多线程的 “返回值”
myThread3 类,注意你想要的线程的返回值也就是 Callable 的泛型
package com.进程和多线程;
import java.util.concurrent.Callable;
/**
* 想要获取多线程的返回值,那么使用第三种方式 利用Callable接口和Future接口实现
* 多线程的第三种实现方式:
* 1.定义一个类实现 Callable 接口重写call方法(有返回值的,表示多线程的运行结果)
* 2.创建 Callable 的对象(表示多线程要执行的任务)
* 3.创建 FutureTask 对象来管理多线程的运行结果
* 4.创建线程对象
* 5.在 FutureTask 里面获取多线程的运行结果
*/
public class myThread3 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 求1-100的和
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
}
}
测试类
package com.进程和多线程;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class demo1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 方式3
method3();
}
// 方式三:利用Callable接口和Future接口实现 - 这种方式可以获取多线程的返回值
private static void method3() throws ExecutionException, InterruptedException {
System.out.println("--------------------------------- 方式三:多线程的第三种实现方式 - 利用Callable接口和Future接口实现 ---------------------------------");
// 1.定义一个类实现 Callable 接口重写call方法(有返回值的,表示多线程的运行结果)
// 2.创建 Callable 的对象(表示多线程要执行的任务)
myThread3 m3 = new myThread3();
// 3.创建 FutureTask 对象来管理多线程的运行结果
FutureTask<Integer> ft = new FutureTask<>(m3);
// 4.创建线程对象
Thread t1 = new Thread(ft);
t1.start();
// 5.在 FutureTask 里面获取多线程的运行结果
Integer result = ft.get();
System.out.println(result);
}
}
四、多线程中常用的成员方法
image.png
下面列出的是几个难以理解的方法
(1) 当前的线程对象 Thread.currentThread()
Thread.currentThread() 代表执行到这行代码时当前的线程对象,如果是main方法里面直接调用会发现有个main线程。
当JVM虚拟机启动之后,会自动启动多条线程,其中一条线程就是main线程,他的作用就是去调用main方法并执行里面的代码。
image.png
(2) 线程的优先级相关方法 setPriority() 和 getPriority()
Java 在主流系统 如Windows/Linux上,线程调度是抢占式的。特点:
线程优先级(Priority):Java 线程有优先级(1~10,默认5),但不保证严格按优先级执行。高优先级线程更可能被调度器选中,也就是带权重的随机。
时间片(Time Slicing):大多数现代操作系统(如Windows、Linux)使用时间片轮转的抢占式调度。每个线程执行一段时间后会被强制暂停,让其他线程运行。
不可控性:开发者无法精确控制线程切换的时机,由JVM和操作系统共同决定。
private static void memberMethods() {
// 采用的上面第二种线程实现方式
myThread2 m = new myThread2();
Thread t1 = new Thread(m, "飞机");
Thread t2 = new Thread(m, "坦克");
System.out.println("默认线程优先级:" + t1.getPriority() + "--" + t2.getPriority());
// 线程优先级是 1~10 之间
t1.setPriority(1);
t2.setPriority(8);
t1.start();
t2.start();
}
(3) 守护线程 setDaemon() (备胎线程)
当其他非守护线程执行完毕,守护线程就会陆续结束,即使它的代码未完全执行完毕。
注意这个结束不是立即结束,是陆续结束。就比如有个聊天窗口,里面的文件传输设置为守护线程,当聊天窗口关闭,那么文件传输也就不用传了,可以停了。
image.png
(4) 礼让线程 Thread.yield() --- 了解,平时很少用
礼让线程就是把cpu的执行权让出去,然后重新开始和其他线程争夺cpu执行权,让几个线程尽可能的均匀执行。
比如这个例子,如果没有下面的礼让线程代码,那么执行结果可能是某个线程执行了很久才轮到其他线程,而有了礼让线程那么打印结果就会比较均匀。
image.png
(5) 插入线程 join() --- 了解,平时很少用
表示的是把某个线程插入到正在执行的线程“前”执行,也就是插个队,先执行这个线程再执行当前线程。
比如下面的代码,如果没有t.join()这行代码,那么t线程和main线程会抢夺执行权,交替执行!如果我想让t线程执行完毕后再去执行main线程后面的代码,那么给t线程插个队就行了。
image.png
五、线程的生命周期
image.png
六、线程的安全问题
因为线程执行的时候有“随机性”,线程在执行代码的时候随时可能被其他线程抢走执行权,那么在“操作共享数据”的时候就有问题。
比如卖票的例子,总共100张票,有3个线程卖,那么可能出现卖的票重复或者超出范围的情况,因为线程的执行有随机性!
为了解决这个问题,那么出现了一个概念:同步代码块。
(1)同步代码块
就像抢厕所一样,如果有人在里面那其他人就不能进去了,只能等里面的人处理完了出来了才能再抢这个厕所。
image.png
代码如下:
售票类 - SellTicket
package com.进程和多线程2_安全问题之卖票例子;
public class SellTicket extends Thread {
// 表示这个类的所有对象都共享 ticket 数据
static int ticket = 0;
// synchronized 需要指定一个唯一对象,用 static 随便定义一个就行了,但是一般会使用这个类的字节码对象
// static Object obj = new Object();
@Override
public void run() {
while (true) {
// 将共享代码用 synchronized 包裹,代表锁起来,当有线程在里面时其他线程不能执行里面的代码
synchronized (SellTicket.class) {
if (ticket >= 100) {
break;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票!!");
}
}
}
}
测试类
package com.进程和多线程2_安全问题之卖票例子;
public class demo1 {
public static void main(String[] args) {
SellTicket t1 = new SellTicket();
SellTicket t2 = new SellTicket();
SellTicket t3 = new SellTicket();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
特别注意这里 synchronized 里面的对象一定要是唯一的,这样才能锁起来。
(2)同步方法
image.png
将上面的 售票类 - SellTicket 改写成同步方法的方式,测试类不变
package com.进程和多线程2_安全问题之卖票例子;
public class SellTicket extends Thread {
// 表示这个类的所有对象都共享 ticket 数据
static int ticket = 0;
// 方式二:同步方法的方式
@Override
public void run() {
while (true) {
// 执行同步方法
if(extracted()) break;
}
}
//synchronized 包裹的是同步方法
static synchronized boolean extracted() {
if (ticket >= 100) {
return true;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票!!");
return false;
}
}
特别注意:这里是用的static synchronized修饰的,因为这个卖票类是直接继承的 Thread 类,我们在使用的时候根据这个类创建了三个不同的对象,这三个对象之间需要共享锁,因此需要用 static 修饰。
如果是实现 Runnable 接口这种方式来创建线程的,那就可以 不用static 修饰,只需要创建一次对象,如对象a,然后再创建三个Thread对象来共享这个对象a就行了。这里 synchronized 锁的对象就是this,也就是对象a。
(3)同步方法的扩展知识:StringBuilder 和 StringBuffer 的区别
StringBuilder 和 StringBuffer 在使用上基本是一模一样的,但 StringBuilder 对于多线程是不安全的,这个时候需要改为使用 StringBuffer。如果是单线程的就直接使用 StringBuilder 就行了
image.png
七、Lock 锁
相比同步代码块synchronized,Lock 可以自己来手动上锁,手动释放锁。注意:Lock 锁是接口不能直接实例化,需要使用他的实现类ReentrantLock来实例化。
image.png
将上面的 售票类 - SellTicket 改写成Lock 锁的方式,测试类不变
package com.进程和多线程2_安全问题之卖票例子;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SellTicket extends Thread {
// 表示这个类的所有对象都共享 ticket 数据
static int ticket = 0;
// 方式三:lock 方法的方式 start ---------
static Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 加锁
lock.lock();
try {
if (ticket >= 100) {
break;
}
Thread.sleep(10);
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票!!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁 - 注意:break 后会执行 finally里面的代码后才终止循序
// finally 的优先级高于 break/return(除非调用 System.exit() 或 JVM 崩溃)。
lock.unlock();
}
}
}
// 方式三:lock 方法的方式 end ---------
}
这里有两个注意点:
1.Lock 这里是 static 修饰的,同样是因为这个类是继承的 Thread ,我们在使用的时候实例化了三次这个类,为了保证三个实例化对象都共享一把锁,所以需要使用 static
2.lock.unlock();释放锁是放到的 try catch 的 finally 里面,这里有个特性是 finally 的优先级高于 break/return(除非调用 System.exit() 或 JVM 崩溃)。所以当执行 break 的时候,会先执行 finally 里面的代码才会去终止 while 循环。
八、死锁的错误
死锁,不需要学习其他代码,这是一种程序的错误,我们需要理解并且避免使用它。死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,导致这些线程都无法继续执行下去,程序无法正常运行。
典型死锁示例:
image.png
要想不出现死锁,我们平时写代码的时候需要注意不要让锁出现嵌套的情况。
九、生产者和消费者(等待唤醒机制)
参考 :黑马教程

image.png
image.png
image.png









网友评论