Java基础知识-第13章-Java多线程编程基础
Java多线程编程(结合计算机操作系统)基础
有效利用多线程的关键是理解程序是并发执行而不是串行执行的。例如:程序中有两个子系统需要并发执行,这时候就需要利用多线程编程。通过对多线程的使用,可以编写出非常高效的程序。不过请注意,如果你创建太多的线程,程序执行的效率实际上是降低了,而不是提升了。请记住,线程上下文的切换开销也很重要,如果你创建了太多的线程,CPU 花费在上下文的切换的时间将多于执行程序的时间!
此文只对Java中多线程编程做一个基础的介绍,使得对多线程并发有一个最基础的了解,详情见专栏:Java并发编程
1、基本概念:程序、进程、线程
1.1、概念
<详见计算机基础知识:操作系统专栏>
程序(program) 是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
进程(process) 是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。——生命周期
- 如:运行中的QQ,运行中的MP3播放器
- 程序是静态的,进程是动态的
- 进程作为资源分配的最小单位,系统在运行时会为每个进程分配不同的内存区域
线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。
- 若一个进程同一时间并行执行多个线程,就是支持多线程的
- 线程作为调度和执行的最小单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
- 一个进程中的多个线程共享相同的内存单元/内存地址空间,它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患
一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程(后台线程)都结束运行后才能结束。
1.2、单核CPU和多核CPU的理解
- 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员。如果有某个人不想交钱,那么收费人员可以把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费)。但是因为CPU时间单元特别短,因此感觉不出来。 (采用时间片轮转策略)
- 如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)。
- 一个Java应用程序
java.exe
,当运行的时候可以看做是一个进程,但是其内部至少有三个线程:main()主线程(可以看做一个进程),gc() 垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
1.3、并行与并发
并行: 多个CPU同时执行多个任务。比如:多个人同时做不同的事。多核也需要并发。
并发: 一个CPU在某个时间段内执行多个任务,从宏观上来说是并行的,其实是CPU采用时间片轮转策略执行每一个IO操作
2、多线程的创建方式和基本使用
2.1、概述
何时需要多线程
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写 操作、网络操作、搜索等。
- 需要一些后台运行的程序时
注意:
下边的程序不是多线程!除非s.method2()
代码下面还有语句,那就是多线程
public class Sample {
public void method1(String str) {
System. out . println(str);
}
public void method2(String str) {
method1(str);
}
public static void main(String[] args) {
Sample S = new Samp1e();
S.method2("hello!");
}
}
2.2、Thread 类概述
Java语言的JVM
允许程序运行多个线程,它通过java.lang.Thread
类来体现。
Thread
类的特性
- 每个线程都是通过某个特定
Thread
对象的run()
方法来完成操作的,经常把run()
方法的主体称为线程体 - 通过该
Thread
对象的start()
方法来启动这个线程,而非直接调用run()
Thread
类的构造器
Thread()
:创建新的Thread对象Thread(String threadname)
:创建线程并指定线程实例名Thread(Runnable target)
:指定创建线程的目标对象,它实现了Runnable
接口中的run
方法Thread(Runnable target, String name)
:创建新的Thread
对象
JDK1.5
之前创建新执行线程有两种方法:
- 继承
Thread
类的方式 - 实现
Runnable
接口的方式
2.3、方式一:继承于Thread类
步骤:
- 创建一个继承于Thread类的子类
- 重写
Thread
类的run()
,并将此线程要执行的操作声明在run()
中 - 创建
Thread
类的子类的对象 - 通过此对象调用
start()
:启动线程,自动调用run方法
例子1:遍历100以内的所有的偶数
public class exer {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();//创建Thread类的子类的对象
t1.start();//通过此对象调用start(),启动线程,自动调用run方法
}
}
class MyThread1 extends Thread{ //创建一个继承于Thread类的子类
@Override //重写Thread类的run()
public void run() { //并将此线程要执行的操作声明在run()中
for (int i = 0; i < 100; i++) {
if (i%2==0)
System.out.println(Thread.currentThread().getName() + ":" +i);//获取线程名
}
}
}
例子2:开启两个线程遍历100以内的所有的偶数
一个线程对象只能调用一次start()
方法启动,如果重复调用了,则将抛出以上的异常 IllegalThreadStateException
。
public class exer {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();//创建Thread类的子类的对象
//通过此对象调用start(),启动当前线程,并自动调用该线程对象所在的run方法,不能直接run
t1.start();
//再启动一个线程,遍历100以内的偶数。不可以还让已经start()的线程t1去执行。
//会报IllegalThreadStateException,我们需要重新创建一个线程的对象
MyThread1 t2 = new MyThread1();
t2.start();
//如下操作仍然是在main线程中执行的,
for (int i = 0; i < 100; i++) {
if (i%2==0)
System.out.println(Thread.currentThread().getName() + ":" +i + "***main()***");
}
}
}
class MyThread1 extends Thread{ //创建一个继承于Thread类的子类
@Override //重写Thread类的run()
public void run() { //并将此线程要执行的操作声明在run()中
for (int i = 0; i < 100; i++) {
if (i%2==0)
System.out.println(Thread.currentThread().getName() + ":" +i);//获取线程名
}
}
}
主线程和创建的新的线程会交替执行
例子3:创建两个线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数
public class exer {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
MyThread2 myThread2 = new MyThread2();
myThread1.start();
myThread2.start();
}
}
class MyThread1 extends Thread{ //用来遍历偶数
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2==0)
System.out.println(Thread.currentThread().getName() + ":" +i);//获取线程名
}
}
}
class MyThread2 extends Thread{ //用来遍历奇数
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2!=0)
System.out.println(Thread.currentThread().getName() + ":" +i);
}
}
}
关于开启线程start两种方法
法一:造俩对象,即
MyThread1 myThread1 = new MyThread1();
MyThread2 myThread2 = new MyThread2();
myThread1.start();
myThread2.start();
法二:使用匿名子类的方式,由于每个线程对象只执行一次,记住里面重写run
方法
public class ThreadDemo {
public static void main(String[] args) {
//方法2:创建Thread类的匿名子类的方式
new Thread(){
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2==0)
System.out.println(Thread.currentThread().getName() + ":" +i);
}
}
}.start();
new Thread(){
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2!=0)
System.out.println(Thread.currentThread().getName() + ":" +i);
}
}
}.start();
}
}
2.4、方式二:实现Runnable接口
- 创建一个实现了
Runnable
接口的类 - 实现类去实现
Runnable
中的抽象方法:run()
- 创建实现类的对象
- 将此对象作为参数传递到
Thread
类的构造器中,创建Thread
类的对象 - 通过
Thread
类的对象调用start()
package com.dreamcold.thread;
public class Demo05 {
public static void main(String[] args){
//1.可以直接创建实现类的对象
ThreadRunable mthread=new ThreadRunable();
//2.将此对象作为参数传递到对应的Thread构造器中,创建Thread类的对象
Thread thread=new Thread(mthread);
//3. 启动线程,调用当前线程的run()-->调用了Runnable类型的target的run()
thread.start();
}
}
//1.创建一个实现了Runnable接口的类
class ThreadRunable implements Runnable{
@Override
public void run() { //2. 实现类去实现Runnable中的抽象方法:run()
for (int i = 0; i < 100; i++) {
if(i%2==0){
System.out.println(i);
}
}
}
}
2.5、方式三:实现Callable接口
如何理解实现Callable
接口的方式创建多线程比实现Runnable
接口创建多线程方式强大?
- call()可以有返回值的,重写
Call
方法 - call()可以抛出异常,被外面的操作捕获,获取异常的信息
- 支持泛型的返回值
- 需要借助
FutureTask
类, 比如获取返回结果
//1.创建一个实现Callable的实现类
class NumThread implements Callable{
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if(i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
try {
//6.获取Callable中call方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
创建线程的三种方式的对比
- 采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。
- 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用
Thread.currentThread()
方法,直接使用this
即可获得当前线程。
2.6、方式四:线程池(重点)
背景: 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程, 对性能影响很大。
思路: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
线程池相关参数
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
线程池相关API
JDK 5.0 起提供了线程池相关API:ExecutorService
和Executors
ExecutorService:真正的线程池接口,常见子类 ThreadPoolExecutor
void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
<T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行Callable
void shutdown():关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool(): 创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n);创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n): 创建一个线程池, 它可安排在给定延迟后运行命令或者定期地执行。
实例演示:开启两个线程,遍历100以内的偶数
package com.dreamcold.thread;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class NumberThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2==0){
System.out.println(Thread.currentThread().getName() + ":" +i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
// 1.调用Executors的newFixedThreadPool(),返回指定线程数量的ExecutorService
ExecutorService pool = Executors.newFixedThreadPool(10);
// 2.将Runnable实现类或Callable实现类的对象作为形参
//传递给ExecutorService的execute()/submit方法中,开启线程,并执行相关的run()
//开启了两个线程
pool.execute(new NumberThread());//适合于实现Runnable对象
pool.execute(new NumberThread());
//service.submit();//适合实现了Callable对象
// 3.关闭连接池,结束线程的使用
pool.shutdown();
}
}
结果:
pool-1-thread-2:0
pool-1-thread-1:0
pool-1-thread-1:2
pool-1-thread-2:2
pool-1-thread-1:4
pool-1-thread-1:6
pool-1-thread-2:4
实例演示:设置连接池属性
//1.提供指定线程数量的战程池
ExecutorService service = Executors. newFixedThreadPool(10);|
//设置线程池的属性
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
System.out.println(service.getClass());
service1.setCorePoolsize(15);
service1.setKeepAliveTime();
3、Thread中的常用方法
3.1、方法概述
下表列出了 Thread
类的一些重要方法:这些方法是被 Thread
对象调用的实例方法
序号 | 方法描述 |
---|---|
1 | public void start() 启动当前线程;Java 虚拟机调用该线程对象的 run 方法。 |
2 | public void run() 线程被调度时执行的操作。通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中 |
3 | public final void setName(String name) 改变线程名称,使之与参数 name 相同。 Strinig getName( ) 返回线程名称 |
4 | public final void setPriority(int priority) 更改线程的优先级。 |
5 | public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。 |
6 | public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。 |
7 | public void interrupt() 使得指定线程中断阻塞状态,并将阻塞标志位置为true |
8 | public final boolean isAlive() 测试线程是否处于活动状态。 |
下面表格的方法是 Thread
类的静态方法。
序号 | 方法描述 |
---|---|
1 | public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。 |
2 | public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
3 | public static boolean holdsLock(Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。 |
4 | public static Thread currentThread() 返回当前线程。在Thread子类中就 是this,通常用于主线程和Runnable实现类 |
5 | public static void dumpStack() 将当前线程的堆栈跟踪打印至标准错误流。 |
重点说一下下面几个方法:
yield()
:释放当前cpu的执行权- 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
- 若队列中没有同优先级的线程,忽略此方法
join()
:在线程a
中调用线程b
的join()
,此时线程a
就进入阻塞状态,直到线程b
完全执行完以后,线程a
才结束阻塞状态。stop()
:已过时。当执行此方法时,强制结束当前线程。sleep(long millitime)
:让当前线程睡眠指定的millitime
毫秒。在指定的millitime
毫秒时间内,当前线程是阻塞状态。- 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,休眠时间到后重新排队。
- 抛出
InterruptedException
异常
isAlive()
:判断当前线程是否存活
3.2、设置线程名字
默认的线程名字
现在,有如下程序
package com.dreamcold.thread;
public class Demo04 {
public static void main(String[] args) {
HelloThread h1=new HelloThread();
h1.start();
}
class HelloThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
程序打印结果:这是默认的,第一个线程命名为0,依次类推
Thread-0:0
Thread-0:1
Thread-0:2
Thread-0:3
Thread-0:4
...............
为什么是Thread-0
- 原因:我们看这一行代码:
HelloThread h1 = new HelloThread( );
- 这句话调用的是子类的默认空参构造器,由于默认空参构造器含有
super()
,所以必定会调用父类Thread
的空参构造器,而父类的空参构造器就显示初始值是Thread-0
修改默认的线程名字
那我们想改变这个名字怎么办,第一个办法就是通过setname()
方法
package com.dreamcold.thread;
public class Demo04 {
public static void main(String[] args) {
HelloThread h1=new HelloThread();
h1.setName("thread 1"); //前提:在start()前面就要设置
h1.start();
//main方法就是主线程,给主线程命名
Thread.currentThread().setName("main thread");
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
class HelloThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
Thread.currentThread()
在主线程里面就是主线程,在子线程里面就是子线程,这就是所谓的当前线程。
当然,我们还可以通过子类构造器给线程命名
class HelloThread extends Thread{
HelloThread(String str){
super(str); //调用父类的有参构造器
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public class Demo04 {
public static void main(String[] args) {
HelloThread h1=new HelloThread("thread1");//调用构造器的时候给其起名,线程的名字就是Thread:1
}
}
3.3、yield()方法
释放当前cpu
的执行权,即当分线程i=20
时,它阻塞,给主线程去执行,然后它在执行
package com.dreamcold.thread;
public class Demo04 {
public static void main(String[] args) {
HelloThread h1=new HelloThread("thread1");
h1.start();
//给主线程命名
Thread.currentThread().setName("main thread");
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
class HelloThread extends Thread{
HelloThread(String str){
super(str);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
if (i%20==0){
this.yield();// this是当前类的对象,即Thread.currentThread().yield();
}
}
}
}
3.4、join()方法
package com.dreamcold.thread;
public class Demo04 {
public static void main(String[] args) {
HelloThread h1=new HelloThread("thread1");
h1.start();
//给主线程命名
Thread.currentThread().setName("main thread");
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
if (i==20) {
try {
h1.join(); //主线程阻塞,等待子线程h1执行完,它才执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class HelloThread extends Thread{
HelloThread(String str){
super(str);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
3.5、sleep方法
如下,子线程用 slepp()
让他阻塞,睡眠时间完毕,然后等CPU分配资源才能继续往下执行
class HelloThread extends Thread{
HelloThread(String str){
super(str);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
if (i%2==0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
3.6、综合案例
主线程加入join()
,子线程sleep
阻塞
package com.dreamcold.thread;
public class Demo04 {
public static void main(String[] args) {
HelloThread h1=new HelloThread("thread1");
h1.start();
//给主线程命名
Thread.currentThread().setName("main thread");
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
if (i==20) {
try {
h1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class HelloThread extends Thread{
HelloThread(String str){
super(str);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
if (i%2==0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
最后的执行结果就是,由于主线程执行较快,当 i=20
的时候,主线程阻塞,由于子线程sleep
,所以出现的效果就是 i=20
时,主线程阻塞,子线程每隔10ms输出一次,而主线程等他执行完才继续输出
main thread:17
main thread:18
main thread:19
main thread:20
thread1:0
thread1:1
thread1:2
thread1:3
正常情况下,子线程和主线程是同时交替执行的。这才叫多线程,就是并发
4、线程的调度和优先级设置
调度策略
【1】时间片轮转
【2】抢占式:高优先级的线程抢占CPU
Java的调度方法
- 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
- 对高优先级,使用优先调度的抢占式策略
在Thread类中有三个常量来进行表示优先级,从1~10
MAX_PRIORITY=10
MIN_PRIORITY=1
NORM_PRIORITY=5 //默认的优先级
如何获取和设置当前线程的优先级?
getPriority();//获取优先级
setPriority(int p);//设置优先级
示例:给主线程更高的优先级,执行的过程中打印优先级
package com.dreamcold.thread;
public class Demo04 {
public static void main(String[] args) {
HelloThread h1=new HelloThread("thread1");
//设置分线程的优先级
//h1.setPriority(Thread.MAX_PRIORITY);
h1.start();
//给主线程命名
Thread.currentThread().setName("main thread");
//给主线程设置比较高的优先级
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+Thread.currentThread().getPriority()+":"+i);
}
}
}
class HelloThread extends Thread{
HelloThread(String str){
super(str);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+Thread.currentThread().getPriority()+":"+i);
if (i%2==0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。
5、典型案例:多窗口卖票问题
5.1、版本1
例子:创建三个窗口卖票,总票数为100张,使用继承Thread类的方式
package com.dreamcold.thread;
public class Window extends Thread{
private static int ticket=100; //100张票,static全局共享,独有一份,否则就会卖重票
@Override
public void run() { //卖票
while(true){
if(ticket>0){
//Thread.currentThread()可以省略,因为就是在子类里面
System.out.println(getName()+",卖票,票号为: "+ticket);
ticket--;
}else{
break;
}
}
}
public static void main(String[] args) {
Window t1=new Window();
Window t2=new Window();
Window t3=new Window();
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
结果:
窗口三,卖票,票号为: 100
窗口二,卖票,票号为: 100
窗口一,卖票,票号为: 100
窗口二,卖票,票号为: 98
窗口三,卖票,票号为: 99
窗口二,卖票,票号为: 96
窗口一,卖票,票号为: 97
窗口一,卖票,票号为: 93
窗口二,卖票,票号为: 94
窗口三,卖票,票号为: 95
窗口二,卖票,票号为: 91
.............
从结果来看:存在线程的安全问题,待解决。虽然票的问题改成了
static
,还是有重票,即3个窗口都有一个票号100的票原因:未加锁,不同步,ticket–票数没有及时更新
5.2、版本2
例子:创建三个窗口卖票,总票数为100张,使用实现Runnable
接口的方式
package com.dreamcold.thread;
public class Window1 implements Runnable{
//这里不需要static修饰
private int ticket=100;
@Override
public void run() {
while(true){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+",卖票,票号为: "+ticket);
ticket--;
}else{
break;
}
}
}
public static void main(String[] args) {
//因为是一个Window对象所以里面的ticket自动就只有一份,ticket不需要加static
Window1 w=new Window1();
Thread t1=new Thread(w);
Thread t2=new Thread(w);
Thread t3=new Thread(w);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
结果:
窗口二,卖票,票号为: 100
窗口一,卖票,票号为: 100
窗口三,卖票,票号为: 100
窗口一,卖票,票号为: 98
窗口二,卖票,票号为: 99
窗口一,卖票,票号为: 96
窗口三,卖票,票号为: 97
窗口一,卖票,票号为: 94
窗口三,卖票,票号为: 93
从结果来看,还是存在线程的安全问题,待解决。现在,我们的ticket属性没有加
staic
,但是使用runable
还是有3个窗口有3个票号为100的原因:未加锁,不同步,ticket–票数没有及时更新
5.3、两种方式的比较
比较创建线程的两种方式
开发中:优先选择:实现Runnable
接口的方式,原因:
- 实现的方式没有类的单继承性的局限性
- 实现的方式更适合来处理多个线程有共享数据的情况。
- 联系:public class Thread implements Runnable
- 相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
6、线程的生命周期
参考:blog.csdn.net/hanchao5272…
线程状态转换图
JDK中用Thread.State
枚举类定义了线程的这几种状态
public enum St就绪状态、可运行状态/ate {
NEW, //初始状态、开始状态
RUNNABLE, //就绪状态、可运行状态。
BLOCKED, //阻塞状态
WAITING, //等待状态
TIMED_WAITING, //限时等待状态
TERMINATED; //终止状态、结束状态
}
可以通过下面代码打印出来
//线程的六种状态
LOGGER.info("======线程的六种状态======");
LOGGER.info("线程-初始状态:" + Thread.State.NEW);
LOGGER.info("线程-就绪状态:" + Thread.State.RUNNABLE);
LOGGER.info("线程-阻塞状态:" + Thread.State.BLOCKED);
LOGGER.info("线程-等待状态:" + Thread.State.WAITING);
LOGGER.info("线程-限时等待状态:" + Thread.State.TIMED_WAITING);
LOGGER.info("线程-终止状态:" + Thread.State.TERMINATED + "\n");
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread
类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
- 新建:当一个Thread类或其 子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪:处于新建状态的线程被star()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定 义了线程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起suspend或执行输入输出IO操作时,让出CPU并临时中止自己的执行,进入阻塞状态
- 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
- 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
- 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
线程的声明周期图如下:
7、线程的同步
问题的提出
- 多个线程执行的不确定性引起执行结果的不稳定
- 多个线程对账本的共享,会造成操作的不完整性,会破坏数据
- 因此线程的同步就是要解决线程的不安全问题
7.1、卖票问题1(同步代码块解决)
例子:创建三个窗口卖票,总票数为100张。使用实现Runnable
接口的方式
- 问题:卖票过程中,出现了重票、错票 -->出现了线程的安全问题
- 问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
- 如何解决:当一个线程a在操作
ticket
的时候,其他线程不能参与进来。直到线程a操作完ticket
时,其他线程才可以开始操作ticket
。这种情况即使线程a出现了sleep
等阻塞,也不能被改变。
方式一:同步代码块
synchronized(同步监视器){
// 需要被同步的代码
}
说明:
- 操作共享数据的代码,即为需要被同步的代码。不能包含代码多了,也不能包含代码少了。
- 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
- 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
- 要求:多个线程必须要共用同一把锁。即只有一个窗口对象
- 多个线程共享数据是出现线程安全问题的原因
- 补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用
this
充当同步监视器。
修改后的代码:
package com.dreamcold.thread;
public class Window1 implements Runnable{
//这里不需要static修饰
private int ticket=100;
Object object=new Object(); //多个线程也必须要共用同一把锁,同理不需要static修饰
@Override
public void run() {
while(true){
synchronized (object){ //执行完内部的代码就会自动释放锁,另外一个线程会抢到
if(ticket>0){
try {
Thread.sleep(100); //使用sleep增加出现错票的概率
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+",卖票,票号为: "+ticket);
ticket--;
}else {
break;
}
}
}
}
public static void main(String[] args) {
//因为是一个Window对象所以里面的ticket只有一份
Window1 w=new Window1();
Thread t1=new Thread(w);
Thread t2=new Thread(w);
Thread t3=new Thread(w);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
结果:
窗口一,卖票,票号为: 79
窗口一,卖票,票号为: 78
窗口一,卖票,票号为: 77
窗口一,卖票,票号为: 76
窗口一,卖票,票号为: 75
窗口一,卖票,票号为: 74
窗口三,卖票,票号为: 73
窗口三,卖票,票号为: 72
窗口三,卖票,票号为: 71
窗口三,卖票,票号为: 70
窗口三,卖票,票号为: 69
优点:同步的方式,解决了线程的安全问题,三个线程可以抢占锁,假如线程1抢到了锁进去了,这时其他线程就进不去了,线程1在里面执行自己的代码,这时候线程1无论经历了什么比如阻塞、挂起 ,其他线程也不会影响到线程1,线程1执行完后退出,释放锁,这时候其他线程竞争来得到锁进而执行。
局限性:操作同步代码时,只能有一个线程参与, 其他线程等待。相当于一个单线程的过程,效率会比较低
关于同步监视器: 在实现Runnable接口创建多线程的方式中,我们可以考虑使用this
充当同步监视器。
@Override
public void run() {
while(true){
synchronized (this){ //此时的this,唯一的windows1对象
if(ticket>0){
try {
Thread.sleep(100); //使用sleep增加出现错票的概率
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+",卖票,票号为: "+ticket);
ticket--;
}else {
break;
}
}
}
}
7.2、卖票问题2(同步代码块解决)
使用同步代码块解决继承Thread
类的方式的线程安全问题
例子:创建三个窗口卖票,总票数为100张,使用继承Thread
类的方式
说明:在继承Thread
类创建多线程的方式中,慎用this
充当同步监视器,考虑使用当前类充当同步监视器。
package com.dreamcold.thread;
public class Window extends Thread{
private static int ticket=100;
//private static Object object;//确保锁住的对象是唯一的,即多个线程锁住一个对象
@Override
public void run() {
while(true){
synchronized (Window.class){ //反射,类只会加载一次,所以可以充当锁
if(ticket>0){
System.out.println(getName()+",卖票,票号为: "+ticket);
ticket--;
}else{
break;
}
}
}
}
public static void main(String[] args) {
Window t1=new Window();
Window t2=new Window();
Window t3=new Window();
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
结果
窗口一,卖票,票号为: 100
窗口一,卖票,票号为: 99
窗口一,卖票,票号为: 98
窗口一,卖票,票号为: 97
窗口一,卖票,票号为: 96
窗口一,卖票,票号为: 95
窗口一,卖票,票号为: 94
窗口三,卖票,票号为: 93
窗口三,卖票,票号为: 92
窗口三,卖票,票号为: 91
窗口三,卖票,票号为: 90
7.3、卖票问题3(同步方法解决)
- 如果操作共享数据的代码完整的声明在一个方法中, 我们不妨将此方法声明同步的。
- 不适合将
run
方法改成我们的同步方法
使用同步方法解决实现Runnable
接口的线程安全问题
package com.dreamcold.thread;
public class Window implements Runnable{
private int ticket=100;
@Override
public void run() {
while(true){
sale();
}
}
private synchronized void sale() { //同步方法,同步监视器:this
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName()+",买票,票号为: "+ticket);
ticket--;
}
}
public static void main(String[] args) {
//因为是一个Window对象所以里面的ticket只有一份
Window w=new Window();
Thread t1=new Thread(w);
Thread t2=new Thread(w);
Thread t3=new Thread(w);
t1.setName("t1");
t2.setName("t2");
t3.setName("t3");
t1.start();
t2.start();
t3.start();
}
}
7.4、卖票问题4(同步方法解决)
将方法声明为synchronized
,使用同步方法解决继承Thread
类的线程安全问题,注意的是同步方法要声明为static
的,因为声明为static
说明其锁住的是class
package com.dreamcold.thread;
public class Window extends Thread{
public static void main(String[] args) {
Window t1=new Window();
Window t2=new Window();
Window t3=new Window();
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
private static int ticket=100;
@Override
public void run() {
while(true){
sale();
}
}
private static synchronized void sale() {//同步监视器:window.class,所以必须加static
//private synchronized void sale() 同步监视器:t1.t2.t3,错误的方式
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//Thread.currentThread()必须加上,因为是静态方法,所以没有对象,只有类去调用
System.out.println(Thread.currentThread().getName()+",卖票,票号为: "+ticket);
ticket--;
}
}
}
7.5、总结
关于同步方法的总结:
- 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
- 非静态的同步方法,同步监视器是:
this
- 静态的同步方法,同步监视器是:当前类本身
8、线程同步机制的应用
使用同步机制将单例模式中的懒汉式改写为线程安全的
单例模式:懒汉式
class Bank{
private Bank(){
}
private static Bank instance=null;
public static Bank getInstance(){
if (instance==null){ //线程不安全的
instance=new Bank();
return instance;
}
return instance;
}
}
1、使用同步方法来实现同步,解决懒汉式的线程不安全
class Bank{
private Bank(){
}
private static Bank instance=null;
public synchronized static Bank getInstance(){ //同步方法
if (instance==null){
instance=new Bank();
return instance;
}
return instance;
}
}
2、使用同步代码块来实现同步,是直接都加到同步代码块中,效率稍微差
class Bank{
private Bank(){
}
private static Bank instance=null;
public static Bank getInstance(){
//效率稍差
synchronized (Bank.class){
if (instance==null){
instance=new Bank();
return instance;
}
return instance;
}
}
}
3、使用同步代码块来实现同步,先简单判断对象存在与否,再加锁,减少不必要的加锁,提高效率
class Bank{
private Bank(){
}
private static Bank instance=null;
public static Bank getInstance(){
if (instance==null){ //一来就立马先判断一下有没有对象 ,有就不进去了
synchronized (Bank.class){
instance=new Bank();
return instance;
}
}
return instance;
}
}
9、死锁
死锁定义
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
死锁的必要条件
- 互斥:每个资源要么已经分配给了一个进程,要么就是可用的。
- 占有和等待:已经得到了某个资源的进程可以再请求新的资源。
- 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。
- 环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。
解决方法
- 专门的算法、原则
- 尽最减少同步资源的定义
- 尽量避免嵌套同步
- 我们使用同步时,要避免出现死锁。
实例
加上休眠后,增加了死锁出现的概率
package com.dreamcold.thread;
public class Demo06 {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread(){
@Override
public void run() {
synchronized (s1){
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2){
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
结果:程序阻塞
- 一把锁,必须执行完里面的程序才会把锁释放掉
- 如程序所示:考虑极端的情况,当在线程1里面他拿着
S1
这把锁执行,但是它要继续往下执行还需拿到S2
这把锁,但此时此刻,线程2也在执行,它拿着S2
这把锁往下执行,但是它要继续往下执行还需拿到S1
这把锁,于是各自需要的锁都没有释放(因为代码执行不下去),所以打印出来没有结果
10、Lock锁解决线程安全问题
- 从JDK 5.0开始,Java提供了更强大的线程同步机制一通过显式定义同步锁对象来实现同步。同步锁使用
Lock
对象充当。 java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock
对象加锁,线程开始访问共享资源之前应先获得Lock
对象。ReentrantLock
类实现了Lock,它拥有与synchronized
相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock
, 可以显式加锁、释放锁。
示例:实现窗口卖票
package com.dreamcold.thread;
import java.util.concurrent.locks.ReentrantLock;
public class Window2 implements Runnable{
private int ticket=100;
//实例化ReentrantLock
private ReentrantLock reentrantLock=new ReentrantLock(true);
//如果构造器传入true就是一个公平的锁,即先进先出的特点,不写默认是false
@Override
public void run() {
try {
reentrantLock.lock();//调用锁定方法lock()
while (true){
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+": 售票,票号为"+ticket);
ticket--;
}else{
break;
}
}
}finally { //调用解锁方法
reentrantLock.unlock();
}
}
public static void main(String[] args) {
Window2 window2=new Window2();
Thread t1=new Thread(window2);
Thread t2=new Thread(window2);
Thread t3=new Thread(window2);
t1.setName("t1");
t2.setName("t2");
t3.setName("t3");
t1.start();
t2.start();
t3.start();
}
}
结果:当每一个线程进来操作票时,就加锁了,等到它执行完毕,他才会释放锁,下一个线程才会进来继续操作
t2: 售票,票号为100
t2: 售票,票号为99
t2: 售票,票号为98
.............
总结
synchronized 与Lock的异同?
- 相同:二者都可以解决线程安全问题
- 不同:
synchronized
机制在执行完相应的同步代码以后,自动的释放同步监视器Lock
需要手动的启动同步(Lock()),同时结束同步也需要手动的实现(unLock() )
synchronized与Lock的对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),
synchronized
是隐式锁,出了作用域自动释放 Lock
只有代码块锁,synchronized
有代码块锁和方法锁- 使用
Lock
锁, JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性( 提供更多的子类)
优先使用顺序:
Lock----→同步代码块(已经进入了方法体,分配了相应资源)----→同步方法(在方法体之外)
11、线程同步-并发安全问题
银行有一个账户,有两个储户分别向同一个账户存3000元,每次存1000, 存3次。每次存完打印账户余额。
问题:该程序是否有安全问题,如果有,如何解决?
[提示]
- 明确哪些代码是多线程运行代码,须写入run()方法
- 明确什么是共享数据。
- 明确多线程运行代码中哪些语句是操作共享数据的。.
- 拓展问题: :可否实现两个储户交替字钱的操作
分析:
- 是否是多线程问题? 是,两个储户线程
- 是否有共享数据?有,账户(或账户余额)
- 是否有线程安全问题?有
- 需要考虑如何解决线程安全问题?同步机制:有
代码实现:
package com.lemon.java;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author Lemons
* @create 2022-02-20-21:50
*/
public class AccountTest{
public static void main(String[] args) {
//第三步:创建两个用户操作,一个账户,开始存钱
Acount acount=new Acount(0);
Customer c1=new Customer(acount);
Customer c2=new Customer(acount);
c1.setName("甲");
c2.setName("乙");
c1.start();
c2.start();
}
}
class Acount {
private double balance;
public Acount(double balance){
this.balance=balance;
}
//存钱,同步方法
public synchronized void desposit(double amt){
if (amt>0){
balance+=amt;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+": 存钱成功,当前余额为"+balance);
}
}
}
//第二步:创建用户线程
class Customer extends Thread{
private Acount acct;
public Customer(Acount acct) {
this.acct = acct;
}
@Override
public void run() { //存钱的操作
for (int i = 0; i < 3; i++) {
acct.desposit(1000); //调用同步方法
}
}
}
结果:
甲: 存钱成功,当前余额为1000.0
甲: 存钱成功,当前余额为2000.0
乙: 存钱成功,当前余额为3000.0
乙: 存钱成功,当前余额为4000.0
乙: 存钱成功,当前余额为5000.0
甲: 存钱成功,当前余额为6000.0
12、线程的通信
1、实例
线程通信的例子:使用两个线程打印 1-100。线程1, 线程2 交替打印
class Number implements Runnable{
private int number=1;
@Override
public void run() {
synchronized (this){
while (true){
notify();//唤醒所有处于阻塞状态的线程
if (number<=100){
try {
Thread.sleep(100);//sleep方法不会释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+number);
number++;
//使得当前线程变成阻塞状态
try {
wait();//wait方法会释放锁,当前线程就进入阻塞状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number=new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
结果:交叉打印,整个过程
- 假设线程1先抢到锁,然后进入,
notify()
唤醒所有处于阻塞状态的线程(这里刚开始其实没啥阻塞的线程) - 然后休眠一会儿,由于
sleep
方法不会释放锁,所以线程1继续在里面执行,线程2进不来,打印1,并将数字++设为2 - 然后执行
wait
方法释放锁,且线程1进入阻塞状态,此时线程2拿到锁,notify()
唤醒处于阻塞状态的线程1,然后休眠一会儿,由于sleep
方法不会释放锁,所以线程2继续在里面执行,打印2,并将数字++设为3 - 然后执行
wait
方法释放锁,且线程2进入阻塞状态,此时线程1拿到锁 - 循环往复执行
线程1:1
线程2:2
线程1:3
线程2:4
线程1:5
线程2:6
.........
2、wait与notify以及notifyAll
涉及到的三个方法:
- wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
- notify():旦执行此方法,就会唤醒被wait的一个线程。 如果有多个线程被wait,就唤醒优先级高的线程
- notifyAll():一且执行此方法,就会唤醒所有被wait的线程。
注意:
- wait(), notify(), notifyAll 三个方法必须使用在同步代码块或同步方法中。
- wait(), notify(), notifyAll三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现
IllegalMonitorStateException
异常 - wait(), notify(), notifyALl() 三个方法是定义在
java. lang. object
类中。
sleep()和wait()的异同
- 相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
- 不同点:
- 两个方法声明的位置不同: Thread类中声明sleep(),object类中声明wait()
- 调用的要求不同: sleep() 可以在任何需要的场景下调用。wait必须在同步代码块或者同步方法中调用
- 释放锁:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁, wait() 会释放锁。
3、生产者/消费者问题
生产者(Productor)将产品交给店员(Clerk), 而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如20),如果生产者试图生产更多的产品,店员会叫生产者停一下,直到店中有空位放产品了再通知生产者继续生产,如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
这里可能出现两个问题:
- 生产者比消费者快时,消费者会漏掉一些数据没有取到。
- 消费者比生产者快时,消费者会取相同的数据。
分析:
- 是否是多线程问题? 是,生产者线程,消费者线程
- 是否有共享数据? 是,店员(产品)
- 如何解决线程的安全问题,同步机制,三种方法
- 是否涉及线程的通信?是
代码实现
public class ProductorTest {
public static void main(String[] args) {
Clerk clerk=new Clerk();
Producer p1=new Producer(clerk);
p1.setName("生产者1");
Consumer c1=new Consumer(clerk);
c1.setName("消费者1");
p1.start();
c1.start();
}
}
class Clerk{
//产品的数量
private int productNum=0;
//生产产品
public synchronized void produceProduct() {
if (productNum<20){
productNum++;
System.out.println(Thread.currentThread().getName()+": 开始生产了"+productNum+"个产品");
//生产了一个产品消费者就可以消费,唤醒消费者线程
notify();
}else {
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费产品
public synchronized void consumeProduct() {
if (productNum>0){
System.out.println(Thread.currentThread().getName()+": 开始消费第"+productNum+"个产品");
productNum--;
//消费了一个元素就可以让生产者生产
notify();
}else {
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//生产者
class Producer extends Thread{
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName()+":开始生产产品...");
while (true){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct(); //生产产品
}
}
}
//消费者
class Consumer extends Thread{
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName()+":开始消费产品...");
while (true){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumeProduct(); //消费产品
}
}
}
结果
生产者1:开始生产产品...
消费者1:开始消费产品...
生产者1: 开始生产了1个产品
消费者1: 开始消费第1个产品
生产者1: 开始生产了1个产品
消费者1: 开始消费第1个产品
生产者1: 开始生产了1个产品
消费者1: 开始消费第1个产品
......