1 引入
首先我们先了解一些概念。
程序、进程、线程
程序(program):是为了完成特定任务、用某种语言编写的一组指令的集合,是一段静态的
代码
。
进程(process):是程序执行的一次
执行过程
,进程是动态的,它有一个产生、存在和消亡的过程即进程的生命周期。
线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径。若一个进程同一时间并行执行多个线程,那它就是支持
多线程
的。
单核CPU与多核CPU的任务执行
单核CPU
举个例子,我们刚刚上完一上午的课买完饭回到宿舍,打开电脑,打开番剧,打开我我们的饭,正准备开动,朋友微信发消息来了,要和你分享一个大瓜,于是你吃上一口饭,开始回复消息,然后在吃饭回复的间隙,又抬头看一眼番剧,你好像是同时在吃饭、看番、吃瓜,但是实际上你是吃一口饭,回一下消息,看一眼番,这个就是单核CPU执行任务时的样子,CPU在执行任务时,按照时间片执行,一个时间片只能执行一个线程,比如只能吃饭或者回消息或者看番,但是因为时间片时间特别短,所以感受就是,“同时”
执行了多个线程,实际上这只是一种假象。
多核CPU
假设我们发生了变异,长出了三个脑子,现在我们就可以一边享受美食、一边享受吃瓜的快乐、一边享受追番的快乐,真正意义上做到一个时间片多线程同时运行,实现了真真正正意义上的多线程,这也就是多核CPU执行任务时的样子。
并行和并发:
并行:多个CPU
同时
执行多个任务
并发:一个CPU
“同时”
执行多个任务(采用时间片轮转)
2 如何创建线程?(重点)
思考一个问题:我们平常编写的代码,是单线程的吗?
答案是:否。我们平常编写的程序,一般有三个线程,一个是main方法对应的主线程,一个是处理异常的线程(异常线程会影响主线程的执行),一个是垃圾收集器线程。
我们现在想要自己制造多线程的话,应该怎么做呢,我们往下看。
2.1 继承Thread类
2.1.1 实现
- 首先我们创建一个线程类,然后继承Thread,然后实现run方法
package com.lly.test01;
/**
* 继承Thread后就具备了争抢资源的能力
*/
public class TestThread extends Thread{
/**
* 一会线程对象就要开始争抢资源了,那这个线程要执行的任务是什么?
* 这个任务要放在方法中
* 这个方法不能是随便写的一个方法,必须是重写Thread类中的run方法
*/
@Override
public void run() {
//输出1-10
for(int i = 1;i <= 10;i++){
System.out.println("回"+(i+1)+"条消息");
}
}
}
- 测试一下
package com.lly.test01;
public class Test {
public static void main(String[] args) {
//主线程吃饭
for (int i = 0; i < 10; i++) {
System.out.println("main----吃"+(i+1)+"口饭");
}
//制造其他线程,和主线程争抢资源
//具体的线程对象:子线程
TestThread tt = new TestThread();
tt.run();//调用run方法,执行线程中的任务
}
}
- 结果
main----吃1口饭
main----吃2口饭
main----吃3口饭
main----吃4口饭
main----吃5口饭
main----吃6口饭
main----吃7口饭
main----吃8口饭
main----吃9口饭
main----吃10口饭
回1条消息
回2条消息
回3条消息
回4条消息
回5条消息
回6条消息
回7条消息
回8条消息
回9条消息
回10条消息
这时我们发现,我们创建的线程和主线程并没有相互争抢资源,然后交叉执行,这时为什么呢?
因为我们直接调用了run方法,这样run方法就会被当成一个普通的方法执行。那怎么解决呢?
我们可以通过start()
方法启动线程,它是父类Thread
的方法。
TestThread tt = new TestThread();
//tt.run();//调用run方法,执行线程中的任务
tt.start();
注意:我们需要将主线程中吃饭的任务放到回消息线程的后面,不然在执行吃饭的时候,子线程还没有开启,达不到相互争抢资源的效果。
测试结果:
main----吃1口饭
回1条消息
main----吃2口饭
回2条消息
main----吃3口饭
回3条消息
main----吃4口饭
main----吃5口饭
main----吃6口饭
main----吃7口饭
回4条消息
main----吃8口饭
main----吃9口饭
main----吃10口饭
回5条消息
回6条消息
回7条消息
回8条消息
回9条消息
回10条消息
原理如下:
2.1.2 补充
设置读取线程的名字:
1、通过setName()
和getName()
主线程:
//给main方法这个主线程设置名字
//Thread.currentThread()作用:获取当前正在执行的线程
Thread.currentThread().setName("主线程");
System.out.println(Thread.currentThread().getName()+"吃"+i+"口饭");
子线程:
TestThread tt = new TestThread();
tt.setName("子线程");
System.out.println(this.getName()+"回"+i+"条消息");
2、通过构造器设置线程的名字
public TestThread(String name){
super(name);
}
TestThread tt = new TestThread("子线程");
这样就可以方便的区分线程了。
2.1.3 案例:买火车票
- BuyTicketThread
package com.lly.test02;
public class BuyTicketThread extends Thread{
public BuyTicketThread(String name){
super(name);
}
//一共10张票
static int ticketNum = 10;//多个对象共享10张票
//每个窗口都是线程对象,每个对象执行的代码放到run方法中
@Override
public void run() {
//每个窗口后面有100个人在抢票
for (int i = 1; i <= 100; i++) {
if (ticketNum>0){
System.out.println("我在"+ this.getName()+"买到了从北京到哈尔滨的第"+ ticketNum-- + "张车票");
}
}
}
}
- main
public static void main(String[] args) {
//多个窗口抢票
BuyTicketThread t1 = new BuyTicketThread("窗口1");
t1.start();
BuyTicketThread t2 = new BuyTicketThread("窗口2");
t2.start();
BuyTicketThread t3 = new BuyTicketThread("窗口3");
t3.start();
}
- 结果,多次运行发现有异常情况
为什么呢?我们先保留这个疑问,继续看下去。
2.2 实现Runnable接口
2.2.1 实现
- 线程类
package com.lly.test03;
/*
TestThread实现了这个接口,才会变成一个线程类
*/
public class TestThread implements Runnable{
@Override
public void run() {
for(int i = 1;i <= 10;i++){
//因为不是继承的Thread所以不能直接掉getName()
System.out.println(Thread.currentThread().getName()+"回"+i+"条消息");
}
}
}
- 测试类
package com.lly.test03;
public class Test {
public static void main(String[] args) {
//创建子线程对象
TestThread tt = new TestThread();
//在Thead里面层层调佣最后调用了tt的run方法
Thread t = new Thread(tt,"子线程");
t.start();
//主线程
//主线程吃饭
for (int i = 1; i <= 10; i++) {
//不设置名字默认为main
System.out.println(Thread.currentThread().getName()+"吃"+i+"口饭");
}
}
}
- 结果
main吃1口饭
子线程回1条消息
main吃2口饭
子线程回2条消息
main吃3口饭
子线程回3条消息
main吃4口饭
子线程回4条消息
子线程回5条消息
子线程回6条消息
子线程回7条消息
main吃5口饭
main吃6口饭
main吃7口饭
子线程回8条消息
main吃8口饭
子线程回9条消息
main吃9口饭
子线程回10条消息
main吃10口饭
2.2.2 案例:买火车票
- 线程类
package com.lly.test04;
public class BuyTicketThread implements Runnable{
int ticketNum = 10;
@Override
public void run() {
for (int i = 1;i<=100;i++){
if (ticketNum>0){
System.out.println("我在"+Thread.currentThread().getName()+"买到了到哈尔滨的第"+ ticketNum-- + "张车票");
}
}
}
}
- 测试
public static void main(String[] args) {
//定义一个线程对象
BuyTicketThread t = new BuyTicketThread();
//窗口1买票
Thread t1 = new Thread(t,"窗口1");
t1.start();
//窗口2买票
Thread t2 = new Thread(t,"窗口2");
t2.start();
//窗口1买票
Thread t3 = new Thread(t,"窗口3");
t3.start();
}
- 结果
我在窗口3买到了到哈尔滨的第6张车票
我在窗口3买到了到哈尔滨的第5张车票
我在窗口1买到了到哈尔滨的第9张车票
我在窗口3买到了到哈尔滨的第4张车票
我在窗口2买到了到哈尔滨的第7张车票
我在窗口3买到了到哈尔滨的第2张车票
我在窗口1买到了到哈尔滨的第3张车票
我在窗口2买到了到哈尔滨的第1张车票
问题1:实际开发中,使用方式一继承Thread类还是方式二实现Runnable接口这种方式多呢?
- 方式一有Java单继承的局限性,因为继承了Thread类,就不能再继承其它的类了。
- 方式二的共享资源的能力也会强一些,不需要非得加个static修饰。
问题2:Thread类和Runnable接口有联系吗?
2.3 实现Callable接口
对比第一种和第二种创建线程的方式,我们可以发现,无论采用继承Thread类的方式还是实现Runnable接口的方式都需要有一个run()
方法,但是这个run方法有两点不足:
- 没有返回值
- 不能抛出异常
基于上面的不足,在jdk1.5以后出现了第三种创建线程的方式,实现Callable接口
.实现Callable接口后,就可以解决上面的不足之处,但缺点是,线程的创建相对而言比较麻烦。
查看Callable:
2.3.1 实现
- 线程类
public class TestRandomNum implements Callable<Integer> {
/**
1.实现Callable接口,可以不带泛型,此时call的返回值是Object类型
2.如果带泛型,那么call的返回值就是泛型对应的返回类型
3.从call方法看到:方法有返回值,可以抛出异常
*/
@Override
public Integer call() throws Exception {
return new Random().nextInt(10);//返回10以内的随机数
}
}
- 测试
public static void main(String[] args) throws ExecutionException, InterruptedException {
//定义一个线程对象
TestRandomNum trn = new TestRandomNum();
/*
要开启线程必须要Thread的start方法,但是Thread的构造器没有参数为Callable的,
所以套一层FutureTask,它继承了Runnable
*/
FutureTask ft = new FutureTask<>(trn);
Thread t = new Thread(ft);
t.start();
//获取线程得到的返回值
Object o = ft.get();
System.out.println(o);
}
- 结果:输出2。
3 线程的生命周期
4 线程常见方法
方法名称 | 说明 |
---|---|
start() | 启动线程,使其进入就绪状态,等待调度器调度 |
run() | 线程的主体方法,包含线程的执行逻辑 |
currentThread() | Thread类中的一个静态方法,获取当前正在执行的线程对象 |
setName(String name) | 设置线程名称 |
getName() | 获取线程名称 |
setPriority(int priority) | 设置线程的优先几倍,最低为1,默认为5,最高为10(优先级只是建议性的) |
join() | 等待该进程终止,该进程结束后才可以去执行其余进程 |
sleep(long millis) | 使当前线程休眠指定的毫秒数,人为的制造阻塞 |
setDeamon() | 设置伴随线程,主线程停止的时候,伴随线程也不要继续执行了 |
stop() |
2.5.1 setPriority(int priority)
package com.lly.test06;
public class TestThread01 extends Thread{
@Override
public void run() {
for (int i = 1;i<=5;i++){
System.out.print(i);
}
}
}
class TestThread02 extends Thread{
@Override
public void run() {
for (int i = 6; i <= 10; i++) {
System.out.print(i);
}
}
}
class Test{
public static void main(String[] args) {
TestThread01 t1 = new TestThread01();
t1.setPriority(10);//优先级别高
t1.start();
TestThread02 t2 = new TestThread02();
t2.setPriority(1);//优先级别低
t2.start();
}
}
结果:
12345678910
2.5.2 join()
package com.lly.test07;
public class TestThread extends Thread{
public TestThread(String name){
super(name);
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(this.getName()+"----"+i);
}
}
}
class Test{
public static void main(String[] args) throws InterruptedException {
for (int i = 1; i <= 5; i++) {
if(i==3){
//创建子线程
TestThread t1 = new TestThread("子线程");
t1.start();
t1.join();
//注意一定先start在join
}
System.out.println("main----"+i);
}
}
}
注意:一定先start在join。
结果:
main----1
main----2
子线程----1
子线程----2
子线程----3
子线程----4
子线程----5
main----3
main----4
main----5
2.5.3 sleep(long millis)
public class Test {
public static void main(String[] args) {
//获取当前时间
Date date1 = new Date();
//定义时间格式
DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
System.out.println(dateFormat.format(date1));
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//获取当前时间
Date date2 = new Date();
System.out.println(dateFormat.format(date2));
}
}
结果:
23:08:21
23:08:24
2.5.4 setDeamon()
通俗的理解,皇上驾崩,妃子陪葬。
public class TestThread extends Thread{
@Override
public void run() {
for (int i = 1; i <= 1000; i++) {
System.out.println("子线程----"+i);
}
}
}
class Test{
public static void main(String[] args) {
//创建并启动子线程
TestThread tt = new TestThread();
tt.setDaemon(true);//设置伴随线程
tt.start();
//主线程中输出1-5的数字
for (int i = 1; i <= 10; i++) {
System.out.println("main----"+i);
}
}
}
注意:要先设置为伴随线程,再启动。
结果:
main----1
子线程----1
main----2
子线程----2
子线程----3
子线程----4
子线程----5
子线程----6
子线程----7
子线程----8
子线程----9
子线程----10
子线程----11
子线程----12
子线程----13
main----3
子线程----14
main----4
main----5
main----6
main----7
main----8
main----9
main----10
子线程----15
子线程垂死挣扎了一下。
2.5.5 stop()
public static void main(String[] args) {
for (int i = 1; i <= 100; i++) {
if(i==6){
Thread.currentThread().stop();//过期的方法,不建议使用
}
System.out.print(i);
}
}
结果:
12345
5 线程的安全问题(重点)
之前买火车票的案例有一个异常情况还记得吗,为什么会出现那样的异常呢?
那个时候出现了同票
和0票
的问题,我们先来看看源代码:
//每个窗口后面有100个人在抢票
for (int i = 1; i <= 100; i++) {
if (ticketNum>0){
System.out.println("我在"+ this.getName()+"买到了从北京到哈尔滨的第"+ ticketNum-- + "张车票");
}
}
我们有三个窗口在进行买票的操作,也就是有三个线程都在买票,具体的买票操作呢,就相当于是输出操作,每次通过if
判断之后,线程就被放行
到输出操作,进行控制台打印和车票余量-1
。
假设此时的车票余量为10,线程1和线程2都通过了if
判断,两个线程都相继在控制台进行打印,然后相继对车票余量进行-1
的操作,此时就出现了同票
。
假设现在车票的余量为1,线程1和线程2都通过了if
判断,此时线程1抢占
到了io资源,进行了输出,同时也抢占
到了车票资源,对车票余量进行-1
操作,然后线程2才得到资源,在控制台打印输出,此时车票的余量已经为0,就会出现0票
的情况,有的时候还会出现票数为负的情况。
这就是出现了所谓的线程安全问题,即多个线程在争抢资源的过程中,导致共享的资源出现问题。那么我们如何解决线程安全问题呢?
首先我们要知道两种编程范式,同步编程模型
和异步编程模型
。
- 同步编程模型:任务按顺序依次执行,如果有两个线程,线程2必须等线程1执行完了再执行,有多个线程时也是排队等待执行。
- 异步编程模型:任务之间是并发执行的,一个任务的执行不会阻塞其它任务的执行。假设现在有两个线程,它们各自执行各自的,不需要相互等待。
我们将采用同步编程模型即线程同步来解决线程安全问题。
5.1 同步代码块
我们通过加锁
即加同步
或者同步监视器
的方式来实现。
首先,我们需要知道,Java的一个关键字synchronized
,它是用来实现线程间的同步的,它能确保多个线程不会同时访问共享资源,从而避免了上述的问题,可以理解为,当一个线程访问共享资源的时候,可以用它给共享资源加一把锁,它访问完了之后释放掉这把锁之后,另一个线程才能够访问共享资源。
我们可以将synchronized
关键字用于代码块上,指定要同步的对象,当一个线程进入同步代码块时,它会尝试获取指定对象的锁,如果获取成功,则执行代码块;如果获取失败,则该线程将被阻塞,直到获取到锁为止。
public class BuyTicketThread implements Runnable{
int ticketNum = 10;
@Override
public void run() {
for (int i = 1;i<=100;i++){
synchronized (this){
//this是指当前对象,可以看做当前锁的钥匙,只有这个钥匙能开锁,关锁
if (ticketNum>0){
System.out.println("我在"+Thread.currentThread().getName()+"买到了到哈尔滨的第"+ ticketNum-- + "张车票");
}
}
}
}
}
注:把具有安全隐患的代码即进行增删改的代码锁住即可,如果锁多了就会效率低。
多次测试发现,没有出现同票、0票或负票的异常,并且买到的票都按顺序输出没有乱序,说明我们对共享资源
加锁成功,解决了线程安全问题。
但是注意,我们这里测试的是通过
实现Runnable接口
实现的线程类,再用同样的方式给继承Thread
的线程类加锁测试,发现没有起到相同的效果,还是有线程安全问题,这是为什么呢?
对于实现Runnable接口
的方式,多个线程可能是是共享一个Runnable对象作为锁,那么多个线程之间就会竞争同一把锁
,这样就可以实现同步的效果,然而,对于继承Thread类
实现的线程类,每个线程都拥有独立的对象实例(this是不同的),因此在使用synchronized关键字时,锁的对象也是独立的,相当于每个线程都有一把自己的锁,这和之前不加锁的时候效果是一样的,所以不行。
我们应该怎么解决呢?
我们在使用synchronized
关键字时,后面括号中的内容代表着锁对象,即指定了用于同步的对象。我们前面出现问题,是因为使用this
作为锁对象,我们可以换一个唯一
的对象来作为锁对象,来保证多个线程用的是一把锁,比如当前对象的字节码信息BuyTicketThread.class(或共享资源)
或者其它的引用类型,注意一定是引用类型
。
synchronized (BuyTicketThread.class){
if (ticketNum>0){
System.out.println("我在"+ this.getName()+"买到了从北京到哈尔滨的第"+ ticketNum-- + "张车票");
}
}
5.2 同步方法
我们把抢票逻辑,单独封装成一个方法,然后加上synchronized
修饰,这个方法就变成了同步方法。
public class BuyTicketThread implements Runnable{
int ticketNum = 10;
@Override
public void run() {
for (int i = 1;i<=100;i++){
buyTicket();
}
}
public synchronized void buyTicket(){
if (ticketNum>0){
System.out.println("我在"+Thread.currentThread().getName()+"买到了到哈尔滨的第"+ ticketNum-- + "张车票");
}
}
}
测试结果正常。
线程方法实际上锁住的是调用方法的对象,那么就还是会出现之前的问题,即用同样的方式给继承Thread
的线程类加锁测试,发现没有起到相同的效果,在方法前加一个static
修饰就可以了,就相当于锁住的是BuyTicketThread.class
.
public synchronized void buyTicket(){
if (ticketNum>0){
System.out.println("我在"+Thread.currentThread().getName()+"买到了到哈尔滨的第"+ ticketNum-- + "张车票");
}
}
这样问题就解决了。
补:同步方法锁的是this,一旦锁住一个方法,就锁住了所有的同步方法,同步代码块只是锁住了当前的代码块,没有锁住其它的代码块。
5.3 Lock锁
JDK1.5后新增一代的线程同步方式:Lock锁
,与采用synchronized相比,lock可提供多种锁方案,更灵活。
synchronnized
是Java中的关键字,这个关键字是靠JVM
来识别完成的,是虚拟机级别的,Lock锁
是API级别的,提供了相应的接口和对应的实现类,这个方式更灵活,表现出来的性能优于之前的方式。
Lock锁使用:
public class BuyTicketThread implements Runnable{
int ticketNum = 10;
//拿来一把锁,可重入锁
Lock lock = new ReentrantLock();
@Override
public void run() {
for (int i = 1;i<=100;i++){
//打开锁
lock.lock();
try{
if (ticketNum>0){
System.out.println("我在"+Thread.currentThread().getName()+"买到了到哈尔滨的第"+ ticketNum-- + "张车票");
}
}catch (Exception e){
e.printStackTrace();
}finally {
//关闭锁,即使有异常,这个锁也可以释放
lock.unlock();
}
}
}
}
Lock锁和synchronized的区别:
- Lock锁是
显式锁
(手动开启和关闭锁),synchronized是隐式锁
- Lock只有
代码块锁
,synchronized有代码块锁和方法锁
- 使用Lock锁,
JVM
将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)。 - 优先使用顺序:Lock > 同步代码块(已经进入了方法,分配了相应资源)> 同步方法(在方法体之外)
5.4 线程同步的缺点
- 当线程很多的时候,每个线程都会去判断同步上面的这个锁,很耗费资源,降低效率。
- 可能造成死锁。
5.4.1 死锁
形成原因:当两个或多个线程分别占用对方需要的同步资源,同时等待对方释放自己需要的同步资源,这样无休止的相互等待,就形成了线程的死锁。当某一个同步代码块同时拥有两个以上对象的锁
,就可能发生死锁
的问题。
死锁:
public class DeadLock {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
// t1和t2两个线程共享o1,o2
Thread t1 = new MyThread1(o1,o2);
Thread t2 = new MyThread2(o1,o2);
t1.start();
t2.start();
}
}
class MyThread1 extends Thread{
Object o1;
Object o2;
public MyThread1(Object o1,Object o2){
this.o1 = o2;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o1){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (o2){
}
}
}
}
class MyThread2 extends Thread{
Object o1;
Object o2;
public MyThread2(Object o1,Object o2){
this.o1 = o2;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o2){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (o1){
}
}
}
}
产生死锁的四个必要条件:
- 互斥条件:一个
资源
每次只能被一个进程使用。 - 请求保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不可剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
我们只要想办法破坏其中任意一个或多个条件就可以避免死锁的发生。
6 线程通信问题
6.1 问题引入
应用场景:生产者和消费者问题
- 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费。
- 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费取走为止。
- 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止。
分析:
这是一个线程同步问题
,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。
- 对于生产者,没有生产产品之前,要通知消费者等待,而生产了产品之后,又需要马上通知消费者消费。
- 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费。
- 在生产者消费者问题中,仅有
synchronized
是不够的。- synchronized可阻止并发更新同一个共享资源,实现了同步。
- synchronized不能用来实现不同线程之间的消息传递(通信)
如果仅使用synchronized(以下是使用同步代码块,也可以用同步方法):
- 产品
public class Product {
//品牌
private String brand;
//商品名
private String name;
//getter,setter方法
}
- 生产者
public class ProducerThread extends Thread{//生产者线程
//共享商品
private Product product;
public ProducerThread(Product product) {
this.product = product;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
synchronized (product){
//生产10个商品
if(i%2==0){
//生产一个吧唧
product.setBrand("东映");
try {
Thread.sleep(100);//更容易出现错误
} catch (InterruptedException e) {
e.printStackTrace();
}
product.setName("日向翔阳镭射吧唧");
}else {
//生产亚克力
product.setBrand("吉卜力");
try {
Thread.sleep(100);//更容易出现错误
} catch (InterruptedException e) {
e.printStackTrace();
}
product.setName("哇啦哇啦亚克力挂件");
}
//将生产信息做一个打印
System.out.println("生产者生产了:"+ product.getBrand()+"---"+product.getName());
}
}
}
}
- 消费者
public class CustomerThread extends Thread{//消费者线程
//共享商品
private Product product;
public CustomerThread(Product product){
this.product = product;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {//消费次数
synchronized (product){
System.out.println("消费者消费了:"+ product.getBrand()+"---"+product.getName());
}
}
}
}
- 测试
public class Test {
public static void main(String[] args) {
//共享的商品
Product product = new Product();
//生产者
ProducerThread producerThread = new ProducerThread(product);
//消费者
CustomerThread customerThread = new CustomerThread(product);
producerThread.start();
customerThread.start();
}
}
- 结果
我们期待的效果是,生产者和消费者交替执行,以上显然不是我们想要的结果。
Java提供了几个方法解决线程之间的通信问题:
方法名 | 作用 |
---|---|
wait() | 表示线程一直等待,直到其他线程通知,与sleep不同,它会释放锁,sleep的时候,不会释放锁 |
wait(long timeout) | 指定等待的毫秒数 |
notify() | 唤醒一个处于等待状态的线程 |
notifyAll() | 唤醒一个对象上 所有调用wait()方法的线程,优先级别高的线程优先调度 |
注意:均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛异常。
接下来我们看看具体的解决方法。
6.2 解决方法——信号灯法
信号灯法就射设置一个标志
,这个标志就是所谓信号灯
,通过这个标识来控制是生产者进行生产还是消费者进行消费。学过操作系统的话,应该就知道,这是个互斥信号量
,具体的请看操作系统去。接下来直接实现:
- 产品
public class Product {
//品牌
private String brand;
//商品名
private String name;
//引入一个信号灯:true:红色 false:绿色
boolean flag = false;//默认情况下没有商品,让生产者先生产,然后消费者再消费
//生产商品
public synchronized void produce(String brand,String name){
if (flag==true){
//灯是红色,说明有商品,生产者不生产,等着消费者消费
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//灯是绿色的,说明没有商品,就生产
this.setBrand(brand);
try {
Thread.sleep(100);//更容易出现错误
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setName(name);
//将生产信息做一个打印
System.out.println("生产者生产了:"+ this.getBrand()+"---"+this.getName());
//生产者生产完以后,灯变色,变成红色
flag = true;
//告诉消费者赶紧来消费
notify();
}
//消费商品
public synchronized void getProduct() {
if (flag==false){
//没有商品,等待生产者生产
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("消费者消费了:"+ this.getBrand()+"---"+this.getName());
//消费完:灯变色
flag = false;
//通知生产者生产
notify();
}
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 product;
public ProducerThread(Product product) {
this.product = product;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
//生产10个商品
if(i%2==0){
product.produce("东映","日向翔阳镭射吧唧");
}else {
product.produce("吉卜力","哇啦哇啦亚克力挂件");
}
}
}
}
- 消费者
public class CustomerThread extends Thread{//消费者线程
//共享商品
private Product product;
public CustomerThread(Product product){
this.product = product;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {//消费次数
product.getProduct();
}
}
}
- 测试结果
生产了再消费,消费了再生产,这就是我们想要的结果。
7 线程池
7.1 什么是线程池?
什么是线程池?
线程池
就是一个可以复用线程的技术。
背景:
线程
经常的创建和销毁,会造成很大的开销(CPU),特别是在**并发(多线程)**的情况下,对性能的影响很大。
思路:
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免线程频繁的创建和销毁,实现资源重复利用。这其实是一种池化思想
,主要用于资源的管理,包括线程池,数据库连接池等。
线程池工作原理图:
好处:
- 提高了响应速度,因为线程池中提前创建好了线程对象,减少了创建线程的时间。
- 实现了资源的复用,减少了资源的消耗。
- 便于线程的管理,线程池会提供一些参数,借此可以方便的对线程进行管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时醉倒保持多长时间会终止。
7.2 如何得到线程池对象?
- JDK5.0 提供了代表线程池的接口:ExecutorService。
- 方式一:使用ExecutorService的实现类
ThreadPoolExecutor
创建一个线程池对象。 - 方式二:使用
Executors(线程池的工具类)
调用方法返回不同特点的线程池对象。
ThreadPoolExecutor构造器:
corePoolSize
:核心线程数,即使空闲时仍保留在池中的线程数。maximumPoolSize
:池中允许的最大线程数。keepAliveTime
:当线程数大于内核时,临时线程在终止前等待新任务的最大时间。unit
:keepAliveTime参数的时间单位。workQueue
:指定线程池的任务队列。threadFactory
:指定线程池的线程工厂。handler
:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务该怎么处理)。
线程池的创建(方式一):
ExecutorService pool = new ThreadPoolExecutor(3,5,8, TimeUnit.SECONDS,new ArrayBlockingQueue<>(4),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
问题:
- 临时线程什么时候创建?
- 新任务提交时,发现核心线程都在忙,而且任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
- 什么时候会开始拒绝新任务?
- 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。
我们如何使用线程池处理任务呢?我们先来看一下ExecutorService的常用方法
方法名称 | 说明 |
---|---|
void execute(Runnable command) | 执行Runnable任务 |
Future< T > submit(Callable< T > task) | 执行Callable任务 ,返回未来任务对象,用于获取线程返回的结果 |
void shutdown() | 等全部任务执行完毕后,再关闭线程池 |
List< Runnable> shutdownNow() | 立即关闭线程池,停止正在执行的任务,并返回队列中未执行的任务 |
接下来看具体实现。
7.3 线程池处理Runnable任务
- 任务类
public class MyRunnable implements Runnable{
@Override
public void run() {
//任务内容
System.out.println(Thread.currentThread().getName() + "===>输出666");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
- 测试
public static void main(String[] args) {
//通过ThreadPoolExecutor创建一个线程池对象
ExecutorService pool = new ThreadPoolExecutor(3,5,8, TimeUnit.SECONDS,new ArrayBlockingQueue<>(4),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
Runnable target = new MyRunnable();
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target);
//pool.shutdown();
}
- 结果
我们发现线程池中的线程复用了。
修改线程任务的延时,演示临时线程的创建。
Thread.sleep(Integer.MAX_VALUE);
测试:
public static void main(String[] args) {
//通过ThreadPoolExecutor创建一个线程池对象
ExecutorService pool = new ThreadPoolExecutor(3,5,8, TimeUnit.SECONDS,new ArrayBlockingQueue<>(4),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
Runnable target = new MyRunnable();
//三个核心线程在忙
pool.execute(target);
pool.execute(target);
pool.execute(target);
//在任务队列里,此时还是三个线程
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target);
}
此时是三个线程:
再来两个任务的话:
pool.execute(target);
pool.execute(target);
此时达到了最大线程数5个。
再来一个任务,抛出异常:
rejected from java.util.concurrent.ThreadPoolExecutor@2503dbd3[Running, pool size = 5, active threads = 5, queued tasks = 4, completed tasks = 0]
这是默认的拒绝策略,不同的拒绝策略有不同的处理方式:
策略 | 详解 |
---|---|
ThreadPoolExecutor.AbortPolicy | 丢弃任务并抛出`RejectedExecutionException异常,是默认的策略 |
ThreadPoolExecutor.DiscardPolicy | 丢弃任务,但是不抛出异常,这是不推荐的做法 |
ThreadPoolExecutor.DiscardOldestPolicy | 抛弃队列中等待最久的任务,然后把当前任务加入队列中 |
ThreadPoolExecutor.CallerRunsPolicy | 由主线程负责调用任务的run()方法从而绕过线程池直接执行 |
以上策略,根据需求选择即可。
7.4 线程池处理Callable任务
- 任务类
public class MyCallable implements Callable<String> {
private int n;
public MyCallable(int n){this.n = n;}
@Override
public String call() throws Exception {
//线程任务:求1-n的和返回
int sum = 0;
for (int i = 1; i <= n; i++) {
sum+=i;
}
return Thread.currentThread().getName() + "线程求出了1-" + n + "的和是:"+sum;
}
}
- 测试
public static void main(String[] args) throws ExecutionException, InterruptedException {
//通过ThreadPoolExecutor创建一个线程池对象
ExecutorService pool = new ThreadPoolExecutor(3,5,8, TimeUnit.SECONDS,new ArrayBlockingQueue<>(4),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
//使用线程处理Callable任务
Future<String> f1 = pool.submit(new MyCallable(100));
Future<String> f2 = pool.submit(new MyCallable(200));
Future<String> f3 = pool.submit(new MyCallable(300));
Future<String> f4 = pool.submit(new MyCallable(400));
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
}
- 结果,实现了线程的复用。
7.5 Executors工具类实现线程池
Executors是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。
注意:这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor
创建的线程池对象(可以跟踪源码得到验证)。
举例使用:
ExecutorService pool = Executors.newFixedThreadPool(3);
在实际使用中,核心线程数量配置成多少合适呢?
- 计算密集型任务:核心线程数量 = CPU的核数 +1.
- IO密集型任务:核心线程数量 = CPU核数*2.
本人目前对多线程的学习告一段落,欢迎提出任何指正或建议。