线程安全问题、同步代码块、同步方法、线程池详解

前言

通过本文我们将会了解到基本的多线程的知识。

一、线程安全的问题

在了解线程的安全问题前,我们先来看一个需求:
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟电影院卖票。
分析:
有三个窗口,窗口各自都是独立的,可以将这3个窗口当作3个线程,在线程中执行的是卖票的代码。
1.创建一个类,继承Thread
2.定义变量int ticked,表示票数
3.在run()方法中执行循环,当票数小于100时,票数自减,继续循环,直到票数卖完,循环结束。
若是根据上述分析,最后的结果是三个线程,每个线程都卖了100张票,总共卖了300张票,并不是正确的需求
那么把变量ticked类型改为static int就可以让三个线程共享一个数据
但此时运行代码会发现,三个线程在执行的时候会有重复出现,甚至还有超出100范围的,这不是我们想要的,还是存在问题。

package com.practice.threaddemo1;

/**
 * @Author YJ
 * @Date 2023/7/21 19:07
 * Description:卖票线程代码
 */
public class MyThread extends Thread {
	//通过静态变量实现三个线程共享
    static int ticked = 0;

    @Override
    public void run() {
        while (true) {
            if (ticked < 100) {
                //每次卖票前睡一会
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticked++;
                System.out.println(getName() + "正在卖第" + ticked + "张票!");
            } else {
                break;
            }
        }
    }
}

package com.practice.threaddemo1;

/**
 * @Author YJ
 * @Date 2023/7/21 19:02
 * Description:模拟电影院卖票
 */
public class MyThreadDemo {
    public static void main(String[] args) {
        //1.创建线程对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        //2.设置线程名字
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        //3.开启线程
        t1.start();
        t2.start();
        t3.start();

    }
}

在这里插入图片描述
在这里插入图片描述

通过上面的买票需求,我们发现线程存在的安全问题,会出现重复和超出范围的情况,为什么会出现这种情况呢,我们可以通过线程的执行结合代码分析:

原因分析:
1.票数重复:
在线程开启的时候,三个线程都在抢夺CPU的资源,假设线程一在开始抢到了CPU的执行权,线程一就会继续往下执行,进入线程后,满足判断条件,接着会立马睡10毫秒(自定义的),此时线程一不会抢夺CPU的执行权,线程二和线程三一定会有一个抢到CPU的执行权,,假设是线程二抢到了,它也会继续往下执行,通过判断条件,也同样会睡10毫秒,此时CPU的执行权一定会被其他线程抢到,所以线程三也会睡10毫秒,于是当线程各自醒来后,继续抢夺CPU执行权,假设是线程一抢到了,ticked会自增变成1,此时线程一还没来得及打印,线程二就抢到了CPU的执行权,ticked又自增变成了2,同样的线程三也会抢到CPU执行权,ticked自增到3,接下来无论是哪个线程继续往下打印,ticked结果都是3,这样就出现了重复的情况。

2.票数超出范围:
当票数到达99张时,三个线程还是在抢夺CPU执行权,线程一抢到后进入循环睡10毫秒,线程二抢到同样睡10毫秒,线程三同样进来睡10毫秒,睡完后陆续醒来继续执行下面的代码,线程一醒来后ticked自增变为100,还没来得及打印,线程二醒来执行代码ticked子增变为101,还没来得及打印,线程三醒来执行代码ticked子增变为了102,接下来无论哪个线程打印,结果都是102,票数超出了范围。
上述根本原因是:线程执行时有随机性

解决方案:
将要执行的循环语句起来,这样当第一个线程抢到了CPU执行权,若是线程一的循环还没有执行完,线程二抢到了CPU执行权,由于循环是被锁住的,线程二就必须等待线程一执行完后才能进入循环。

二、同步代码块

2.1同步代码块实现方式

格式:
synchronized(锁对象){操作共享数据的代码}
特点1:锁默认打开,有一个线程进去了,锁自动关闭
特点2:里面的代码全部执行完毕,线程出来,锁自动打开
锁对象一定要是唯一的
通过锁来解决线程安全问题被叫做同步代码块

package com.practice.threaddemo1;

/**
 * @Author YJ
 * @Date 2023/7/21 19:07
 * Description:同步代码块
 */
public class MyThread extends Thread {
    static int ticked = 0;
    //锁对象要唯一
    static Object obj = new Object();

    @Override
    public void run() {
        synchronized (obj) {
        while (true) {
            if (ticked < 100) {
                //每次卖票前睡一会
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticked++;
                System.out.println(getName() + "正在卖第" + ticked + "张票!");
            } else {
                break;
            }
        }
        }
    }
}


在这里插入图片描述

2.1同步代码块实现细节

分析上述同步代码块实现卖票的方式,结果只有一个线程卖完了所有的票,也就是说,一个线程抢到CPU执行权后进入循环,直到这个线程执行完所有的代码后循环结束,票数也增加到了100,后面的线程再进入循环时已经不符合循环条件,所以循环直接结束。
所以要注意的是,synchronized 锁应该放在循环里面。
既然锁对象是唯一的,我们可以直接将当前类的字节码对象作为唯一的锁对象,字节码对象一定是唯一的。

package com.practice.threaddemo1;

/**
 * @Author YJ
 * @Date 2023/7/21 19:07
 * Description:同步代码块
 */
public class MyThread extends Thread {
    static int ticked = 0;

    @Override
    public void run() {
        while (true) {
            synchronized (MyThread.class) {
                if (ticked < 100) {
                    //每次卖票前睡一会
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticked++;
                    System.out.println(getName() + "正在卖第" + ticked + "张票!");
                } else {
                    break;
                }
            }
        }
    }
}

结果:
在这里插入图片描述

二、同步方法

同步方法:就是将synchronized 直接加在方法上
格式:
修饰符 synchronized 返回值类型 方法名(方法参数){...}
**特点1:**同步方法是锁住方法里面所有的代码
**特点2:**锁对象不能自己指定(非静态的:this静态的:当前的字节码对象)
我们可以通过同步方法实现上述卖票需求:

package com.practice.threaddemo2;

/**
 * @Author YJ
 * @Date 2023/7/21 21:00
 * Description:同步方法
 */
public class MyRunnable implements Runnable {
    //只创建一次,不需要static修饰
    int ticket = 0;

    @Override
    public void run() {
        //1.循环
        //2.同步代码块
        //3.判断共享数据是否到了末尾,如果到了末尾
        //4.判断共享数据是否到了末尾,如果没有到末尾
        while (true) {
            //同步方法
            if (method()) break;
        }
    }

    private synchronized boolean method() {
        if(ticket==100) {
            return true;
        } else {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ticket++;
            System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票!!!");
        }
        return false;
    }
}

在这里插入图片描述

StringBuffer线程安全的原因是它的所有方法都有synchronized 修饰而StringBuilder 没有synchronized 修饰,这就是同步方法保证线程安全的原因。

三、Lock锁

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5之后提供了一个新的锁对象Lock锁
Lock实现提供比使用synchronized 方法和语句获得更广阔的锁定操作
Lock中提供了获得锁和释放锁的方法
void lock():获得锁
void unlock():释放锁
手动上锁,手动释放锁
Lock是接口,不能实例化,这里采用它的实现类ReentrantLock来实例化
ReentrantLock的构造方法
ReentrantLock():创建一个ReentrantLock的实例

package com.practice.threaddemo3;

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

/**
 * @Author YJ
 * @Date 2023/7/21 19:07
 * Description:Lock锁
 */
public class MyThread extends Thread {
    static int ticked = 0;

    static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
           // synchronized (MyThread.class) {
            lock.lock();
            try {
                if (ticked == 100) {
                    break;
                } else {
                    //每次卖票前睡一会
                    Thread.sleep(10);
                    ticked++;
                    System.out.println(getName() + "正在卖第" + ticked + "张票!");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            //}
        }
    }
}

注意:在多线程使用锁的时候,不能让两个锁嵌套起来,两个锁嵌套有可能导致死锁的产生

四、生产者和消费者(等待唤醒机制)

生产者消费者模式是一个十分经典的多线程协作的模式。

  • void wait()当前线程等待,直到被其他线程唤醒
  • void notify()随机唤醒单个线程
  • void notifyAll()唤醒所有线程

4.1生产者和消费者的思路分析

  • 假设有一个吃货线程表示消费者,厨师线程表示生产者,有一个桌子,桌子上有面条,吃货线程执行吃,厨师线程负责等,桌子上没有面条,吃货就负责等,厨师生产面条。
  • 生产者和消费者的理想情况:
  • 厨师线程生产了一碗面条,放到桌子上,吃货线程吃一碗面条,相当于厨师做一碗面条,吃货吃一碗面条。
    但是线程执行具有随机性,并不一定会是这种理想情况。

生产者和消费者(消费者等待):
当两个线程启动时,若是消费者线程先抢到CPU执行权,但发现并没有任务要执行,这时消费者线程就需要等待wait,此时CPU执行权一定会被生产者线程抢到,生产者开始布置任务,布置完成后,消费者线程还是处于等待状态的,此时生产者线程就需要告诉消费者线程可以执行任务了,这个动作叫做唤醒notify

  • 消费者(消费数据):
  • 1.判断桌子上是否有食物
  • 2.如果没有就等待
  • 生产者(生产数据):
  • 1.制作食物
  • 2.把食物放在桌子上
  • 3.叫醒等待的消费者开吃

生产者和消费者(生产者等待):
当两个线程启动时,生产者抢到了CPU执行权,没有任务要执行,生产者开始布置任务,布置完成后,即使没有消费者在等待,仍然可以执行唤醒notify操作,而在下一步还是生产者抢到了CPU执行权,但此时已经有任务了,生产者就不能再去布置任务了,所以生产者就要等待wait

  • 消费者(消费数据):
  • 1.判断桌子上是否有食物
  • 2.如果没有就等待
  • 生产者(生产数据):
  • 1.判断桌子上是否有食物
  • 2.有:等待
  • 3.没有:制作食物
  • 4.制作食物
  • 5.把食物放在桌子上
  • 6.叫醒等待的消费者开吃

4.2生产者和消费者的代码实现

  • 桌子:
package com.practice.waitandnotify;

/**
 * @Author YJ
 * @Date 2023/7/22 8:12
 * Description:控制生产者和消费者的执行(桌子)
 */
public class Desk {
    //是否有面条:0.没有  1.有
    public static int foodFlag = 0;
    //总个数
    public static int count = 10;
    //锁对象
    public static Object lock = new Object();

}

  • 生产者(厨师):
package com.practice.waitandnotify;

/**
 * @Author YJ
 * @Date 2023/7/22 8:11
 * Description:生产者(厨师)
 */
public class Cook extends Thread{
    @Override
    public void run() {
        /**
         * 1.循环
         * 2.同步代码块
         * 3.判断共享数据是否到了末尾(到了末尾)
         * 4.判断共享数据是否到了末尾(没到末尾)
         */
        while (true) {
            synchronized (Desk.lock) {
                if(Desk.count == 0) {
                    break;
                } else{
                    //1.判断桌子上是否有食物
                    if(Desk.foodFlag == 1) {
                        //2.有:等待
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    } else {
                        //3.没有:制作食物
                        System.out.println("厨师做了一碗面条");
                        //4.修改食物状态
                        Desk.foodFlag = 1;
                        //5.唤醒等待的消费者
                        Desk.lock.notifyAll();
                    }
                }
            }
        }
    }
}

  • 消费者(吃货):
package com.practice.waitandnotify;

/**
 * @Author YJ
 * @Date 2023/7/22 8:11
 * Description:消费者(吃货)
 */
public class Foodie extends Thread{
    @Override
    public void run() {
        /**
         * 1.循环
         * 2.同步代码块
         * 3.判断共享数据是否到了末尾(到了末尾)
         * 4.判断共享数据是否到了末尾(没到末尾)
         */
        while (true) {
            synchronized(Desk.lock) {
                if(Desk.count == 0) {
                    System.out.println("已经吃不下了~~");
                    break;
                } else {
                    //判断桌子上是否有面条
                    if(Desk.foodFlag == 0) {
                        //没有:等待
                        try {
                            Desk.lock.wait();//让当前锁跟这个线程绑定
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    } else {
                        //把吃的总数-1
                        Desk.count--;
                        //有:开吃
                        System.out.println("正在吃,还能再吃" + Desk.count + "碗~");
                        //吃完了:唤醒厨师
                        Desk.lock.notifyAll();
                        //修改桌子状态
                        Desk.foodFlag = 0;
                    }
                }
            }
        }
    }
}

  • 代码运行:**
package com.practice.waitandnotify;

/**
 * @Author YJ
 * @Date 2023/7/22 8:34
 * Description:运行
 */
public class ThreadDemo {
    public static void main(String[] args) {
        //创建线程对象
        Cook cook = new Cook();
        Foodie foodie = new Foodie();
        cook.setName("厨师");
        foodie.setName("吃货");
        cook.start();
        foodie.start();
    }
}

  • 结果:
    在这里插入图片描述

4.3等待唤醒机制(阻塞队列方式实现)

  • 阻塞队列的继承结构:
  • Iterable
  • Collection
  • Queue
  • BlockingQueue
  • 实现类:
  • ArrayBlockingQueue:底层是数据,有界,必须指定长度
  • LinkedBlockingQueue:底层是链表,无界,但不是真正的无界,最大为int的最大值

代码实现:

细节:生产者和消费者必须使用同一个阻塞队列。

package com.practice.waitandnotifyblockingqueue;

import java.util.concurrent.ArrayBlockingQueue;

/**
 * @Author YJ
 * @Date 2023/7/22 9:13
 * Description:生产者
 */
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();
            }
        }
    }
}

package com.practice.waitandnotifyblockingqueue;

import java.util.concurrent.ArrayBlockingQueue;

/**
 * @Author YJ
 * @Date 2023/7/22 9:14
 * Description:消费者
 */
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);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

package com.practice.waitandnotifyblockingqueue;

import java.util.concurrent.ArrayBlockingQueue;

/**
 * @Author YJ
 * @Date 2023/7/22 9:13
 * Description:阻塞队列方式实现
 */
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);
        //3.开启线程
        cook.start();
        foodie.start();
    }
}

4.4线程的状态

在这里插入图片描述

五、线程池

以前写多线程的弊端:

  • 1.用到线程的时候就创建(效率低)
  • 2.用完后线程消失(浪费资源)

改进:
我们可以准备一个容器,用来存放线程,这个容器就叫做线程池,刚开始,容器中是空的,当给线程池提交一个任务时,线程池会自动地创建一个线程,用这个线程执行任务,执行完后,把线程返回给容器,等到下次再执行任务时,就不需要重新创建线程了。
特殊情况:
当第二个任务执行时,第一个任务还没有执行结束,线程池就要再创建一个新的线程,用这个新的线程执行任务,再来任务,继续创建线程,执行完后,都返回给线程池。
线程池中的线程创建是有上限的,可以自己定义最大线程数量,当任务过多,线程创建也达到上限时,未获取线程的任务只能排队等待。
核心原理:

  • 1.创建一个池子,池子中是空的
  • 2.提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子,下次再次提交任务时,不需要创建新的线程,直接复用已有的线程即可
  • 3.如果提交任务时,池子中没有空闲的线程,也无法创建新的线程,任务就会排队等待。

代码实现:

  • 1.创建线程池
  • 2.提交任务
  • 3.所有任务执行完毕,关闭线程池
    Excutors:线程池的工具类,通过调用方法返回不同类型的线程池对象。
    public static ExcutorService newCachedThreadPool():创建一个没有上限的线程池
    public static ExcutorService newFixedThreadPool():创建有上限的线程池
package com.practice.mythreadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @Author YJ
 * @Date 2023/7/22 10:20
 * Description:创建没有上限的线程池
 */
public class MyThreadPoolDemo1 {
    public static void main(String[] args) throws InterruptedException {
        //1.获取线程池对象
        ExecutorService pool1 = Executors.newCachedThreadPool();
        Thread.sleep(1000);
        //2.提交任务
        pool1.submit(new MyRunnable());
        Thread.sleep(1000);
        pool1.submit(new MyRunnable());
        Thread.sleep(1000);
        pool1.submit(new MyRunnable());
        Thread.sleep(1000);
        pool1.submit(new MyRunnable());
        Thread.sleep(1000);
        pool1.submit(new MyRunnable());
        //3.销毁线程池
        //pool1.shutdown();
    }
}

package com.practice.mythreadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @Author YJ
 * @Date 2023/7/22 10:20
 * Description:创建有上限的线程池
 */
public class MyThreadPoolDemo2 {
    public static void main(String[] args) throws InterruptedException {
        //1.获取线程池对象
        ExecutorService pool1 = Executors.newFixedThreadPool(3);
        Thread.sleep(100);
        //2.提交任务
        pool1.submit(new MyRunnable());
        Thread.sleep(100);
        pool1.submit(new MyRunnable());
        Thread.sleep(100);
        pool1.submit(new MyRunnable());
        Thread.sleep(100);
        pool1.submit(new MyRunnable());
        Thread.sleep(100);
        pool1.submit(new MyRunnable());
        //3.销毁线程池
        //pool1.shutdown();
    }
}

六、自定义线程池

核心参数:

  • 1.核心线程的数量(不能小于0)
  • 2.线程池中最大线程数量(最大数量>=核心线程数量)
  • 3.空闲时间(值),如60(不能小于0)
  • 4.空闲时间(单位),如s(用TimeUnit指定)
  • 5.阻塞队列(不能为null)
  • 6.创建线程的方式(不能为null)
  • 7.要执行的任务过多时的解决方案(不能为null)

注意:
自定义线程池可以创建核心线程和临时线程。
假设核心线程有3个,临时线程是3个,队伍长度为3个,表示线程池中最多有6个线程可用,而且其中3个临时线程只有在队伍满的情况下又来了任务才会创建并执行,先提交的任务不一定先执行。
若有8个任务要执行,3个核心线程执行3个任务,三个任务在队伍中等待,此时还有两个任务,那么此时就要创建2个临时线程执行两个任务,队伍中还有3个任务在等待。
若是任务过多,线程池满了,队伍也满了,还是有任务,这时就会触发任务拒绝策略:

  • ThreadPoolExcutor.AbortPolicy:默认策略:丢弃任务并抛出RejectedExecutionException异常
  • ThreadPoolExcutor.DiscardPolicy:丢弃任务,但不抛出异常,不推荐
  • ThreadPoolExcutor.DiscarOldestPolicy:抛弃队列中等待最持久的任务,然后把当前任务加入队列中
  • ThreadPoolExcutor.CallerRunsPolicy:调用任务的run()方法绕过线程池直接执行
package com.practice.mythreadpool2;


import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @Author YJ
 * @Date 2023/7/22 10:50
 * Description:创建自定义线程池
 */
public class MyThreadPoolDemo1 {
    public static void main(String[] args) throws InterruptedException {
        //创建自定义线程池对象
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                3,//核心线程的数量(不能小于0)
                6,//线程池中最大线程数量(最大数量>=核心线程数量)
                60,//空闲时间(值)(不能小于0)
                TimeUnit.SECONDS,//空闲时间(单位),如s(用TimeUnit指定)
                new ArrayBlockingQueue<>(3),//阻塞队列(不能为null)
                Executors.defaultThreadFactory(),//创建线程的方式(不能为null)
                // -- Executors.defaultThreadFactory()底层就是new了一个Thread
                new ThreadPoolExecutor.AbortPolicy()//任务拒绝策略
        );
        //提交任务
        //...
    }
}

6.1、最大并行数

以4核8线程为例:
4核表示的是电脑有4个大脑,利用超线程技术,就可以把原本的4个大脑虚拟成8个,也就是8线程。
可以在设备管理器或任务管理器中看到自己电脑的最大并行数:


在这里插入图片描述


在这里插入图片描述


也可通过Java虚拟机用代码查看:

package com.practice.mythreadpool2;

/**
 * @Author YJ
 * @Date 2023/7/22 11:50
 * Description:获取电脑最大并行数
 */
public class MyThreadPoolDemo2 {
    public static void main(String[] args) throws InterruptedException {
        int count = Runtime.getRuntime().availableProcessors();
        System.out.println(count);
    }
}

6.2、线程池多大合适

CPU密集型运算: 最大并行数+1
I/O密集型运算:(读取本地文件较多、读取数据库文件较多) 最大并行数 * 期望CPU利用率 * (总时间(CPU计算时间+等待时间)) / CPU计算时间

总结

关于多线程的学习其实还有很多,目前介绍学习的是我们平时会用到的,希望会有帮助,我会继续学习并记录博客的学习笔记,欢迎大家关注+点赞!!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

叶落闲庭

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

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

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

打赏作者

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

抵扣说明:

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

余额充值