文章目录
JavaSE
多线程
1、什么是线程?
线程就是操作系统能够进行运算调度的最小单位,它被包含在进程(进程是程序的基本执行实体,任务管理器种选择的进程就可以看到每个软件的进程)中,是进程中实际运作的单位。
比如:360杀毒软件,包括木马查杀、电脑清理、系统修复功能。其中360杀毒软件就是进程,其中的功能就是线程。
简单理解:应用软件种相互独立,可以同时运行的功能就是线程。
2、多线程的应用场景?
拷贝迁移大文件、加载大量的资源文件
3、并发和并行
并发:在同一时刻,有多个指令在单个cpu上交替执行
并行:在同一时刻,有多个指令在多个cpu上同时执行
4、多线程的实现方式
4.1 继承Thread类的方式进行实现
public class MyThread extends Thread{
/**
* 重写run方法
*/
@Override
public void run() {
//书写线程要执行的代码
for (int count = 0; count < 100; count++) {
System.out.println(getName() + "hello");
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
/*
* 多线程的第一种实现方式:
* 1、自定义一个类继承Thread类
* 2、重写run()方法
* 3、创建子类对象,调用start()方法启动线程
*
* */
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("线程1:");
t2.setName("线程2:");
t1.start();
t2.start();
}
}
运行结果:
线程2:hello
线程2:hello
线程1:hello
线程2:hello
线程1:hello
...
根据运行结果可以发现,线程1和线程2在同时执行.
4.2 实现Runable接口方式进行实现
public class MyRunable implements Runnable{
/**
* 重写Run方法
*/
@Override
public void run() {
//书写线程要执行的代码
for (int count = 0; count < 100; count++) {
//Thread.currentThread():获取当前线程对象
//.getName():获取当前线程对象的名字
System.out.println(Thread.currentThread().getName() + "hello");
}
}
}
public class MyThread2 {
public static void main(String[] args) {
/*
* 多线程的第二种实现方式:
* 1、自定义一个类实现Runable接口
* 2、重写run()方法
* 3、创建自己的类的对象(任务对象)
* 4、创建一个Thread类的对象,将自己的类放入进去,调用start()方法启动线程
* */
//创建自己的类的对象(任务对象)
MyRunable mr = new MyRunable();
//创建Thread类的对象
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
//给线程设置名字
t1.setName("线程1:");
t2.setName("线程2:");
//启动线程
t1.start();
t2.start();
}
}
运行结果:
线程2:hello
线程2:hello
线程1:hello
线程2:hello
线程1:hello
...
根据运行结果可以发现,线程1和线程2在同时执行.
4.3 使用Callable接口和Future接口方式进行实现
public class MyCallable implements Callable<Integer> { //Callable<Integer> 其中Integer:返回值的类型
/**
* 重写call方法
* @return
* @throws Exception
*/
@Override
public Integer call() throws Exception {
//书写线程要执行的代码
//求1~100之间的和
int sum = 0;
for (int i = 1; i <= 100; i++) {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName());
sum = sum + i;
}
return sum;
}
}
public class MyThread3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/*
* 多线程的第三种实现方式:
* 特点:可以获取到多线程运行的结果
*
* 1、自定义一个类实现Callable接口
* 2、重写call()方法。(这个方法是有返回值的,返回值表示多线程运行的结果)
* 3、创建自己的类的对象(任务对象)
* 4、创建一个FutureTask类的对象。(作用:管理多线程的运行结果)
* 5、创建一个Thread类的对象(表示线程),调用start()方法启动线程
* */
//创建自己的类的对象(任务对象)
MyCallable mc = new MyCallable();
//创建一个FutureTask类的对象。(作用:管理多线程的运行结果)
FutureTask<Integer> ft = new FutureTask<>(mc);
//创建一个Thread类的对象(表示线程)
Thread t1 = new Thread(ft);
Thread t2 = new Thread(ft);
//设置名字
t1.setName("线程1");
t2.setName("线程2");
//启动线程
t1.start();
t2.start();
//获取多线程的运行结果
Integer result = ft.get();
System.out.println("运行结果:" + result);
}
}
运行结果:
线程1
线程1
线程1
线程1
线程1
线程1
...
线程1
运行结果:5050
为什么线程1和线程2没有交替执行?
因为FutureTask只会执行一次Callable的call方法,并将结果缓存起来供后续调用。因为对于返回值总是不变的,我们总是要考虑把他缓存一下,下一次的时候,直接把值返回出去,就不用再进行计算了,大大的提高了效率。
4.4 多线程三种实现方式对比
5、多线程常见的成员方法
代码实现1(线程默认名字):
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":执行了");
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
//创建自己的类的对象(任务对象)
MyRunnable mr = new MyRunnable();
//创建Thread类的对象
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
//启动线程
t1.start();
t2.start();
}
}
运行结果:
Thread-0:执行了
Thread-1:执行了
Thread-0:执行了
Thread-1:执行了
Thread-1:执行了
...
可以如果没有个线程起名字,则默认的名字为 Thread-线程的编号
代码实现2(main线程):
public static void main(String[] args) {
//获取当前线程名
String name = Thread.currentThread().getName();
System.out.println(name);
}
运行结果:
main
可以发现我们所有的代码都是执行在main线程里面的。
代码实现3(设置线程名和获取线程名,线程睡眠):
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//获取当前线程对象并获取线程名称
System.out.println(Thread.currentThread().getName() + ":执行了");
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
//创建自己的类的对象(任务对象)
MyRunnable mr = new MyRunnable();
//创建Thread类的对象,并使用构造方法设置线程的名字
Thread t1 = new Thread(mr,"线程1");
Thread t2 = new Thread(mr,"线程2");
//也可以使用setName给线程设置名字
//t1.setName("线程1");
//t2.setName("线程2");
//启动线程
t1.start();
t2.start();
}
}
运行结果:
线程1:执行了
线程2:执行了
线程1:执行了
线程2:执行了
线程1:执行了
线程2:执行了
...
可以发现控制台先输出
线程1:执行了
线程2:执行了
两秒钟之后再输出
线程1:执行了
线程2:执行了
说明睡了两秒。
代码实现4(设置线程优先级):
public class MyRunnable2 implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + "----------->" + i);
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) throws InterruptedException {
//创建自己的类的对象(任务对象)
MyRunnable2 mr = new MyRunnable2();
//创建Thread类的对象,并使用构造方法设置线程的名字
Thread t1 = new Thread(mr, "线程1");
Thread t2 = new Thread(mr, "线程2");
//获取线程的优先级,发现默认线程的优先级为5
int priority = t1.getPriority();
int priority2 = t2.getPriority();
System.out.println("默认线程1的优先级:" + priority + ",默认线程2的优先级:" + priority2);
//给线程1设置优先级为1,线程2设置优先级为10
t1.setPriority(1);
t2.setPriority(10);
//启动线程
t1.start();
t2.start();
//睡3秒等线程都执行完之后再打印这句话
Thread.sleep(3000);
System.out.println("此时线程1的优先级为:" + t1.getPriority() +",线程2的优先级为:" + t2.getPriority());
}
}
运行结果:
默认线程1的优先级:5,默认线程2的优先级:5
线程1----------->1
线程2----------->1
线程1----------->2
线程2----------->2
线程2----------->3
线程2----------->4
线程2----------->5
线程2----------->6
线程2----------->7
线程2----------->8
线程2----------->9
线程2----------->10
线程1----------->3
线程1----------->4
线程1----------->5
线程1----------->6
线程1----------->7
线程1----------->8
线程1----------->9
线程1----------->10
此时线程1的优先级为:1,线程2的优先级为:10
从运行结果可以发现因为线程2的优先级比线程1的优先级高,所以线程2抢占cpu的概率比较高,所以线程2先执行完for循环。
但是,虽然线程2比线程1的优先级高,并不一定代码每次执行都是线程2先执行完,也有线程1先执行完的情况。
代码实现5(设置守护线程):
守护线程特点:当其他非守护线程执行完毕后,守护线程会陆续结束。
public class MyRunnableDaemon1 implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + "----------->" + i);
}
}
}
public class MyRunnableDaemon2 implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 99; i++) {
System.out.println(Thread.currentThread().getName() + "----------->" + i);
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) throws InterruptedException {
//创建自己的类的对象(任务对象)
MyRunnableDaemon1 mr = new MyRunnableDaemon1();
//创建Thread类的对象,并使用构造方法设置线程的名字
Thread t1 = new Thread(mr, "线程1");
Thread t2 = new Thread(mr, "线程2");
//设置线程2为守护线程
t2.setDaemon(true);
//启动线程
t1.start();
t2.start();
}
}
执行结果:
线程1----------->1
线程2----------->1
线程1----------->2
线程1----------->3
线程1----------->4
线程1----------->5
线程1----------->6
线程1----------->7
线程1----------->8
线程1----------->9
线程1----------->10
线程2----------->2
线程2----------->3
线程2----------->4
线程2----------->5
线程2----------->6
线程2----------->7
线程2----------->8
线程2----------->9
线程2----------->10
线程2----------->11
线程2----------->12
我们发现,原本来说线程2要循环100次,线程1执行10次,肯定比线程1要晚一点执行结束,但是将线程2设置为守护线程后发现,当线程1执行完成后,线程2(守护线程)也会陆续执行完毕。
代码实现6(礼让线程):
public class MyRunnable4 implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + "----------->" + i);
//表示出让cpu的执行权
Thread.yield();
}
}
}
public class ThreadDemo4 {
public static void main(String[] args) throws InterruptedException {
//创建自己的类的对象(任务对象)
MyRunnable4 mr = new MyRunnable4();
//创建Thread类的对象,并使用构造方法设置线程的名字
Thread t1 = new Thread(mr, "线程1");
Thread t2 = new Thread(mr, "线程2");
//启动线程
t1.start();
t2.start();
}
}
执行结果:
线程1----------->1
线程2----------->1
线程1----------->2
线程2----------->2
线程1----------->3
线程2----------->3
线程1----------->4
线程1----------->5
线程2----------->4
线程2----------->5
发现设置了礼让机制后,线程1和线程2尽可能的轮换抢夺cpu的执行权。尽可能让结果均匀一点。
代码实现7(插入线程):
public class MyRunnable5 implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "----------->" + i);
}
}
}
public class ThreadDemo5 {
public static void main(String[] args) throws InterruptedException {
//创建自己的类的对象(任务对象)
MyRunnable5 mr = new MyRunnable5();
//创建Thread类的对象,并使用构造方法设置线程的名字
Thread t1 = new Thread(mr, "线程1");
//启动线程
t1.start();
//设置t1线程为插入线程
//表示把t1这个线程,插入到当前线程之前
//t1:线程1
//当前线程:main线程
t1.join();
//这段循环是执行在main线程中的
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "-----" + i);
}
}
}
执行结果:
...
线程1----------->98
线程1----------->99
线程1----------->100
main-----0
main-----1
main-----2
main-----3
main-----4
main-----5
main-----6
main-----7
main-----8
main-----9
因为把线程1设置为了插入线程,所以当线程1执行完后,main线程才会执行。
6、线程的生命周期
7、线程的安全问题
7.1 同步代码块
使用影院买票的案例来展示一下线程的安全问题:
public class MyThread extends Thread{
//票号,使用了static关键字后,表示这个类所有的对象,都共享ticket数据。
static int ticket = 0; // 0 ~ 99
@Override
public void run() {
while (true){
if (ticket < 100){
try {
//睡1毫秒
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName() + "正在卖弟" + ticket + "张票!");
}else {
break;
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
/*
需求:
某电影院正在上映国产大片,共100张票,而有3个窗口买票,请设计一个程序模拟该电影院卖票。
*/
//创建线程对象
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();
}
}
执行结果:
...
窗口3正在卖弟97张票!
窗口1正在卖弟99张票!
窗口3正在卖弟100张票!
窗口2正在卖弟100张票!
窗口1正在卖弟101张票!
窗口3正在卖弟101张票!
窗口2正在卖弟102张票!
我们发现:
1、同一张票卖了多次
2、只有100张票,但是已经执行到102张票了
所以线程引发的安全问题为:
1、相同的票出现了多次
2、出现了超出范围的票
引发上述问题的原因:
因为当我们线程1(窗口1)线程抢到cpu的执行权后,会睡10毫秒,所以线程2(窗口2)会抢到cpu的执行权,也会睡10毫秒,线程3(窗口3)同理。当线程1醒了之后,在抢到cpu的执行权。所以会去将ticket加1,还没来得及打印的时候,cpu的执行权被线程2抢走了,线程2也会将ticket加1,线程3同理,所以就会出现了相同的票出现了多次的情况了。产生这一切的原因是:线程执行时,有随机性。
出现了超出范围的票其实也跟上面一样,也是因为线程执行时,有随机性。
解决办法:
当线程进去循环后,把操作共享数据的代码锁起来。
格式:
synchronized(锁对象){
操作共享数据的代码
}
注意:锁对象一定要是唯一的,一般来用当前类的class文件。即:当前类.class
特点1:锁默认打开,当有一个线程进去了,锁会自动关闭。
特点2:里面的代码全部执行完毕后,线程出来,锁会自动打开。
代码实现:
public class MyThread extends Thread{
//票号,使用了static关键字后,表示这个类所有的对象,都共享ticket数据。
static int ticket = 0; // 0 ~ 99
//锁对象,锁对象可以是任意的一个对象,但是一定要是唯一的,所以我们加了static关键字
static Object object = new Object();
@Override
public void run() {
while (true){
//同步代码块
synchronized (object){
if (ticket < 100){
try {
//睡1毫秒
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName() + "正在卖弟" + ticket + "张票!");
}else {
break;
}
}
}
}
}
public class MyThread extends Thread{
//票号,使用了static关键字后,表示这个类所有的对象,都共享ticket数据。
static int ticket = 0; // 0 ~ 99
//锁对象,锁对象可以是任意的一个对象,但是一定要是唯一的,所以我们加了static关键字
//static Object object = new Object();
@Override
public void run() {
while (true){
//同步代码块
//synchronized (object){ 可以这样写,只要确保object是唯一的就行。
//使用这种锁对象,一般把当前类的字节码文件当作锁对象。因为在同一个文件夹中只有一个这个类(MyThread)的class文件,所以这个对象(MyThread)是 唯一的,所以就把它当作锁对象。
synchronized (MyThread.class){
if (ticket < 100){
try {
//睡1毫秒
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName() + "正在卖弟" + ticket + "张票!");
}else {
break;
}
}
}
}
}
执行结果:
...
窗口3正在卖弟94张票!
窗口3正在卖弟95张票!
窗口3正在卖弟96张票!
窗口3正在卖弟97张票!
窗口3正在卖弟98张票!
窗口3正在卖弟99张票!
窗口3正在卖弟100张票!
我们发现,加了同步代码块之后,重复票没有了,超出范围的票也没有了。
7.2 同步方法
同步方法也就是将同步代码块中的内容放到了一个方法里面,如何写同步方法呢?
特点:
1、同步方法锁住的是方法里面的所有代码。
2、锁对象不能自己指定。(当前的对象如果是非静态的,则锁对象是this;如果当前的对象是静态的,则锁对象是当前类的字节码文件对象。)
格式: 修饰符 synchronized 返回值 方法名(方法参数){…}
技巧:
如果不知道哪部分代码需要放到同步方法中,可以先写同步代码块,之后按快捷键 Alt+Shift+M即可将选中的内容自动生成在一个方法,再将方法返回值前面加上synchronized关键字即可变成同步方法。
public class MyRunnable implements Runnable{
//票号 为什么不加static?因为我们MyRunnable对象只需要在实现类中创建一次就行,所以他是唯一的。
int ticket = 0; // 0 ~ 99
@Override
public void run() {
//1.循环
while (true){
if (extracted()) {
break;
}
}
}
//2.同步代码块(同步方法)
//因为这个方法是非静态的,所以当前的锁对象是this,而this指的是MyRunnable对象,而MyRunnable对象是唯一的,所以锁对象也是唯一的。
private synchronized boolean extracted() {
//3.判断共享数据是否到了末尾,如果到了末尾
if (ticket == 100){
return true;
}else {//4.判断共享数据是否到了末尾,如果没有到末尾
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName() + "正在卖弟" + ticket + "张票!");
}
return false;
}
}
public class ThreadDemo {
public static void main(String[] args) {
/*
需求:
某电影院正在上映国产大片,共100张票,而有3个窗口买票,请设计一个程序模拟该电影院卖票。
*/
//创建我们自定义的任务对象
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
Thread t3 = new Thread(mr);
t1.start();
t2.start();
t3.start();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
}
}
运行结果:
...
窗口1正在卖弟97张票!
窗口1正在卖弟98张票!
窗口2正在卖弟99张票!
窗口3正在卖弟100张票!
7.3 StringBuffer和StringBuilder
通过源码可以发现,在StringBuffer的所有成员方法上都有synchronized关键字,而StringBuilder都没有,所以StringBuffer是线程安全的,因为它里面所有的方法都是同步的。
如何选择?
如果你的代码是单线程的,不需要考虑数据的安全情况,则使用StringBuilder;
如果你的代码是多线程的,需要考虑数据的安全情况,则使用StringBuffer。
7.4 Lock锁
public class MyRunnable implements Runnable{
//票号 为什么不加static?因为我们MyRunnable对象只需要在实现类中创建一次就行,所以他是唯一的。
int ticket = 0; // 0 ~ 99
//Lock锁对象是一个接口,所以要new它的实现类ReentrantLock 为什么不加static?因为我们MyRunnable对象只需要在实现类中创建一次就行,所以他是唯一的。
Lock lock = new ReentrantLock();
@Override
public void run() {
//1.循环
while (true){
//2.手动上锁
lock.lock();
try {
//3.判断共享数据是否到了末尾,如果到了末尾
if (ticket == 100){
break;
}else {//4.判断共享数据是否到了末尾,如果没有到末尾
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName() + "正在卖弟" + ticket + "张票!");
}
}catch (Exception e) {
e.printStackTrace();
}finally {
//4.手动释放锁
lock.unlock();
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
/*
需求:
某电影院正在上映国产大片,共100张票,而有3个窗口买票,请设计一个程序模拟该电影院卖票。
使用JDK5的Lock锁实现手动上锁手动释放锁。
*/
//创建我们自定义的任务对象
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
Thread t3 = new Thread(mr);
t1.start();
t2.start();
t3.start();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
}
}
运行结果:
...
窗口1正在卖弟97张票!
窗口1正在卖弟98张票!
窗口2正在卖弟99张票!
窗口3正在卖弟100张票!
7.5 死锁
死锁是一种错误,如何避免?写代码的时候不要让两个锁嵌套起来,否则会形成死锁。
8、等待唤醒机制
8.1 使用锁对象实现等待唤醒机制
生产者和消费者常见方法:
使用锁实现等待唤醒机制:
package MoreThread.waitAndNotify;
/**
* @ClassName: 桌子
* @Description:
* @author: ZhangJian
* @Create: 2024-08-06 14:47
**/
public class Desk {
/**
* 作用:控制生产者和消费者的执行
*/
//桌子上是否有面条 0:没有面条 1:有面条
public static int foodFlag = 0;
//总个数
public static int count = 10;
//锁对象
public static Object lock = new Object();
}
package MoreThread.waitAndNotify;
/**
* @ClassName: 生产者(厨师)
* @Description:
* @author: ZhangJian
* @Create: 2024-08-06 14:48
**/
public class Cook extends Thread{
@Override
public void run() {
/**
* 1、循环
* 2、同步代码块
* 3、判断共享数据是否到了末尾(到了末尾)
* 4、判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
*/
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 {
//如果没有面条,就继续制作面条
System.out.println("厨师做了一碗面条");
//修改桌子上面条的状态
Desk.foodFlag = 1;
//叫醒等待的消费者开吃
Desk.lock.notifyAll();
}
}
}
}
}
}
package MoreThread.waitAndNotify;
import sun.security.krb5.internal.crypto.Des;
/**
* @ClassName: 消费者(吃货)
* @Description:
* @author: ZhangJian
* @Create: 2024-08-06 14:48
**/
public class Foodie extends Thread{
@Override
public void run() {
/**
* 1、循环
* 2、同步代码块
* 3、判断共享数据是否到了末尾(到了末尾)
* 4、判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
*/
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 {
//桌子上有面条
//面条总数-1
Desk.count--;
//如果有,就开吃
System.out.println("吃货在吃面条,还能再吃" + Desk.count + "碗!!!");
//吃完之后,唤醒厨师继续做
Desk.lock.notifyAll(); //唤醒跟锁对象相关的线程的线程
//修改桌子是否有面条的状态
Desk.foodFlag = 0;
}
}
}
}
}
}
package MoreThread.waitAndNotify;
/**
* @ClassName:
* @Description:
* @author: ZhangJian
* @Create: 2024-08-06 14:27
**/
public class ThreadDemo {
private static Cook c;
public static void main(String[] args) {
/**
* 需求:
* 完成生产者和消费者(等待唤醒机制)的代码。
* 实现现成轮流交替执行的结果。
*/
//创建线程对象
c = new Cook();
Foodie f = new Foodie();
//给线程设置名字
c.setName("厨师");
f.setName("吃货");
//开启线程
c.start();
f.start();
}
}
运行结果:
厨师做了一碗面条
吃货在吃面条,还能再吃9碗!!!
厨师做了一碗面条
吃货在吃面条,还能再吃8碗!!!
厨师做了一碗面条
吃货在吃面条,还能再吃7碗!!!
厨师做了一碗面条
吃货在吃面条,还能再吃6碗!!!
厨师做了一碗面条
吃货在吃面条,还能再吃5碗!!!
厨师做了一碗面条
吃货在吃面条,还能再吃4碗!!!
厨师做了一碗面条
吃货在吃面条,还能再吃3碗!!!
厨师做了一碗面条
吃货在吃面条,还能再吃2碗!!!
厨师做了一碗面条
吃货在吃面条,还能再吃1碗!!!
厨师做了一碗面条
吃货在吃面条,还能再吃0碗!!!
8.2使用阻塞队列实现等待唤醒机制
使用阻塞队列实现等待唤醒机制:
package MoreThread.watiAndNotify2;
import java.util.concurrent.ArrayBlockingQueue;
/**
* @ClassName: 生产者(厨师)
* @Description:
* @author: ZhangJian
* @Create: 2024-08-06 15:22
**/
public class Cook extends Thread{
//创建阻塞队列的对象
ArrayBlockingQueue<String> queue;
//重写Cook的有参构造
public Cook(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true){
try {
//不断把面条放入阻塞队列中
//为什么不用加锁?
//因为put方法底层已经加锁了,如果我们再加锁,会变成锁的嵌套,变成死锁。
//给阻塞队列添加数据
queue.put("面条");
System.out.println("厨师放了一碗面条。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package MoreThread.watiAndNotify2;
import java.util.concurrent.ArrayBlockingQueue;
/**
* @ClassName: 消费者(吃货)
* @Description:
* @author: ZhangJian
* @Create: 2024-08-06 15:23
**/
public class Foodie extends Thread{
//创建阻塞队列的对象
ArrayBlockingQueue<String> queue;
//重写Foodie的有参构造
public Foodie(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true){
try {
//不断从阻塞队列中获取面条
//为什么不用加锁?
//因为take方法底层已经加锁了,如果我们再加锁,会变成锁的嵌套,变成死锁。
//取出阻塞队列中的数据
String food = queue.take();
System.out.println("吃货从阻塞队列中获取到了一碗" + food);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package MoreThread.watiAndNotify2;
import MoreThread.watiAndNotify2.Cook;
import java.util.concurrent.ArrayBlockingQueue;
/**
* @ClassName:
* @Description:
* @author: ZhangJian
* @Create: 2024-08-06 15:21
**/
public class ThreadDemo {
public static void main(String[] args) {
/**
* 需求:利用阻塞队列完成生产者和消费者(等待唤醒机制)的代码。
* 细节:
* 生产者和消费者必须使用同一个阻塞队列
*/
//1、创建阻塞队列的对象并指定上限
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(1);
//2、创建线程对象,并把阻塞队列传递过去
Cook c = new Cook(queue);
Foodie f = new Foodie(queue);
//3、开启线程
c.start();
f.start();
}
}
运行结果:
厨师放了一碗面条。
厨师放了一碗面条。
吃货从阻塞队列中获取到了一碗面条
吃货从阻塞队列中获取到了一碗面条
厨师放了一碗面条。
厨师放了一碗面条。
吃货从阻塞队列中获取到了一碗面条
吃货从阻塞队列中获取到了一碗面条
...
为什么打印出来的来过不是顺序执行的?因为的put方法和take都是在底层加的锁,我们打印的话并没有在它们锁对象里面,
所以打印出来的结果不是顺序的,但是存的数据肯定是先往阻塞队列中存,然后从阻塞队列中取,取出来之后,才会再次从
阻塞队列中存,再取... 按照这样的顺序执行的。
9、线程的状态
方便理解的线程的状态:
Java定义的线程的6大状态:
多线程综合练习
综合练习1
一共有1000张电影票,可以在两个窗口处领取,假设每次领取的时间为3000毫秒。
要求:请用多线程模拟卖票过程并打印剩余电影票的数量
public class MyThread extends Thread{
//电影票数量
private static int ticketNum = 10;
@Override
public void run() {
while (true){
synchronized (MyThread.class){
if (ticketNum == 0){
break;
}else {
try {
//当前线程睡3000毫秒
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//电影票-1
ticketNum--;
System.out.println(Thread.currentThread().getName() + "售出一张电影票,还剩" + ticketNum + "张!");
}
}
}
}
}
public class ThreadDemo {
/**
* 一共有1000张电影票,可以在两个窗口处领取,假设每次领取的时间为3000毫秒。
* 要求:请用多线程模拟卖票过程并打印剩余电影票的数量
*/
public static void main(String[] args) {
//创建线程
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
//设置线程名字
t1.setName("窗口1");
t2.setName("窗口2");
//开启线程
t1.start();
t2.start();
}
}
运行结果:
...
窗口1售出一张电影票,还剩4张!
窗口1售出一张电影票,还剩3张!
窗口2售出一张电影票,还剩2张!
窗口1售出一张电影票,还剩1张!
窗口2售出一张电影票,还剩0张!
综合练习2
有100份礼品,两人同时发送,当剩下的礼品小于10份的时候则不在送出。
利用多线程模拟该过程并将线程的名字和礼物的剩余数量打印出来。
public class MyRunnable implements Runnable {
//第二种方式实现多线程,测试类中MyRunable只创建一次,所以不需要加static
int count = 100;
@Override
public void run() {
while (true){
synchronized (MyRunnable.class){
if (count < 10){
System.out.println( "礼物还剩下" + count + "个,不在赠送。");
break;
}else {
count--;
System.out.println(Thread.currentThread().getName() + "在赠送礼物,还剩下" + count + "个。");
}
}
}
}
}
public class ThreadDemo {
/**
* 有100份礼品,两人同时发送,当剩下的礼品小于10份的时候则不在送出。
* 利用多线程模拟该过程并将线程的名字和礼物的剩余数量打印出来。
*/
public static void main(String[] args) {
//创建任务对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
t1.setName("张三");
t2.setName("李四");
t1.start();
t2.start();
}
}
运行结果:
...
张三在赠送礼物,还剩下13个。
李四在赠送礼物,还剩下12个。
张三在赠送礼物,还剩下11个。
李四在赠送礼物,还剩下10个。
李四在赠送礼物,还剩下9个。
礼物还剩下9个,不在赠送。
礼物还剩下9个,不在赠送。
综合联系3
同时开启两个线程,共同获获取1-100之间的所有数字。
要求:将输出所有的奇数。
public class MyThread extends Thread{
//因为需要共用number,所以需要加上static
private static int number = 1;
@Override
public void run() {
//循环
while(true){
//同步代码块
synchronized (MyThread.class){
//判断共享数据,到末尾
if (number > 100){
break;
}else {
//判断共享数据,没有到末尾
if (number % 2 == 1){ //判断奇数
System.out.println(Thread.currentThread().getName() + "打印奇数:" + number);
}
number++;
}
}
}
}
}
public class ThreadDemo {
/**
* 同时开启两个线程,共同获获取1-100之间的所有数字。
* 要求:将输出所有的奇数。
*/
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
运行结果:
...
线程1打印奇数:95
线程2打印奇数:97
线程2打印奇数:99
线程池
线程池的作用:创建线程将线程放入到线程池中(可以确定线程池存放线程的数量),当执行完这个线程任务时,不会销毁。会放入到线程池中进行复用。
一、使用Executors类创建线程池
线程池主要核心原理:
1、创建一个池子,池子中是空的。
2、提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子下次再提交任务时,不需要创建新的线程,直接复用已有的线程即可。
3、但是如果提交任务时,池子中没有空闲的线程,也无法创建新的线程,任务就会排队等待。
代码实现:
1、创建没有上限的线程池。
public class MyThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
//创建一个没有上限的线程池
ExecutorService pool1 = Executors.newCachedThreadPool();
//提交任务
pool1.submit(new MyRunnable());
Thread.sleep(1);
pool1.submit(new MyRunnable());
Thread.sleep(1);
pool1.submit(new MyRunnable());
Thread.sleep(1);
//销毁线程(线程池一般不用销毁)
pool.shutdown();
}
}
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "----");
}
}
运行结果:
pool-1-thread-1----
pool-1-thread-1----
pool-1-thread-1----
发现公用的是一个线程(线程1),睡一秒是的目的是让线程执行完放回线程池中.
2、创建有上限的线程池(最大的线程数为3)。
public class MyThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
//创建一个有上限的线程池(最大上限为3)
ExecutorService pool2 = Executors.newFixedThreadPool(3);
//提交任务
pool2.submit(new MyRunnable());
pool2.submit(new MyRunnable());
pool2.submit(new MyRunnable());
pool2.submit(new MyRunnable());
pool2.submit(new MyRunnable());
//销毁线程
pool.shutdown();
}
}
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "----");
}
}
运行结果:
pool-2-thread-2----
pool-2-thread-1----
pool-2-thread-2----
pool-2-thread-3----
发现只有3个线程,当线程执行任务时,因为最多只有3个线程,所以会进入等待状态,当线程执行完时,才会去执行下一个.
二、自定义创建线程池
使用ThreadPoolExecutor类创建自定义线程的7个参数解释:
核心元素一:正式员工数量 -------------------------------> 核心线程数量(不能小于0)
核心元素二:餐厅最大员工数量 -------------------------------> 线程池中最大线程数量(最大数量>=核心线程数量)
核心元素三:临时员工空闲多长时间被辞退 (值) -------------------------------> 空闲时间(值)(不能小于0)
核心元素四:临时员工空闲多长时间被辞退 (单位) -------------------------------> 空闲时间(单位) (使用TimeUtil类中指定)
核心元素五:排队的客户 -------------------------------> 阻塞队列(不能为null)
核心元素六:从哪里招人 -------------------------------> 创建线程的方式(不能为null)
核心元素七:当排队人数过多时,超出的顾客请下次在来(拒绝服务) -------------------------------> 要执行的任务过多时的解决方案(不能为null)
代码实现:
/**
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
核心线程数量,
最大线程数量,
空闲线程最大存活时间,
空闲线程最大存活时间单位,
任务队列,
创建线程工厂,
任务的拒绝策略
);
**/
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
3, //核心线程数量
6, //最大线程数量
60, //空闲线程存活时间
TimeUnit.SECONDS, //空闲线程存活时间单位
new ArrayBlockingQueue<>(3), //任务队列
Executors.defaultThreadFactory(), //创建线程工厂
new ThreadPoolExecutor.AbortPolicy() //任务的拒绝策略
);
}
三、最大并行数设置多大合适?
最大并行数和自己的电脑有关系,可以通过代码来计算出java可以使用的并行数有几个。比如4核8线程。
public class GetAvaiableProcessors {
//获取java虚拟机可以用的处理器的数量
public static void main(String[] args) {
int count = Runtime.getRuntime().availableProcessors();
System.out.println(count);
}
}
运行结果:8
四、线程池设置多大合适呢?
设置多大合适需要分两种情况来考虑:
CPU密集型(当该项目进行运算比较多,读取本地文件比较少,使用这种):最大并行数 + 1
I/O密集型(当项目读取本地文件比较多,或者操作数据库时,使用这种):
最大并行数 * 期望CPU利用率 * (总时间(CPU计算时间+等待时间)/CPU计算时间) CPU计算时间+等待时间可以通过工具 thread dump工具测试出来。
例子:从本地文件中读取数据,读取两个数据,并进行相加。电脑的CPU是4核8线程的。
操作1:读取两个数据(1秒钟)
操作2:进行相加(1秒中)
答: 8 * 100% * ( (2/1) ) = 16秒