- hi,我是逸尘,一起学java吧
目标(任务驱动)
本章没有任务目标,略述概述多线程一隅,全是精华。
线程
进程
在提到线程是什么之前我们还需要提到另一个名词 他就是进程
- 进程:是指一个内存中运行的应用程序,每个进程都有⼀个独立的内存空间,⼀个应用程序可以同时运行多个进程;
- 进程也是程序的⼀次执⾏过程,是系统运行程序的基本单位;
- 系统运行⼀个程序即是 ⼀个进程从创建、运行到消亡的过程。
我们可以打开我们的任务管理器
我们通俗所说的一个正在运行的程序,其实它就可以说是一个进程。
线程
在上面学习了每个进程都有⼀个独立的内存空间,可以有同时运行很多线程的能力,线程就包含在进程里,那么线程就是一个进程的子概念。
- 线程(thread)是一个程序内部 的一条执行路径
- 是操作系统能够进行运算调度的最小单位。
- 我们之前启动程序执行后,main方法的执行其实就是一条单独的执行路径。
进程和线程的关系
多线程
多线程是指从软硬件上实现多条执行流程的技术(也可以单说是在程序里可以”同时“运行多个不同的任务的技术)这里的“同时”需要加上双引号。
简单的说,程序同时完成很多事情时,就是多线程。
我们为什么需要多线程
在前面的内容中我们学习的所有代码都是顺序执行的(跳转的不算),一个任务完成之后才去执行下一个,但是说代码源于生活,我们在日常生活中,并不是这样呆板的活着的(也不可能),比如我们先呼吸一下,然后血液循环,然后说话,然后思考。我们一般是都是一并发生的,呼吸的同时可以血液循环可以说话,正如屏幕前看文章的你是可以呼吸的,是可以同时发生的,你可以一边呼吸一边思考我这句话是不是正确的。
在java中同样,需要这样的一并发生的设计处理,比如,一边实现服务的文上传一边下载,,需要协同处理,所以我们java中支持多线程,我们需要多线程。
ps:并不是所有的语言都支持多线程
并发
可以”同时“运行多个不同的任务(活动)这样的思想也被成为并发。
多线程就一种并发编程的技术。
但是需要了解的是我们的并发并不是真正意义上的同时它是伪同时
ps:那是因为早期计算机的 CPU 都是单核的,一个 CPU 在同一时间只能执行一个进程/线程,当系统中有多个进程/线程等待执行时,CPU 只能执行完一个再执行下一个,为了解决所谓同时问题(并发),只能通过一种算法将 CPU 资源合理地分配给多个任务,轮询为系统的每个线程服务, 由于cpu切换的速度很快,给我们的感觉这些线程在同时执行,这就是真正的并发。
简单的说就是语言层面可以实现同时,但是我们早期电脑的单核cpu无法从技术满足。
并行
并行则是针对多核 CPU 提出的。和单核 CPU 不同,多核 CPU 真正实现了“同时执行多个任务。
真同时
我们现在的多线程是并发还是并行
因为多线程在单核下,多线程必定是并发的,不过现在的统一进程的多线程是可以运行在多核CPU下,所以可以是并行的。
并发和并行同时的,咱们就知道就可以了
实现线程
实现多线程的方法在我们的JAVA官方中指出的是两种
方法一 实现Runnable接口
Runnable接口是实现接口,可以继续继承类和实现接口,扩展性强这是这种方法的优点。
回顾接口内容:这是一个类实现接口,必须重写完全部接口的全部抽象方法,否则这个类需要定义成抽象类。
我们需要重写run()方法,而run()方法里面就是我们真正的任务功能内容。
实现过程
- 我们需在类上要实现Runnable接口
- 需要我们重写run()方法
- 创建一个任务对象(其实就是实例化该类的对象)
- 把任务对象交给Thread的对象处理
- 调用start()启动线程,调用run()的内容(最终调用target.run())
实际上是创建一个任务对象,把任务给线程对象处理。
package com.yd.thread;
/**
* 实现Runnable接口实现创建线程
*/
//我们需在类上要实现Runnable接口
public class RunnableStyle implements Runnable{
public static void main(String[] args) {
//Runnable对象和Thread对象相关联
//创建一个任务对象(其实就是实例化该类的对象)
RunnableStyle runnableStyle = new RunnableStyle();
//把任务对象交给Thread的对象处理
Thread threadOne = new Thread(runnableStyle);
//调用start()启动线程
threadOne.start();
}
//需要我们重写run()方法
@Override
public void run() {
System.out.println("用Runnable接口来实现多线程");
}
}
这样我们就实现了一个线程。
最终调用target.run()是什么意思
我们把Runnable任务对象交给Thread处理,我们来看一下Thread的run()内容,(我们可以ctrl+F12搜寻对应类的方法)
有值,运行
target是我们的本Runnable任务
这样就是我们的Thread调用Runnable()重写的run方法,只是加上了一个判断后,运行了run()
简单的说我们这个run()还是是对Runnable()重写的run方法,而不是Thread的run()
方法二 继承Thread类
我们查看源码可以看出来其实Thread类实质上是实现了Runnable的接口,
它run方法是对Runnable接口中的run()方法的具体实现(run()整个都被重写)。
当我们启动一个程序时,自动生成一个线程,这个线程就是我们主方法的运行路径,当我们自己实现线程时,我们程序员自己负责自己的线程(启动或者是什么的),我们的主方法线程的启动是java虚拟级负责的。
比如我们下面的创建 那么我们的单线程程序就变成了主线程和我们创建的线程,也就是多线程了
实现过程
- 我们需在类上要继承Thread类
- 需要我们重写run()方法
- 创建该类对象
- 调用start()启动线程,调用run()的内容(run()重写)
package com.yd.thread;
public class ThreadStyle extends Thread{
public static void main(String[] args) {
new ThreadStyle().start();
}
//需要我们重写run()方法
@Override
public void run() {
System.out.println("用继承Thread来实现多线程");
}
}
run()重写是什么意思
以上面代码为例,这个run的内容将覆盖if判断的内容,而不是调用(重名run)
总结
准确的讲,创建线程只有一种方式那就是构造Thread类,而实现线程的执行单元有两种方式
方法一:实现Runnable接口的run方法,并把Runnable实例传给Thread类
方法二:重写Thread的run方法(继承Thread类)推荐使用方法一
我们提到当我们启动一个程序时,自动生成一个线程,这个线程就是我们主方法的运行路径,当我们自己实现线程时,我们程序员自己负责自己的线程(启动或者是什么的),我们的主方法线程的启动是java虚拟级负责的。
Thread提供了很多与线程操作相关的方法
常用的就是获取当前线程对象currentthread()设置名称setname(),获取线程名称getname()
当然还有很多在实际开发中不常用的操作方法,但是也会在后面去一一讲解。
扩展 其他形式(万变不离其宗)
Lambda表达式的写法来表达线程实现【写法简单】
package com.yd.thread;
public class One {
public static void main(String[] args) {
//简化写法
new Thread(()->System.out.println(Thread.currentThread().getName())).start();
//1.匿名内部类
// new Runnable() {
// @Override
// public void run() {
// System.out.println(Thread.currentThread().getName());
// }
// };
//2.lambda写法 放入任务放到Thread类
// Runnable runnable = () -> System.out.println(Thread.currentThread().getName());
// new Thread(runnable);
}
}
ps:函数式接口,所以可以简化
用Callable接口,结合FutureTask类完成【可以返回结果】
我们的前面的实现是把实现Runnable接口的类的对象放入Thread中,运行,但是我们重写的run方法均不能直接返回结果。
这个时候我们可以用Callable结合FutureTask类来实现有返回值的方式放入Thread中,运行。
package com.yd.thread;
import java.util.concurrent.Callable;
public class Two implements Callable<String> {
private int n;
//构造函数的重载
public Two(int n){
this.n=n;
}
//重写的是call
@Override
public String call() throws Exception {
int sum=0;
for (int i = 0; i <= n; i++) {
sum+=i;
}
return "n"+"和是"+sum;
}
}
package com.yd.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class TwoTest {
public static void main(String[] args) {
//创建一个Callable对象 /他不是线程任务,所以要再封装一下,但是需要它能返回的能力
Callable<String> twoCallA = new Two(1);
//交给FutureTask,封装Callable对象,变成未来任务对象 可以在线程执行完成后调用get方法,获取返回的结果 /创建一个Runnable对象任务
FutureTask<String> stringFutureTaskA = new FutureTask<>(twoCallA);
//交给Thread类
Thread tA = new Thread(stringFutureTaskA);
//启动线程
tA.start();
//同样再开一个线程
Callable<String> twoCallB = new Two(100);
FutureTask<String> stringFutureTaskB = new FutureTask<>(twoCallB);
Thread tB = new Thread(stringFutureTaskB);
tB.start();
//获取线程执行完毕的结果
try {
String sA = stringFutureTaskA.get();
System.out.println(sA);
String sB = stringFutureTaskB.get();
System.out.println(sB);
}catch (Exception e){
e.printStackTrace();
}
}
}
线程池的方式【是一种复用线程的技术】
不使用线程池,如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能。
使用线程池不用每次重新创建线程,保持重复利用。
方法一
我们通常使用Executorservice的实现类ThreadpoolExecutor自创建一个线程池对象
ThreadPoolExcecutor构造器参数说明
参数一:指定线程池的线程数量(核心线程):corepoolsize 不能小于0
参数二:指定线程池可支持的最大线程数: maximumpoolsize 最大数量>核心线程数量
参数三:指定临时线程的最大存活时间: keepalivetime 不能小于0
参数四:指定存活时间的单位(秒,分,时,天): unit 时间单位
参数五:指定任务队列:workqueue 不能为null
参数六:指定用哪个线程工厂创建线程:threadfactory 不能为null
参数七:指定线程忙,任务满的时候,新任务来了怎么办: handler 不能为null
参数五一般我们选用的就是ArrayBlockingQueue和LinkedBlockingQueue,
队列是一种数据结构,我们会在下一个课程去学习,不过我们可以简单了解一下我们配置的通常几个
- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)。
- LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE。
- PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现 Comparable 接口也可以提供 Comparator 来对队列中的元素进行比较。跟时间没有任何关系,仅仅是按照优先级取任务。
- DelayQueue:类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
简单的说参数五的作用也就是我们需要把任务怎么放进去,怎么排列。
参数六的我们默认写Executors.defaultThreadFactory()就可以了它的本质其实就是创建一个
Thread类对象,只不过多了一写处理
参数七的策略如下
一般是用这三个
Abortpolicy 中止策略 默认
DiscardOldestPolicy 放弃最旧的策略
callerRunsPolicy调用主方运行策略
然后是Executorservice常用方法
就是执行和关闭(线程池正常不会关闭)的方法
package com.yd.thread;
import java.util.concurrent.*;
public class ExecutorDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//指定线程池的线程数量是3 相当于工作的员工是三个
//指定线程池可支持的最大线程数7 相当于公司总共最多有七个人 临时工两个【等着】
//指定临时线程的最大存活时间6 相当于临时工空闲多久被开除6加上后面的单位
//指定任务队列 相当于吃饭的座位 任务相当于是菜
//指定线程任务池的线程工厂 相当于HR 创建线程的方式
//任务策略 相当于忙不过来的解决办法,拒绝方法
ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 7, 6, TimeUnit.MINUTES,
new ArrayBlockingQueue<>(2), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
//现在是提交任务给线程池执行,我们之前是给给Tread类运行
//执行Callable任务
Future<String> submitA = pool.submit(new Two(1));
Future<String> submitB = pool.submit(new Two(100));
System.out.println(submitA.get());
System.out.println(submitB.get());
//执行Runnable任务
pool.execute(new RunnableStyle());
pool.execute(new RunnableStyle());
pool.execute(new RunnableStyle());
pool.execute(new RunnableStyle());
pool.execute(new RunnableStyle());
}
}
我们的Runnable任务
package com.yd.thread;
/**
* 实现Runnable接口实现创建线程
*/
//我们需在类上要实现Runnable接口
public class RunnableStyle implements Runnable{
public static void main(String[] args) {
//Runnable对象和Thread对象相关联
//创建一个任务对象(其实就是实例化该类的对象)
RunnableStyle runnableStyle = new RunnableStyle();
//把任务对象交给Thread的对象处理
Thread threadOne = new Thread(runnableStyle);
//调用start()启动线程
threadOne.start();
}
//需要我们重写run()方法
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"用继承Runnable来实现多线程");
}
}
我们的callable任务
package com.yd.thread;
import java.util.concurrent.Callable;
public class Two implements Callable<String> {
private int n;
//构造函数的重载
public Two(int n){
this.n=n;
}
//重写的是call
@Override
public String call() throws Exception {
int sum=0;
for (int i = 0; i <= n; i++) {
sum+=i;
}
return Thread.currentThread().getName()+" n"+"和是"+sum;
}
}
可复用
方法二 【存在风险,了解即可】
还有一种创建线程池的方案
使用Executors(线程池的工具类)调用已经搭配好的线程池对象
注意:Executors的底层其实也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的,只是搭配好了。
但是在大型并发的使用可能会有风险,所以我们了解即可
定时器【周期调用的技术】
定时器类似于我们生活中的“闹钟”,达到设定的时间后,就执行某个指定的代码。
但是我们需要注意的是它是保持大约的固定的时间间隔进行,但是一般来说没啥问题
引入了这个并发包单线程变为多线程
-
-
task(command)
要安排的任务。delay
- 任务执行前的延迟毫秒数。period
- 连续任务执行之间的时间(以毫秒为单位)。
-
第一个参数:是一个TimerTask对象,TimerTask是一个继承了Runnable接口的类,我们只需要new一个TimerTask对象,重写里面的run方法即可。
第二个参数:表示指定的时间,默认单位是毫秒,即多长时间之后执行任务代码。
第三个参数:是连续任务执行之间的时间(以毫秒为单位)
最后一个参数 :是单位
NANOSECONDS(纳秒)、MICROSECONDS(微秒)、MILISECONDS(毫秒)、SECONDS(秒)、MINUTE(分钟)、HOURS(小时)和DAYS(天)
- 创建Timer定时器,调用定时器的方法执行定时器任务
- 创建TimerTask定时器任务,可以通过匿名内部类的方式创建
package com.yd.thread;
import java.util.Timer;
import java.util.TimerTask;
public class TimerDemo {
public static void main(String[] args) {
// 创建定时器
Timer timer = new Timer();
// 创建定时器任务
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println("1"+Thread.currentThread().getName());
}
}, 1, 1000);
//任务2
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println("2"+Thread.currentThread().getName());
}
}, 1000, 200);
}
}
操作线程的方法
在操作线程之前我们要学习一下线程周期
线程周期
七种状态
接下来我们线程状态将结合着操作来谈
线程的加入
线程的加入就是:某个程序为多线程程序,假如存在线程A,现在需要插入B,并且要求B先执行完毕。
这就好比你现在看文章然后父母喊你吃饭一样,先得吃饭,然后才可以继续看,或者没有话费这样的更准确,先去充话费然后再打电话。
线程的加入可以用thread的join()方法来完成。
join()它的作用是将当前线程挂起,等待其他线程结束后再执行当前线程,
即当前线程等待另一个调用join()方法的线程执行结束后再往下执行。
通常在main主线程内,等待其它调用join()方法的线程执行结束再继续执行main主线程。
package com.yd.thread;
public class Synchronized {
public static void main(String[] args) throws InterruptedException {
//实例化对象
RunnableStyle one = new RunnableStyle();
SynchronizedTest t = new SynchronizedTest();
//把任务放到Thread
Thread A = new Thread(t, "小尘");
Thread a = new Thread(one);
A.start();
A.join(1000);
a.start();
// thread.join(1000);
System.out.println(Thread.currentThread().getName()+"主线程的内容");
}
}
线程的睡眠
线程的睡眠可以使用 sleep()方法,需要指定一个毫秒为单位,使线程在规定参数时间不能到就绪状态,同时醒来不能保证运行状态,但是能保证就绪状态,所以需要抛出异常。
线程的中断
废除了stop()方法
我们会用提倡使用在run()中使用无限循环的形式,然后使用一个布尔类型标记控制循环的停止。
还有如果是使用sleep()或者wait()方法进入就绪状态,我们可以使用Thread的interrupt()方法来通知线程离开run()方法,同时抛出一个异常,可以在处理异常的时候完成业务中断,如终止while循环,或者是在while做一个isinterrupt判断。
package com.yd.thread;
public class RunnableDemo implements Runnable{
public static void main(String[] args) throws InterruptedException {
RunnableDemo runnableDemo = new RunnableDemo();
Thread thread = new Thread(runnableDemo);
thread.start();
Thread.sleep(2000);
//我们可以做一个对比,把下面注释去掉
thread.interrupt();
}
//需要我们重写run()方法
@Override
public void run() {
int num=0;
//Integer.MIN_VALUE为int的最大值,还没有被中断就可以运行
while (!Thread.currentThread().isInterrupted()&&num<=Integer.MAX_VALUE/2){
if (num%100==0){
System.out.println(num+"是10000倍数");
}
num++;
}
System.out.println("运行结束");
}
}
线程的优先级
线程可以划分优先级,优先级较高的线程得到的 CPU 资源较多,即 CPU 优先执行优先级较高的线程对象的任务,但是需要注意的是优先级小并不是得不到运行,只是概率小。
在 Java 中使用 setPriority
方法来设置优先级,同时把优先级划分成 1~10 这10个等级,
如果小于 1 或者大于 10,则 JDK 会抛出异常
// 线程最小的优先级等级
public final static int MIN_PRIORITY = 1;// 线程默认的优先级等级
public final static int NORM_PRIORITY = 5;// 线程最大的优先级等级
public final static int MAX_PRIORITY = 10;
线程的礼让
线程礼让是指在某个特定的时间点,让线程暂停抢占CPU资源的行为,但是其只是一种暗示,不能保证,在我们线程中操作线程都不能一定保证操作。
线程的礼让使用yield方法来实现
package com.mie.yield;
public class YieldThread1 extends Thread{
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 10; i++) {
if (i==5) {
yield();//当i==5时线程礼让
}
System.out.println(Thread.currentThread().getName()+"----"+i);
}
}
}
package com.mie.yield;
public class YieldThread2 extends Thread{
@Override
public void run() {
// TODO Auto-generated method stub
for (int j = 0; j < 10; j++) {
System.out.println(Thread.currentThread().getName()+"====="+j);
}
}
}
package com.mie.yield;
public class Test {
public static void main(String[] args) {
YieldThread1 yieldThread1=new YieldThread1();
YieldThread2 yieldThread2=new YieldThread2();
yieldThread1.setName("线程1");
yieldThread2.setName("线程2");
yieldThread1.start();
yieldThread2.start();
}
}
线程的守护
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
-
垃圾回收器线程就是一种守护线程
要将普通线程设置为守护线程,方法很简单,只需要调用 Thread.setDaemon() 方法即可。
public class MyThread extends Thread{
private int i = 0;
@Override
public void run() {
try {
while (true) {
i++;
System.out.println("i=" + (i));
Thread.sleep(1000);
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class Run3 {
public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.setDaemon(true); //该线程为守护线程
thread.start();
Thread.sleep(5000);
System.out.println("主线程(用户线程)结束,thread线程(守护线程)也不再打印了!");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
线程的安全
多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。
最典型的案例就是账户取钱
小尘和小土是兄弟,他们有一个共同的账户,余额是2k,模拟2人同时去取钱2k。
如果说同时执行取2k,用代码实现发现钱都可以取到,平白无故多2k。
同样的案例还有很多比如,银行排号,火车站售票
为了解决这样奇葩的问题,我们得让多个线程实现先后依次访问共享资源,这样可以解决安全问题。
加锁
我们第一个想法就是,加锁,抢一个锁,谁先拿到谁先访问
这就好比上洗手间,一个人先到把门关上,出来再将门打开,下一个人才可以进入。
那么我们可以怎么操作呢
同步块
把访问共享资源的核心代码给上锁,以此保证线程安全
每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。
ctrl+art+t 快捷键
synchronized(Object){
}
同步机制使用了synchronized关键字,使用该关键字的代码块被称为同步块
我们将共享资源放在synchronized定义的区域,当其他线程获取这个锁的时候,就必须等待锁释放后才可以进入该区域。
Object是任意一个对象,每一个对象都存在一个标志位,并且有两个值,0和1。
如果是0代表同步块有线程运行,当期线程处于就绪状态,直到处于同步代码块内容执行完毕,值为1,当期线程才可以执行代码块内容。
我们模拟一个买橘子的场景,五个人买橘子每次只能买30个,一共老板只有100个。
package com.yd.thread;
public class SynchronizedTest implements Runnable{
int num=100;
public void BuyOranges(int buyNum){
String name = Thread.currentThread().getName();
synchronized (this){
if (this.num>buyNum){
System.out.println(name+"买了"+buyNum+"橘子");
//更新数量
this.num-=buyNum;
}else {
System.out.println(name+"没有那么多橘子让你买");
}
}
}
@Override
public void run() {
BuyOranges(30);
}
}
package com.yd.thread;
public class Synchronized {
public static void main(String[] args) {
//实例化一个对象
SynchronizedTest t = new SynchronizedTest();
//把任务放到Thread
Thread A = new Thread(t, "小尘");
Thread B = new Thread(t, "小土");
Thread C = new Thread(t, "小水");
Thread E = new Thread(t, "小火");
Thread D = new Thread(t, "小金");
A.start();
B.start();
C.start();
D.start();
E.start();
}
}
我们的锁对象不要影响其他线程执行
规范上:建议使用共享资源作为锁对象。
对于实例方法建议使用this作为锁对象。
对于静态方法建议使用字节码(类名.class)对象作为锁对象。
同步方法
把访问共享资源的核心方法给上锁,以此保证线程安全。
修饰符synchronized返回值类型方法名称(形参列表){
操作共享资源的代码}
原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码,
如果方法是实例方法:同步方法默认用this作为的锁对象,但是代码要高度面向对象。
如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
lock锁
lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。
Lock并不是用来代替synchronized的而是当使用synchronized不满足情况或者不合适的时候来提供高级功能的
lock是接口,不能直接实例化,这里采用它的实现类Reentrantlock来构建lock锁对象。
锁对象创建完以后,在方法的对应的位置添加。
灵活性地提高带来了额外的责任。 缺少块结构锁定需要手动地去释放锁。 在大多数情况下,应使用以下惯用法:
Lock lock = new ReentrantLock();
lock.lock();
try{
}finally {
lock.unlock();
}
package com.yd.thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SynchronizedTest implements Runnable{
int num=100;
Lock lock = new ReentrantLock();
public void BuyOranges(int buyNum) {
try {
String name = Thread.currentThread().getName();
lock.lock();
if (this.num > buyNum) {
System.out.println(name + "买了" + buyNum + "橘子");
//更新数量
this.num -= buyNum;
} else {
System.out.println(name + "没有那么多橘子让你买");
}
} finally {
lock.unlock();
}
}
@Override
public void run() {
BuyOranges(30);
}
}
当然我们线程的内容(涉及到并发,安全内容)需要结合我们后面高级内容具体的来谈,在这里只是基础的把知识点内容复述一下。