一、多线程介绍
1.1并发与并行
二、创建线程三种方法
- 继承Thread类:创建线程的最直接方法是继承java.lang.Thread类,并重写run()方法,将线程要执行的任务写入其中。创建一个类的实例并调用其start()方法即可启动线程。这种方法简单直观,适合简单的线程任务。但这种方法有一个缺点,由于Java不支持多继承,如果该类已经继承了其他类,则无法通过继承Thread类来创建线程。
- 实现Runnable接口:另一种创建线程的方法是实现java.lang.Runnable接口。创建一个实现Runnable接口的类,并实现其run()方法,将线程要执行的任务写入其中。然后,创建一个Thread对象,将Runnable实现类的实例作为参数传递给Thread构造函数,最后调用Thread实例的start()方法启动线程。这种方法的优点是可以继承其他类,同时避免了单继承的限制。而且,多个线程可以共享同一个Runnable实例,从而实现资源共享。
- 使用Callable和Future:如果需要在线程执行完毕后获取结果,可以使用java.util.concurrent.Callable接口和java.util.concurrent.Future接口。创建一个实现Callable接口的类,并在call()方法中定义线程任务及返回值。然后,创建一个FutureTask对象,并将Callable实现类的实例传递给它。再将这个FutureTask对象传递给Thread对象并启动线程。最后,通过FutureTask对象的get()方法获取线程执行的结果。这种方法适用于需要获取线程执行结果的场景,且call()方法可以抛出异常,使得错误处理更为方便。
2.1继承Thread类
- 继承Threadk
- 重写run方法
- 创建类对象,调用start方法开启线程
class MyThread extends Thread {
@Override
public void run() {
// 线程要执行的任务
System.out.println("MyThread is running");
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start(); // 启动线程
}
}
2.2实现Runnable接口
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程要执行的任务
System.out.println("MyRunnable is running");
}
}
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start(); // 启动线程
}
}
2.3实现Callable接口
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
// 线程要执行的任务,并返回结果
return "MyCallable result";
}
}
public class Main {
public static void main(String[] args) {
MyCallable myCallable = new MyCallable();
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> future = executorService.submit(myCallable);
executorService.shutdown(); // 关闭线程池
try {
String result = future.get(); // 获取线程执行的结果
System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
2.4线程中常见的成员方法
1、getname/setname/currentThread()方法
public class MyThread01 extends Thread{
//01、getname/setname 获取/设置线程名称
@Override
public void run() {
Thread.currentThread().setName("我的线程1");
System.out.println("输出线程的名称:"+Thread.currentThread().getName());
}
public static void main(String[] args) {
MyThread01 myThread01 = new MyThread01();
myThread01.start();
}
}
- sleep()方法
package JavaSE多线程_并发编程.创建线程01.线程常用方法;
import java.time.LocalDateTime;
import java.util.Date;
public class MyThread01 extends Thread{
//01、getname/setname 获取/设置线程名称
@Override
public void run() {
Thread.currentThread().setName("我的线程1");//setName()
System.out.println("调用sleep,让线程休眠3秒"+ LocalDateTime.now());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程休眠3秒后"+LocalDateTime.now());
System.out.println("输出线程的名称:"+Thread.currentThread().getName());//currentThread().getName()
}
public static void main(String[] args) {
MyThread01 myThread01 = new MyThread01();
myThread01.start();
}
}
2、设置优先级setpriority()/获取优先级getpriority()
1、正常运行两个线程各100次查看结果状态
(1)线程实现类:
public class MyRunnable implements Runnable{
@Override
public void run() {
//输出当前线程的名字一百次
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}
(2)测试类
public class Main {
public static void main(String[] args) {
//创建两个线程,并设置线程名称分别是飞机和坦克
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable,"飞机");
Thread thread2 = new Thread(myRunnable,"坦克");
thread.start();
thread2.start();
}
}
2、默认情况下的两个线程优先级查看
优先级变量,最小为1,最大为10,默认是5
3、设置线程优先级后运行
设置优先级,优先级越高抢到CPU的概率越大,并不代表一定比优先级低的先执行
package JavaSE多线程_并发编程.创建线程01.线程常用方法.线程优先级;
public class MyRunnable implements Runnable{
@Override
public void run() {
//输出当前线程的名字一百次
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
}
package JavaSE多线程_并发编程.创建线程01.线程常用方法.线程优先级;
public class Main {
public static void main(String[] args) {
//创建两个线程,并设置线程名称分别是飞机和坦克
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable,"飞机");
Thread thread2 = new Thread(myRunnable,"坦克");
//查看默认情况下两个线程的优先级
System.out.println(thread.getPriority());
System.out.println(thread2.getPriority());
//获取main线程的优先级
System.out.println(Thread.currentThread().getPriority());
//设置优先级,优先级越高抢到CPU的概率越大,并不代表一定比优先级低的先执行
thread.setPriority(1);
thread2.setPriority(10);
thread.start();
thread2.start();
}
}
3、守护线程setDeamon()
public class Mythread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
}
package JavaSE多线程_并发编程.创建线程01.线程常用方法.守护线程;
public class Mythread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
}
package JavaSE多线程_并发编程.创建线程01.线程常用方法.守护线程;
public class Main {
public static void main(String[] args) {
//01、创建两个线程
Mythread1 mythread1 = new Mythread1();
mythread1.setName("女神");
Mythread2 mythread2 = new Mythread2();
mythread2.setName("备胎");
//02、将备胎线程设置为守护线程
mythread2.setDaemon(true);
//03、开启线程
mythread1.start();
mythread2.start();
}
}
1、应用场景
4、礼让线程yield(),只是尽可能礼让
- 让运行的结果尽可能地均匀一点,不会某一个线程抢占CPU之后执行很多次,执行完后释放CPU,重新抢夺。
public static void yield()
静态方法,直接用类名Thread调用,此方法直接写在run()方法中
package JavaSE多线程_并发编程.创建线程01.线程常用方法.礼让线程;
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i <50 ; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
}
package JavaSE多线程_并发编程.创建线程01.线程常用方法.礼让线程;
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.setName("飞机");
MyThread myThread2 = new MyThread();
myThread2.setName("坦克");
myThread.start();
myThread2.start();
}
}
引入礼让线程方法:
5、插队线程join()
d0ab5f95096417fb010e87b221ae2b3.png)
如果想将土豆线程插入到main线程之前,等土豆线程执行完了,在执行main线程
2.5线程的生命周期
2.6线程安全问题
需求:电影院有100张票,三个窗口进行售卖
1、正常逻辑代码出现的问题
改进1:共享数据
2、问题:仍然又重复卖票的问题和超出范围的问题
3、原因分析
线程执行时具有随机性,都有可能在其他线程占用CPU的时候对CPU进行抢夺,其他线程操作的数据也就会被抢夺CPU的线程拿来继续使用,而抢夺CPU的线程以为拿到的还是原始数据,属于脏数据。
线程1抢到CPU执行权后执行run(),睡眠10毫秒,其他线程会趁机抢夺CPU
同理2,3线程都会执行到睡眠状态
线程1醒来后继续执行,使得tickcet++,ticket=1
此时Ticket已经是1,线程1还没来得及执行下面的打印票量=1,线程2就将CPU执行权夺走,并对ticket进行了+1,此时ticket=2,同理线程3也进行了抢夺进行了+1,此时的Ticket已经是3,接下来,不管谁先执行下面的代码,输出的都是3。这是重复票的由来,三个窗口卖的都是3号票。
当执行到第99张票的时候,又重复以上情况,if中判断的的确是ticket=99满足小于100条件,但在执行ticket++的时候,由于线程运行的随机性,三个线程的抢夺执行权,使得在最终输出ticket之前都进行了+1操作,就会出现总票数可能是101或者102
4、解决问题-线程加锁-Synchronized
线程1执行共享数据这段代码的时候,其他线程进不来
确保锁对象的唯一性:是为了让抢夺CPU资源的线程针对同一把锁,如果该锁被使用,其他线程无法抢占资源
5、synchronized锁注意事项
- 要保证锁的唯一性。
- 锁要加在循环里面,否则否一个进程一旦抢到锁会将所有票卖完。
- 当多线程是通过继承Thread类实现的时候,公共变量需要定义为static类型,因为继承Thread类实现的多线程,在创建的对象的时候会new出多个对象,导致公共变量也不唯一,必须使得多个线程在执行同一个公共变量才行。这样可以防止多个线程同时修改它,从而避免了数据不一致的问题。
6、同步方法
可以先写成同步代码块的形式,然后选中同步代码块代码->Ctrl+alt+M抽取方法->方法前面加Synchronized修饰
public class MySynchnized extends Thread{
static int tickets=0;
@Override
public void run() {
while (true){
if (extracted()) break;
}
}
//同步方法
private synchronized static boolean extracted() {
if (tickets==100){
return true;
}else {
tickets++;
System.out.println("线程"+Thread.currentThread().getName()+"卖第"+tickets+"张票");
}
return false;
}
}
7、Lock锁
public class MyLock extends Thread{
static int tickets=0;
Lock lock=new ReentrantLock();//需要像共享数据一样定义为static类型
@Override
public void run() {
while (true){
lock.lock();
try {
if (tickets==100){
break;
}else {
tickets++;
System.out.println("线程"+Thread.currentThread().getName()+"卖第"+tickets+"张票");
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// lock.unlock();
}
}
}
}
问题1:超出
原因:
解决:锁对象前面加static关键字
问题2:程序未停止
原因:
解决:想办法让解锁程序无论如何都要执行,使用try-catch-finally
public class MyLock extends Thread{
static int tickets=0;
static Lock lock=new ReentrantLock();//需要像共享数据一样定义为static类型
@Override
public void run() {
while (true){
lock.lock();
try {
if (tickets==100){
break;
}else {
tickets++;
System.out.println("线程"+Thread.currentThread().getName()+"卖第"+tickets+"张票");
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
}
2.7等待唤醒机制(生产者消费者)
桌子上有面条-》吃货执行
桌子上没面条-》生产者制造执行
1、消费者等待
消费者先抢到CPU执行权,发现桌子上没有面条,于是变成等待wait状态,并释放CPU执行权,此时的CPU肯定会被厨师抢到,初始开始做面条,当厨师做完后会对吃货进行提示,notify唤醒吃货来吃。
2、生产者等待
厨师先抢到CUP执行权,但是桌子上有面条,就不能再制作面条,只能等待消费者吃完面条才能做,消费者吃完后需要唤醒厨师继续做
代码逻辑:
厨师:
public class Cook extends Thread{
@Override
public void run() {
//1循环
//2同步代码块
//3共享数据是否到末尾,Yes
//4共享数据是否到末尾,No
while (true){
synchronized (Desk.lock){
if (Desk.count==0){
break;//10碗吃完
}else {
//厨师的核心逻辑
//01判断桌子上是否有食物
if (Desk.foodflag==1){
//02有食物就等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else {
//03没有
System.out.println(Thread.currentThread().getName()+"制作食物");
//04改变桌子状态
Desk.foodflag=1;
//05唤醒消费者吃
Desk.lock.notifyAll();
}
}
}
}
}
}
吃货:
public class Customer extends Thread{
@Override
public void run() {
while (true){
synchronized (Desk.lock){
if (Desk.count==0){
break;//10碗吃完
}else {
//吃货的核心逻辑
/*
* 1.判断桌子上有无面条
* 2.没有:自己等待,
* 3.有:吃完,并唤醒厨师做面条,count--
* 4.修改桌子状态*/
if (Desk.foodflag==0){//1.判断桌子上有无面条
try {
Desk.lock.wait();//2.没有:自己等待,
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else {//3.有:吃完,并唤醒厨师做面条,count--
Desk.count--;
System.out.println(Thread.currentThread().getName()+"还能再吃"+Desk.count+"碗");
Desk.lock.notifyAll();
//4.修改桌子状态
Desk.foodflag=0;
}
}
}
}
}
}
桌子:
public class Desk {
//通过变量来控制 0:没食物 1:有食物
public static int foodflag=0;
//总个数,最多做十碗
public static int count=10;
//锁对象
public static Object lock=new Object();
}
//测试类
public class Test {
public static void main(String[] args) {
Customer customer = new Customer();
Cook cook = new Cook();
customer.setName("吃货");
cook.setName("厨师");
customer.start();
cook.start();
}
}
3、阻塞队列实现
接口无法new对象,只能通过两个实现类,第一个可以自定义队列长度。
注意:生产者与消费者必须针对同一个阻塞队列,阻塞队列可以创建在测试类中
厨师:
public class Cook extends Thread{
ArrayBlockingQueue<String> queue;
//创建构造函数,创建对象的时候进行赋值,指定同一个阻塞队列
public Cook(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true){
try {
queue.put("面条");
System.out.println("厨师做了一碗面条");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
消费者:
public class Customer extends Thread{
ArrayBlockingQueue<String> queue;
public Customer(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true){
try {
String food=queue.take();//tack底层也进行了加锁,不需要我们自己定义
System.out.println("获取食物"+food);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
测试类:
public class Test {
public static void main(String[] args) {
ArrayBlockingQueue<String> queue=new ArrayBlockingQueue<>(1);
Customer customer = new Customer(queue);
Cook cook = new Cook(queue);
customer.setName("吃货");
cook.setName("厨师");
customer.start();
cook.start();
}
}
2.8案例:抢红包
package JavaSE多线程_并发编程.案例;
import java.util.Random;
/*需求:
* 假设有100块的红包,分成3个包,现在五个人去抢
* 1.红包总金额100为共享数据
* 2.红包数量3也是共享数据
* 3.五个人为五个线程
* 打印结果:
* XXX抢到了XXX元
* XXX抢到了XXX元
* XXX抢到了XXX元
* XXX没抢到
* XXX没抢到
* */
public class MyThread extends Thread {
//01、定义共享数据
public static double money=100;
public static int count=3;
//红包最小金额
public static final double Minmoney=0.01;
@Override
public void run() {
//1.循环,抢红包中一个人(线程)只能抢一次,所以不需要循环
//2.同步代码块
synchronized (MyThread.class){
//3.判断共享数据是否执行到末尾:Yes
if (count==0){
System.out.println("红包已经抽完");
System.out.println(Thread.currentThread().getName()+"没有抽到红包");
}else {
//4.判断共享数据是否执行到末尾:No
//定义一个抽中的金额
double prize=0;
//(1)如果还剩一个包,剩下的钱都给他
if (count==1){
prize=money;
System.out.println(getName()+"抽到了"+prize+"元");
}else {
//(2)第一次和第二次抽需要随机,但是第一次抽的金额最大只能是99.8
Random random = new Random();
//但是第一次抽的金额最大只能是99.8
double bounds= money-(count-1)* Minmoney;
//prize =random.nextDouble(bounds);//nextDouble只能在JDK17以后使用
//random的范围是0-bounds,包括0
if (prize<Minmoney){
prize=Minmoney;//如果分到的钱小于最小金额0.01那么就将其设置为0.01
}
}
//每次抽完要减去分掉的钱
money=money-prize;
//红包个数减一
count--;
System.out.println(getName()+"抢到了"+prize+"元");
}
}
}
}
测试类:
public class Test {
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
MyThread myThread3 = new MyThread();
MyThread myThread4 = new MyThread();
MyThread myThread5 = new MyThread();
//Alt+鼠标左键竖着选中多个变量
//先空格出对应的行数,再Alt加左键选中行,粘贴
myThread1.setName("线程1");
myThread2.setName("线程2");
myThread3.setName("线程3");
myThread4.setName("线程4");
myThread5.setName("线程5");
myThread1.start();
myThread2.start();
myThread3.start();
myThread4.start();
myThread5.start();
}
}
三、线程池创建线程
线程池用来存储线程,刚开始也是空的,当第一个任务到来,需要创建一个线程的时候,会在线程池中进行创建线程,任务执行完后线程自动返回线程池,当后续再来其他任务的时候,如果之前创建的线程够用,就直接从线程池中拿,如果不够再创建。
线程池也有上线,可自定义大小
public class MyThreadPool {
public static void main(String[] args) {
//01、获取一个没有数量上线的的线程池对象
ExecutorService pool1= Executors.newCachedThreadPool();
//ExecutorService pool1= Executors.newFixedThreadPool(3);//设定线程池大小
//02、提交4个任务
pool1.submit(new MyRunnable());//类型选中一个runnable的
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
//03、销毁线程池,一般不销毁
//pool1.shutdown();
/*线程池中会创建多个线程执行任务
* pool-1-thread-2执行任务100
pool-1-thread-4执行任务96
pool-1-thread-4执行任务97
pool-1-thread-4执行任务98
pool-1-thread-4执行任务99
pool-1-thread-4执行任务100
pool-1-thread-3执行任务96
pool-1-thread-3执行任务97
pool-1-thread-3执行任务98
pool-1-thread-3执行任务99
pool-1-thread-3执行任务100*/
}
}
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 1; i <=100 ; i++) {
System.out.println(Thread.currentThread().getName()+"执行任务"+i);
}
}
}