Java -- 多线程

本文详细介绍了Java中的多线程概念,包括并发与并行的区别,多线程的实现方式如继承Thread类、实现Runnable接口和使用Callable/Future接口。还探讨了线程的生命周期、线程同步(synchronized、Lock)、死锁以及线程池的使用,包括自定义线程池和任务拒绝策略。此外,文章还提到了CPU利用率和线程池大小的考量因素。
摘要由CSDN通过智能技术生成

多线程

并发

在同一时刻,有多个指令在单个CPU上交替执行

CPU在多个线程之间交替执行

并行

在同一时刻,有多个指令在多个CPU上同时执行

多线程的实现方式

  1. 继承Thread类的方法进行实现
  2. 实现Runnable接口的方式进行实现
  3. 利用Callable接口和Future接口方式实现

继承于Thread类

//Thread子类
public class MyThread extends Thread {

    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(this.getName() + "Hello world!");
        }
    }
}


public class Main {

    public static void main(String[] args) {
        MyThread thread1 = new MyThread("thread1: ");
        MyThread thread2 = new MyThread("thread2: ");
        thread2.start();
        thread1.start();
    }

}

Runnable方法实现

  1. 自定义一个类实现Runnable接口
  2. 重写里面的run方法
  3. 创建自己的类对象
  4. 创建一个Thread对象开启线程

public class MyThread implements Runnable {

    public MyThread() {

    }

    @Override
    public void run() {
        Thread curThread = Thread.currentThread();
        for (int i = 0; i < 10; i++) {
            System.out.println(curThread.getName() + " Hello");
        }
    }
}


public class Main {

    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        Thread thread3 = new Thread(thread1);
        Thread thread4 = new Thread(thread2);
        thread3.start();
        thread4.start();
    }

}

Callable与Future接口

特点:可以获取多线程运行的结果

  1. 创建一个MyCallable实现Callable接口
  2. 重写call(有返回值的,表示多线程运行的结果)
  3. 创建MyCallable对象,表示多线程要执行的任务
  4. 创建Future对象(用来管理多线程运行的结果)
  5. Future是一个接口,要创建它的实现类FutureTask
  6. 创建Thread类对象然后启动线程
import java.util.concurrent.Callable;

public class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        return 100;
    }
}

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Main {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable mc = new MyCallable();
        FutureTask<Integer> fc = new FutureTask<>(mc);
        Thread thread = new Thread(fc);
        thread.start();
        System.out.println(fc.get());
    }

}

Thread常见的成员方法

方法作用
String getName()返回此线程的名称
void setName(String name)设置线程名字,构造方法也可以设置
static Thread currentThread()获取当前线程对象
static void sleep(long time)让线程休眠指定的时间,单位为毫秒
setPriority(int newPriority)设置线程的优先级
final int getPriority()获取线程的优先级
final void setDaemon(boolean on)设置为守护线程
public static void yield()出让/礼让线程
public static void join()插入/插队线程

get/setName上面已经使用过

需要注意的是,如果不设置线程的名字,线程有默认的名字Thread-X格式(x从0开始计数)

关于主线程的一点:

当JVM虚拟机启动之后,会自动启动多条线程,其中有一条叫做main线程,它的作用就是去调用main方法并执行里面的代码,我们以前所写的所有代码都是运行在main线程中

优先级

抢占式调度:随机性

非抢占式调度:顺序性

随机等级:1-10

默认优先级都为5

main线程的优先级也为5

优先级越高抢到cpu的概率也越高

可以使用对应的get/setPriority方法调整优先级

守护线程

setDaemon设置为守护线程(备胎线程)

其他非守护线程执行完毕后,守护线程也会陆续结束


public class Main {

    public static void main(String[] args) {
        MyCallable thread1 = new MyCallable();
        MyCallable thread2 = new MyCallable();
        Thread thread3 = new Thread(thread1, "3: ");
        Thread thread4 = new Thread(thread2, "4: ");
        thread4.setPriority(1);
        thread3.setPriority(10);
        thread4.setDaemon(true);
        thread3.start();
        thread4.start();
    }

}

线程3执行完后就会告诉线程4他不需要继续执行了,然后他就会停止

比如说聊天窗口和文件发送,聊天窗口关闭后,正在发送的文件也没必要继续发送了

礼让线程

出让线程的执行权,让当前抢到CPU的线程让出其执行权,再来让所有的线程竞争执行权。

尽可能的让线程更加的均匀。

    public void run() {
        Thread curThread = Thread.currentThread();
        for (int i = 0; i < 100; i++) {
            System.out.println(curThread.getName() + " Hello" + i);
            Thread.yield();
        }
    }

插入线程

把对应线程插入到当前线程之前,对应线程执行完毕后再执行当前线程

        Thread thread = new Thread(new MyCallable());
        thread.start();

        thread.join();

        for (int i = 0; i < 10; i++) {
            System.out.println("main");
        }

线程的执行周期

start方法
抢到CPU执行权
run完毕
sleep或者其他阻塞方法
sleep完毕或者其他阻塞方式
创建线程对象--新建
有执行资格
没有执行权--就绪
有执行资格
有执行权--运行
线程死亡
变为垃圾--死亡
没有执行资格
没有执行权--阻塞等待
sleep
时间到
wait
notify
无法获取锁
获得锁
运行
就绪
计时等待
等待
阻塞

同步代码块

把操作共享数据的代码块锁起来

格式如下:

synchronized(){
    操作共享数据的代码
}
  • 锁默认打开,当有一个线程进去了,锁自动关闭
  • 里面的代码全部执行完毕,线程出来,锁自动打开

其中的锁对象要是唯一的,可以是任意一个对象,但是在多个线程中也要是唯一的,可以在类中声明一个静态的对象作为锁。


public class MyCallable extends Thread {
    static private int tickets = 100;
    static private Object lock = new Object();

    @Override
    public void run() {
        synchronized (lock) {
            while (true) {
                if (tickets <= 0)
                    break;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                tickets--;
                System.out.println("卖出了一张票,还有" + tickets + "张票");
            }
        }
    }
}


上面的代码中有一点小问题,其中synchronized块不可以放在循环的外边,因为如果放在外边,在第一个线程抢夺到CPU的执行权后就会开始循环,就会将票全部卖出去,因此需要放到循环内部,卖出一张票就解开锁。

  public void run() {

        while (true) {
            synchronized (lock) {
                if (tickets <= 0)
                    break;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                tickets--;
                System.out.println("卖出了一张票,还有" + tickets + "张票");
            }
        }
    }

还需要注意,锁对象一定要唯一,如果不唯一,相当于没有写

这个锁一般来写本文件的字节码对象:

类名.class
 synchronized (MyCallable.class) {
                if (tickets <= 0)
                    break;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                tickets--;
                System.out.println("卖出了一张票,还有" + tickets + "张票");
            }
}

同步方法

如果要把一个方法中的所有代码块都锁起来,可以把关键字加到方法上

修饰符 synchronized 返回类型 方法名(参数){
    
}
  • 同步方法是锁住方法中的所有代码
  • 锁对象不能自己指定
    • 非静态时锁对象为this
    • 静态时锁对象为当前类的字节码文件对象

把锁住的代码块做成一个方法,然后加上修饰符即可

StringBuilder对象是多线程不安全的,如果要多线程构造字符串请使用StringBuffer

Lock锁

JDK5以后提供了一个新的锁对象Lock,方便我们更加清晰地表达如何加锁释放锁

void lock();获得锁

void unlock();释放锁

手动上锁,手动释放锁

Lock是一个接口,我们需要使用它的实现类ReentrantLock

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyCallable extends Thread {
    private int tickets = 100;
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            lock.lock();
            if (saleTicket()) break;
        }
    }

    private boolean saleTicket() {
        try {
            if (tickets <= 0)
                return true;
            Thread.sleep(10);
            tickets--;
            System.out.println(Thread.currentThread().getName() + " 卖出了一张票,还有" + tickets + "张票");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return false;

    }
}

要注意锁不打开直接break出循环,造成其他两个线程在锁前等待的情况。所以通常要用try catch块将代码包围,然后使用finally解锁

死锁

是一种书写代码的错误

比如说下面的情况

等待唤醒机制

生产者消费者是典型的多线程协作模式

消费者机制:

  1. 判断桌子上是否有食物
  2. 如果没有就等待
  3. 如果有就开吃
  4. 吃完之后唤醒厨师继续做

生产者机制:

  1. 判断桌子上是否有食物
  2. 有则等待
  3. 无则制作
  4. 把食物放在桌子上
  5. 叫醒等待的消费者开吃
方法作用
void wait()当前线程等待,直到被其他线程唤醒
void notify()随机唤醒单个线程
void notifyAll()唤醒所有线程
//生产者
public class Productor extends Thread {

    public void run() {
        while (true) {
            synchronized (tempFlag.lock){

                if (tempFlag.count == 0)
                    break;
                else {
                    if (tempFlag.tempFlag == 1) {
                        try {
                            tempFlag.lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    } else {
                        System.out.println("厨师做了一碗面条");
                        tempFlag.tempFlag = 1;

                        tempFlag.lock.notifyAll();
                    }
                }
            }
        }
    }
}

//消费者
public class Consumer extends Thread {

    public void run() {
        while (true) {
            synchronized (tempFlag.lock) {
                if (tempFlag.count == 0)
                    break;
                else {
                    if (tempFlag.tempFlag == 0) {
                        try {
                            tempFlag.lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    } else {
                        tempFlag.count--;
                        System.out.println("还可以再吃" + tempFlag.count + "碗");
                        tempFlag.tempFlag = 0;
                        tempFlag.lock.notifyAll();

                    }
                }
            }
        }
    }
}

//桌子
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class tempFlag {
    public static int tempFlag = 1;
    public static int count = 10;
    public static final Lock lock = new ReentrantLock();
}

//测试类

public class Main {

    public static void main(String[] args) throws InterruptedException {

        Productor productor = new Productor();
        Consumer consumer = new Consumer();
        productor.setName("生产者");
        consumer.setName("消费者");
        productor.start();
        consumer.start();

    }

}

以上是一种实现等待唤醒机制的方式,下面还有第二种方式,阻塞队列方式实现:

需要注意生产者和消费之需要使用同一个阻塞队列

阻塞的话是不需要锁的,底层自动有锁和释放(put和take)

线程池

  1. 创建一个池子,池子是空的
  2. 提交任务时,池子会创建新的线程对象,任务执行完毕后,线程归还给池子,下次再提交任务时,不需要创建新的线程,直接复用已有的线程即可
  3. 但是如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待
方法作用
public static ExecutorService newCachedThreadPool()创建一个没有上限的线程池
public static ExecutorService newFixedThreadPool(int nThreads)创建有上限的线程池
submit(参数)将任务交给线程池
shutdown()销毁线程池
        ExecutorService pool = Executors.newFixedThreadPool(2);
        pool.submit(new A());
        pool.submit(new A());
        pool.submit(new A());
        pool.submit(new A());
        pool.submit(new A());
        pool.submit(new A());
        pool.shutdown();

自定义线程池

任务拒绝策略

策略说明
ThreadPoolExecutor.
AbortPolicy
默认策略,抛出异常
ThreadPoolExecutor.
DiscardPolicy
丢弃任务但是不抛弃异常
ThreadPoolExecutor.
DiscardOldestPolicy
丢弃队列中等待最久的任务,然后把当前任务加入队列中

自定义线程池可以使用ThreadPoolExecutor对象,在创建时定义指定的参数即可。

ThreadPoolExecutor();
//参数一:核心线程数量
//参数二:最大线程数
//参数三:空闲线程最大存活时间
//参数四:时间的单位
//参数五:任务队列
//参数六:创建线程工厂
//参数七:任务的拒绝策略,注意此为静态内部类
    ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(3, 6, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
 

然后使用submit提交shutdown关机即可

最大并行数

可以使用下面的代码查看电脑的最大并行数

System.out.println(Runtime.getRuntime().availableProcessors());

线程池多大合适

如果为CPU密集型运算:最大并行数加一

数据运算较多,IO较少

加一是为了预防前面的线程出问题,可以当备用线程

IO密集型运算:
最大并行数 ∗ 期望 C P U 利用率 ∗ 总时间 C P U 计算时间 总时间 = C P U 计算时间 + 等待时间 最大并行数*期望CPU利用率*\frac{总时间}{CPU计算时间}\\ 总时间=CPU计算时间+等待时间 最大并行数期望CPU利用率CPU计算时间总时间总时间=CPU计算时间+等待时间
CPU计算时间等需要使用工具测试,比如说thread dump

读取数据库或者其他IO操作较多

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_南明_离火_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值