一篇搞懂Java多线程(全网最细)

目录

前言 

一、线程的基本介绍

1. 中央处理器(CPU)

2. 程序

3. 进程(Process)

4. 线程

5. 进程与线程的关系

6. 多线程开发

6.1 并发 

6.2 并行 

6.3 串行 

7. 多线程的优点 

7.1 何时需要多线程

二、线程的创建和启动

1. 多线程实现的原理

2. 线程的创建及注意事项

3. Thread类的常用方法 

4. 实现java.lang.Runnable接口

5. 采用匿名内部类创建

6. Thread和Runnable的区别

三、线程的生命周期

四、线程调度

1. 调整线程优先级 :

2. 线程睡眠

3. 线程让步

4. sleep()和yield()的区别

5. 线程加入

五、线程同步

1. 概念 

2. 同步互斥访问(synchronized)

3. Synchronized关键字

4. synchronized同步代码块实现

5. 什么是类锁和对象锁 

5.1 概念

5.2 释放锁时机

6. 线程等待与线程唤醒

六、线程安全问题(生产者和消费者模式)

七、多线程小案例

1. 老虎机案例

2. FX版无限聊天


前言 

        DOS系统有一个非常明显的特点,只要一中病毒之后系统就会立刻死记,因为传统的DOS系统是采用单进程的处理方式,所以只能有一个程序独自运行,其他程序无法运行。在Windows系统中,即使出现了病毒,系统照样可以正常使用,因为在Windows中采用的是多进程的处理方式,那么在同一个时间段上会有多个程序同时运行。线程实际上就是在进程的基础上的进一步划分,如果一个进程都没有,则线程肯定会消失;而如果线程消失了,进程未必会消失。而且,所有的线程都是在进程的基础上并发运行的(同时运行)。

一、线程的基本介绍

 多线程:相当于老板请员工来帮我做事。

1. 中央处理器(CPU)

        CPU的中文名称是中央处理器,是进行逻辑运算用的,主要由运算器、控制器、寄存器三部分组成,从字面意思看运算就是起着运算的作用,控制器就是负责发出CPU每条指令所需要的信息,寄存器就是保存运算或指令的一些临时文件,这样可以保证更高的速度,也就是我们的线程运行在CPU之上。

  • 单核 :单核的CPU是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。同时间段内有几个多线程需要CPU去运行时,CPU也只能交替去执行多个线程中的一个线程,但是由于其执行速度特别快,因此感觉不出来。
  • 多核 :多核的CPU才能更好的发挥多线程的效率。

2. 程序

开发写的代码称之为程序。程序就是一堆代码·,一组数据和指令集,是一个静态的概念。

3. 进程(Process)

  • CPU从硬盘中读取一段程序到内存中,该执行程序的实例就叫做进程
  • 一个程序如果被CPU多次被读取到内存中,变成多个独立的进程。将程序运行起来,我们称之为进程。进程是执行程序的一次执行过程,他是动态的概念。进程存在生命周期,也就是说程序随着程序的终止而销毁。进程之间是通过TCP/IP端口实现交互的。

简单的理解:一个应用程序(一个进程就是一个软件),一个程序至少包含一个进程,一个进程中至少包含一条线程;

4. 线程

  • CPU处理数据时,某一个时刻点任何CPU都只能处理一个程序。
  • 线程是进程中的实际运作的单位,是进程的一条流水线,是程序的实际执行者,是最小的执行单位。通常在一个进程中可以包含若干个线程。线程是CPU调度和执行的最小单位
  • 一个进程可以有多个线程,如视频可以同时看图像、听声音、看弹幕,等等;
  • 很多线程都是模拟出来的,真正的多线程是指有多个CPU,即多核,如服务器,如果是模拟出来的多线程,即一个CPU的情况下,在同一个时间点,CPU只能执行一个代码,因为切换很快,所以就有同时执行的错觉。

对于java程序来说,当在DOS命令窗口中输入:

  1. java HelloWorld 回车之后。会先启动JVM,而JVM就是一个进程。
  2. JVM再启动一个主线程调用main方法(main方法就是主线程)。
  3. 同时再启动一个垃圾回收线程负责看护,回收垃圾。

注意 :使用多线程机制之后,main方法结束只是主线程结束了,其他线程还没结束,但没有主线程也不能运行。最起码,现在的java程序中至少有两个线程并发,一个是 垃圾回收线程,一个是 执行main方法的主线程。

5. 进程与线程的关系

  • 进程 :可以看做是现实生活当中的公司。
  • 线程 :可以看做是公司当中的某个员工。

注意 :进程A和进程B的内存独立不共享。多线程开发

6. 多线程开发

6.1 并发 

        同一对象被多个线程同时操作;(这是一种假并行。即一个CPU的情况下,在同一个时间点,CPU只能执行一个代码,因为切换的很快,所以就有同时执行的错觉)。

        特点 :同时安排若干个任务,这些任务可以彼此穿插着进行;有些任务可能是并行的,比如买菜、发邮件和去洗脚的某些路是重叠的,这时你的确同时在做三件事;但进菜市场和发邮件和接娃三者是互斥的,每个时刻只能完成其中一件。换句话说,并发允许两个任务彼此干扰。

6.2 并行 

你(线程)做你的事,我(线程)做我的事,咱们互不干扰并同时进行。

6.3 串行 

一个程序处理当前进程,按顺序接着处理下一个进程,一个接着一个进行

特点 : 前一个任务没搞点,下一个任务就只能等着。

7. 多线程的优点 

  1. 提高应用程序的响应。堆图像化界面更有意义,可以增强用户体验。
  2. 提高计算机系CPU的利用率
  3. 改善程序结构,将即长又复杂的进程分为多个线程,独立运行,利于理解和修改。

7.1 何时需要多线程

  1. 程序需要同时执行两个或多个任务
  2. 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
  3. 需要一些后台运行的程序时。

二、线程的创建和启动

1. 多线程实现的原理

        Java语言的JVM允许程序运行多个线程,多线程可以通过java中的java.lang.Thread类来体现。

Thread特性:

  • 每个线程多事通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体。
  • 通过Thread方法的start()方法来启动这个线程,而非直接调用run()。

2. 线程的创建及注意事项

第一种方式:继承Thread类

  1. 创建一个继承Thread类的子类
  2. 重写Thread类的run()方法
  3. 创建Thread类的子类的对象
  4. 通过此对象调用start()来启动一个线程
package com.thread;

/**
 * 我的线程
 * 
 * @author 云村小威
 *
 * @2023年7月21日 下午12:02:44
 */
public class MyThread extends Thread {

	@Override
	public void run() {
		// 编写程序,这段程序运行在分支线程中

	}

}
package com.thread;

/**
 * 
 * @author 云村小威
 *
 * @2023年7月21日 下午12:37:53
 */
public class Text01 {

	public static void main(String[] args) {
		MyThread t = new MyThread();
		// 启动线程
		t.start();
		// run方法不会启动线程,不会分配新的分支栈(这种方式就是单线程)
		t.run();
	}

}

注意 :

  • t.run()不会启动线程,只是普通的调用方法而已。不会分配新的分支栈。(这种方法就是单栈程)
  • t.start()方法的作用是 :启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。

        这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。

package com.thread;


/**
 * 我的线程
 * 
 * @author 云村小威
 *
 * @2023年7月21日 下午12:02:44
 */
public class MyThread extends Thread {
	private String name;
	
	public MyThread(String name) {
		this.name=name;
	}
	
	@Override
	public void run() {
		// 编写程序,这段程序运行在分支线程中
		for (int i = 0; i < 5; i++) {
			System.out.println(name+"运行:"+i);
			//休眠
			try {
				sleep((int)Math.random()*10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

}
package com.thread;

/**
 * 
 * @author 云村小威
 *
 * @2023年7月21日 下午12:37:53
 */
public class Text01 {

	public static void main(String[] args) {
		MyThread t1 = new MyThread("A");
		MyThread t2 = new MyThread("B");
		// 启动线程
		t1.start();
		t2.start();
	}

}

运行:

注意 :

  1. start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。
  2. 从程序运行的结果可以发现,多线程程序是乱序执行。因此,只有乱序执行的代码才有必要设计为多线程。
  3. Thread.sleep()方法调用目的是不让当前线程独自霸占该进程所获取的CPU资源,以留出一定时间给其他线程执行的机会。
  4. 实际上所有的多线程代码执行顺序都是不确定的,每次执行的结果都是随机的。
  5. 但是start方法重复调用的话,会出现java.lang.IllegalThreadStateException异常。

3. Thread类的常用方法 

Start()启动当前线程的run()方法
run()通常需要重写Thread类的此方法,将创建的线程要执行的操作声明在此方法中
currentThread()静态方法,返回当前代码执行的线程
getName()获取当前线程的名字
setName()设置当前线程的名字
yield()暂停当前正在执行的线程对象,并执行其他线程(释放当前CPU的执行权)
join()等待该线程终止的时间(millis毫秒),再可执行其他线程
stop()已过时,当执行此方法时,强制结束当前线程
sleep(long millitime)让线程睡眠指定的毫秒数,在指定时间内,线程是阻塞状态
isAlive()判断当前线程是否处于活动状态
setPriority()更改线程的优先级
setDaemon()将该线程标记为守护线程或用户线程

4. 实现java.lang.Runnable接口

第二种方式:实现java.lang.Runnable接口

步骤同继承Thread类;

package com.thread;


/**
 * 通过实现Runnable接口创建线程
 * 
 * @author 云村小威
 *
 * @2023年7月21日 下午12:02:44
 */
public class MyThread2 implements Runnable {
	private String name;
	
	public MyThread2(String name) {
		this.name=name;
	}
	
	@Override
	public void run() {
		// 编写程序,这段程序运行在分支线程中
		for (int i = 0; i < 5; i++) {
			System.out.println(name+"运行:"+i);
			//休眠
			try {
				Thread.sleep((int)Math.random()*10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

}
package com.thread;

/**
 * 
 * @author 云村小威
 *
 * @2023年7月21日 下午12:37:53
 */
public class Text02 {

	public static void main(String[] args) {
		new Thread(new MyThread2("A")).start();
		new Thread(new MyThread2("B")).start();
	}

}

运行:

5. 采用匿名内部类创建

第三种方式:采用匿名内部类创建线程

package com.thread;

/**
 * 匿名内部类创建线程
 * @author 云村小威
 *
 * @2023年7月21日 下午12:37:53
 */
public class Text03 {
	public static void main(String[] args) {
		Thread t = new Thread(new Runnable() {
			
			@Override
			public void run() {
				for (int i = 0; i < 5; i++) {
					System.out.println("t线程"+i);
				}
			}
		});
		//启动线程
		t.start();
		
		for (int i = 0; i < 5; i++) {
			System.out.println("main线程"+i);
		}
	}

}

运行:

说明 :

  1. Thread2类通过Runnable接口,使得该类有了多线程类的特征。Run()方法是多线程程序的一个约定。所有的多线程代码都在 run()方法里面。Thread类实际上也是实现了Runnable接口的类。
  2. 在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target)构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
  3. 实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是扩展Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。

6. Thread和Runnable的区别

        如果一个类继承Thread,则不适合资源共享。但是如果实现了Runnable接口的话,则很容易的实现资源贡共享。

总结 : 实现Runnable接口比继承Thread类具有的有优势

  • 适合多个相同的程序代码的线程去处理统一资源
  • 可以避免java中的单继承的限制
  • 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
  • 线程池只能放入实现Runnable或callable类线程,不能直接放入继承Therad的类

        main方法其也是一个线程。在java中所有的线程都是同时启动的,至于什么时候、那哪个先执行,完全看谁先得到CPU的资源。

        在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM实习就是在操作系统中启动了一个进程。

三、线程的生命周期

1、新建状态(new):新建一个线程对象

2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行的线程池中,变得可运行,等待获取CPU的使用权

3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码

4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态

阻塞的情况分为三种 :

  • 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait方法会释放持有的锁)
  • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
  • 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状。(sleep是不会释放持有的锁)

死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

四、线程调度

1. 调整线程优先级 :

Java线程有优先级,优先级高的线程会获得较多的运行机会。

Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:

static int MAX_PRIORITY
        线程可以具有的最高优先级,取值为10。
static int MIN_PRIORITY
        线程可以具有最低优先级,取值为1。
static int NORM_PRIORITY
        分配给线程的默认优先级,取值为5。

Thread类的setPriority和getPriority()方法分别用来设置和获取线程的优先级。

每个线程都有默认的优先级,主线程的默认优先级为Thread.NORM_PRIORITY。

用法:

Thread t1 = new Thread("t1");
Thread t2 = new Thread("t2");
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORIRY);

注意: 

        并不是设置了优先级就一定先执行,根据电脑性能等一些有影响还有一点随机性的,只是优先概率大一点。

2. 线程睡眠

Thread.sleep(long millis(毫秒)):让线程进入休眠(阻塞状态)放弃占有CPU时间片,让给其他线程使用。

sleep方法可做到间隔特定的时间,去执行一段特定的代码,每隔多久就执行一次。

package com.thread;

/**
 * 线程睡眠
 * @author 云村小威
 *
 * @2023年7月21日 下午12:37:53
 */
public class Text04 {
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName()+"-->"+i);
			//睡眠0.8秒
			try {
				Thread.sleep(800);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
	}
}

运行:

3. 线程让步

Thread.yield()方法:暂停当前正在执行的线程对象,把执行机会让給相同或者更高优先级的线程。

package com.thread;

/**
 * 线程让步
 * @author 云村小威
 *
 * @2023年7月21日 下午12:37:53
 */
public class Text05 {
	public static void main(String[] args) {
		Thread t = new Thread(new Runnable() {
		
			@Override
			public void run() {
				for (int i = 0; i < 15; i++) {
					Thread.yield();
					System.out.println(Thread.currentThread().getName()+"-->"+i);
				}
				
			}
		});
		t.setName("t");
		t.start();
		
		//主线程
		for (int i = 0; i < 15; i++) {
			System.out.println(Thread.currentThread().getName()+"-->"+i);
		}
	}
}

运行结果:让主线程快走完了才运行 

4. sleep()和yield()的区别

  1. sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
  2. sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。
  3. 实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程
  4. 另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。

5. 线程加入

Thread.join() 方法:用于等待其他线程终止
        如果线程A中调用了线程B的join方法,那么线程A阻塞,直到线程B执行完后,线程A从阻塞状态转为就绪状态,等待获取CPU的使用权。join方法要在start方法调用后调用才有效,线程必须启动,再加入。

package com.thread;

/**
 * 线程加入
 * 
 * @author 云村小威
 *
 * @2023年7月21日 下午12:37:53
 */
public class Text06 {
	public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread(new JoinThread("t1"));
		Thread t2 = new Thread(new JoinThread("t2"));
		t1.start();
		t1.join();
		t2.start();
	}
}

class JoinThread implements Runnable {
	private String name;

	public JoinThread(String name) {
		this.name = name;
	}

	@Override
	public void run() {
		for (int i = 1; i <= 3; i++) {
			System.out.println(name + "-->" + i);
		}
	}
}

运行结果:

 主线程一定会等子线程都结束了才结束!

五、线程同步

1. 概念 

        即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多种。

1、为什么要创建多线程?

在一般情况下,创建一个线程是不能提高程序执行效率的,所以要创建多个线程。

2、为什么要线程同步?

多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使线程同步。

3、线程同步是什么意思?

同步就是协同步调,按预定的先后次序进行运行。如:你做完,我再做。

错误理解 “同”字从字面上容易理解为一起动作,其实不是。“同”字应是指协同、协助、互相配合。

正确理解 :所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,同时其他线程也不能调用这个方法。

线程同步的作用 :

  • 线程有可能和其他线程共享一些资源,比如:内存、文件、数据库等
  • 当多个线程同时读写同一份共享资源的时候,可能会引起冲突。这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到,不能一窝蜂挤上去抢作一团。
  • 线程同步的真是意思其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。

2. 同步互斥访问(synchronized)

        基本上所有解决线程安全问题的方式都是采用“序列化临界资源访问”的方式,即在同一时刻只有一个线程操作临界资源,操作完了才能让其他线程进行操作,也称同步互斥访问。

在Java中一般采用synchronized和Lock来实现同步互斥访问。

3. Synchronized关键字

首先了解一下互斥锁 :就是能达到互斥访问目的的锁

  1. 如果对一个变量加上互斥锁,那么在同一时刻,该变量只能有一个线程能访问,即当一个线程访问临界资源时,其他线程只能等待。
  2. 在Java中,每一个对象都有一个锁标记(monitor),也被称为监视器,当多个线程访问对象时,只有获取了对象的锁才能访问。
  3. 在我们编写代码的时候,可以使用synchronized修饰对象的方法或者代码块,当某个线程访问这个对象synchronized方法或者代码块时,就获取到了这个对象的锁,这个时候其他对象是不能访问的,只能等待获取到锁的这个线程执行完该方法或者代码块之后,才能执行对象的方法。

4. synchronized同步代码块实现

  1. 第一种:synchronized代码块方式(灵活)
  2. 第二种:在实例方法上使用synchronized
  3. 第三种 : 在静态方法上使用synchronized
package com.thread;

/**
 * synchronized同步代码块实现
 * 
 * @author 云村小威
 *
 * @2023年7月21日 下午12:37:53
 */
public class Text07 {
	// 第一种
	public void myThread1(Thread thread) {
		// this代表当前对象
		synchronized (this) {
			for (int i = 0; i < 5; i++) {
				System.out.println(thread.getName() + ":" + i);
			}
		}
	}

	// 第二种
	public synchronized void myThread2(Thread thread) {
		// this代表当前对象
		for (int i = 0; i < 5; i++) {
			System.out.println(thread.getName() + ":" + i);
		}
	}

	// 第三种
	public static synchronized void myThread3(Thread thread) {
		// this代表当前对象
		for (int i = 0; i < 5; i++) {
			System.out.println(thread.getName() + ":" + i);
		}
	}

}

5. 什么是类锁和对象锁 

5.1 概念

对象锁:在java中每个对象都有一个唯一的锁,对象锁用于对象实例方法或者一个对象实例上面的   —— 一个对象一把锁,100个对象100把锁。

类锁:是用于一个类静态方法或者class对象的,一个类的实例对象可以有多个,但是只有一个class对象 —— 100个对象,也只是1把锁。

注意上述第三种方式synchronized同步代码块实现在静态方法添加synchronized这把锁属于类了,所有这个类的对象都共享这把锁,这就叫类锁。

5.2 释放锁时机

        如果一个方法或者代码块被synchronized关键字修饰,当线程获取到该方法或代码块的锁,其他线程是不能继续访问该方法或代码块的。而其他线程想访问该方法或代码块,就必须要等待获取到锁的线程释放这个锁,而在这里释放锁只有两种情况 :

  1. 线程执行完代码块,自动释放锁;
  2. 程序报错,JVM让线程自动释放锁;

举例:这里我调用了上述的第一种和第二种实现同步方式

public static void main(String[] args) {
		new Text07().myThread1(new Thread());
		new Text07().myThread2(new Thread());
	}

6. 线程等待与线程唤醒

Object类作用
Void wait()让活动在当前对象的线程无限等待(释放之前占有的锁)
Void notify()唤醒当前对象正在等待的线程(只提示唤醒,不会释放锁)
Void notifyAll()唤醒在此对象监视器上等待的所有线程。

方法详解 :

        wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方法是object类自带的。wait和notify方法不是通过线程对象调用的;

六、线程安全问题(生产者和消费者模式)

1、什么是 “ 生产者和消费者模式 ” ?

  1. 生产线负责生产,消费线程负责消费。
  2. 生产线程和消费线程要达到均衡
  3. 这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法。

2、模拟一个业务需求

  • 仓库我们采用List集合
  • List集合中假设只能存储1个元素
  • 1个元素就表示仓库满了
  • 如果List集合中元素个数是0,就表示仓库空了
  • 保证List集合永远都是最多存储1个元素
  • 必须做到这种效果:生产1个消费1个。

模拟生产者与消费者案例: 

        首先我创建了一个产品类来定义我们需要生产的对象,接着创建一个工厂类写了生产和出售两个方法并实现同步锁,通过put方法将生产的鸡脚保存到容器里如果容器小于10就消费者等待(wait() 方法),如果大于就唤醒( notifyAll()方法 )所有等待的消费者。然后再定义生产者类它需要不停做事,它什么时候休息根据工厂类生产的鸡脚是否大于20,否则不可休息得一直做事。最后是消费者调用工厂类的sale方法只要保存鸡脚的容器大于10就开始购买对容器进行减少,如果没有了就让消费者进行等待。

package com.thread;

import java.util.Vector;

/**
 * 生产者与消费者案例
 * 
 * @author 云村小威
 *
 * @2023年7月22日 上午10:51:47
 */
public class Text08 {

	public static void main(String[] args) {
		// 店铺运营
		// 创建一个工厂
		Factory f = new Factory();

		// 招三个成产员工
		Producer p1 = new Producer(f);
		Producer p2 = new Producer(f);
		Producer p3 = new Producer(f);

		new Thread(p1, "工作人员A").start();
		new Thread(p2, "工作人员B").start();
		new Thread(p3, "工作人员C").start();

		// 引入三个消费者
		Sale s1 = new Sale(f);
		Sale s2 = new Sale(f);
		Sale s3 = new Sale(f);

		new Thread(s1, "消费者A").start();
		new Thread(s2, "消费者B").start();
		new Thread(s3, "消费者C").start();
	}

}

/**
 * 消费者
 */
class Sale implements Runnable {
	Factory f = null;

	public Sale(Factory f) {
		this.f = f;
	}

	public void run() {
		while (true) {
			// 让消费者延迟购买的时间
			try {
				Thread.sleep(1500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			f.sale();
		}
	};

}

/**
 * 生产类
 */
class Producer implements Runnable {
	Factory f = null;

	public Producer(Factory f) {
		this.f = f;
	}

	@Override
	public void run() {
		// 需要不停的做事
		while (true) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			// 调用生产数据的方法
			f.put(new Product(1, "鸡脚"));
		}
	}

}

/**
 * 工厂类
 */
class Factory {
	// 准备储存产品的集合容器,多线程优先使用Vector集合,该集合类中的方法大部分都是带有同步的概念机制。
	Vector<Product> vc = new Vector<Product>();

	// 专门定义一个生产数据方法
	public synchronized void put(Product product) {
		// 判断产品于目标数量是否相同
		if (vc.size() >= 20) {
			// 产品数量满足让工作人员处于等待状态
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		} else {
			// 让等待的工作人员生产产品
			this.notifyAll();
			System.out.println("正在生产中...当前货架的鸡脚数目是:" + vc.size());
			vc.add(product);
			System.out.println("货架鸡脚总数目是:" + vc.size());
		}
	}

	// 专门定义一个负责消费数据方法
	public synchronized void sale() {
		// 只要容器存在货物就可以进行购买
		if (vc.size() > 10) {
			this.notifyAll(); // 唤醒所有等待的消费者
			System.out.println("当前货架上的鸡脚数目是:" + vc.size() + ",正在出售鸡脚。");
			vc.remove(0);
			System.out.println("\t目前货架上的鸡脚数目是:" + vc.size());
		} else {
			// 没有鸡脚,让消费者处于等待状态
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

/**
 * 产品类
 */
class Product {
	private int pid;
	private String name;

	public Product() {
		// TODO Auto-generated constructor stub
	}

	public int getPid() {
		return pid;
	}

	public void setPid(int pid) {
		this.pid = pid;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Product(int pid, String name) {
		super();
		this.pid = pid;
		this.name = name;
	}

}

七、多线程小案例

1. 老虎机案例

package com.thread;

import java.util.Random;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;

/**
 * 老虎机游戏程序
 * @author 云村小威
 *
 * @2023年7月22日 上午11:27:56
 */
public class Game extends Application {

	Random rd = new Random(); // 随机数对象
	boolean flag = true; // 控制开关默认打开
	int timer = 50; // 控制事件变量
	// 通过数组保存没有规律的多张图片
	String[] images = { "lib\\7.png", "lib\\橙子.png", "lib\\柠檬.png", "lib\\苹果.png", "lib\\西瓜.png", "lib\\香蕉.png", };

	// 总布局
	Pane pane = new Pane();
	private Label a1 = new Label("", new ImageView(new Image("lib\\苹果.png", 150, 150, false, false)));
	private Label a2 = new Label("", new ImageView(new Image("lib\\西瓜.png", 150, 150, false, false)));
	private Label a3 = new Label("", new ImageView(new Image("lib\\柠檬.png", 150, 150, false, false)));

	private Button btn1 = new Button("开始游戏");
	private Button btn2 = new Button("点击停止");

	{
		a1.setStyle("-fx-border-color:#ccc;");
		a1.setLayoutX(60);
		a1.setLayoutY(40);
		pane.getChildren().add(a1);

		a2.setStyle("-fx-border-color:#ccc;");
		a2.setLayoutX(225);
		a2.setLayoutY(40);
		pane.getChildren().add(a2);

		a3.setStyle("-fx-border-color:#ccc;");
		a3.setLayoutX(390);
		a3.setLayoutY(40);
		pane.getChildren().add(a3);

		btn1.setLayoutX(180);
		btn1.setLayoutY(220);
		btn1.setPrefSize(100, 40);
		pane.getChildren().add(btn1);

		btn2.setLayoutX(340);
		btn2.setLayoutY(220);
		btn2.setPrefSize(100, 40);
		pane.getChildren().add(btn2);
	}

	@Override
	public void start(Stage stage) throws Exception {
		// 创建一个场景
		Scene scene = new Scene(pane, 600, 300);
		stage.setScene(scene);
		stage.show();

		/**
		 * 开始游戏点击事件
		 */
		btn1.setOnAction(e -> {
			flag = true;
			new Thread() {
				public void run() {
					while (flag) {
						try {
							Thread.sleep(timer);
						} catch (InterruptedException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}

						Platform.runLater(new Runnable() {
							@Override
							public void run() {
								int sj1 = rd.nextInt(6);
								a1.setGraphic(new ImageView(new Image(images[sj1])));
								int sj2 = rd.nextInt(6);
								a2.setGraphic(new ImageView(new Image(images[sj2])));
								int sj3 = rd.nextInt(6);
								a3.setGraphic(new ImageView(new Image(images[sj3])));
							}
						});

						timer += 6;
						if (timer >= 300) {
							timer = 50;
							break;
						}
						
					}
				}
			}.start();

		});

		/**
		 * 停止点击事件
		 */
		btn2.setOnAction(e -> {
			flag = false;
		});

	}

	public static void main(String[] args) {
		launch();
	}

}

运行效果:

2. FX版无限聊天

 1. 服务器

package com.net;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

/**
 * 服务器
 * 
 * @author 云村小威
 *
 * @2023年7月19日 下午9:40:54
 */
public class ServerFx extends Application {
	/**
	 * 总布局 BorderPane
	 */
	BorderPane bor = new BorderPane();

	// 上
	HBox hb = new HBox();
	TextField t = new TextField();
	Button btn = new Button("启动服务");
	{
		hb.getChildren().addAll(t, btn);
		hb.setAlignment(Pos.CENTER);
		hb.setPadding(new Insets(20));
		hb.setSpacing(20);
		btn.setPrefSize(100, 40);
		bor.setTop(hb);
	}

	// 中
	TextArea ta = new TextArea();
	{
		ta.setEditable(false); // 文本域不可修改
		ta.setStyle("-fx-border-color: #ccc;-fx-border-width: 10 10 10 10;");
		bor.setCenter(ta);
	}

	// 下
	HBox hb2 = new HBox();
	TextField text = new TextField();
	Button btn1 = new Button("发送");
	{
		hb2.getChildren().addAll(text, btn1);
		hb2.setAlignment(Pos.CENTER);
		hb2.setPadding(new Insets(20));
		hb2.setSpacing(20);
		bor.setBottom(hb2);
	}

	/**
	 * 将通信对象声明在start方法外面
	 */
	Socket s = null;
	ServerSocket ss = null;

	/**
	 * 定义所需的流对象 扩大权限方便关闭流
	 */
	InputStream inputStream = null;
	BufferedReader br = null;
	OutputStream outputStream = null;
	BufferedWriter bw = null;

	@Override
	public void start(Stage stage) throws Exception {
		stage.setTitle("服务器");
		Scene scene = new Scene(bor, 800, 700);
		stage.setScene(scene);
		stage.show();

		/**
		 * 启动服务点击事件
		 */
		btn.setOnAction(e -> {
			// 创建服务器
			try {
				ss = new ServerSocket(Integer.parseInt(t.getText()));
			} catch (IOException e1) {
				e1.printStackTrace();
			}
			ta.appendText("服务已启动,等待客户端连接...\n");
			// 开启一个线程 自动接受客户端发送的信息
			new Thread() {
				public void run() {
					// 多线程控制接受客户端信息
					try {
						s = ss.accept();
						ta.appendText("客户端已连接...\n");
					} catch (IOException e) {
						e.printStackTrace();
					}
					while (true) {
						// 读取 获取输入流
						try {
							inputStream = s.getInputStream();
							br = new BufferedReader(new InputStreamReader(inputStream));
							String count = br.readLine();
							ta.appendText("\t客户端:" + count + "\n");
						} catch (IOException e) {
							e.printStackTrace();
						}
					}
				}
			}.start();
		});

		/**
		 * 发送信息点击事件
		 */
		btn1.setOnAction(e -> {
			try {
				outputStream = s.getOutputStream();
				bw = new BufferedWriter(new OutputStreamWriter(outputStream));
				// 获取文本框内容
				String count = text.getText();
				bw.write(count);
				bw.newLine();
				bw.flush();
				ta.appendText("服务器:" + count + "\n");
				text.setText("");
			} catch (IOException e1) {
				e1.printStackTrace();
			}
		});
	}

	public static void main(String[] args) {
		launch();
	}
}

2. 客户端

package com.net;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;

/**
 * 客户端
 * 
 * @author 云村小威
 *
 * @2023年7月19日 下午9:41:46
 */
public class ClientFx extends Application {
	/**
	 * 总布局 BorderPane
	 */
	BorderPane bor = new BorderPane();

	// 上
	HBox hb = new HBox();
	Text ip = new Text("IP");
	TextField t = new TextField("127.0.0.1");
	Text tcp = new Text("端口号");
	TextField t2 = new TextField();
	Button btn = new Button("连接");
	{
		t.setDisable(true);// 设置ip地址不可编辑
		hb.getChildren().addAll(ip, t, tcp, t2, btn);
		hb.setAlignment(Pos.CENTER);
		hb.setPadding(new Insets(20));
		hb.setSpacing(20);
		btn.setPrefSize(100, 40);
		bor.setTop(hb);
	}

	// 中
	TextArea ta = new TextArea();
	{
		ta.setEditable(false); // 文本域不可修改
		ta.setStyle("-fx-border-color: #ccc;-fx-border-width: 10 10 10 10;");
		bor.setCenter(ta);
	}

	// 下
	HBox hb2 = new HBox();
	TextField text = new TextField();
	Button btn1 = new Button("发送");
	{
		hb2.getChildren().addAll(text, btn1);
		hb2.setAlignment(Pos.CENTER);
		hb2.setPadding(new Insets(20));
		hb2.setSpacing(20);
		bor.setBottom(hb2);
	}

	/**
	 * 定义所需的流对象 扩大权限方便关闭流
	 */
	InputStream inputStream = null;
	BufferedReader br = null;
	OutputStream outputStream = null;
	BufferedWriter bw = null;

	// 定义客户端对象
	Socket s = new Socket();

	@Override
	public void start(Stage stage) throws Exception {

		stage.setTitle("客户端");
		stage.setResizable(false); // 设置窗口不可动
		Scene scene = new Scene(bor, 800, 700);
		stage.setScene(scene);
		stage.show();

		/**
		 * 连接点击事件
		 */
		btn.setOnAction(e -> {
			String ip = t.getText(); // 获取ip地址
			int port = Integer.parseInt(t2.getText());
			try {
				s = new Socket(ip, port);
				ta.appendText("客户端成功连接服务器...\n");
			} catch (IOException e1) {
				e1.printStackTrace();
			}

			// 开启一个线程 (让它自动接受数据)
			new Thread() {
				public void run() {
					while (true) {
						try {
							inputStream = s.getInputStream();
							br = new BufferedReader(new InputStreamReader(inputStream));
							String count = br.readLine();
							ta.appendText("\t服务器:" + count + "\n");
						} catch (IOException e) {
							e.printStackTrace();
						}
					}
				}
			}.start();
		});

		/**
		 * 发送信息点击事件
		 */
		btn1.setOnAction(e -> {
			try {
				outputStream = s.getOutputStream();
				bw = new BufferedWriter(new OutputStreamWriter(outputStream));
				// 获取文本框内容
				String count = text.getText();
				bw.write(count);
				bw.newLine();
				bw.flush();
				ta.appendText("客户端:" + count + "\n");
				text.setText("");
			} catch (IOException e1) {
				e1.printStackTrace();
			}
		});

	}

	public static void main(String[] args) {
		launch();
	}

}

 运行效果:


  • 47
    点赞
  • 172
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 19
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

云村小威

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

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

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

打赏作者

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

抵扣说明:

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

余额充值