线程安全问题、同步代码块、同步方法、线程池详解
1.核心线程的数量(不能小于0)2.线程池中最大线程数量(最大数量>=核心线程数量)3.空闲时间(值),如60(不能小于0)4.空闲时间(单位),如s(用TimeUnit指定)5.阻塞队列(不能为null)6.创建线程的方式(不能为null)7.要执行的任务过多时的解决方案(不能为null)自定义线程池可以创建核心线程和临时线程。
深入理解多线程
前言
通过本文我们将会了解到基本的多线程的知识。
一、线程安全的问题
在了解线程的安全问题前,我们先来看一个需求:
某电影院目前正在上映国产大片,共有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计算时间
总结
关于多线程的学习其实还有很多,目前介绍学习的是我们平时会用到的,希望会有帮助,我会继续学习并记录博客的学习笔记,欢迎大家关注+点赞!!!
更多推荐
所有评论(0)