创建线程的方式
1. 继承Thread类,重写run方法
2. 实现Runnable接口,重写run方法
3. 实现Callable接口,重写run方法
方式一、继承Thread类
特点:继承了Thread类,能够在run方法中调用Thread的方法,如getName()等
使用该方法开启不同的线程时,需要创建多个线程对象,因此,在使用同步代码块给代码加锁的时候,不能直接在这个类中对this对象进行加锁,因为不同对象,它们的this是不同的
缺点:因为在Java中,类是单继承的,因此当使用这种方式时,就不能同时继承其他的类了
代码实现
创建线程类
public MyThread extends Thread{
@Override
public void run(){
// 这里是线程的要实现的代码
for(int i = 0 ; i <= 10 ; i++){
System.out.println(getName()+"========="+i);
}
}
}
测试类
public class ThreadDemo{
public static void main(String[] args){
// 创建线程对象
MyThread mt = new MyThread();
MyThread mt1 = new MyThread();
// 设置线程名称
mt.setName("Thread-1");
mt1.setName("Thread-2");
// 开启子线程
// 使用start方法,而不使用run方法:因为使用start方法才能开启线程,而run方法只是普通方法
mt.start();
mt1.start();
}
}
方式二、实现Runnable接口
特点:
- 避免单继承限制
由于Java只支持单继承,如果一个类已经继承了其他类,则无法直接继承
Thread
类来创建线程。通过实现Runnable
接口,可以避免这个限制,因为类可以实现多个接口。
- 提高代码的重用性
通过实现
Runnable
接口,将线程逻辑独立于线程类之外,使得线程类更加专注于线程的生命周期管理以及与线程相关的操作。这样可以提高代码的重用性,因为多个线程可以共享同一个Runnable
实例。
- 支持资源共享
当多个线程使用同一个
Runnable
实例时,它们可以共享同一个实例变量,从而实现资源的共享。这在某些场景下非常有用,例如线程池中的线程、多个线程访问同一个共享资源等。
- 降低耦合性
通过实现
Runnable
接口,线程对象与线程逻辑相分离,使得线程对象与特定的线程逻辑解耦。这允许在不修改线程对象的情况下,更换不同的线程逻辑,提高了代码的灵活性。
实现步骤
* 1. 自己定义一个类实现Runnable接口 * 2. 重写里面的run()方法 * 3. 创建自己的类的对象 * 4. 创建爱你一个Thread类的对象,并开启线程
代码示例
线程类和测试类分开实现
线程类
public class MyRun implements Runnable{
@Override
public void run() {
// 书写线程要执行的代码
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"Hello World!!");
}
}
}
测试类
public class ThreadDemo {
public static void main(String[] args) {
/*
*多线程的第二种启动方式:
* 1. 自己定义一个类实现Runnable接口
* 2. 重写里面的run()方法
* 3. 创建自己的类的对象
* 4. 创建爱你一个Thread类的对象,并开启线程
**/
// 创建实现了Runnable接口的对象 MyRun
// 这个对象表示要执行的任务
MyRun myRun = new MyRun();
// 创建线程对象
Thread t1 = new Thread(myRun);
Thread t2 = new Thread(myRun);
// 设置线程名称
t1.setName("Thread1:");
t2.setName("Thread2:");
// 开启线程
t1.start();
t2.start();
}
}
匿名内部类实现
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "Hello World!!");
}
}
});
t3.start();
lambda表达式实现
Thread t3 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "Hello World!!");
}
});
t3.start();
方式三、实现Callable接口
特点:可以获取到多线程运行的结果
区别:Callable与Runnable以及Thread最大的区别就是,Callable有返回值
实现步骤
* 1. 创建一个类MyCallable,实现Callable() * 2. 重写call (是有返回值的,表示多线程运行的结果) * * 3. 创建MyCallable的对象(表示 多线程要执行的任务) * 4. 创建FutureTask的对象(作用:管理多线程运行的结果) * 5. 创建Thread类的对象
代码样例
线程类
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 求 1- 100 之间的和
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
return sum;
}
}
测试类
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/*
* 多线程的第三种实现方式
* 特点: 可以获取到多线程运行的结果
*
* 1. 创建一个类MyCallable,实现Callable()
* 2. 重写call (是有返回值的,表示多线程运行的结果)
*
* 3. 创建MyCallable的对象(表示 多线程要执行的任务)
* 4. 创建FutureTask的对象(作用:管理多线程运行的结果)
* 5. 创建Thread类的对象
*
* **/
// * 3. 创建MyCallable的对象(表示 多线程要执行的任务)
MyCallable mc = new MyCallable();
// * 4. 创建FutureTask的对象(作用:管理多线程运行的结果)
FutureTask<Integer> task = new FutureTask<>(mc);
// * 5. 创建Thread类的对象
Thread t1 = new Thread(task);
// 启动线程
t1.start();
// 获取多线程运行的结果
Integer res = task.get();
System.out.println("res = " + res);
}
}
线程常用方法
总汇
void setName(String name): 设置线程名称 String getName(): 获取线程名称 static Thread currentThread(); : 获取当前线程对象 static void sleep(long time) :让线程休眠time毫秒 setPriority(int newPriority): 设置线程的优先级 final int getPriority(): 获取线程的优先级 final void setDaemon(boolean on): 设置为守护线程 public static void yield() 礼让线程、出让线程 public static void join() 礼让线程、出让线程
使用
设置线程名称
给线程设置名称的方法:
①线程对象.setName② 在继承Thread的类中重新含有设置名称的构造方法,然后在新建对象的时候,在形参部分传入线程名称即可,
线程优先级
前言
计算机当中线程的调用方式:1. 抢占式调度 多个线程抢夺 CPU 的执行权,CPU在某个时候执行某条线程是不确定的,执行多少时间也是不确定的 特点:主打一个 随机性2. 非抢占式调度 所有的线程轮流地执行,执行的时间也是差不多的在Java中,采用的是 抢占式翘度的方式:随机
方法
setPriority(int newPriority): 设置线程的优先级final int getPriority():获取线程的优先级线程优先级:默认和main线程的是5 ,最大是10,最小是1
// main线程的优先级
System.out.println("Thread.currentThread().getPriority() = " + Thread.currentThread().getPriority());
Thread t1 = new Thread(mr,"飞机");
Thread t2 = new Thread(mr,"大炮");
t1.setPriority(1);
t2.setPriority(10);
设置守护线程
细节
当其他的非守护线程执行完毕后,守护线程陆续结束
通俗说法: 当女神线程结束了,那么备胎也没有什么存在的必要了
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.setName("女神");
t2.setName("备胎");
// 将t2 设置为守护线程
t2.setDaemon(true);
t1.start();
t2.start();
礼让线程
public static void join() 礼让线程、出让线程public static void yield() 礼让线程、出让线程
yield():主动礼让,礼让后还能和其他线程争抢CPU资源
/*
* public static void yield() 礼让线程、出让线程
* */
MyThread1 t1 = new MyThread1();
MyThread1 t2 = new MyThread1();
t1.setName("飞机");
t2.setName("坦克");
// 将t2 设置为守护线程
t2.setDaemon(true);
t1.start();
t2.start();
join:表示把 t1 这个线程插入到当前线程(执行在哪个线程上,当前线程就表示哪个线程)之前
/*
* public static void join() 礼让线程、出让线程
* */
MyThread1 t1 = new MyThread1();
t1.setName("tomato");
t1.start();
// join 表示把 t1 这个线程插入到当前线程(执行在哪个线程上,当前线程就表示哪个线程)之 前
// 当前线程:main线程
t1.join();
// 执行main中的线程
for (int i = 0; i < 11; i++) {
System.out.println("main线程:"+i);
}
线程安全
小引
需求:
某电影院目前正在上映国产大片,共有10张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
代码实现
线程类
public class MyThread extends Thread{
// static 表示这个类所有对象共享该属性
static int ticket = 0;
@Override
public void run() {
while (true){
if (ticket < 20){
try {
Thread.sleep(10);
ticket++;
System.out.println(getName()+" 正在卖票 "+ticket);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
System.out.println(getName()+" 票卖完了....");
break;
}
}
}
}
测试类
public class ThreadDemo {
public static void main(String[] args) {
// 创建线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
// 起名
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
// 开启
t1.start();
t2.start();
t3.start();
}
}
分析
按这种方式执行,卖票的情况 可能出现:当卖完最后一张票的时候,还会有其他窗口(线程)进行卖票。因为在线程类中有个暂停10毫秒的方法(就算没有,也可能出现),当最后一个线程卖完票,还没来得及增加票数并打印卖票信息的时候,其他两个窗口(线程)以及抢到了CPU的资源,就在这段时间它们也去增加票数并打印卖票信息了。这就造成了线程安全问题。
简单地来说就是,由于共享资源ticket是这三个窗口都能访问的,而在访问的时候没有对这三个窗口进行限制(没有加锁等操作),导致超卖了。
解决
思想:给线程访问共享资源的这段代码加锁
方式:加 synchronized
1. 同步代码块方式
特点1: 锁默认打开,有一个线程进去了,锁自动关闭
特点2: 里面的代码全部执行完毕,线程出来,锁自动打开
2. 同步方法方式
特点1:同步方法是锁住方法里面所有的代码
特点2:锁对象不能自己指定
非静态:this
静态:当前类的字节码文件对象
同步代码块方式
public class MyThread extends Thread{
// static 表示这个类所有对象共享该属性
static int ticket = 0;
@Override
public void run() {
while (true){
synchronized (MyThread.class){
if (ticket < 20){
try {
Thread.sleep(10);
ticket++;
System.out.println(getName()+" 正在卖票 "+ticket);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
System.out.println(getName()+" 票卖完了....");
break;
}
}
}
}
}
细节
①同步代码块不能写在循环的外面:因为如果写在循环外面,一旦有一个线程对象获取到了锁,该对象就会一直执行,直至循环条件结束。这样就会导致其他线程对象就算抢夺到了CPU 的执行权,也没能得到锁,不能参与执行,也就没有意义了 ②锁对象:可以是任意一个对象,但一定要是唯一的。因此这里不能用this,因为this代表当前对象;在测试类中创建对象时,有三个线程类对象,即三个对象都是当前对象,导致对象不唯一,达不到锁的效果 class对象是唯一的
同步方法方式
技巧:
先写同步代码块,将其改为同步方法
步骤
// 1. 循环 // 2. 同步代码块 //3. 判断共享数据是否到末尾,如果到了末尾的操作 //4. 判断共享数据是否到末尾,如果没有到末尾的操作
线程类(通过实现Runnable方式)
public class MyRunnable implements Runnable{
// 因为MyRunnable是作为参数传入Thread中进行创建线程的,因此只需要传一次,故不会出现像extends Thread实现创建线程的方式那样创建多个线程对象
// 所以也就不用加static实现共享变量了
int ticket = 0;
@Override
public void run() {
while (true){
if (method()) break;
}
}
// 锁对象:this--> mr
private synchronized boolean method() {
if (ticket == 100){
return true;
}else {
try {
Thread.sleep(10);
ticket ++;
System.out.println(Thread.currentThread().getName()+" 在卖第 "+ticket+"张票");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return false;
}
}
Lock锁方式
线程类
public class MyThread extends Thread{
// static 表示这个类所有对象共享该属性
static int ticket = 0;
// 使用Lock接口下的ReentrantLock
// 因为使用的是继承Thread类的方式创建的多线程,因此在创建该类的对象时,会创建多个,
// 多个线程对象有着自己的Lock属性,因此为了实现锁的效果,需要将lock使用static修饰
static Lock lock = new ReentrantLock();
@Override
public void run() {
while (true){
// synchronized (MyThread.class){
lock.lock();
try {
if (ticket < 20){
Thread.sleep(10);
ticket++;
System.out.println(getName()+" 正在卖票 "+ticket);
}else{
System.out.println(getName()+" 票卖完了....");
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
测试类
public class ThreadDemo {
public static void main(String[] args) {
// 创建线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
// 起名
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
// 开启
t1.start();
t2.start();
t3.start();
}
}
生产者和消费者(等待唤醒机制)
特点:该模式是一个十分经典的多线程写作的模式
作用:在Java中,线程的执行是有随机性的,而这个等待唤醒机制就要打破这种随机规则,它会让两个线程轮流执行
情景
情景一:消费者等待
* 消费者(消费数据): * 1. 判断桌子上是否有食物 * 2. 如果没有就等待(wait) * 生产者(生产数据): * 1. 制作食物 * 2. 把食物放在桌子上 * 3. 叫醒等待的消费者(notify)
情景二:生产者等待
* 消费者(消费数据): * 1.判断桌子上是否有食物 * 2.如果没有,等待 * 3. 如果有,开吃 * 4. 吃完之后,唤醒厨师继续做 * * 生产者(生产数据): * 1. 判断桌子上是否有食物 * 2. 没有,制作食物 * 3. 有,等待 * 4. 把食物放在桌子上 * 5 叫醒等待的消费者(notify)
完整过程
* 生产者(生产数据): * 1. 判断桌子上是否有食物 * 2. 没有,制作食物 * 3. 有,等待 * 4. 把食物放在桌子上 * 5 叫醒等待的消费者(notify) * * 消费者(消费数据): * 1.判断桌子上是否有食物 * 2.如果没有,等待 * 3. 如果有,开吃 * 4. 吃完之后,唤醒厨师继续做
代码实现
生产者类(厨师类)
步骤
* 1. 循环 * 2. 同步代码块(可改写为同步方法) * 3. 判断共享数据是否到达末尾(到了末尾) * 3. 判断共享数据是否到达末尾(没到末尾,执行核心逻辑)
public class Cook extends Thread{
@Override
public void run() {
while (true){
synchronized (Desk.lock){
if (Desk.count == 0){
break;
}else{
// 判断桌子上是否有食物
if (Desk.foodFlag==1){
//如果有,等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
// Desk.count++;
// 如果没有,制作食物
if (Desk.count>0){
System.out.println("厨师又做了一碗面条");
}else{
System.out.println("厨师做了一碗面条");
}
// 修改桌子上的食物状态
Desk.foodFlag = 1;
// 叫消费者来吃,唤醒
Desk.lock.notifyAll();
}
}
}
}
}
}
消费者类(吃货类)
步骤
* 1. 循环 * 2. 同步代码块(可改写为同步方法) * 3. 判断共享数据是否到达末尾(到了末尾) * 3. 判断共享数据是否到达末尾(没到末尾,执行核心逻辑)
public class Foodie extends Thread{
@Override
public void run() {
while (true){
synchronized (Desk.lock){
if (Desk.count == 0){
break;
}else{
// 先判断桌子上是否有面条
if (Desk.foodFlag == 0){
// 如果没有,等待
//让当前线程跟锁进行绑定
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
// 吃完后要将面条总数减一
Desk.count--;
// 如果有,开吃
if (Desk.count==0){
System.out.println("吃货吃饱了,嗝~,吃不下了");
}else{
System.out.println("吃货在吃面条,还能吃 "+Desk.count+"碗!!");
}
//吃完后唤醒厨师继续做
Desk.lock.notifyAll();
// 修改桌子状态
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 ProducterAndComsummer {
public static void main(String[] args) {
// 创建线程对象
Cook cook = new Cook();
Foodie foodie = new Foodie();
// 设置线程名
cook.setName("厨师 ");
foodie.setName("吃货 ");
// 开启线程
cook.start();
foodie.start();
}
}
阻塞队列方式
代码实现
厨师类
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) {
e.printStackTrace();
}
}
}
}
吃货类
public class Foodie extends Thread {
// 创建阻塞队列
ArrayBlockingQueue<String> queue ;
public Foodie(ArrayBlockingQueue<String> queue){
this.queue = queue;
}
@Override
public void run() {
while (true){
// 厨师不断地把面条放到阻塞队列中
try {
String food = queue.take();
System.out.println("吃货吃了一碗"+food);
System.out.println();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试类
public class ThreadDemo {
public static void main(String[] args) {
// 1. 创建阻塞队列
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1) ;
// 2. 创建线程的对象,并把阻塞队列传递过去
Cook cook = new Cook(queue);
Foodie foodie = new Foodie(queue);
cook.start();
foodie.start();
}
}
细节
生产者和消费者必须使用同一个阻塞队列
线程生命周期
生命周期状态
状态
创建 :start()
就绪:不停地抢CPU资源
运行:运行代码
阻塞:sleep()或其他阻塞方式
死亡:run()执行完
图解
所有状态及产生状态的原因
代码中的生命周期
线程池(重点)
线程池类实现
步骤
1. 获取线程池对象
2. 提交任务
3. 所有的任务全部执行完毕,关闭线城池
例
public class MyThreadPoolDemo {
/*
public static ExecutorService newFixedThreadPool(int nTheads);
public static ExecutorService newCachedThreadPool();
*/
public static void main(String[] args) {
// 1. 获取线程池对象
// ExecutorService pool = Executors.newCachedThreadPool();
ExecutorService pool = Executors.newFixedThreadPool(3);
// 2.提交任务
pool.submit(()->{
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"------------"+i);
}
});
pool.submit(()->{
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"------------"+i);
}
});
// 3. 所有的任务全部执行完毕,关闭线城池
pool.shutdown();
}
}
自定义线程池
自定义线程池参数
corePoolSize: 核心线程数
maximumPoolSize: 最大线程数
keepAliveTime: 临时线程数空闲存活时间(值)
TimeUnit: 临时线程数空闲存活时间单位(单位)
BlockingQueue<Runnable>: 阻塞队列类型
ThreadFactory: 线程工厂(线程创建方式)
RejectedExecutionHandler: 拒绝线程处理器(拒绝策略)
线程池参数理解
将饭店和线程池联系起来,理解如下
各个参数的限制
临时线程的错误理解
临时线程的正确理解
阻塞队列的理解
拒绝策略的理解
拒绝策略
类型
AbortPolicy
DiscardPolicy
DiscardOldestPolicy
CallerRunsPolicy
说明
本次多线程的总结是观看黑马视频的总结,其视频链接如下: