文章目录
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210410092702870.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzNzg4NTIy,size_16,color_FFFFFF,t_70)
一、 基本概念:程序、进程、线程
- 程序(program)
是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。 - 进程(process)
是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。——生命周期- 如:运行中的QQ,运行中的MP3播放器
- 程序是静态的,进程是动态的
- 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
- 线程(thread)
进程可进一步细化为线程,是一个程序内部的一条执行路径。- 若一个进程同一时间并行执行多个线程,就是支持多线程的
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
- 一个进程中的多个线程共享相同的内存单元/内存地址空间–>它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
- 单核CPU和多核CPU的理解
- 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费
才能通过,那么CPU就好比收费人员。如果有某个人不想交钱,那么收费人员可以
把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费)。但是因为CPU时
间单元特别短,因此感觉不出来。 - 如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
- 一个Java应用程序java.exe,其实至少有三个线程:main()主线程,gc()
垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
- 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费
- 并行与并发
- 并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
- 并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。
二、 线程的创建和使用
- Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来体现。
- Thread类的特性
- 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常
把run()方法的主体称为线程体。 - 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
- 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常
Thread类的构造器
构造器 | 描述 |
---|---|
Thread() | 创建新的Thread对象 |
Thread(String threadname) | 创建线程并指定线程实例名 |
Thread(Runnable target) | 指定创建线程的目标对象,它实现了Runnable接中的run方法 |
Thread(Runnable target, String name) | 创建新的Thread对象 |
线程的创建方式一:继承Thread类
- 定义子类继承Thread类。
- 子类中重写Thread类中的run方法
- 创建Thread子类对象,即创建了线程对象。
- 调用线程对象start方法(注意有两个作用):启动线程;调用run方法。
/**
*
* 多线程的创建,方式一:继承于Thread类
* 1.创建一个继承于Thread类的子类
* 2.重写Thread类的run()-->将此线程执行的操作声明在run()中
* 3.创建Thread类的子类对象
* 4.通过此对象调用start()
*
* 例子:遍历100以内的所有的偶数
* @author 刘瘦瘦
* @create 2021-03-29-12:19
*/
//1.创建一个继承于Thread类的子类
class MyThread extends Thread{
// 2.重写Thread类的run()-->将此线程执行的操作声明在run()中
@Override
public void run() {
System.out.println(Thread.currentThread()==this);//true
for (int i = 0; i < 100; i++) {
if(i % 2 ==0){
System.out.println(Thread.currentThread().getName());
}
}
}
}
public class ThreadTest{
public static void main(String[] args) {
// 3.创建Thread类的子类对象
MyThread t1=new MyThread();
Thread t3=new Thread(t1);
// 4.通过此对象调用start():①启动当前线程 ②调用当前线程的run()
t1.start();
//问题一:我们不能通过直接调用run()的方式启动线程
//t1.run();相当于调用普通方法
//问题二:再启动一个线程,遍历100以内的偶数。不可以还让已经start的线程去执行。会会报IllegalThreadStateException
// t1.start();
//我们需要重新创建一个线程的对象
MyThread t2=new MyThread();
t2.start();
//如下操作仍然是在main线程中执行的
for (int i = 0; i < 100; i++) {
if(i%2==0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
其实run方法是Runnable接口中的抽象方法(Runnable接口中只有一个run方法),只不过我们在Thread类中重写了run()方法。
而采用继承Thread的方式时,我们有一步操作是重写了run()方法,那么由继承的知识可以知道,当调用了父类重写的方法时,实际执行的是子类重写后的方法,当启动多线程时(调用start方法),JVM会调用run()方法,那么调用的就是我们重写后的run()方法,而run()方法里面就是我们想让线程做的事情。
关于Thread.currentThread与this的关系
注意:
- 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
- run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
- 想要启动多线程,必须调用start方法。
- 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常“IllegalThreadStateException”。
线程的创建方式二:实现Runnable接口
- 定义子类,实现Runnable接口。
- 子类中重写Runnable接口中的run方法。
- 通过Thread类含参构造器创建线程对象。
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
- 调用Thread类的start方法:开启线程,调用Runnable子类接口中的run方法。
/**
*
* 创建线程的方式二:实现Runnable接口
*1.创建一个实现了Runnable接口的类
*2.实现类去实现Runnable中的抽象方法:run()
*3.创建实现类的对象
*4.创建Thread类的对象,将此对象作为参数传递到Thread类的构造器中
*5.通过Thread类的对象调用start()
*
* 比较创建线程的两种方式。
* 开发中:优先选择:实现Runnable接口的方式
* 原因:1.实现的方式没有类的单继承性的局限性
* 2.实现的方式更适合来处理多个线程有共享数据的情况
*
* 联系:public class Thread implements Runnable
* 相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
*
* Runnable对象仅仅作为Thread对象的target(可以看看源码),Runnable实现类里包含的run()方法
* 仅仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行
* 其target的run()方法。
*
* 通过继承Thread类来获得当前线程对象比较简单,直接使用this就可以了;
* 但通过实现Runnable接口来获得当前线程对象,则必须使用Thread.currentThread()方法。
*
* @author 刘瘦瘦
* @create 2021-03-30-8:48
*/
//1.创建一个实现Runnable接口的类
class MThread implements Runnable {
//2.实现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 RunnableTest{
public static void main(String[] args) {
//3.创建一个实现类对象
MThread m=new MThread();
//4.创建一个Thread类对象,并把实现类对象作为其参数
Thread t=new Thread(m);
t.setName("线程1");
//5.通过Thread类的对象调用start():①启动线程 ②调用当前线程的run-->调用了Runnable类型的target的run(),可以看底层源码
t.start();
//再启动一个线程,遍历100以内的偶数
Thread t2=new Thread(m);
t2.setName("线程2");
t2.start();
}
}
采用实现Runnable的底层原理分析
对于上述程序,我们点进去看一下底层的执行原理
Thread t=new Thread(m);
我们发现构造器的参数是Runnable类型,而我们传递的参数实现了Runnable接口,此时发生了多态。
我们在查看Init方法,如下
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}
/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
g.checkAccess();
/*
* Do we have the required permissions?
*/
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
我们只关注其中的重要一行代码:如下
它把我们传递的参数赋给了Thread类中的target,下面查看run()方法
此处run方法中调用了我们传递过来的实现Runnable类的对象的run方法,当我们调用Thread对象的start()方法时(start()方法作用,创建一个线程,执行run()方法),也就执行了我们在实现类重写的run()方法。
线程的创建方式三:使用Callable接口
对比第一种和第二种创建线程的方式发现,无论第一种继承Thread类的方式还是第二种实现Runnable接口的方式。都需要有一个run方法,但是这个run方法有不足:
1、没有返回值 2、不能抛出异常
基于以上两个不足,在JDK1.5以后出现了第三种创建线程的方式:实现Callable接口:实现Callable接口好处:1、有返回值 2、抛出异常 3、支持泛型操作 缺点:创建线程比较麻烦。
实现步骤:
- 创建一个实现Callable的实现类。
- 实现call方法,将此线程需要执行的操作声明在call()中
- 创建Callable接口实现类的对象
- 将此Callable接口实现类的对象作为参数传递到FutureTask构造器中
- 将FutureTask的对象作为参数传递到Thred类的构造器中,创建Thread对象,并调用start()
- 获取Callable中call方法的返回值
方法 | 描述 |
---|---|
FutureTask中的get() | 返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。 |
代码演示
/**
* 创建线程的方式三:实现Callable接口。---JDK5.0新增
*
* 如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
* 1.call()可以有返回值的
* 2.call可以抛出异常,被外面的操作捕获,获取异常信息
* 3.Callable是支持泛型的
*
* @author 刘瘦瘦
* @create 2021-04-02-14:20
*/
//1.创建一个实现Callable的实现类
class NumThread implements Callable{
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() {
int sum=0;
for (int i = 0; i < 100; i++) {
if(i%2==0){
System.out.println(Thread.currentThread().getName()+":"+i);
sum+=i;
}
}
return sum;//自动装箱
}
}
public class CallableTest {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
NumThread numThread=new NumThread();
//4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,
FutureTask futureTask=new FutureTask(numThread);
//5.将FutureTask的对象作为参数传递到Thred类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
//6.获取Callable中call方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
try {
Object o = futureTask.get();//调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值。
System.out.println(o);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
分析:
我们知道启动一个线程必须调用Thread类中的start()方法,上述程序我们创建了一个实现Callable接口的实例对象,并把它丢入到Thread中的构造器中,但程序报错了,说明我们Thread中的构造器没有参数是Callable的:
但是我们注意到存在Runnable类型参数的构造器:
所以我们可以传入一个实现了Runnable接口的实现类对象,但是Callable接口不是Runnable接口的实现类,所以我们就像借助一个FutureTask类,FutureTask类的声明如下
可以看到,它实现了RunableFuture接口,我们在点入到RunnableFuture接口中瞧瞧
可见RunnableFuture继承了Runnable类,所以我们的FutureTask对象可以作为Thread构造器中参数为Runnable的实参传入。而FutureTask类中存在参数为Callable类型参数的构造器,所以这样就可以顺利的执行start()方法了。
线程的创建方式四:使用线程池
【1】线程池使用到的是阻塞队列
线程的生命周期:出生----》死亡
线程正常状态:
新生(假设用时3s)----》就绪(2s)-----》运行(1)-----》死亡(3s)
使用线程池目的:将运行之前的时间节省,将运行之后的时间节省—》线程池:减少创建和消亡的时间。
class TestThread implements Runnable {
@Override
public void run() {
System.out.println("当前执行任务的线程为:"+Thread.currentThread().getName());
}
}
public class ThreadPoolTest{
public static void main(String[] args) {
//创建一个线程池
ThreadPoolExecutor t=new ThreadPoolExecutor(
1,//设置核心线程数为1
2,//设置最大线程数为2
3, TimeUnit.MILLISECONDS,//3毫秒,当任务
//量大于队列长度需要创建新线程的时候, 新线程执行完该当前任务后,新创建的线程等任务最多3毫秒,如果3ms内没有任务可以被执行,该线程就销毁了
//如果阻塞队列里还有任务没执行则创建的新线程和核心线程都会执行阻塞队列中的任务。
new LinkedBlockingDeque<>(3));//利用了阻塞队列长度为三
//执行任务
//放入第1个任务
t.execute(new TestThread());//此时线程执行该任务,线程阻塞队列里面还没有线程,里面可以放3个
//放入第2个任务---》在核心线程忙着的时候才放入队列中,如果第一个执行的非常快,第二个就不放队列了直接执行
t.execute(new TestThread());
//放入第3个任务
t.execute(new TestThread());//放入队列,核心线程执行
//放入第4个任务
t.execute(new TestThread());//放入队列,核心线程执行
//放入第5个任务
t.execute(new TestThread());//这个时候队列满了,创建新的线程来执行第5个任务了:
//并且与核心线程一起分摊任务
//关闭线程池
t.shutdown();
}
}
执行线程5时,队列以及满了,就会创建新线程执行该任务。创建的新线程是“pool-1-thread-2",它执行完并不是立即销毁,如果3ms内有任务,他就会和核心线程
"pool-1-thread-1"分摊执行剩下的任务,如果3ms内没任务,该新线程消亡。试着在运行上述程序,查看“pool-1-thread-2"确实分摊任务了。
加入第六个任务
报错了,因为线程队列以及满了,此时的新任务要创建新的线程来执行,但是目前已经有两个线程了,我们初始也设置了最大线程数为2,所以此时就拒绝执行了。
在java5以前,开发者必须手动实现自己的线程池;从Java5开始,Java内建支持线程池。Java5新增了一个Executors工厂类来实现线程池,该工厂类包含如下几个静态工厂方法来创建线程池。
方法 | 描述 |
---|---|
newCachedThreadPool() | 创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中 |
newFixedThreadPool(int nThreads) | 创建一个可重用的、具有固定线程数的线程池。 |
newSingleThreadExecutor() | 创建一个只有单线程的线程池,它相当于调用newFixedThreadPool()方法时传入参数为1. |
newScheduledThreadPool(int corePoolSize) | 创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。corePoolSize指池中所保存的线程数,即使线程是空闲的也被保存在线程池内。 |
newSingleThreadScheduledExecutor() | 创建只有一个线程的线程池,它可以在指定延迟后执行线程任务。 |
上面5个方法中的前3个方法返回一个ExecutorService对象,该对象代表一个线程池,它可以执行Runnable对象或Callable对象所代表的线程;而后两个方法返回一个ScheduledExecutorService线程池,它是ExecutorService的子类,它可以在指定延迟后执行线程任务。
JDK1.5之后提供了内置线程池
【1】可缓存线程池:
/**
* 内置线程池
* @author 刘瘦瘦
* @create 2021-05-22-16:08
*/
public class ThreadPoolTest1 {
public static void main(String[] args) {
//可缓存线程池
ExecutorService es = Executors.newCachedThreadPool();
//执行任务
for (int i = 0; i < 100; i++) {
es.execute(new TestThread());
}
//关闭
es.shutdown();
}
}
最开始没有核心线程,来一个任务,新建一个线程来执行这个任务,当这个任务执行完以后,这个线程继续执行其他的任务,所以在结果中可以看到线程大量重复的
【2】定长线程池
public class ThreadPoolTest2 {
public static void main(String[] args) {
//可缓存线程池
ExecutorService es = Executors.newFixedThreadPool(3);
//执行任务
for (int i = 0; i < 100; i++) {
es.execute(new TestThread());
}
//关闭
es.shutdown();
}
}
运行结果:只有3个线程在执行任务
【3】定时线程池
public class ThreadPoolTest3 {
public static void main(String[] args) {
//可缓存线程池
ScheduledExecutorService ses = Executors.newScheduledThreadPool(3);
//执行任务
for (int i = 0; i < 100; i++) {
ses.schedule(new TestThread(),3, TimeUnit.SECONDS);//延迟3s后执行任务
}
//关闭
ses.shutdown();
}
}
底层源码还是:
延迟3s执行线程
【4】单例线程池
/**
* 单例线程池
* @author 刘瘦瘦
* @create 2021-05-22-16:08
*/
public class ThreadPoolTest4 {
public static void main(String[] args) {
//单例线程池
ExecutorService es = Executors.newSingleThreadExecutor();
//执行任务
for (int i = 0; i < 100; i++) {
es.execute(new TestThread());
}
//关闭
es.shutdown();
}
}
就一个线程来执行任务
底层:
运行结果:
体会:
1、通过继承Thread类的方法来创建线程时,多个线程之间无法共享线程类的实例变量。除非加static变成类变量。或者把继承Thread的子类对象作为参数传递给Thread对象(其实相当于实现了Runnable接口,具体看下文演示)。
2、采用Runnable接口的方式创建的多个线程可以共享线程类的实例属性。这是因为在这种方式下,程序所创建的Runnable对象只是线程的target,而多个线程可以共享一个target,所以多个线程可以共享同一个线程类(实际上应该是线程的target类对象)的实例属性。
1、继承Thread类的方式来演示抢票操作,不能实现数据的共享
/**
*
* 例子:创建三个窗口卖票,总票数为100张.使用继承Thread类的方式
*存在线程的安全问题,待解决(看lock包)。
*
* 1、创建一个继承于Thread类的子类
* 2、重写run()方法
* 3、创建一个Thread类的子类对象
* 4、通过该对象调用start()方法
*
* 体会:通过继承Thread类的方法来创建线程时,多个线程之间无法共享线程类的实例变量。除非加static变成类变量
*
* @author 刘瘦瘦
* @create 2021-03-30-8:29
*/
class Window extends Thread{
private int tickets=100;
// private static int tickets=100;
@Override
public void run() {
while (true){
if(tickets>0){
System.out.println(Thread.currentThread().getName()+"买票,票号为:"+tickets);
tickets--;
}else{
break;
}
}
}
}
public class WindowTest{
public static void main(String[] args) {
Window w1=new Window();
Window w2=new Window();
Window w3=new Window();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
/*
上述对应tickets属性的定义,如果不加static抢票就会发生错误
tickets是实例属性,因为程序每次创建线程对象时都需要创建一个Window对象,所以
“窗口1“线程、”窗口2“线程不能共享属性,解决就是加static,让其成为类属性
*/
}
}
运行结果如下:如果不加static抢票就会发生错误(每个窗口买票都会从100号开始卖)。tickets是实例属性,因为程序每次创建线程对象时都需要创建一个Window对象,所以“窗口1“线程、”窗口2“线程不能共享属性,解决就是加static,让其成为类属性。
把继承Thread的子类对象作为参数传递给Thread对象也可以实现数据共享(其实相当于实现了Runnable接口,具体看下文演示)。
/**
* 通过把继承自Thread类的子类对象作为参数传递给Thread类
* @author 刘瘦瘦
* @create 2021-04-04-10:50
*/
class MyThreadr extends Thread {
private int tickets=100;
@Override
public void run() {
while (true){
if(tickets>0){
System.out.println(Thread.currentThread().getName()+":抢到票,票号为"+tickets);
tickets--;
}else{
break;
}
}
}
}
public class WindowTest2{
public static void main(String[] args) {
MyThreadr m=new MyThreadr();
new Thread(m,"窗口1").start();
new Thread(m,"窗口2").start();
new Thread(m,"窗口3").start();
}
}
因为MyThreadr继承了Thread类,Thread类又实现了Runnable接口,相当于MyThreader间接的实现了Runnable接口,所以可以把MyThread的对象传递给声明为Runnable类型的参数,此时发生了多态。虽然我们new了好几个线程对象,但其实操作的都是同一个MyThread的子类对象,当然可以共享tickets属性。
创建线程的三种方式对比
通过继承Thread类或实现Runnable、Callable接口都可以实现多线程,不过实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常而已。因此可以实现Runnable接口和实现Callable接口归为一种方式。这种方式与继承Thread方式之间的主要差别如下
采用实现Runnable、Callable接口的方式创建多线程—
- 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
- 在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将cpu、代码和数据分开,形成清晰的模型,较好体现了面向对象的思想。
- 劣势是:编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。
采用继承Thread类的方式创建多线程— - 因为线程类以及继承了Thread类,所以不能再继承其他父类。
- 优势是:编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
鉴于分析,因此一般推荐采用实现Runnable接口、Callable接口的方式来创建多线程。
二、线程的优先级
- 每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会。
- 每个线程默认的优先级都与创建它的父线程的优先级相同,默认情况下,main线程具有普通优先级,由main线程创建的子线程也具有普通优先级。
- Thread类提供了setPrority(int newPriority)、getPrority()方法来设置和返回指定线程的优先级,其中setPrority()方法的参数可以是一个整数,范围是1~10之间,也可以使用Thread类的如下3个静态常量。
- MAX_PRIORITY:其值是10.
- MIN_PRIORITY:其值是1.
- NORM_PRIORITY:其值是5
public class ThreadPriority extends Thread {
public ThreadPriority(String name){
super(name);
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(getName()+",其优先级是:"+getPriority()+",循环变量的值为:"+i);
}
}
public static void main(String[] args) {
//改变主线程的优先级
Thread.currentThread().setPriority(6);
for (int i = 0; i < 30; i++) {
if(i==10){
ThreadPriority low=new ThreadPriority("低级");
low.start();
System.out.println("创建之初的优先级:"+low.getPriority());
//设置线程为最低优先级
low.setPriority(Thread.MIN_PRIORITY);
}
if(i==20){
ThreadPriority high=new ThreadPriority("高级");
high.start();
System.out.println("创建之初的优先级:"+high.getPriority());
//设置线程为最高优先级
high.setPriority(Thread.MAX_PRIORITY);
}
}
/*
上面程序中第一行代码改变了主线程的优先级6,这样由main线程所创建的子线程的优先级默认都是6,所以程序直接输出
low\high两个线程的优先级是应该看到6
从运行结果看:优先级高的线程获得更多的执行机会
*/
}
}
**注意:**优先级高的线程获得更多的执行机会,并不是一定先执行。
三、线程的相关方法
方法 | 描述 |
---|---|
void start() | 启动线程,并执行对象的run()方法 |
run() | 线程在被调度时执行的操作 |
String getName() | 返回线程的名称 |
void setName(String name) | 设置该线程名称 |
static Thread currentThread() | 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类 |
yield() | 释放当前cpu的执行权,让其它的线程去获取,但该方法只会给优先级相同,或优先级更高的线程执行机会。它可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程进入就绪状态。yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。。 |
join() | 在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。 |
sleep(long millitime) | 让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。 |
stop() | 已过时。当执行方法时,强制结束当前线程。 |
isAlive() | 判断当前线程是否存活 |
join()方法
public class ThreadMethodJoinTest extends Thread {
//提供一个有参数的构造器,用于设置该线程的名字
public ThreadMethodJoinTest(String name){
super(name);
}
//重写run()方法,定义线程执行体
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args) throws InterruptedException {
new ThreadMethodJoinTest("新线程").start();
for (int i = 0; i < 100; i++) {
if (i==20){
ThreadMethodJoinTest tjt=new ThreadMethodJoinTest("被Join的线程");
tjt.start();
//main线程调用了tjt线程的join()方法,main线程必须等tjt线程执行结束才会向下执行
tjt.join();
}
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
/*
上面程序一共有三个线程,主方法开始时就启动了名为“新线程”的子线程,该子线程将会和main线程并发执行。当主线程
的循环遍历i等于20时,启动了名为“被Join的线程”的线程,该线程不会和main线程并发执行,main线程必须等该线程执行结束后才可以向下执行。在名为
“被Join的线程”的线程执行时,实际上只有两个子线程并发执行,而主线程处于等候状态。
*/
sleep()方法
public class ThreadMethodSleepTest {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
System.out.println("当前时间:" + new Date());
//调用sleep()方法让当前线程暂停1s
Thread.sleep(1000);
}
}
}
yield()方法
public class ThreadMethodYieldTest extends Thread {
public ThreadMethodYieldTest(String name){
super(name);
}
//定义run方法作为线程执行体
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(getName()+" "+i);
//当i=20时,使用yield()方法让当前线程让步
if(i==20){
Thread.yield();
}
}
}
public static void main(String[] args) {
//启动两个并发线程
ThreadMethodYieldTest tyt1=new ThreadMethodYieldTest("高级");
//将tyt1线程设置成最高优先级
tyt1.setPriority(Thread.MAX_PRIORITY);
tyt1.start();
ThreadMethodYieldTest tyt2=new ThreadMethodYieldTest("低级");
//将tyt2线程设置成最低优先级
tyt2.setPriority(Thread.MIN_PRIORITY);
tyt2.start();
/*
关于sleep方法和yield方法
①sleep方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级:但yield()
方法只会给优先级相同,或优先级更高的线程执行机会。
②sleep方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态:而yield()不会将线程转入阻塞转态,
它只是强制当前线程进入就绪状态。因此完全有可能某个线程调用yield()方法暂停之后,立即再次获得处理器资源被执行。
③sleep方法声明抛出了InterruptedException异常,所以调用sleep()方法时要么捕捉该异常,要么
显示声明抛出该异常;而yield()方法则没有声明抛出任何异常。
④slee()方法比yield方法有更好的可移植性,通常不建议使用yield()方法来控制并发进程的执行。
*/
}
}
重点是这句话:yield()不会将线程转入阻塞转态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程调用yield()方法暂停之后,立即再次获得处理器资源被执行。
.
四、线程的生命周期
JDK中用Thread.State类定义了线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
- 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源。
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能。
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态。
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束。
五、线程的同步
先看如下买票代码:
class BuyTicketThread implements Runnable {
private int ticketNum=10;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(ticketNum>0){
System.out.println("我在"+Thread.currentThread().getName()+"买到了北京到哈尔滨的第"+(ticketNum--) +"张车票");
}
}
}
}
public class TestTicket{
public static void main(String[] args) {
BuyTicketThread bt=new BuyTicketThread();
Thread t1=new Thread(bt);
t1.setName("窗口1");
Thread t2=new Thread(bt);
t2.setName("窗口2");
Thread t3=new Thread(bt);
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
出现问题(继承和实现都会出现下面问题):
1、出现了两张10号票或者3张10号票。
2、出现了0 -1 -2号票
重票分析:
错票分析:
上面的代码出现问题:出现了重票,错票----》线程安全引起的问题
原因:多个线程,在争抢资源的过程中,导致共享的资源出现问题。一个线程还没执行完,另一个线程就参与进来了,开始争抢。
解决方式:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。–Java 中通过加**“锁”(同步机制、同步监视器)**
方式一:使用同步代码块
class BuyTicketThread implements Runnable {
private int ticketNum=10;
@Override
public void run() {
synchronized (this){//把具有安全隐患的代码锁住即可。如果锁多了,就会效率低--this就是这个锁
for (int i = 0; i < 100; i++) {
if(ticketNum>0){
System.out.println("我在"+Thread.currentThread().getName()+"买到了北京到哈尔滨的第"+(ticketNum--) +"张车票");
}
}
}
}
}
public class TestTicket{
public static void main(String[] args) {
BuyTicketThread bt=new BuyTicketThread();
Thread t1=new Thread(bt);
t1.setName("窗口1");
Thread t2=new Thread(bt);
t2.setName("窗口2");
Thread t3=new Thread(bt);
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
运行结果
下面我们对通过继承Thread实现的抢票代码执行同样的操作。
class BuyTicketThread extends Thread {
private static int ticketNum=10;//多个对象共享10张票
//每个窗口都是一个线程对象;每个对象执行的代码放入run方法中
@Override
public void run() {
synchronized (this){
//每个窗口后面有100个人抢票
for (int i = 0; i < 100; i++) {
if(ticketNum>0){//对票数进行判断,票数大于零我们才抢票
System.out.println("我在"+Thread.currentThread().getName()+"买到了北京到哈尔滨的第"+(ticketNum--) +"张车票");
}
}
}
}
}
public class TestTicket{
public static void main(String[] args) {
BuyTicketThread t1=new BuyTicketThread();
t1.setName("窗口1");
BuyTicketThread t2=new BuyTicketThread();
t2.setName("窗口2");
BuyTicketThread t3=new BuyTicketThread();
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
运行结果如下:
说明并没有解决线程的安全问题,要想保证安全,锁必须只有一个,而上述程序我们锁用this的话指的是t1、t2、t3(this含义是哪个对象调用了该方法this就是哪个对象)。我们在synchronized中做个改变如下:用类的字节码,因为一个类只有一份。
运行如上代码发现没有出现问题。
同步代码块的总结:
总结1:认识同步监视器(锁子)—synchronized(同步监视器){ …}
-
必须是引用数据类型,不能是基本数据类型
-
也可以创建一个专门的同 步监视器,没有任何业务含义
-
一般使用共享资源做同步监视器即可
-
在同步代码块中不能改变同步监视器对象的引用
-
尽量不要String和包装类Integer做同步监视器
-
建议使用final修饰同步监视器
总结2:同步代码块的执行过程
1) 第一个线程来到同步代码块,发现同步监视器open状态,需要close,然后执行其中的代码
2)第一个线程执行过程中 ,发生了线程切换(阻塞 就绪),第一个线程失去了cpu,但是没有开锁open
3)第二个线程获取了Cpu,来到了同步代码块,发现同步监视器close状态,无法执行其中的代码,第二个线程也进入阻塞状态
4)第一个线程再次获取cpu,接着执行后续的代码;同步代码块执行完毕,释放锁open
5)第二个线程也再次获取cpu,来到了同步代码块,发现同步监视器open状态,拿到锁并且上锁,由阻塞状态进入就绪状态,再进入运行状态,重复第一个线程的处理过程(加锁)
强调:同步代码块中能发生Cpu的切换吗?能!!! 但是后续的被执行的线程也无法执行同步代码块(因为锁close)
总结3:其他
1)多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,其他线程无法访问其中的任何一个代码块
2)多个代码块使用了同一个同步监视器(锁),锁住了一个代码块的同时,也锁住了所有使用该锁的所有代码块,但是没有锁住使用其他同步监视器的代码块,其他线程有机会访问其他同步监视器的代码块。
方式二:使用同步方法
1、同步方法解决实现Runnable接口买票的方式
class BuyTicketThread implements Runnable{
//一共10张票:
private int tickets=10;//多个对象共享10张票
//每个窗口都是一个线程对象:每个对象执行的代码放入run方法中
@Override
public void run() {
//每个窗口后面有100个人在抢票:
for (int i = 0; i < 100; i++) {
buyTickets();
}
}
public synchronized void buyTickets(){//锁住的 同步监视器: this
if(tickets>0){
System.out.println("我在"+Thread.currentThread().getName()+"买到了北京到哈尔滨的"+tickets--+"张火车票");
}
}
}
public class Test{
public static void main(String[] args) {
BuyTicketThread bt=new BuyTicketThread();
Thread t1=new Thread(bt);
t1.setName("窗口1");
Thread t2=new Thread(bt);
t2.setName("窗口2");
Thread t3=new Thread(bt);
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
使用同样的方式对继承Thread的方式进行修改
运行结果:
并没有解决问题,因为synchronized锁住的是this,而对于继承方式来说,this代表的是不同的窗口对象。解决方法是在同步方法加static让其在内存中只有一份,那么它的同步监视器就是当前类名.class
同步方法的总结:
总结1:
- 不要将run()定义为同步方法
- 非静态同步方法的同步监视器是this 静态同步方法的同步监视器是 类名.class 字节码信息对象
- 同步代码块的效率要高于同步方法 原因:同步方法是将线程挡在了方法的外部,而同步代码块锁将线程挡在了代码块的外部,但是却是方法的内部
- 同步方法的锁是this,一旦锁住一个方法,就锁住了所有的同步方法;同步代码块只是锁住使用该同步监视器的代码块,而没有锁住使用其他监视器的代码块
**
对同步方法和同步代码块解决线程安全的总结:
多线程在争抢资源,就要实现线程的同步(就要进行加锁,并且这个锁必须是共享的,必须是唯一的。咱们的锁一般都是引用数据类型的。
方式三:lock锁
JDK1.5后新增新一代的线程同步方式:Lock锁
与采用synchronized相比,lock可提供多种锁方案,更灵活
synchronized是Java中的关键字,这个关键字的识别是靠JVM来识别完成的呀。是虚拟机级别的。
但是Lock锁是API级别的,提供了相应的接口和对应的实现类,这个方式更灵活,表现出来的性能优于之前的方式。
**
*
* 解决线程安全问题的方式三:Lock锁---JDK5.0新增
*
* 1.面试题:synchronized 与 Lock的异同
* 相同:二者都可以解决线程安全问题
* 不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
* Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())
* 2.优先使用顺序:
* Lock 同步代码块(已经进入了方法体,分配了相应资源) 同步方法(在方法体之外)
*
*
* 面试题:如何解决线程安全问题?有几种方式
*
* @author 刘瘦瘦
* @create 2021-04-01-21:08
*/
public class LockTestThread extends Thread {
private int tickets=100;
//1.实例化ReentrantLock
private ReentrantLock lock=new ReentrantLock();
@Override
public void run() {
while (true){
//2.调用锁定方法lock()
lock.lock();
try {
if(tickets>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"获得票,票号为"+tickets);
tickets--;
}else {
break;
}
}finally {
//3.调用解锁方法:unlock()
lock.unlock();
}
}
}
public static void main(String[] args) {
LockTestThread l1=new LockTestThread();
Thread t1 = new Thread(l1,"窗口1");
Thread t2 = new Thread(l1,"窗口2");
Thread t3 = new Thread(l1,"窗口3");
t1.start();
t2.start();
t3.start();
}
}
【3】 Lock和synchronized的区别
1.Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁
2.Lock只有代码块锁,synchronized有代码块锁和方法锁
3.使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
【4】优先使用顺序:
Lock----同步代码块(已经进入了方法体,分配了相应资源)----同步方法(在方法体之外)
六、死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
public class DeadLock1 {
public static void main(String[] args) {
StringBuilder str1=new StringBuilder();
StringBuilder str2=new StringBuilder();
//创建一个线程
new Thread(){
@Override
public void run() {
synchronized (str1){
str1.append('a');
str2.append('c');
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (str2) {
System.out.println(str1);
System.out.println(str2);
}
}
}
}.start();
//创建一个线程
new Thread(new Runnable() {
@Override
public void run() {
synchronized (str2) {
str1.append('b');
str2.append('d');
synchronized (str1) {
System.out.println(str1);
System.out.println(str2);
}
}
}
}).start();
}
}
运行结果:
原因分析:本程序中使用的是相同的监视器。比如cpu先执行如下这个线程,我们称这个线程为线程1
线程1拿到str1这把锁,进入到同步代码块中,此时线程睡了10毫秒,那么此时cpu可能切换到下面的线程,我们成为线程2
线程2的同步代码块需要str2这把锁,而此时这把锁还没被占用,所以此时线程2继续执行。当执行到如下的代码处时:
它需要str1这把锁,而这把锁str1正在使用,所以该线程就在这阻塞等着str1这把锁释放。比如此时cpu又切换到线程1,执行到如下代码之处
它需要str2这把锁,而这把锁正被线程2使用,所以他等着线程2释放str2这把锁,就这样线程1等着线程2释放str2锁,线程2等着线程1释放str1这把锁,两个线程就阻塞在这了。
演示二:
class A {
public synchronized void foo(B b) { //同步监视器:A类的对象:a
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了A实例的foo方法"); // ①
// try {
// Thread.sleep(200);
// } catch (InterruptedException ex) {
// ex.printStackTrace();
// }
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用B实例的last方法"); // ③
b.last();
}
public synchronized void last() {//同步监视器:A类的对象:a
System.out.println("进入了A类的last方法内部");
}
}
class B {
public synchronized void bar(A a) {//同步监视器:b
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了B实例的bar方法"); // ②
// try {
// Thread.sleep(200);
// } catch (InterruptedException ex) {
// ex.printStackTrace();
// }
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用A实例的last方法"); // ④
a.last();
}
public synchronized void last() {//同步监视器:b
System.out.println("进入了B类的last方法内部");
}
}
public class DeadLock implements Runnable {
A a = new A();
B b = new B();
public void init() {
Thread.currentThread().setName("主线程");
// 调用a对象的foo方法
a.foo(b);
System.out.println("进入了主线程之后");
}
public void run() {
Thread.currentThread().setName("副线程");
// 调用b对象的bar方法
b.bar(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args) {
DeadLock dl = new DeadLock();
new Thread(dl).start();
dl.init();
}
}
解决死锁方法:减少同步资源的定义,避免嵌套同步
线程同步的优缺点
1、线程安全,效率低;线程不安全,效率高
2、可能造成死锁现象。
七、线程通信
线程通信例子1:使用两个线程打印1-100。线程1,线程2 交替打印
class Number implements Runnable{
private int number=1;
private Object obj=new Object();
@Override
public void run() {
while(true){
synchronized (this){
notify();
if(number<=100){
try {
Thread.sleep(100);
} 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 ThreadCommunication{
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();
}
}
线程通信例子2:生产者消费者问题
实现要求:生成者和消费者要交替输出
分解1:
创建Product类
public class Product {//商品类
//品牌
private String brand;
//名字
private String name;
//setter getter方法;
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
创建生成者线程
public class ProducerThread extends Thread{//生产者线程
//共享商品
private Product p;
public ProducerThread(Product p){
this.p=p;
}
@Override
public void run() {
//生产十个商品 i:生产的次数
for (int i = 1; i <= 10; i++) {
if(i%2==0){
//生产费列罗巧克力
p.setBrand("费列罗");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
p.setName("巧克力");
}else{
//生产哈尔滨啤酒
p.setBrand("哈尔滨");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
p.setName("啤酒");
}
//将生产信息做一个打印:
System.out.println("生产者生产了:" + p.getBrand() + "---" + p.getName());
}
}
}
创建消费者线程
public class CustomerThread extends Thread {//消费者线程
//共享商品:
private Product p;
public CustomerThread(Product p){
this.p=p;
}
@Override
public void run() {
for (int i = 1; i <=10 ; i++) {//i消费次数
System.out.println("消费者消费了:" + p.getBrand() + "---" + p.getName());
}
}
}
测试类
public class Test {
//这是main方法,程序的入口
public static void main(String[] args) {
//共享的商品:
Product p = new Product();
//创建生产者和消费者线程:
ProducerThread pt = new ProducerThread(p);
CustomerThread ct = new CustomerThread(p);
pt.start();
ct.start();
}
}
出现的问题:
1、生产者和消费者没有交替输出(我们的要求是要交替输出)
2、打印数据错乱:
哈尔滨–null
费列罗啤酒
哈尔滨巧克力
----》原因没有加同步
分解2:解决线程安全
方式一:使用同步代码块
生产者类
public class ProducerThread extends Thread{//生产者线程
//共享商品
private Product p;
public ProducerThread(Product p){
this.p=p;
}
@Override
public void run() {
//生产十个商品 i:生产的次数
for (int i = 1; i <= 10; i++) {
synchronized (p){
if(i%2==0){
//生产费列罗巧克力
p.setBrand("费列罗");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
p.setName("巧克力");
}else{
//生产哈尔滨啤酒
p.setBrand("哈尔滨");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
p.setName("啤酒");
}
//将生产信息做一个打印:
System.out.println("生产者生产了:" + p.getBrand() + "---" + p.getName());
}
}
}
}
消费者类
public class ProducerThread extends Thread{//生产者线程
//共享商品
private Product p;
public ProducerThread(Product p){
this.p=p;
}
@Override
public void run() {
//生产十个商品 i:生产的次数
for (int i = 1; i <= 10; i++) {
synchronized (p){
if(i%2==0){
//生产费列罗巧克力
p.setBrand("费列罗");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
p.setName("巧克力");
}else{
//生产哈尔滨啤酒
p.setBrand("哈尔滨");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
p.setName("啤酒");
}
//将生产信息做一个打印:
System.out.println("生产者生产了:" + p.getBrand() + "---" + p.getName());
}
}
}
}
方式二:使用同步方法只需在Product中写就行
分别在生产者类和消费者类把run方法中的代码剪切到一个同步方法中可不行,因为此时的同步监视器是this(即当前类的对象),这样的话生产者和消费者的同步方法中用的就不是同一个监视器了,没有解决线程安全。
public class Product {//商品类
//品牌
private String brand;
//名字
private String name;
//setter getter方法;
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public synchronized void setProduct(String brand,String name){
this.setBrand(brand);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setName(name);
System.out.println("生产者生产了:" + brand + "---" + name);
}
//消费商品:
public synchronized void getProduct(){
System.out.println("消费者消费了:" + this.getBrand() + "---" + this.getName());
}
}
运行结果如下:
线程安全问题解决,但是先执行生产者,在执行消费者还没实现,而实现该功能就要用到线程之间的通信了。
分解3:实现线程之间通信
就如我们在饭店吃饭一样,当我们付完款之后,服务员会给我们一个号,如果饭做好了服务员叫号,我们就去端饭,如果没叫到我们的号我们就等着;类比,如果生成者生产好了产品把灯泡置为红色,生产者停下来,通知消费者进行消费,否则的话,生产者就生产;生产者如果看到是绿色说明还没做好饭,那么消费者就等着,让生产者进行生产,否则的话消费者消费掉商品,并通知生产者进行生产。
只需要更改product中的方法即可
public class Product {//商品类
//品牌
private String brand;
//名字
private String name;
//setter getter方法;
//true:红色 false:绿色
boolean flag=false;//默认情况下没有商品 让生产者先生产 然后消费者再消费
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public synchronized void setProduct(String brand,String name){
if(flag==true){//灯是红色,证明有商品,生产者不生产,等着消费者消费
try {
wait();//wait一定是同步监视器在调用
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//这里千万不要加else
/*
原因:
在Java对象中,有两种池
锁池-----------synchronized
等待池---------wait(),notify(),notifyAll()
如果一个线程调用了某个对象的wait方法,那么该线程进入到对象的额等待池中(并且已经将锁释放),
如果未来的某一个时刻,另外一个线程调用了相同对象的notify方法或者notifyAll()方法,
那么该等待池中的线程就会被唤起,然后进入到对象的锁池里去获得该对象的锁,
如果获得锁成功后,那么该线程就会沿着wait方法之后的路径继续执行。注意是沿着wait方法之后
*/
//灯是绿色的,就生产
this.setBrand(brand);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setName(name);
//将生产信息做一个打印
System.out.println("生产者生产了:" + brand + "---" + name);
//生产完以后,灯变色,变成红色
flag=true;
//告诉消费者赶紧来消费
notify();
}
//消费商品:
public synchronized void getProduct(){
if(!flag){//flag==false没有商品,等待生产者生产
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//此处不能加else,原因同上
//有商品 消费
System.out.println("消费者消费了:" + this.getBrand() + "---" + this.getName());
//消费完:灯变色
flag=false;
//通知生产者生产
notify();
}
}
方法 | 描述 |
---|---|
wait() | 一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。 |
notify() | 一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个 |
notifyAll() | 一旦执行此方法,就会唤醒所有被wait的线程。 |
说明:
1、wait方法和notify方法 是必须放在同步方法或者同步代码块中才生效的 (因为在同步的基础上进行线程的通信才是有效的)
2、wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同
步方法中的同步监视器。否则,会出现IllegalMonitorStateException异常
3、wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
面试题:sleep()和wait()的异同?
1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
不同点:1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
3)如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
Lock锁情况下的线程通信
问题:
由以上知识知道生产者和消费者轮流持有锁。谁持有了这个锁,另一个线程就进入到等待池中。比如说生产者已经生产出商品了,那么我就不需要在生产了,就进入到等待池中进行等待;如果是消费者的话,现在想进行消费,但发现没有商品的话,我还在同一个等待池中进行等待。所以我们发现,生产者消费者等待时都在同一个等待池中进行等待。比如当前等待池中有一个生产者P1、消费者c1,那么这个等待池要么p1用,要么c1用,此时没啥关系。如果现在等待池中有多个生产者、消费者。比如现在存在4个线程分别是生产者:p1、p2和消费者c1和c2。目前p1持有这锁了,那么p2 c1 c2都在同一个线程池中等待,然后p1释放锁了,要通知等待池中的线程持有这个锁,但是p1通知的时候线程池里的生产者p1和消费者c1和c2都有资格拿到这个锁。如我本应该唤醒的是一个消费者,确有可能唤醒的是生产者c1和c2,那么这个情况就不友好了。我们上面的刚好是一个生产者一个消费者,所以他们没有问题,当有多个生产者、消费者就有问题了。
解决:就是生产者p1、p2一个等待池,c1、c2一个等待池,比如p1持有这个锁进行生产,生产完之后,它应该通知c1、c2这个等待池。所以我们应该解决把生产者和消费者分别放置子两个不同的池子中,lock锁可以解决该问题。结构如图。
lock锁解决上述问题代码如下:
product类代码修改如下:
/**
* lock锁实现多个线程池
* @author 刘瘦瘦
* @create 2021-04-05-16:20
*/
public class Product {//商品类
//品牌
private String brand;
//名字
private String name;
//声明一个lock锁:
Lock lock=new ReentrantLock();
//搞一个生产者的等待队列
Condition produceCondition=lock.newCondition();
//搞一个消费者的等待队列
Condition consumeCondition=lock.newCondition();
//true:红色 false:绿色
boolean flag=false;//默认情况下没有商品 让生产者先生产 然后消费者再消费
//setter getter方法;
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setProduct(String brand,String name){
//加锁操作
lock.lock();
try {
if(flag==true){//灯是红色,证明有商品,生产者不生产,等着消费者消费
try {
// wait();//wait一定是同步监视器在调用
//生产者等待,生产者线程进入等待队列
produceCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//这里千万不要加else
/*
原因:
在Java对象中,有两种池
锁池-----------synchronized
等待池---------wait(),notify(),notifyAll()
如果一个线程调用了某个对象的wait方法,那么该线程进入到对象的额等待池中(并且已经将锁释放),
如果未来的某一个时刻,另外一个线程调用了相同对象的notify方法或者notifyAll()方法,
那么该等待池中的线程就会被唤起,然后进入到对象的锁池里去获得该对象的锁,
如果获得锁成功后,那么该线程就会沿着wait方法之后的路径继续执行。注意是沿着wait方法之后
*/
//灯是绿色的,就生产
this.setBrand(brand);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setName(name);
//将生产信息做一个打印
System.out.println("生产者生产了:" + brand + "---" + name);
//生产完以后,灯变色,变成红色
flag=true;
//告诉消费者赶紧来消费
// notify();
consumeCondition.signal();
}finally {
//关闭锁
lock.unlock();
}
}
//消费商品:
public void getProduct(){
//加锁
lock.lock();
try {
if(!flag){//flag==false没有商品,等待生产者生产
try {
//消费者等待,消费者线程进入等待队列
consumeCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//此处不能加else,原因同上
//有商品 消费
System.out.println("消费者消费了:" + this.getBrand() + "---" + this.getName());
//消费完:灯变色
flag=false;
//通知生产者生产
// notify();
produceCondition.signal();
}finally {
lock.unlock();
}
}
}
Condition是在Java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition1的await()、signal()这种方式实现线程间协作更加安全和高效。
它的更强大的地方在于:能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition
一个Condition包含一个等待队列。一个Lock可以产生多个Condition,所以可以有多个等待队列。
在Object的监视器模型上,一个对象拥有一个同步队列和等待队列, 而Lock(同步器)拥有一个同步队列(锁池)和多个等待队列。
Object中的wait(),notify(),notifyAll()方法是和"同步锁"(synchronized关键字)捆绑使用的;而Condition是需要与"互斥锁"/"共享锁"捆绑使用的。
调用Condition的await()、signal()、signalAll()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
· Conditon中的await()对应Object的wait();
· Condition中的signal()对应Object的notify();
· Condition中的signalAll()对应Object的notifyAll()。
void await() throws InterruptedException
造成当前线程在接到信号或被中断之前一直处于等待状态。
与此 Condition 相关的锁以原子方式释放,并且出于线程调度的目的,将禁用当前线程,且在发生以下四种情况之一 以前,当前线程将一直处于休眠状态:
· 其他某个线程调用此 Condition 的 signal() 方法,并且碰巧将当前线程选为被唤醒的线程;或者
· 其他某个线程调用此 Condition 的 signalAll() 方法;或者
· 其他某个线程中断当前线程,且支持中断线程的挂起;或者
· 发生“虚假唤醒”
在所有情况下,在此方法可以返回当前线程之前,都必须重新获取与此条件有关的锁。在线程返回时,可以保证它保持此锁。
void signal()
唤醒一个等待线程。
如果所有的线程都在等待此条件,则选择其中的一个唤醒。在从 await 返回之前,该线程必须重新获取锁。
void signalAll()
唤醒所有等待线程。
如果所有的线程都在等待此条件,则唤醒所有线程。在从 await 返回之前,每个线程都必须重新获取锁。