前言
本文为作者学习过程中所写,全文共计约三万八千余字,讲述了线程的概念、状态、基本操作、创建方法、调度、同步、死锁、通信等知识,结合案例和个人理解做出了一定分析,基本涵盖了线程的所有基础知识以及可能出现的问题,可能由于疏漏以及个人理解的问题。若你发现文中有知识错误或者知识点遗漏,烦请及时指正,以便及时修改,避免误导他人。若感觉文章对你有所帮助,可以点赞关注,后续还出继续更新Java有关的知识。
个人主页:👉👉👉点这里👈👈👈
目录
🌼7.2 Lock锁的使用方法(ReentrantLock)
🌻7.2.3 tryLock()和tryLock(long time, TimeUnit unit)
🌼9.2 生产者/消费者问题——wait()/notify()机制
🌷一、线程的概念
🌼1.1 进程与线程
程序是一顿静态的代码,是应用程序执行的蓝本。进程就是程序的一次动态执行,对应了从代码加载、执行至执行完毕的一个完整的过程,或者说,进程就是程序在处理机中的一次运行。一个进程既包括多要执行的指令,也包括执行指令所需的任何系统资源,如CPU、内存空间、I/O端口等,不同进程所占用的系统资源相对独立。简单来说,我打开一个Word,就产生了一个进程,我再打开eclipse,又产生了另一个进程,即我们所见的应用都是一个进程。打开任务管理器,点击“进程”,发现我们的电脑上时刻都有很多进程正在执行。所以说进程的转态就只有就绪、运行和死亡三种。
线程就是进程执行过程中产生的多条执行线索,是比进程单位更小的执行单位,是进程的一个执行单元,是进程内调度的实体。线程自身不能自动运行,必须栖身于某一进程之中,由进程触发执行,属于同一进程的所有线程共享该进程的系统资源,但是线程直接的切换速度比进程切换要快得多。
通常一个进程中可以包含多个线程,并且至少要有一个线程。在程序运行时即便没有自己创建线程,后台也会有多个线程,例如主线程(main())和gc线程(JVM提供的垃圾回收线程).
✨在Java中。线程可以认为是由三部分组成的:
(1)虚拟CPU:封装在java.lang.Thread中,控制着整个线程的运行;
(2)执行的代码:传递给Thread类,由Thread类控制顺序执行;
(3)处理的数据:传递给Thread类,是在代码执行过程中所要处理的数据。
🌼1.2 多线程
多线程就是指在同一时间有多个线程被同时执行,但是在微观上一颗内核CPU一个时刻只能执行一个线程,所以要实现多线程就需要多态计算机同时工作或者需要多核CPU来执行程序。单核CPU上所谓的多线程是假的多线程,同一时刻只会处理一段逻辑,只是因为线程之间切换的比较快,让人看上去有一种多个线程“同时”执行的假象。多核CPU的多线程才是真正的多线程,同时可以处理多个逻辑,因此多线程可以使多核CPU发挥出其优势,达到充分利用CPU的目的。
✨多线程的优势:
(1)编程简单,效率高
(2)适合于开发服务程序,如Web服务,聊天服务等
(3)可以减轻编写交互频繁、涉及面多的程序的困难
(4)利于发挥多核CPU的优势
(5)能够有效防止阻塞
🌷二、线程的状态
在官方文档中,线程有6中状态:创建(new)、运行(runnable)、阻塞(blocked)、等待(waiting)、计时等待(timed_waiting)和终止(terminated)。
(1)创建(NEW):新创建了一个线程对象,但还没有调用start()方法。
(2)运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
(3)阻塞(BLOCKED):表示线程阻塞于锁。
(4)等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
(5)计时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
(6)终止(TERMINATED):表示该线程已经执行完毕或者遇到异常退出。
✨线程的生命周期:
🌷三、线程的操作
🌷四、线程创建的方法
🌼4.1 继承Thread类
🎈实现步骤:
(1)定义一个类MyThread继承Thread类
(2)在MyThread类中重写run()方法
(3)创建MyThread类的对象
(4)启动线程
✨(1)定义MyThread类继承Thread类并重写run()方法
run()方法中的代码块就是我们启动线程后执行的内容
public class MyThread extends Thread{
@Override
public void run() {
for(int i=0;i<10;i++) {
System.out.println(getName()+"-->"+i);
}
}
}
上述代码中getName()的做事用是获取当前线程的名字。其完整的书写方式应为:Thread.currentThread().getName(),但是MyThread类本身是直接继承Thread类的,所以可以简写为:getName()。但是利用其他方式创建线程,获得当前线程名时就需要写完整而不能简写。
✨(2) 创建MyThread类的对象并启动线程
public void thread1() {
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
my1.start();//开启线程
my2.start();
}
🎈输出如下:
我们可以看到每个线程都从0执行到了9.
✨(3)设置线程名称
设置线程名我们有两种方法:利用setName()函数和在创建线程时命名。
🎈利用setName()函数设置线程名称
public void thread1() {
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
my1.setName("飞机");//设置线程名称
my2.setName("火箭");
my1.start();//开启线程
my2.start();
}
🎈 输出如下:
🎈在创建线程时设置线程名称
如果我们需要在创建线程时设置线程名称的话还需要在继承Thread类的MyThread类中添加一个带参构造方法。
public class MyThread extends Thread{
public MyThread() {
// TODO 自动生成的构造函数存根
}
//添加了这个带参构造方法才能在创建对象时给线程重命名
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for(int i=0;i<10;i++) {
System.out.println(getName()+"-->"+i);
}
}
}
public void thread1() {
//要想在创建对象时就重命名,需要在MyThread写一个带参构造方法
MyThread my1 = new MyThread("飞机");
MyThread my2 = new MyThread("火箭");
my1.start();//开启线程
my2.start();
}
🎈输出如下:
🌼4.2 实现Runnable接口
🎈实现步骤:
(1)定义一个类MyRunnable实现Runnable接口
(2)在MyRunnable类中重写run()方法
(3)创建MyRunnable类的对象
(4)创建Thread类的对象,把MyRunnable对象作为构造方法的参数
(5)启动线程
✨(1)定义MyRunnable类并重写run()方法
public class MyRunnable implements Runnable{
@Override
public void run() {
for(int i=0;i<5;i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);//注意在这里不能直接使用getName方法
}
}
}
✨(2)创建并启动线程
public void thread2() {
//创建MyRunnable类的对象
MyRunnable my = new MyRunnable();
//创建Thread类的对象,把MyRunnable对象作为构造方法的参数
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
t1.start();
t2.start();
}
🎈输出如下:
✨(3)设置线程名
通过实现Runnable接口创建线程时可以直接在创建对象的同时设置线程名,而不用添加构造函数
public void thread2() {
//创建MyRunnable类的对象
MyRunnable my = new MyRunnable();
//创建对象的同时设置线程名称
Thread t1 = new Thread(my,"高铁");
Thread t2 = new Thread(my,"飞机");
t1.start();
t2.start();
}
🎈 输出如下:
🌼4.3 实现Callable接口
🎈实现步骤:
(1)创建一个实现Callable的实现类
(2)实现call方法,将此线程需要执行的操作声明在call()中
(3)创建Callable接口实现类的对象
(4)将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
(5)将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
(6)获取Callable中call方法的返回值
✨(1)创建MyCallable类并重写call()方法
注意:这里的call()方法与前面的run()方法属于同一性质,里面的语句都是开启线程后执行的内容;实现Callable类需要一个返回值,并且支持指定泛型。
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String>{//注意设置返回值的泛型
@Override
public String call() throws Exception {
for(int i= 0;i<5;i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
return "执行完毕!";
}
}
✨(2)创建线程并启动
public void thread3() throws InterruptedException, ExecutionException {
MyCallable my = new MyCallable();
FutureTask<String> mycall1 = new FutureTask<String>(my);
Thread th1 = new Thread(mycall1);
th1.start();
System.out.println(mycall1.get());//获取返回值
}
🎈输出如下:
✨(3)利用Callable创建多线程
Callable和Runnable的使用方法形式上基本一致,差别就是Callable多了返回值,但是有一点要注意,就是在创建多线程时,Runnable只需要创建一个对象,可以用多个Thread对象接收一个Runnable对象实现多线程,但是利用Callable创建多线程时,如果使用多个Thread对象接收同一个FutureTask对象时,始终只能有一个线程执行,如要要实现多线程就要创建多个FutureTask对象。
🎈多个Thread对象接收一个FutureTask对象,发现始终只有一个线程执行
public void thread3() throws InterruptedException, ExecutionException {
MyCallable my = new MyCallable();
FutureTask<String> mycall1 = new FutureTask<String>(my);
Thread th1 = new Thread(mycall1);
Thread th2 = new Thread(mycall1);
th1.start();
th2.start();
System.out.println(mycall1.get());//获取返回值
}
🎈 输出如下:
🎈创建多个线程,每个都由一个新的FutureTask对象接收
public void thread3() throws InterruptedException, ExecutionException {
MyCallable my = new MyCallable();
FutureTask<String> mycall1 = new FutureTask<String>(my);
FutureTask<String> mycall2 = new FutureTask<String>(my);
Thread th1 = new Thread(mycall1);
Thread th2 = new Thread(mycall1);
th1.start();
th2.start();
System.out.println(mycall1.get());//获取返回值
System.out.println(mycall2.get());//获取返回值
}
🎈输出如下:
✨(4)设置线程名
public void thread3() throws InterruptedException, ExecutionException {
MyCallable my = new MyCallable();
FutureTask<String> mycall1 = new FutureTask<String>(my);
FutureTask<String> mycall2 = new FutureTask<String>(my);
Thread th1 = new Thread(mycall1,"飞机");
Thread th2 = new Thread(mycall2,"汽车");
th1.start();
th2.start();
System.out.println(mycall1.get());//获取返回值
System.out.println(mycall2.get());//获取返回值
}
🎈输出如下:
🌼4.4 通过线程池创建
🎈实现步骤:
(1)以方式二或方式三创建好实现了Runnable接口的类或实现Callable的实现类
(2)实现run或call方法
(3)创建线程池
(4)调用线程池的execute方法执行某个线程,参数是之前实现Runnable或Callable接口的对象
在这里我们使用之前已经实现好了的Runnable接口的类和实现Callable的实现类。
public void thread4() {
//1. 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//2. 执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new MyRunnable());//适用于Runnable
service.execute(new MyRunnable());//适用于Runnable
// service.submit(new MyCallable());//适合使用于Callable
//3. 关闭连接池
service.shutdown();
}
🎈输出如下:
🌼4.5 各种创建线程方法特点
✨Thread和Runnable的比较:
* 开发中优先选择实现Runnable接口的方式
原因:(1)实现的方式没有类的单继承性的局限性
(2)实现的方式更适合来处理多个线程有共享数据的情况
相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中
✨ 实现Callable接口的方式创建线程的强大之处:
(1) call()可以有返回值的
(2)call()可以抛出异常,被外面的操作捕获,获取异常的信息
(3)Callable是支持泛型的
✨线程池好处:
(1)提高响应速度(减少了创建新线程的时间)
(2)降低资源消耗(重复利用线程池中线程,不需要每次都创建)
(3)便于线程管理
🌷五、线程的调度
线程有两种调度模型:分时调度和抢占式调度。
分时调度模型:所有线程轮流拥有CPU的使用权,平均分配每个线程占用CPU的时间片
抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么就随机选择一个,优先级高的线程获取的CPU时间片相对多一点。
Java的线程调度通常是抢占式。
🌼5.1 优先级策略
✨Java的线程调度通常采用如下的优先级策略:
(1)优先级高的先执行,优先级低的后执行;
(2)多线程系统会自动为每个线程分配一个优先级,默认时,继承其父类的优先级;
(3)紧急任务的线程其优先级较高;
(4)同优先级的线程按“先进先出”的原则;
(4)优先级越高代表其被CPU运行的机会越高,并非独占CPU。
✨Java中有三个与线程优先级有关的静态量:
MAX_PRIORITY:最高优先级,值为10
MIN_PRIORITY:最低优先级,值为1
NORM_PRIORITY:默认(平均)优先级,值为5
System.out.println(Thread.MAX_PRIORITY);//最大优先级
System.out.println(Thread.MIN_PRIORITY);//最小优先级
System.out.println(Thread.NORM_PRIORITY);//平均优先级
🎈输出如下:
🌻5.1.1 获得当前线程优先级
public static void main(String[] args) throws InterruptedException {
MyThread m1 =new MyThread();
MyThread m2 =new MyThread();
MyThread m3 =new MyThread();
m1.setName("飞机");//设置线程名称
m2.setName("火箭");
m3.setName("汽车");
//pubLic finaL int getPpiority():返回此线程的优先级
System.out.println(m1.getPriority());
System.out.println(m1.getPriority());
System.out.println(m1.getPriority());
m1.start();
m2.start();
m3.start();
}
🎈输出如下:
可见线程的默认优先级为5。
🌻5.1.2 设置优先级
//public final void setPriority(int newPriority):更改此线程的优先级
//优先级越高只是代表轮到的机会越高(被CPU运行的机会越高),并非独占CPU
m1.setPriority(3);
m2.setPriority(10);
m3.setPriority(1);
🎈输出如下:
🌼5.2 用户线程和守护线程
用户线程:平时所用到的普通线程都是用户线程,当在一个Java程序中创建一个线程,它默认就是用户线程,也可以称之为主线程。
守护线程:指在程序运行时在后台提供服务的线程,它依赖于用户线程而存在。
区别:用户线程是独立存在的,不会因为其他用户线程的退出而退出;守护线程是依赖于用户线程存在的,当JVM中所有的线程都是守护线程时,JVM退出(注意:是逐渐退出,而不是立马退出)。
✨设置用户线程和守护线程的方法:
setDaemon(boolean):参数为true时表示该线程为守护线程,为false时表示为用户线程,不调用此方法时线程默认为用户线程。
将MyThread中的for循环次数改为50次,m1设为用户线程,其他设置为守护线程
public static void main(String[] args) throws InterruptedException {
MyThread m1 =new MyThread();
MyThread m2 =new MyThread();
MyThread m3 =new MyThread();
m1.setName("飞机");//设置线程名称
m2.setName("火箭");
m3.setName("汽车");
// //设置守护线程:主线程执行完毕后,守护线程会很快退出(不会立刻退出)
m1.setDaemon(false);//false表示为用户线程,不设参数默认为用户线程
m2.setDaemon(true);
m3.setDaemon(true);
m1.start();
m2.start();
m3.start();
}
🎈输出如下:
通过输出可以看到当飞机线程执行完毕后火箭和汽车线程没有立马退出,而是又执行了一段才逐渐退出。
🌼5.3 yield()
yied()方法的作用是:让当前处于运行状态的线程回到可运行状态,让出抢占CPU的机会。
就好像跑步比赛时,使用yield方法后一个人突然被传送到了起点,那么其他人赢得机会就提高了。
🎈MyThread类修改如下:
@Override
public void run() {
for(int i=0;i<4;i++) {
Thread.yield();
System.out.println(getName()+"-->"+i);
}
}
public static void main(String[] args) throws InterruptedException {
MyThread m1 =new MyThread();
MyThread m2 =new MyThread();
MyThread m3 =new MyThread();
m1.setName("飞机");//设置线程名称
m2.setName("火箭");
m3.setName("汽车");
m1.start();
m2.start();
m3.start();
}
🎈输出如下:
🌼5.4 isAlive()
作用:判断当前线程是否存活
🎈只启动m1和m2,利用isAlive()检测其状态
public static void main(String[] args) throws InterruptedException {
MyThread m1 =new MyThread();
MyThread m2 =new MyThread();
MyThread m3 =new MyThread();
m1.setName("飞机");//设置线程名称
m2.setName("火箭");
m3.setName("汽车");
m1.start();
m2.start();
// m3.start();
System.out.println(m1.isAlive());
System.out.println(m3.isAlive());
}
🎈输出如下;
🌼5.5 sleep()
作用:让线程休眠一段时间
🎈 run()方法修改如下:
@Override
public void run() {
try {
long start = System.currentTimeMillis();
Thread.sleep(1000);//当前正在执行的线程休眠,可能是m1,m2,m3其中的任意一个
long end = System.currentTimeMillis();
System.out.println("此次休眠了:"+ (end - start) +"ms");
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
for(int i=0;i<5;i++) {
System.out.println(getName()+"-->"+i);
}
}
🎈 输出如下:
因为有三个线程,每次线程启动时都要休眠,所以共有三次休眠输出。
🌼5.6 join()
作用:导致当前的线程等待,直到join方法调用的线程m1执行完毕后在执行其他线程
🌷六、线程同步
🌼6.1 线程安全
线程安全问题发生在多个线程共享数据的时候,当多个线程同时对共享的数据进行写操作的时候就可能发生数据冲突问题,也就是线程安全问题。举个例子,某商场只有一个更衣室,没有门,只有一个帘子遮挡,更衣室只有一个,但需要试衣服的人有很多,但是由于没有门,谁也不知道里面有没有人,如果里面有人,你不知道但是你又闯进去了,那么会出安全事故,你就变成了流氓。
🌻6.1.1 原子性
访问(读,写)某个共享变量的时候,从其他线程来看,该操作要么已经执行完毕,要么尚未发生。其他线程看不到当前操作的中间结果。 访问同一组共享变量
的原子操作是不能够交错的,如现实生活中从ATM取款。
🌻6.1.2 可见性
一个线程对共享变量更新之后,后续访问该变量的其他线程可以读到更新的结果。多线程程序因为可见性问题可能会导致其他线程读取到旧数据(脏数据)。
🌻6.1.3 有序性
如果在本线程内观察,所以操作都是有序的。如果从另一个线程观察该线程,所有操作都是无序的。
🌼6.2 概念解释
✨同步: 顺序执行,执行完一个再执行下一个,需要等待,协调运行。
✨异步:彼此独立,在等待某事件的过程中继续做自己的事情,不需要等待这件事完成后载工作。
多线程就是实现异步的一个方式,但异步不等于多线程,异步时最终摸底,多线程只是实现异步的一种手段。
✨线程同步:多线程通过特定的设置(如互斥量、事件对象、临界区)来控制线程之间的执行顺序(即所谓的同步),若没有同步,则线程之间是各自运行各自的,同步可以理解为多个线程为一个整体,所有数据共享,操作同步。
✨线程互斥:指对于共享的进程系统资源,在各单个线程访问是的排他性,当有多个线程都要使用某一资源时,任何时刻最多只允许一个线程使用,其他要使用该资源的线程必须等待,直到占用资源者释放资源。(线程互斥可以理解为一种特殊的线程同步)
线程同步可以理解为线程按照一定的顺序执行,线程同步就是为了解决线程安全问题。
🌼6.3 线程安全实例
现假设有一款限量版鞋子要出售,全国限量100双,只在京东,淘宝和得物三个平台出售,设计一个程序,帮助广大民众抢鞋。
🌻6.3.1 设计思路
我们新建一个卖鞋类,重写run()方法,在方法里面我们只需要输出第几双鞋被卖出,替代卖鞋的操作,由于鞋子是限量的并且在三个平台同时出售,所以鞋子数量的变量必须为全局变量,被三个平台共享,因此我们可以这样定义:private static int shoes = 100;
此外,当鞋子有余量是才能卖出,因此我们需要判断,当鞋子余量大于0(shoes>0)时才能被卖出,每卖出一双鞋子数量便减一(shoes--),当鞋子余量小于0(shoes<0)时输出“鞋子卖完了”,因为抢鞋是一个持续过程,要一直运行,所需要用一个死循环将上述操作包含,并且当鞋子卖完时退出循环,即模拟停售。
由于科技的发展处理器的性能越来越好,有些时候一些问题在不同的电脑上可能不会出现,这时候我们可以适当增加鞋子的数量或添加sleep函数,增大事件发生的可能性。
🌻6.3.2 代码实现
public class SellShoesDemo {
public static void main(String[] args) {
SellShoes ss = new SellShoes();
Thread t1 = new Thread(ss, "京东");
Thread t2 = new Thread(ss, "淘宝");
Thread t3 = new Thread(ss, "得物");
t1.start();
t2.start();
t3.start();
}
}
//卖鞋类实现
class SellShoes implements Runnable{
private static int shoes = 100;
@Override
public void run() {
while(true) {
if(shoes > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":卖出了第"+shoes+"双鞋子");
shoes--;
}else {
System.out.println(Thread.currentThread().getName()+":鞋子买完了!");
break;
}
}
}
}
🌻6.3.3 结果分析
🎈运行程序得到以下结果:
🎈出现的问题:
(1)有同一双鞋子被重复卖出;
(2)出现了第0双鞋子和第-1双鞋子。
🎈问题分析:
(1)这两个问题的出现都是由线程安全问题引起的,因为三个线程共享同一个数据,那么很有可能在某一时刻他们的数据都显示某一双鞋存在,于是都卖出了该鞋,例如在一开始的时候,三个平台看到的都是100双鞋的余量,可能某一个平台要先卖出于该鞋,但是由于是多个平台同时卖出,可能数据来没来得及更新,另一个平台又有人来买,此时这一个平台看到的数据还是100双的余量,于是就出现同一双鞋被重复卖出的现象。
(2)为什么会出现第0双鞋子和第-1双鞋子的情况呢,这是因为当还剩下最后一双鞋时,其中一个平台正在卖出该鞋,但由于数据库的余量还没更新,另一个平台看到还有余量,于是也想卖出该鞋,但是当进行到中间某个时刻的时数据库又更新了,此时的shoes就是执行减减操作后的数据,于是就出现了第0双第-1双鞋。
简单来说就是某一时刻数据库还未更新,但这三个线程都进入了该数据库,由于每个线程处理的进度存在差异,可能有的线程处理到一半时数据更新了,但此时该线程以及进入了if语句块,但还未执行输出操作,就导致有的线程虽然想卖出的是第1双鞋,但是实际卖出的是第0双。
🎈解决问题:
就像前面举的商场试衣间的例子,之所以会出现多个人进入同一个试衣间的情况是因为我不知道里面有没有人,那么要解决这个问题就需要让外面的人知道,这里已经有人了,你不能进,要等里面的人出来了再进。所以,最简单的方法就是给这个试衣间加个锁,有人进去了,门就被锁上了,外面的人打不开门就知道里面有人了,等里面的人出来了下一个进去,再锁上就可以了。因此在程序中我们也可以使用此方法,动态变化的数据量就类似于试衣间,只需要对这一块代码加上一个锁,那我在用的时候别人就用不了,就保证了数据的安全性。
🌼6.4 Synchronized锁
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。所谓“临界区”,指的是某一块代码区域,它同一时刻只能由一个线程执行。如果
synchronized`关键字在方法上,那临界区就是整个方法内部。而如果是使用synchronized代码块,那临界区就指的是代码块内部的区域。
Synchronized有4种应用场景:修饰代码块、修饰方法、修饰静态方法和修饰类
🌻6.4.1 修饰代码块
Synchronized修饰代码块时的格式为:
Synchronized(this){
要锁住的代码
}
其作用范围为{}中的代码部分。
在本例中,我们将if语句套入Synchronized代码块中,因为if语句的判断是决定该线程应该执行什么,该怎么修改数据的门槛。
//卖鞋类实现
class SellShoes implements Runnable{
private static int shoes = 100;
@Override
public void run() {
while(true) {
synchronized(this) {
if(shoes > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":卖出了第"+shoes+"双鞋子");
shoes--;
}else {
System.out.println(Thread.currentThread().getName()+":鞋子买完了!");
break;
}
}
}
}
}
🎈运行结果如下:
🎈看似代码正常,那么我们做出如下改动,再观察其结果:
public static void main(String[] args) {
SellShoes ss = new SellShoes();
SellShoes ss2 = new SellShoes();
SellShoes ss3 = new SellShoes();
Thread t1 = new Thread(ss, "京东");
Thread t2 = new Thread(ss2, "淘宝");
Thread t3 = new Thread(ss3, "得物");
t1.start();
t2.start();
t3.start();
}
发现其输出又出现了相同的问题,这是为什么呢?因为我们在创建线程时创建了三个对象,每个线程对应一个对象,而Synchronized修饰代码块时只锁定对象,每个对象只有一个锁与之相连,当创建多个对象时,相当于有三把锁,自然他们之间就无法相互约束了。
🌻6.4.2 修饰方法
Synchronized修饰方法时只需要在方法名前加上Synchronized字段即可,与修饰代码块一样,Synchronized修饰方法时锁定的也只是对象,当创建多个对象时便无法起到约束作用了,在此不再过多演示。
//修饰方法
@Override
public synchronized void run() {
while(true) {
if(shoes > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":卖出了第"+shoes+"双鞋子");
shoes--;
}else {
System.out.println(Thread.currentThread().getName()+":鞋子买完了!");
break;
}
}
}
🎈输出如下:
🌻6.4.3 修饰静态方法
Synchronized修饰静态方法时也是只需要在方法名前加上Synchronized字段即可。
//修饰静态代码块,将run方法中内容写入静态方法,再在run方法中调用该方法
public synchronized static void runn() {
while(true) {
if(shoes > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":卖出了第"+shoes+"双鞋子");
shoes--;
}else {
System.out.println(Thread.currentThread().getName()+":鞋子买完了!");
break;
}
}
}
@Override
public void run() {
runn();
}
🎈 输出如下:
🎈那么如果我们每个线程都新创建一个对象呢?
通过输出我们可以看到即便是每个线程都对应一个新的对象输出也是正确的,这是因为静态方法是属于类的,synchronized作用于静态方法时相当于作用于类,所以无论多少个对象使用的都是同一把锁。
🌻6.4.4 修饰类
通过修饰静态方法的分析我们可以得出:当synchronized修饰类时,无论创建线程时是否使用同一个对象,使用的都是同一把锁。
//修饰类
@Override
public void run() {
while(true) {
synchronized(SellShoes.class) {
if(shoes > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":卖出了第"+shoes+"双鞋子");
shoes--;
}else {
System.out.println(Thread.currentThread().getName()+":鞋子买完了!");
break;
}
}
}
}
🎈 输出如下:
🌷七、Lock锁
🌼7.1 Lock锁简介
在前面我们已经了解到可以利用synchronized字段对代码块、方法和类加锁,但是它并不完美。synchronized锁无法中断一个等候的线程,如果该线程一直无法获得锁就只能一直等下去,这样就会造成资源的浪费,降低了程序的性能,因此就需要一个新的方法:Lock锁。
相较于synchronized锁,Lock可以手动获取锁和释放锁,可以中断锁的获取、超时获取锁。synchronized是Java的一个关键字,是基于JVM层面的,而Lock是一个接口。它有三个实现类:ReentrantLock和ReentrantReadWriteLock中的静态类ReadLock和WriteLock。
因为Lock必须主动去释放锁,并且在发生异常时,不会自动释放锁。所以一般来说,使用Lock必须在try{ }catch{ }块中进行,并且将释放锁的操作放在finally{ }块中进行,以保证锁一定被被释放,防止死锁的发生。
🌼7.2 Lock锁的使用方法(ReentrantLock)
🌻7.2.1 获得锁和释放锁
使用lock锁时最常见的是使用lock()方法获得锁,用unlock()方法释放锁。
public class LockDemo {
public static void main(String[] args) {
TestLock tl = new TestLock();
Thread t1 = new Thread(tl);
Thread t2 = new Thread(tl);
Thread t3 = new Thread(tl);
t1.start();
t2.start();
t3.start();
}
}
class TestLock implements Runnable{
@Override
public void run() {
Lock lock = new ReentrantLock();//锁在方法内
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"-->得到了锁");
} finally {
System.out.println(Thread.currentThread().getName()+"-->释放了锁");
lock.unlock();
}
}
}
🎈输出如下:
在这里有一个问题,按照锁的定义和用途。同一时刻只能有一个线程用有锁,并且只有等该线程释放锁以后其他线程才能够拥有该锁,那么为什么在上面的输出中Thread2和Thread1在Thread0未释放锁的情况下就获得了锁呢?这是因为我们建立的锁的对象是在方法内部,相当于局部变量,该方法每被调用一次就会新建一个锁,因此对其他线程就起不到约束作用了,因此,只有我们把Lock作为类的属性才能对所有线程起到约束作用。
🎈 修改后代码如下:
public class LockDemo {
public static void main(String[] args) {
TestLock tl = new TestLock();
Thread t1 = new Thread(tl);
Thread t2 = new Thread(tl);
Thread t3 = new Thread(tl);
t1.start();
t2.start();
t3.start();
}
}
class TestLock implements Runnable{
private Lock lock = new ReentrantLock();//锁作为类的属性,private修饰词可要可不要
@Override
public void run() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"-->得到了锁");
} finally {
System.out.println(Thread.currentThread().getName()+"-->释放了锁");
lock.unlock();
}
}
}
🎈输出如下:
因此Lock锁的正确使用方法为: 创建锁的对象时注意其必须为类的属性,不能在方法中。
🌻7.2.2 lockInterruptibly()
该方法也可以用来获得锁,可中断。举个例子,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
简单来说就是有人比我先获得锁,所以我要等,但是我可以被打断,不等了
🎈输出如下:
🌻7.2.3 tryLock()和tryLock(long time, TimeUnit unit)
锁在空闲的时候才能获取锁(未获得锁不会等待)。举个例子:当两个线程同时通过lock.trylock()想获取某个锁时,假若此时线程A获取到了锁,而线程B不会等待,直接放弃获取锁。tryLock(long time, TimeUnit unit)时tryLock()的重载方法,在原有基础上添加了一个时间限制,即在该时间限制内都会一直尝试获取锁
注意:tryLock()和tryLock(long time, TimeUnit unit)都会返回一个Boolean值,true代表获取锁成功罚款色则代表获取失败,两种方法使用方法一样。与lockInterruptibly()不同,tryLock()方法在获取锁失败后不会继续等待,而是直接走开。
public class LockDemo {
public static void main(String[] args) {
TestLock tl = new TestLock();
Thread t1 = new Thread(tl);
Thread t2 = new Thread(tl);
Thread t3 = new Thread(tl);
t1.start();
t2.start();
t3.start();
}
}
class TestLock implements Runnable{
private Lock lock = new ReentrantLock();//锁作为类的属性,private修饰词可要可不要
@Override
public void run() {
if(lock.tryLock()) {
try {
System.out.println(Thread.currentThread().getName()+"-->得到了锁");
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName()+"-->释放了锁");
}
}else {
System.out.println(Thread.currentThread().getName()+"-->获取锁失败");
}
}
}
🎈输出如下:
🌼7.3 ReentrantLock
ReentrantLock是我们平时创建lock锁的方法,利用lock锁实现上述卖鞋程序,代码如下:
import java.util.concurrent.locks.ReentrantLock;
public class SellShoes_lock {
public static void main(String[] args) {
SellShoes_l ss1 = new SellShoes_l();
SellShoes_l ss2 = new SellShoes_l();
SellShoes_l ss3 = new SellShoes_l();
Thread t1 = new Thread(ss1, "京东");
Thread t2 = new Thread(ss2, "淘宝");
Thread t3 = new Thread(ss3, "得物");
t1.start();
t2.start();
t3.start();
}
}
//卖鞋类实现
class SellShoes_l implements Runnable{
private int shoes = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true) {
lock.lock();//获取锁
try {
if(shoes > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":卖出了第"+shoes+"双鞋子");
shoes--;
}else {
System.out.println(Thread.currentThread().getName()+":鞋子买完了!");
break;
}
} finally {
lock.unlock();//释放锁,一般放在finally块里面,确保锁的释放
}
}
}
}
🎈输出如下:
输出发现结果又出问题了,这是为什么呢?这里就需要一点基础知识的掌握了,大家注意到,shoes我们使用了static修饰,确保三个线程使用的都是同一个shoes,这是因为static修饰的变量或方法都是属于类的,对由该类创建的所有对象都适用,也就是说只要你加了static,无论创建多少对象,用的始终是这一个变量。大家可以尝试去掉shoes和static,你会发现每个对象的线程都会输出1到第100双鞋子,这是因为每个线程都有独立的shoes内存空间。所以为了让所有的对象线程都共享同一把锁,对lock也要用static修饰。
🎈修改后代码如下:
package thread_lx;
import java.util.concurrent.locks.ReentrantLock;
public class SellShoes_lock {
public static void main(String[] args) {
SellShoes_l ss1 = new SellShoes_l();
SellShoes_l ss2 = new SellShoes_l();
SellShoes_l ss3 = new SellShoes_l();
Thread t1 = new Thread(ss1, "京东");
Thread t2 = new Thread(ss2, "淘宝");
Thread t3 = new Thread(ss3, "得物");
t1.start();
t2.start();
t3.start();
}
}
//卖鞋类实现
class SellShoes_l implements Runnable{
private static int shoes = 100;
private static ReentrantLock lock = new ReentrantLock();//使用静态变量,表示该变量为所有归属该类的对象共有,即实现所有对象共享同一把锁
@Override
public void run() {
while(true) {
lock.lock();//获取锁
try {
if(shoes > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":卖出了第"+shoes+"双鞋子");
shoes--;
}else {
System.out.println(Thread.currentThread().getName()+":鞋子买完了!");
break;
}
} finally {
lock.unlock();//释放锁,一般放在finally块里面,确保锁的释放
}
}
}
}
🎈输出如下:
🌼7.4 ReadWriteLock
ReadWriteLock也是一个接口,只定义了readlock()和writelock()两个方法,一个用来获取读锁,一个用来获取写锁,将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作,ReentrantReadWriteLock就是实现了ReadWriteLock接口。
🌼7.5 ReentrantReadWriteLock
ReentrantReadWriteLock锁是一个读写分离的锁,这种锁主要用于读多写少的业务场景。ReentrantReadWriteLock中有很多方法,但是常用的只有:readlock()和writelock()。
✨不用readlock()方法,用synchronized字段锁定run()方法:
public class LockDemo {
public static void main(String[] args) {
TestLock tl = new TestLock();
Thread t1 = new Thread(tl);
Thread t2 = new Thread(tl);
Thread t3 = new Thread(tl);
t1.start();
t2.start();
t3.start();
}
}
class TestLock implements Runnable{
private Lock lock = new ReentrantLock();//锁作为类的属性,private修饰词可要可不要
@Override
public synchronized void run() {
try {
for(int i=0;i<5;i++) {
System.out.println(Thread.currentThread().getName()+"-->正在读取文件");
}
System.out.println(Thread.currentThread().getName()+"-->读取文件完毕");
} finally {
System.out.println(Thread.currentThread().getName()+"-->释放了锁");
}
}
}
🎈输出结果:
可以观察到利用synchronized字段修饰run()方法时,只有当一个线程的读操作执行完毕后下一个线程才会执行读操作。那么用普通的lock锁呢?最后结果也是一样的,在此不再演示。
✨使用readlock()方法读取文件
public class LockDemo {
public static void main(String[] args) {
TestLock tl = new TestLock();
Thread t1 = new Thread(tl);
Thread t2 = new Thread(tl);
Thread t3 = new Thread(tl);
t1.start();
t2.start();
t3.start();
}
}
class TestLock implements Runnable{
ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();
@Override
public void run() {
rwlock.readLock().lock();
try {
for(int i=0;i<5;i++) {
System.out.println(Thread.currentThread().getName()+"-->正在读取文件");
}
System.out.println(Thread.currentThread().getName()+"-->读取文件完毕");
} finally {
rwlock.readLock().unlock();
System.out.println(Thread.currentThread().getName()+"-->释放了锁");
}
}
}
🎈输出如下:
说明三个线程在同时进行读操作,这样就大大提升了读操作的效率。不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。即进行写操作时还是会一个线程一个线程的写。
✨使用writelock()方法写文件
public class LockDemo {
public static void main(String[] args) {
TestLock tl = new TestLock();
Thread t1 = new Thread(tl);
Thread t2 = new Thread(tl);
Thread t3 = new Thread(tl);
t1.start();
t2.start();
t3.start();
}
}
class TestLock implements Runnable{
ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();
@Override
public void run() {
rwlock.writeLock().lock();
try {
for(int i=0;i<5;i++) {
System.out.println(Thread.currentThread().getName()+"-->正在写文件");
}
System.out.println(Thread.currentThread().getName()+"-->写文件完毕");
} finally {
rwlock.writeLock().unlock();
System.out.println(Thread.currentThread().getName()+"-->释放了锁");
}
}
}
🎈输出如下:
🌼7.6 锁的分类
🌻7.6.1 可重入锁
如果一个锁具备可重入性,那么该锁就被称为可重入锁。synchronized锁和Lock锁都是可重入锁。可重入锁时基于线程分配的锁,而非基于方法调用而分配,通俗来说就是:假设有两个由synchronized字段修饰的方法,其中一个方法调用了另一个方法,那么线程在调用方法时就不会为第二个方法重新申请锁,而是直接执行,使用同一把锁。
🌻7.6.2 可中断锁
即可以被中断的锁,synchronized锁和Lock锁都是可中断锁。
🌻7.6.3 公平锁
按申请锁的顺序来获取锁,即奉行先到先得的原则,谁先来排队等候谁就先获得锁,如果无法保证该原则就叫非公平锁,即奉行谁抢到算谁的的原则。其中synchronized是非公平锁,ReentrantLock和ReentrantReadWriteLock在默认情况下是非公平锁,但是可以设置为公平锁,设置方法如下:
//参数为true表示为公平锁,参数为false和无参数时为非公平锁
ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock(true);
ReentrantLock rwlock = new Reentrant(true);
🌻7.6.4 读写锁
读写锁实现了读和写的分离,提高了读写速率。
🌼7.7 synchronized与Lock比较
(1)synchronized锁会自动获取和释放锁,并且无法中断锁的获取,Lock需要手动获取锁和释放锁,可以中断锁的获取、超时获取锁;
(2)synchronized是Java的一个关键字,是基于JVM层面的,而Lock是一个接口;
(3)Lock 在发生异常时,如果没有主动通过 unLock() 去释放锁,很可能会造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;synchronized 不需要手动获取锁和释放锁,在发生异常时,会自动释放锁,因此不会导致死锁现象发生;
(4)Lock 的使用更加灵活,可以有响应中断、有超时时间等;而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,直到获取到锁;
(5)synchronized只能随机唤醒线程或唤醒全部线程,ReetantLock结合Condition可以实现分组唤醒需要唤醒的线程,实现精确唤醒。
(6)一般情况下建议使用synchronized锁,如果synchronized满足不了性能需求时再使用Lock锁。
🌷八、死锁
🌼8.1 概念解释
死锁现象只有存在两个或两个以上的线程时才会发生,他们互相持有对方所需要的资源(锁),导致这些线程一直处于等待状态,无法继续运行。例如现在有两个人,他们都需要写东西,但是一个人只有笔,另一个人只有纸,可是他们又都想自己先写,这样谁也不让水,就形成了死锁,一直僵持下去。
🎈死锁发生的四个必要条件:
(1)互斥条件:一个资源每次只能被一个进程使用。
(2)请求与保持条侔:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
🌼8.2 死锁复现
定义两个方法:pen()方法先获得pen,再获得note,最后成功写字;note()方法先获得note,再获得pen,最后成功写字。对获取代码块上锁,表示同一时刻笔和纸只能归属一个人,再创建两个线程,模拟两个人。
public class DeadLock {
public static void main(String[] args) {
DLock dl = new DLock();
Thread t1 = new Thread(dl);
Thread t2 = new Thread(dl);
t1.start();
t2.start();
}
}
class DLock implements Runnable{
static Object pen = new Object();//pen的锁
static Object note = new Object();//note的锁
@Override
public void run() {
pen();
note();
}
private static void pen() {
synchronized (pen) {//先获得笔
System.out.println(Thread.currentThread().getName()+"--> I hava a pen!");
synchronized (note) {//还要获得纸
System.out.println(Thread.currentThread().getName()+"--> I also have a note! And now I can write!");
}
}
}
private static void note() {
synchronized (note) {//先获得纸
System.out.println(Thread.currentThread().getName()+"--> I hava a note!");
synchronized (pen) {//还要获得笔
System.out.println(Thread.currentThread().getName()+"--> I also have a pen! And now I can write!");
}
}
}
}
🎈输出如下:
通过输出可以看到上述程序发送了死锁现象,接下来我们分析一下为什么会出现这种情况:
因为我们对获取代码块用synchronized上锁,所以同一时刻只能有一个线程执行,假设线程0先执行,那么它先执行pen()方法,先获得pen,然后又获得note,最后成功书写,此时线程0释放pen和note锁,继续执行note()方法,同时,因为线程0释放了pen()方法的资源,所以线程1就可以执行pen()方法,此时:线程0获得了note,线程1获得了pen,接下来线程0要获得pen才能继续执行,但是此时pen在线程1手中,而线程1需要note才能继续执行,但note此时在线程0手里,就这样双方都等待对方释放资源,一直僵持下去。
🌼8.3 解决方法
上面我们讲到发生死锁的4个条件,我们只需要破坏其中的任意一条即可打破死锁:
(1)破坏互斥条件和不剥夺条件:对sychronized来说,是JVM底层设定的,每个对象都有一个monitor(监视器),在同步操作中,一个线程(或进程)持有一个对象的monitor后,有monitorenter操作,同步方法结束或异常时,有monitorexit操作。所以我们是破坏不了互斥条件和不剥夺条件的;如果不是sychronized,如通过Lock完成的互斥条件和不剥夺条件,就可以更改;
(2)破坏请求和保持条件:常见的操作有:加锁顺序(指定加锁的顺序或者线程执行的顺序)、加锁时限(对加锁的对象增加超时限制,避免一直占用);
(3)破坏环路等待条件:死锁检测:多线程对期望持有的锁对象不能形成相互(环状)依赖的情况。
以破坏请求和保持条件为例,我们修改加锁的顺序,都改为先获得pen再获得note
public class DeadLock {
public static void main(String[] args) {
DLock dl = new DLock();
Thread t1 = new Thread(dl);
Thread t2 = new Thread(dl);
t1.start();
t2.start();
}
}
class DLock implements Runnable{
static Object pen = new Object();//pen的锁
static Object note = new Object();//note的锁
@Override
public void run() {
pen();
note();
}
private static void pen() {
synchronized (pen) {//先获得笔
System.out.println(Thread.currentThread().getName()+"--> I hava a pen!");
synchronized (note) {//还要获得纸
System.out.println(Thread.currentThread().getName()+"--> I also have a note! And now I can write!");
}
}
}
//不会产生死锁
private static void note() {
synchronized (pen) {//先获得笔
System.out.println(Thread.currentThread().getName()+"--> I hava a note!");
synchronized (note) {//再获得纸
System.out.println(Thread.currentThread().getName()+"--> I also have a pen! And now I can write!");
}
}
}
}
🎈输出如下:
🌷九、线程通信
🌼9.1 概念解释
为了完成多个任务,常常会创建多个线程,但是线程与线程之间不是相互独立的个体,并且有时他们各自完成的任务在某种程度上又有一定的关系,此时各线程之间就需要交互通信、相互协作。
那么为什么线程之间要进行交互呢?举个简单的例子,一个饭店里,后厨有两个人,一个负责洗碗,一个负责烘干,整两个人各自代表一个线程,他们之间都有一个共享的对象——碗架。负责洗碗的人把洗干净的碗放在碗架上,负责烘干的人从碗架上拿出碗烘干,显然,只有碗架上有碗时烘干的人才工作,没有碗时就要通知洗碗,只有碗架上的碗没放满时洗碗的人才会工作,放满时就要通知人烘干。这就是典型的多线程事例:生产者-消费者问题。
涉及到多线程之间共享数据的操作时,除了数据同步问题外还有另一个问题:相互交互的线程之间的进度问题,即多线程的同步问题。
为了解决这一类问题,Java提供了wait()/notify()机制,协调线程之间的运行速度和读取关系。
🌼9.2 生产者/消费者问题——wait()/notify()机制
🎈问题假设:
假设现在有一个公寓,里面有一户人家订购了牛奶,牛奶工每天负责把生产出来的牛奶送到奶箱中,居民从奶箱中取奶消费,现在使用多线程模拟该过程。
🎈问题分析:
当奶箱中有奶时才能消费,没有时就要等待并通知生产者生产,当奶箱装满时生产者就要等待并通知消费者消费,既然是奶箱,说明其就有一定的容量,我们可以利用数组或集合实现。并且要注意,既然奶箱有容量,当容量不为1时,不可能生产一瓶酒消费一瓶,可能是生产很多瓶才消费一瓶或者不消费,也可能一次消费很多瓶,所以这都是实现过程中要考虑的问题。
通过分析我们可以发现想要实现该过程,需要创建以下几个类:
(1)生产者类:负责生产牛奶并放进奶箱;
(2)消费者类:从奶箱中取出牛奶;
(3)奶箱类:用来装牛奶;
此外可能牛奶还有自己的属性,例如编号,容量,生产日期等等。
🌻9.2.1 一个厂家供应一个住户(一对一)
一个厂家供应一个住户即创建对象时创建一个生产者和一个消费者,以下为代码实现:
public class SellMilk_1 {
public static void main(String[] args) {
MilkBox box = new MilkBox();
Productor p1 = new Productor(box);//创建生产者对象
Consumer c1 = new Consumer(box);//创建消费者对象
Thread t1 = new Thread(p1,"蒙牛");//创建生产者线程
Thread t2 = new Thread(c1,"3601");//创建消费者线程
t1.start();
t2.start();
}
}
//生产者
class Productor implements Runnable{
private MilkBox box;
public Productor(MilkBox box) {
this.box = box;
}
@Override
public void run() {
for(int i=0;i<20;i++) {//假设共有20瓶奶需要送
box.push(i);
}
}
}
//消费者
class Consumer implements Runnable{
private MilkBox box;
public Consumer(MilkBox box) {
this.box = box;
}
@Override
public void run() {
for(int i=0;i<20;i++) {
box.pop();
}
}
}
//缓冲区:奶箱
class MilkBox{
//需要一个容器大小
static int[] milkbox = new int[5];
//容器计数器
static int count = 0;
//生产者放入产品
public synchronized void push(int milkid) {
//如果容器满了就需要等待消费者消费
if(count == milkbox.length) {
//通知消费者消费,生产者等待
try {
this.wait();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}else {
//如果没有满就要放入产品
milkbox[count] = milkid;
System.out.println("【"+Thread.currentThread().getName()+"】"+"生产了第"+milkid+"瓶奶");
count++;
}
//可以通知消费者消费了
this.notifyAll();
}
public synchronized void pop() {
//判断能否消费
if(count == 0) {
//等待生产者生产
try {
this.wait();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}else {
//如果可以消费
count--;
System.out.println("【"+Thread.currentThread().getName()+"】"+"消费了第"+milkbox[count]+"瓶奶");
}
//吃完了通知生产者生产
this.notifyAll();
}
}
🎈输出如下:
🌻9.2.2 一个厂家供应多个住户 (一对多)
一个厂家供应多个住户即创建对象时创建一个生产者和多个消费者,就像一个奶场供应一片人的牛奶。或者是一户人家订购了多个厂家的牛奶。
在上面我们已经创建了一对一的模式,既然一对多是指有多个消费者或生产者,那么我们创建多个消费者对象或生产者对象,然后再运行程序,是不是就能达到我们的目的了呢?
public static void main(String[] args) {
MilkBox box = new MilkBox();
Productor p1 = new Productor(box);//创建生产者对象
Consumer c1 = new Consumer(box);//创建消费者对象
Consumer c2 = new Consumer(box);//创建消费者对象
Thread t1 = new Thread(p1,"蒙牛");//创建生产者线程
Thread t2 = new Thread(c1,"3601");//创建消费者线程
Thread t3 = new Thread(c2,"3602");//创建消费者线程
t1.start();
t2.start();
t3.start();
}
通过运行程序发现最后报错了,提示数组越界,这是为什么呢?我们仔细看,在报错之前是正常输出的,并且也已经生产完了20瓶奶,那这是怎么回事呢?而为什么生产完也消费完奶后程序还没有停止运行呢?
因为我们对push和pop方法都加了锁,因为同一时刻只能有一个消费者拿奶,保证了不会有不同的消费者拿到相同编号的奶,通过程序输出可以看到,功能正常,那么为什么还会出错呢?这里我们就要用到之前所学的知识,我们知道,synchronized修饰方法作用的范围只是这一个对象,但是我们每一个消费者都是不同的对象并且i还是局部变量,这就导致在执行for循环时针对每一个消费者都有一个0-20的for循环,但是他们共享的只有20瓶奶,当最后一瓶奶被消费完后,生产者便不会再执行,但是消费者还没有拿够20瓶奶,所以就一直在等待,便造成了程序一直在运行的情况。
我们把每个消费者需要的奶的数量改为10 ,生产者还是生产20瓶奶,运行程序输出如下:
发现程序又正常了, 但是当我多运行几次时发现还是有一定的几率会报错,也是数组越界
那么为什么会报错数组越界呢?难道同一时刻两个线程进入了同一个方法?可是我们是设置了锁了呀,在这里我也很疑惑,我想可能是因为我们的synchronized锁只对对象生效,而我们创建了两个消费者对象,所以这是两把不同的锁,并且我们只创建了一个容器(奶盒)对象,也就是说两个不同的消费者对象共用了相同的奶盒,相当于一个容器有两个门,两个消费者各持一把锁。因为是两把不同的锁,这时就有可能发生两个消费者都执行了pop方法(即都打开了门),但是其中一个快一点,另一个慢一点,快的刚好拿走了最后一瓶奶,按道理慢的就不能执行了,但是此时他已经进来了,拦不住了,当慢的消费者执行完count--,准备输出时由于不存在-1这个下标,于是就是报错数组越界。
分析了原因,是因为消费者的锁不同,那么我们有没有办法让他们都只用一把锁,也就是奶箱只有一个门呢?当然可以,前面学习到synchronized还可以修饰静态方法或者类,其作用于是这个类创建的所有对象,也就是说只要是通过这个类创建的对象,都是同一把锁,所以我们只需要用synchronized修饰容器类或者静态方法即可,或者我们把消费者和生产者类里面的run方法利用synchronized修饰该类。所以接下来让我们试一试。
✨(1)synchronized修饰容器类的静态方法
我们把MilkBox类的push和pop方法改为静态方法,发现报错了,提示我们静态方法里面不能用this,也就是说用不了wait好notify方法,显然,此路不通。
✨(2) synchronized直接修饰容器类
运行发现还是报错,此路还是不通
✨(3)synchronized修饰run法
依旧报错,还是不行。
✨(4)synchronized修饰生产者/消费者类
通过输出发现好像这个方法可以,但是好像又不可以,我们的希望时让两个消费者共同享有这20瓶奶,但是现在根据输出发现还是每个消费者各自享有20瓶,显然违背了我们的意愿,所以说这个方法也不行。
试过了这4中方法,大大小小总是会有一些问题,那现在怎么办呢?我们想一想,无论怎么便,无论有多少个消费者和生产者,唯一不变的是什么?是我们的奶箱,任何时刻我们只需要创建一个奶箱,这把锁是唯一的,所以如果我们把奶的数量(编号)这一变量放在容器里,会不会得到我们想要的结果呢?
改进:为了更详细的展示剩余奶量的信息,我们将数组改为集合形式,集合的大小即为当前剩余奶的数量。还可以在每次放奶和拿奶的时候设置延时,模拟存取时花费的时间。将for循环改为while循环,传入参数:需要生产奶的数量,创建一个Boolean类型的参数决定是否继续循环,以避免for循环中临时变量i的影响。
package thread_lx;
import java.util.ArrayList;
public class SellMilk_2 {
public static void main(String[] args) {
Box b = new Box();
Producer p1 = new Producer(b);
Customer c1 = new Customer(b);
Customer c2 = new Customer(b);
Thread tp1 = new Thread(p1,"蒙牛");
Thread tc1 = new Thread(c1,"3601");
Thread tc2 = new Thread(c2,"3602");
tp1.start();
tc1.start();
tc2.start();
}
}
class Producer implements Runnable{
private Box box;
boolean flag = true;
public Producer(Box box) {
this.box = box;
}
@Override
public void run() {
while(flag){
try {
Thread.sleep(100);
flag = box.put(20);//传入供应牛奶的数量
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Customer implements Runnable{
private Box box;
boolean flag = true;
public Customer(Box box) {
super();
this.box = box;
}
@Override
public void run() {
while(flag) {
try {
Thread.sleep(200);
flag = box.pop();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Box{
private ArrayList<Integer> milk = new ArrayList<Integer>();//记录奶的容量,编号等信息
private static final int SIZE = 10;//奶箱容量上限
private int count = 0;//下标计数
private int num = 0;//编号计数
private static boolean Pflag=true;
private static boolean Cflag=true;
public synchronized boolean put(int MAX) {
if(SIZE == milk.size()) {//奶的数量达到上限
try {
this.wait();//生产者等待消费者消费
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {//容量未达上限
System.out.println("【"+Thread.currentThread().getName()+"】-->生产了第"+num+"瓶奶");
milk.add(num);//生产编号为num的奶
num++;//编号加1
count++;//下标加1
}
if(num == MAX) Pflag = false;//如果编号等于奶的数量,返回false,结束循环,生产者停止生产
this.notifyAll();//唤醒所有线程,主要目的是为了让消费者消费
return Pflag;
}
public synchronized boolean pop() {
if(milk.size() == 0) {//如果奶的数量为0
try {
this.wait();//消费者等待生产者生产
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
count--;//拿走一瓶奶,下标减1
System.out.println("【"+Thread.currentThread().getName()+"】-->消费了第"+milk.get(count)+"瓶奶");
System.out.println("余量:"+ (milk.size()-1));
milk.remove(count);//除去拿的奶
System.out.print("剩余牛奶编号为:");//遍历打印剩余奶的编号
for(int i : milk) {
System.out.print(i+" ");
}
System.out.println();
}
if(Pflag == false && milk.size() == 0) Cflag = false;//如果生产者停止生产并且奶箱奶量为0,返回false,结束循环,消费者停止消费
this.notifyAll();//唤醒所有线程,主要目的是唤醒生产者生产
return Cflag;
}
}
🎈输出如下:
在这里虽然我们没有直接控制让消费者或者生产者都去用同一把锁,但是我们将产品信息放入了容器中,容器对象只创建了一个,相当于产品信息加上了唯一的一把锁,而且每消费一瓶牛奶,其编号就会被删除,这样即便是有多个消费者同时调用pop也不会出现同一瓶牛奶被多人消费的情况。
那么为什么出现会一直不退出呢?我们在while循环时设置了一个参数,当其值为false时退出循环,那么既然程序不退出,肯定是flag值还是true,但是按照我们的逻辑取完最后一品奶后flag值应该是false才对,问题究竟出在哪里了呢?我们把每一步的flag值打印出来看看。
当生产者退出循环后,消费者继续消费奶箱中的牛奶,直到最后一瓶奶被消费,3601退出,但是3602迟迟不退出。这是为什么呢?因为我们对flag进行赋值判断的时候是在最后,但是如果奶箱中没有奶了,就会执行if语句,然后执行this.wait(),线程就进入了阻塞状态,只有当奶箱中有奶时才会执行其他的,但是此时生产者已经退出,所以该线程便一直处于阻塞状态,无法退出。所以我们只需在if语句中加一句判断,只有当生产者没有退出时线程才执行wait(),否则就不执行,直接顺序执行后面的判断复制语句,因为退出的条件是生产者退出并且奶箱中数量为0,而只要奶箱中奶的数量为0,无论哪一个线程都会执行if语句,如果此时生产者还没推出,说明还会往奶箱中补货,如果生产者已经退出,说明奶箱中就不会再有奶,就不需要再等待,所以保证了所有线程都能顺路退出。修改后代码如下:
只需要对pop函数进行修改就可以了,因为生产者是主动退出。
public synchronized boolean pop() {
if(milk.size() == 0) {//如果奶的数量为0
try {
if(Pflag == true) this.wait();//只要走到这里就说明奶箱奶量为0,再判断生产者是否退出,如果是,则达到消费者退出条件,线程便不再等待
//this.wait();//消费者等待生产者生产
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
count--;//拿走一瓶奶,下标减1
System.out.println("【"+Thread.currentThread().getName()+"】-->消费了第"+milk.get(count)+"瓶奶");
System.out.println("余量:"+ (milk.size()-1));
milk.remove(count);//除去拿的奶
System.out.print("剩余牛奶编号为:");//遍历打印剩余奶的编号
for(int i : milk) {
System.out.print(i+" ");
}
System.out.println();
}
this.notifyAll();//唤醒所有线程,主要目的是唤醒生产者生产
//如果生产者停止生产并且奶箱奶量为0,返回false,结束循环,消费者停止消费
if(Pflag == false && count == 0) Cflag = false;
// System.out.println(Thread.currentThread().getName()+Cflag);
return Cflag;
}
🎈输出如下:
🌻9.2.3 多工厂对应多用户(多对多)
多工厂对应多用户即有多个消费者和多个生产者,在上述程序的基础上我们增加消费者和生产者的数量,观察其是否正常运行。
public static void main(String[] args) {
Box b = new Box();
Producer p1 = new Producer(b);//生产者1
Producer p2 = new Producer(b);//生产者2
Customer c1 = new Customer(b);//消费者1
Customer c2 = new Customer(b);//消费者2
Thread tp1 = new Thread(p1,"蒙牛");
Thread tp2 = new Thread(p2,"伊利");
Thread tc1 = new Thread(c1,"3601");
Thread tc2 = new Thread(c2,"3602");
tp1.start();
tp2.start();
tc1.start();
tc2.start();
}
🎈输出如下:
输出结果正常,程序也正常退出,说明上述代码对一对一,一对多,多对多模式都适用。
🌼9.3 生产者/消费者问题——Condition机制
🌻9.3.1 概念解释
Condition是个接口,基本的方法就是await()和signal()方法;
Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition();
调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
condition机制和Synchronized的wait/notify机制一样,都是实现线程间通信的方法,其主要有以下机制方法:
await():使当前线程等待,同时释放锁
signal():唤醒等待线程中的其中一个
signalAll():唤醒所有等待中的线程
🌻9.3.2 一对一、一对多、多对多测试
我们对上面的代码进行修改,可以得到如下代码:
package thread_lx;
import java.util.ArrayList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class SellMilk_condition {
public static void main(String[] args) {
Box_lock b = new Box_lock();
Producer_lock p1 = new Producer_lock(b);//生产者1
Producer_lock p2 = new Producer_lock(b);//生产者2
Customer_lock c1 = new Customer_lock(b);//消费者1
Customer_lock c2 = new Customer_lock(b);//消费者2
Thread tp1 = new Thread(p1,"蒙牛");
Thread tp2 = new Thread(p2,"伊利");
Thread tc1 = new Thread(c1,"3601");
Thread tc2 = new Thread(c2,"3602");
tp1.start();
tp2.start();
tc1.start();
tc2.start();
}
}
class Producer_lock implements Runnable{
private Box_lock box;
boolean flag = true;
public Producer_lock(Box_lock box) {
this.box = box;
}
@Override
public void run() {
while(flag){
try {
Thread.sleep(100);
flag = box.put(20);//传入供应牛奶的数量
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Customer_lock implements Runnable{
private Box_lock box;
boolean flag = true;
public Customer_lock(Box_lock box) {
super();
this.box = box;
}
@Override
public void run() {
while(flag) {
try {
Thread.sleep(200);
flag = box.pop();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Box_lock{
private ArrayList<Integer> milk = new ArrayList<Integer>();//记录奶的容量,编号等信息
private static final int SIZE = 5;//奶箱容量上限
private int count = 0;//下标计数
private int num = 0;//编号计数
private static boolean Pflag=true;
private static boolean Cflag=true;
private ReentrantLock lock = new ReentrantLock();//创建lock锁对象
private Condition Pcondition = lock.newCondition();//生产者线程
private Condition Ccondition = lock.newCondition();//消费者线程
public boolean put(int MAX) {
lock.lock();//上锁
try {
if (SIZE == milk.size()) {//奶的数量达到上限
System.out.println("------奶箱已满!------");
Pcondition.await();//生产者等待
} else {//容量未达上限
System.out.println("【" + Thread.currentThread().getName() + "】-->生产了第" + num + "瓶奶");
milk.add(num);//生产编号为num的奶
num++;//编号加1
count++;//下标加1
}
Ccondition.signalAll();//唤醒消费者线程
//如果编号等于奶的数量,返回false,结束循环,生产者停止生产
if (num == MAX)
Pflag = false;
// System.out.println(Thread.currentThread().getName()+Pflag);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();//释放锁
}
return Pflag;
}
public boolean pop() {
lock.lock();
try {
if (milk.size() == 0) {//如果奶的数量为0
System.out.println("------奶箱已空!------");
//只要走到这里就说明奶箱奶量为0,再判断生产者是否退出,如果是,则达到消费者退出条件,线程便不再等待
if (Pflag == true) Ccondition.await();
} else {
count--;//拿走一瓶奶,下标减1
System.out.println("【" + Thread.currentThread().getName() + "】-->消费了第" + milk.get(count) + "瓶奶");
System.out.println("余量:" + (milk.size() - 1));
milk.remove(count);//除去拿的奶
System.out.print("剩余牛奶编号为:");//遍历打印剩余奶的编号
for (int i : milk) {
System.out.print(i + " ");
}
System.out.println();
}
Pcondition.signalAll();;//唤醒所有生产者线程
//如果生产者停止生产并且奶箱奶量为0,返回false,结束循环,消费者停止消费
if (Pflag == false && count == 0)
Cflag = false;
// System.out.println(Thread.currentThread().getName()+Cflag);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return Cflag;
}
}
🎈 输出如下:
🎈一对一
🎈一对多
🎈 多对多
通过输出可以看出利用condition机制也能轻松实现我们的需求。
🌷总结
至此有关Java线程的知识差不多就这么多了,此文为本人学习过程中所写,可能有地方疏漏或错误,如有读者发现还请及时指正,如果文章对你有所帮助的话,麻烦点个赞,后续还会继续更新Java的基础知识。完结撒花🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸