前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java 多线程基础

Java 多线程基础

作者头像
星姮十织
发布2022-01-02 01:05:55
3070
发布2022-01-02 01:05:55
举报
文章被收录于专栏:技术-汇集区技术-汇集区

0. 概述

多线程(multithreading):指从软件或者硬件上实现多个线程并发执行的技术。

进程与线程

  • 进程(process):程序是静态的,进入 CPU 运行时变为动态。任一时刻,CPU 总是运行一个进程,使其他进程处于非运行状态。
  • 线程(thread):一个进程可以包含若干个线程,它是操作系统能够进行运算调度的最小单位。

并发与并行

  • 并发:无论上一个开始执行的任务是否完成,当前任务都可以开始执行。
  • 并行:有多个任务执行单元,从物理上就可以多个任务一起执行。

1. 使用多线程

在 Java 中,共有三种方式可以使用多线程:

  • 继承 Thread 类
  • 实现 Runnable 接口
  • 实现 Callable 接口

1.1 Thread

  • 方式一:继承 Thread 类,重写 run() 方法,调用 start() 方法开启线程

示例:

代码语言:javascript
复制
package com.wmwx.thread;

//方式一:继承 Thread 类,重写 run() 方法,调用 start() 方法开启线程
//总结:线程不一定立即执行,由 CPU 调度执行
public class TestThread1 extends Thread{
    @Override
    public void run(){
        //run 方法线程体
        for (int i=0;i<20;i++){
            System.out.println("我在阅读源码第"+(i+1)+"段");
        }
    }

    public static void main(String[] args) {
        //创建一个线程对象
        TestThread1 testThread1 = new TestThread1();

        //调用线程
        testThread1.start();

        //main 线程,即主线程
        for (int i=0;i<20;i++){
            System.out.println("我在看视频学习第"+(i+1)+"集");
        }
    }
}

输出结果:

代码语言:javascript
复制
我在看视频学习第1集
我在阅读源码第1段
我在看视频学习第2集
我在阅读源码第2段
我在看视频学习第3集
我在阅读源码第3段
我在看视频学习第4集
我在阅读源码第4段
我在看视频学习第5集
我在阅读源码第5段
我在看视频学习第6集
我在阅读源码第6段
我在看视频学习第7集
我在阅读源码第7段
我在看视频学习第8集
我在阅读源码第8段
我在看视频学习第9集
我在阅读源码第9段
我在看视频学习第10集
我在阅读源码第10段
我在看视频学习第11集
我在阅读源码第11段
我在看视频学习第12集
我在阅读源码第12段
我在看视频学习第13集
我在阅读源码第13段
我在看视频学习第14集
我在阅读源码第14段
我在看视频学习第15集
我在阅读源码第15段
我在看视频学习第16集
我在阅读源码第16段
我在看视频学习第17集
我在阅读源码第17段
我在看视频学习第18集
我在阅读源码第18段
我在看视频学习第19集
我在阅读源码第19段
我在看视频学习第20集
我在阅读源码第20段

1.2 Runnable

  • 方式二:实现 Runnable 接口,重写 run() 方法,执行线程需丢入该实现类,调用 start() 方法
代码语言:javascript
复制
package com.wmwx.thread;

//方式二:实现 Runnable 接口,重写 run() 方法,执行线程需丢入该实现类,调用 start() 方法
public class TestThread2 implements Runnable{
    @Override
    public void run(){
        //run 方法线程体
        for (int i=0;i<20;i++){
            System.out.println("我在阅读源码第"+(i+1)+"段");
        }
    }

    public static void main(String[] args) {
        //创建一个接口实现类
        TestThread2 testThread2 = new TestThread2();
        //创建线程对象,通过线程对象来开启线程
        Thread thread = new Thread(testThread2);
        thread.start();

        //main 线程,即主线程
        for (int i=0;i<20;i++){
            System.out.println("我在看视频学习第"+(i+1)+"集");
        }
    }
}

输出结果:

代码语言:javascript
复制
我在看视频学习第1集
我在阅读源码第1段
我在看视频学习第2集
我在阅读源码第2段
我在看视频学习第3集
我在阅读源码第3段
我在看视频学习第4集
我在阅读源码第4段
我在看视频学习第5集
我在阅读源码第5段
我在看视频学习第6集
我在阅读源码第6段
我在看视频学习第7集
我在阅读源码第7段
我在看视频学习第8集
我在阅读源码第8段
我在看视频学习第9集
我在阅读源码第9段
我在看视频学习第10集
我在阅读源码第10段
我在看视频学习第11集
我在阅读源码第11段
我在看视频学习第12集
我在阅读源码第12段
我在看视频学习第13集
我在阅读源码第13段
我在看视频学习第14集
我在阅读源码第14段
我在看视频学习第15集
我在阅读源码第15段
我在看视频学习第16集
我在阅读源码第16段
我在看视频学习第17集
我在阅读源码第17段
我在看视频学习第18集
我在阅读源码第18段
我在看视频学习第19集
我在阅读源码第19段
我在看视频学习第20集
我在阅读源码第20段

1.3 Thread 对比 Runnable

  • 继承 Thread 类
    • 子类继承 Thread 类具备多线程能力
    • 启动线程:子类对象.start()
    • 不建议使用的原因:受单继承局限
  • 实现 Runnable 接口
    • 实现接口 Runnable 具备多线程能力
    • 启动线程:传入目标对象后的 Thread 对象.start()
    • 推荐使用的原因:避免单继承局限,灵活方便,方便同一个对象被多个线程使用

1.4 并发问题

问题:多个线程操作同一个资源时,可能会导致线程不安全,数据发生紊乱。

示例:

代码语言:javascript
复制
package com.wmwx.thread;

public class Demo01 {
    public static void main(String[] args) {
        TestRunnable runnable = new TestRunnable();
        new Thread(runnable, "天圣").start();
        new Thread(runnable, "花泪").start();
        new Thread(runnable, "雪仙").start();
    }
}

class TestRunnable implements Runnable {

    //票数
    private int tickets = 10;

    @Override
    public void run() {
        while (true){
            if (tickets<=0) {
                break;
            }
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"拿了第"+tickets--+"张票");
        }
    }
}

输出结果:

代码语言:javascript
复制
花泪拿了第10张票
天圣拿了第9张票
雪仙拿了第8张票
天圣拿了第7张票
花泪拿了第7张票
雪仙拿了第6张票
花泪拿了第5张票
天圣拿了第4张票
雪仙拿了第3张票
花泪拿了第2张票
天圣拿了第2张票
雪仙拿了第1张票
天圣拿了第0张票
花泪拿了第-1张票

可以看到,输出中不仅出现了 -1 张票的情况,还出现了多个人拿走同一张票的情况,这就是多线程操作同一个对象时带来的并发问题。

1.5 Callable

使用步骤:

  1. 实现 Callable 接口,需要返回值类型
  2. 重写 call() 方法,需要抛出异常
  3. 创建目标对象
  4. 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
  5. 提交执行:Future result1 = ser.submit(t1);
  6. 获取结果:boolean r1 = result1.get()
  7. 关闭服务:ser.shutdownNow();

示例:

代码语言:javascript
复制
package com.wmwx.thread;

import java.util.concurrent.*;

public class TestCallable implements Callable<Integer> {
    private Integer num = 0;

    @Override
    public Integer call() throws Exception {
        for (int i=0;i<20;i++){
            System.out.println(Thread.currentThread().getName()+"==>"+(i+1));
        }
        num++;
        return num;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        TestCallable t1 = new TestCallable();
        //创建执行服务
        ExecutorService service = Executors.newFixedThreadPool(3);
        //提交执行
        Future<Integer> r1 = service.submit(t1);
        Future<Integer> r2 = service.submit(t1);
        Future<Integer> r3 = service.submit(t1);
        //获取结果
        Integer b1 = r1.get();
        Integer b2 = r2.get();
        Integer b3 = r3.get();
        System.out.println("Thread1 第"+b1+"个执行结束");
        System.out.println("Thread2 第"+b2+"个执行结束");
        System.out.println("Thread3 第"+b3+"个执行结束");
        //关闭服务
        service.shutdown();
    }
}

输出结果:

代码语言:javascript
复制
pool-1-thread-1==>1
pool-1-thread-1==>2
pool-1-thread-1==>3
pool-1-thread-1==>4
pool-1-thread-1==>5
pool-1-thread-1==>6
pool-1-thread-1==>7
pool-1-thread-1==>8
pool-1-thread-1==>9
pool-1-thread-1==>10
pool-1-thread-1==>11
pool-1-thread-1==>12
pool-1-thread-1==>13
pool-1-thread-1==>14
pool-1-thread-1==>15
pool-1-thread-1==>16
pool-1-thread-1==>17
pool-1-thread-1==>18
pool-1-thread-1==>19
pool-1-thread-1==>20
pool-1-thread-3==>1
pool-1-thread-3==>2
pool-1-thread-3==>3
pool-1-thread-3==>4
pool-1-thread-3==>5
pool-1-thread-3==>6
pool-1-thread-3==>7
pool-1-thread-3==>8
pool-1-thread-3==>9
pool-1-thread-3==>10
pool-1-thread-3==>11
pool-1-thread-3==>12
pool-1-thread-3==>13
pool-1-thread-3==>14
pool-1-thread-3==>15
pool-1-thread-3==>16
pool-1-thread-3==>17
pool-1-thread-3==>18
pool-1-thread-3==>19
pool-1-thread-3==>20
pool-1-thread-2==>1
pool-1-thread-2==>2
pool-1-thread-2==>3
pool-1-thread-2==>4
pool-1-thread-2==>5
pool-1-thread-2==>6
pool-1-thread-2==>7
pool-1-thread-2==>8
pool-1-thread-2==>9
pool-1-thread-2==>10
pool-1-thread-2==>11
pool-1-thread-2==>12
pool-1-thread-2==>13
pool-1-thread-2==>14
pool-1-thread-2==>15
pool-1-thread-2==>16
pool-1-thread-2==>17
pool-1-thread-2==>18
pool-1-thread-2==>19
pool-1-thread-2==>20
Thread1 第1个执行结束
Thread2 第3个执行结束
Thread3 第2个执行结束

Callable 的优点

  1. 拥有返回值
  2. 可以抛出异常

2. Lambda 表达式

lambda 表达式是 Java 1.8 中的新特性。使用 lambda 表达式有以下好处:

  • 避免匿名内部类定义过多
  • 可以让代码看起来更简洁
  • 去掉了一堆没有意义的代码,只留下了核心逻辑

2.1 函数式接口

定义:任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口。

示例:

代码语言:javascript
复制
public interface Runnable {    public abstract void run();}

对于函数式接口,我们可以通过 lambda 表达式创建该接口的对象

示例:

代码语言:javascript
复制
new Thread(()->System.out.println("这里是 lambda 表达式"));

2.2 推导过程

定义一个函数式接口 示例:

代码语言:javascript
复制
package com.wmwx.thread;  	//1.定义一个函数式接口 	interface ILike { 		void lambda(); 	}  	/* 	推导 lambda 表达式 	 */ 	public class TestLambda1 { 		public static void main(String[] args) {  		} 	}

编写实现类 示例:

代码语言:javascript
复制
package com.wmwx.thread;  	//1.定义一个函数式接口 	interface ILike { 		void lambda(); 	} 	//2.编写实现类 	class Like implements ILike { 		@Override 		public void lambda() { 			System.out.println("I like lambda!"); 		} 	}  	/* 	推导 lambda 表达式 	 */ 	public class TestLambda1 { 		public static void main(String[] args) { 			//接口类型 new 实现类 			ILike like = new Like(); 			like.lambda(); 		} 	}
输出结果:
I like lambda!

将实现类改为静态内部类

代码语言:javascript
复制
	package com.wmwx.thread;  	//1.定义一个函数式接口 	interface ILike { 		void lambda(); 	} 	//2.编写实现类 	class Like implements ILike { 		@Override 		public void lambda() { 			System.out.println("I like lambda!"); 		} 	}  	/* 	推导 lambda 表达式 	 */ 	public class TestLambda1 { 		//3.静态内部类 		static class Like2 implements ILike { 			@Override 			public void lambda() { 				System.out.println("I like lambda2!"); 			} 		}  		public static void main(String[] args) { 			//接口类型 new 实现类 			ILike like = new Like(); 			like.lambda();  			like = new Like2(); 			like.lambda(); 		} 	}
输出结果:
I like lambda! I like lambda2!

将静态内部类改为局部内部类 示例:

代码语言:javascript
复制
package com.wmwx.thread;  	//1.定义一个函数式接口 	interface ILike { 		void lambda(); 	} 	//2.编写实现类 	class Like implements ILike { 		@Override 		public void lambda() { 			System.out.println("I like lambda!"); 		} 	}  	/* 	推导 lambda 表达式 	 */ 	public class TestLambda1 { 		//3.静态内部类 		static class Like2 implements ILike { 			@Override 			public void lambda() { 				System.out.println("I like lambda2!"); 			} 		}  		public static void main(String[] args) { 			//接口类型 new 实现类 			ILike like = new Like(); 			like.lambda();  			like = new Like2(); 			like.lambda();  			//4.局部内部类 			class Like3 implements ILike { 				@Override 				public void lambda() { 					System.out.println("I like lambda3!"); 				} 			} 			like = new Like3(); 			like.lambda(); 		} 	}
输出结果:
I like lambda! I like lambda2! I like lambda3!

将局部内部类改为匿名内部类 示例:

代码语言:javascript
复制
package com.wmwx.thread;  	//1.定义一个函数式接口 	interface ILike { 		void lambda(); 	} 	//2.编写实现类 	class Like implements ILike { 		@Override 		public void lambda() { 			System.out.println("I like lambda!"); 		} 	}  	/* 	推导 lambda 表达式 	 */ 	public class TestLambda1 { 		//3.静态内部类 		static class Like2 implements ILike { 			@Override 			public void lambda() { 				System.out.println("I like lambda2!"); 			} 		}  		public static void main(String[] args) { 			//接口类型 new 实现类 			ILike like = new Like(); 			like.lambda();  			like = new Like2(); 			like.lambda();  			//4.局部内部类 			class Like3 implements ILike { 				@Override 				public void lambda() { 					System.out.println("I like lambda3!"); 				} 			} 			like = new Like3(); 			like.lambda();  			//5.匿名内部类(没有类的名称, 必须借助接口或父类) 			like = new ILike() { 				@Override 				public void lambda() { 					System.out.println("I like lambda4!"); 				} 			}; 			like.lambda(); 		} 	}
输出结果:
I like lambda! I like lambda2! I like lambda3! I like lambda4!

用 Lambda 简化 示例:

代码语言:javascript
复制
package com.wmwx.thread;  	//1.定义一个函数式接口 	interface ILike { 		void lambda(); 	} 	//2.编写实现类 	class Like implements ILike { 		@Override 		public void lambda() { 			System.out.println("I like lambda!"); 		} 	}  	/* 	推导 lambda 表达式 	 */ 	public class TestLambda1 { 		//3.静态内部类 		static class Like2 implements ILike { 			@Override 			public void lambda() { 				System.out.println("I like lambda2!"); 			} 		}  		public static void main(String[] args) { 			//接口类型 new 实现类 			ILike like = new Like(); 			like.lambda();  			like = new Like2(); 			like.lambda();  			//4.局部内部类 			class Like3 implements ILike { 				@Override 				public void lambda() { 					System.out.println("I like lambda3!"); 				} 			} 			like = new Like3(); 			like.lambda();  			//5.匿名内部类(没有类的名称, 必须借助接口或父类) 			like = new ILike() { 				@Override 				public void lambda() { 					System.out.println("I like lambda4!"); 				} 			}; 			like.lambda();  			//6.使用 lambda 简化 			like = () -> { 				System.out.println("I like lambda5!"); 			}; 			like.lambda(); 		} 	}
输出结果:
I like lambda! I like lambda2! I like lambda3! I like lambda4! I like lambda5!

需要传参数时,也可以用同样的方式: 示例:

代码语言:javascript
复制
package com.wmwx.thread;  	//1.定义函数式接口 	interface ILove { 		void love(String A, String B); 	} 	//2.编写实现类 	class Love1 implements ILove { 		@Override 		public void love(String A, String B) { 			System.out.println(A + "与" + B + "相爱已经一年了!"); 		} 	}  	/* 	带参数的lambda表达式 	 */ 	public class TestLambda2 { 		//3.编写静态内部类 		static class Love2 implements ILove { 			@Override 			public void love(String A, String B) { 				System.out.println(A + "与" + B + "相爱已经两年了!"); 			} 		}  		public static void main(String[] args) { 			ILove love = new Love1(); 			love.love("花泪", "天圣");  			love = new Love2(); 			love.love("花泪", "天圣");  			//4.局部内部类 			class Love3 implements ILove { 				@Override 				public void love(String A, String B) { 					System.out.println(A + "与" + B + "相爱已经三年了!"); 				} 			} 			love = new Love3(); 			love.love("花泪", "天圣");  			//5.匿名内部类 			love = new ILove() { 				@Override 				public void love(String A, String B) { 					System.out.println(A + "与" + B + "相爱已经四年了!"); 				} 			}; 			love.love("花泪", "天圣");  			//6.lambda 表达式 			love = (String A, String B) -> { 				System.out.println(A + "与" + B + "相爱已经五年了!"); 			}; 			love.love("花泪", "天圣"); 		}  	}
输出结果:
花泪与天圣相爱已经一年了! 花泪与天圣相爱已经两年了! 花泪与天圣相爱已经三年了! 花泪与天圣相爱已经四年了! 花泪与天圣相爱已经五年了!

2.3 表达式简化

2.2 中分别演示了 lambda 表达式无参和有参的推导过程。事实上,这样的表达式依旧可以继续简化下去。以有参表达式为例:

代码语言:javascript
复制
//lambda 表达式
love = (String A, String B) -> {
    System.out.println(A + "与" + B + "相爱已经五年了!");
};
love.love("花泪", "天圣");

可以去除所有的参数类型。 示例:

代码语言:javascript
复制
//lambda 表达式 	love = (String A, String B) -> { 		System.out.println(A + "与" + B + "相爱已经五年了!"); 	}; 	love.love("花泪", "天圣");  	//lambda 表达式简化1:去除所有的参数类型 	love = (A, B) -> { 		System.out.println(A + "与" + B + "相爱已经五年了!"); 	}; 	love.love("花泪", "天圣");
输出结果:
花泪与天圣相爱已经五年了! 花泪与天圣相爱已经五年了!

当接口中抽象方法的方法体内只有一条语句时可以去除大括号。 示例:

代码语言:javascript
复制
	//lambda 表达式 	love = (String A, String B) -> { 		System.out.println(A + "与" + B + "相爱已经五年了!"); 	}; 	love.love("花泪", "天圣");  	//lambda 表达式简化1:去除参数类型 	love = (A, B) -> { 		System.out.println(A + "与" + B + "相爱已经五年了!"); 	}; 	love.love("花泪", "天圣");  	//lambda 表达式简化2:去除大括号 	love = (A, B) -> System.out.println(A + "与" + B + "相爱已经五年了!"); 	love.love("花泪", "天圣");
输出结果:
花泪与天圣相爱已经五年了! 花泪与天圣相爱已经五年了! 花泪与天圣相爱已经五年了!

当接口中的抽象方法有且只有一个参数时可以去除小括号。 示例:

代码语言:javascript
复制
//定义函数式接口 	interface ILove { 		void love(String name); 	}  	//...中间代码...  	//lambda 表达式简化3:去除小括号 	love = name -> System.out.println(name + "发觉自己爱上了那个人。"); 	love.love("诗人");
输出结果:
诗人发觉自己爱上了那个人。

3. 线程状态

3.1 线程的五大状态

  1. 新建状态:线程对象一旦创建就进入了新建状态。
  2. 就绪状态:线程对象被创建后,其它线程调用了该对象的 start() 方法,从而来启动该线程。处于就绪状态的线程,随时可能被CPU调度执行。
  3. 运行状态:线程获取 CPU 权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态
  4. 阻塞状态:阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。阻塞结束后将进入就绪状态。阻塞的情况分三种:
    • 等待阻塞:通过调用线程的 wait() 方法,让线程等待某工作的完成。
    • 同步阻塞:线程获取 synchronized 同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
    • 其他阻塞:通过调用线程的 sleep() 或 join() 或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep() 状态超时、join() 等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
  5. 死亡状态:线程执行完毕或者因异常退出了 run() 方法,该线程结束生命周期。已经结束的线程不能重新被启动。

3.2 线程方法

方法

说明

setPriority (int newPriority)

更改线程的优先级

static void sleep (long millis)

在指定的毫秒数内让当前正在执行的线程休眠

void join ()

等待该线程中止

static void yield ()

暂停当前正在执行的线程对象,并执行其他线程

void interrupt ()

中断线程(不推荐使用)

boolean isAlive()

测试线程是否处于活动状态

3.3 线程停止

  • 不推荐使用 JDK 中提供的 stop()、destroy() 方法(已被官方废弃)。
  • 推荐使用次数限制线程,令线程自己终止
  • 推荐使用一个标志位进行终止变量:当 flag==false 时,令线程终止运行

示例:

代码语言:javascript
复制
实现 Runnable 接口
package com.wmwx.thread;  	//测试线程终止 	public class TestStop implements Runnable{  		@Override 		public void run() {  		}  		public static void main(String[] args) {  		} 	}

设置标志位,并重写 run() 方法

代码语言:javascript
复制
package com.wmwx.thread;  	//测试线程终止 	public class TestStop implements Runnable{ 		//设置一个标志位 		private boolean flag = true;  		@Override 		public void run() { 			int i = 0; 			while (flag) { 				System.out.println("runnable:"+i++); 			} 		}  		public static void main(String[] args) {  		} 	}

编写转换标志位的方法

代码语言:javascript
复制
package com.wmwx.thread;  	//测试线程终止 	public class TestStop implements Runnable{ 		//1.设置一个标志位 		private boolean flag = true; 		//2.设置一个方法转换标志位 		public void stopRunnable(){ 			this.flag = false; 			System.out.println("线程停止了。"); 		}  		@Override 		public void run() { 			int i = 0; 			while (flag) { 				System.out.println("runnable:"+i++); 			} 		}  		public static void main(String[] args) {  		} 	}

编写 main() 方法,并在其中创建新线程

代码语言:javascript
复制
package com.wmwx.thread;  	//测试线程终止 	public class TestStop implements Runnable{ 		//1.设置一个标志位 		private boolean flag = true; 		//2.设置一个方法转换标志位 		public void stopRunnable(){ 			this.flag = false; 			System.out.println("线程停止了。"); 		}  		@Override 		public void run() { 			int i = 0; 			while (flag) { 				System.out.println("runnable:"+i++); 			} 		}  		public static void main(String[] args) { 			TestStop testStop = new TestStop(); 			new Thread(testStop).start(); 			for (int i = 0; i < 1000; i++) { 				System.out.println("main线程:"+i); 				if (i==900){ 					//3.调用stopRunnable()切换标志位,停止线程 					testStop.stopRunnable(); 				} 			} 		} 	}
输出结果:
//...省略上文... 	main线程:899 	main线程:900 	runnable:1844 	线程停止了。 	main线程:901 	main线程:902 	//...省略下文...

3.4 线程休眠

  • sleep() 可以用来指定当前线程阻塞的毫秒数
  • sleep() 存在异常 InterruptException
  • sleep() 时间结束后,线程进入就绪状态
  • sleep() 可以模拟网络延时、倒计时等,来放大问题的发生性
  • 每个对象都有一把锁,sleep() 不会释放锁

1.4 中的代码为例,便是模拟网络延时的用法。

示例:

代码语言:javascript
复制
package com.wmwx.thread;

//模拟网络延时
public class TestSleep implements Runnable{
    //票数
    private int tickets = 10;

    @Override
    public void run() {
        while (true){
            if (tickets<=0) {
                break;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"拿了第"+tickets--+"张票");
        }
    }

    public static void main(String[] args) {
        TestSleep testSleep = new TestSleep();
        new Thread(testSleep, "天圣").start();
        new Thread(testSleep, "花泪").start();
        new Thread(testSleep, "雪仙").start();
    }
}

并且,sleep() 还可以用来模拟倒计时。

示例:

代码语言:javascript
复制
package com.wmwx.thread;

import java.text.SimpleDateFormat;
import java.util.Date;

//模拟倒计时
public class TestSleep2{
    public static void countDown(int second) throws InterruptedException {
        int num = second;
        while (true) {
            Thread.sleep(1000);
            System.out.println(num--);
            if (num<=0) {
                break;
            }
        }
    }

    public static void main(String[] args) {
        //打印系统当前时间
        Date startTime = new Date(System.currentTimeMillis());  //获取当前系统时间

        while (true) {
            try {
                Thread.sleep(1000);
                System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
                startTime = new Date(System.currentTimeMillis());  //更新时间为当前系统时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

输出结果:

代码语言:javascript
复制
15:32:05
15:32:06
15:32:07
15:32:08
15:32:09

3.5 线程礼让

礼让:令当前正在执行的线程暂停,但不进入阻塞态

  • 让线程从运行态转为就绪态
  • 让 CPU 重新调度
  • 礼让不一定成功

示例:

代码语言:javascript
复制
package com.wmwx.thread;

//测试礼让线程
//礼让不一定成功
public class TestYield {
    public static void main(String[] args) {
        MyYield myYield = new MyYield();
        new Thread(myYield, "A").start();
        new Thread(myYield, "B").start();
    }
}

class MyYield implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"==>线程开始执行。");
        Thread.yield();     //线程礼让
        System.out.println(Thread.currentThread().getName()+"==>线程结束执行。");
    }
}

输出结果:

代码语言:javascript
复制
A==>线程开始执行。
A==>线程结束执行。
B==>线程开始执行。
B==>线程结束执行。

此时,线程 A 执行结束后线程 B 才开始执行,这意味着线程 A 礼让失败。

不改变任何代码,重新启动测试,可以看到输出结果:

代码语言:javascript
复制
A==>线程开始执行。
B==>线程开始执行。
A==>线程结束执行。
B==>线程结束执行。

这次线程 A 开始执行后,线程 B 便开始执行,由此可见礼让成功。

3.6 线程插队

通过 join() 方法可以强制令其他线程先阻塞,等到该线程执行完毕后,再去执行其他线程。

示例:

代码语言:javascript
复制
package com.wmwx.thread;

public class TestJoin implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("我是VIP"+i+"号,都给我让道!");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //启动线程
        TestJoin testJoin = new TestJoin();
        Thread thread = new Thread(testJoin);
        thread.start();

        //主线程
        for (int i = 0; i < 500; i++) {
            if (i==200) {
                thread.join();
            }
            System.out.println("main:"+i);
        }
    }
}

输出信息:

代码语言:javascript
复制
main:0
我是VIP0号,都给我让道!
main:1
我是VIP1号,都给我让道!
main:2
我是VIP2号,都给我让道!
main:3
我是VIP3号,都给我让道!
... ...
main:199
我是VIP173号,都给我让道!
我是VIP174号,都给我让道!
我是VIP175号,都给我让道!
我是VIP176号,都给我让道!
... ...
我是VIP999号,都给我让道!
main:200
main:201
main:202
... ...
main:497
main:498
main:499

3.7 观测线程状态

通过 Thread.State 可以获取 Java 中线程对应的六种状态常量,分别为:

  1. NEW:尚未启动的线程处于此状态
  2. RUNNABLE:在 Java 虚拟机中执行的线程处于此状态
  3. BLOCKED:被阻塞等待监视器锁定的线程处于此状态
  4. WAITING:正在等待另一个线程执行特定动作的线程处于此状态
  5. TIMED_WAITING:正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
  6. TERMINATED:已退出的线程处于此状态

一个线程可以在给定时间点处于一个状态。这些状态是不反应任何操作系统线程状态的虚拟机状态。

示例:

代码语言:javascript
复制
package com.wmwx.thread;

//观测线程的状态
public class TestState {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("==============");
        });

        //观测线程创建后的状态
        Thread.State state = thread.getState();
        System.out.println(state);
        //观测线程启动后的状态
        thread.start();
        state = thread.getState();
        System.out.println(state);
        //只要线程不终止,就一直输出状态
        while (state != Thread.State.TERMINATED) {
            Thread.sleep(500);
            state = thread.getState();  //切记:更新线程状态!
            System.out.println(state);
        }
        //报错:java.lang.IllegalThreadStateException 原因:死亡之后的线程不能再次启动!
        thread.start();
    }
}

输出结果:

代码语言:javascript
复制
NEW
RUNNABLE
TIMED_WAITING
RUNNABLE
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
RUNNABLE
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
==============
TERMINATED
Exception in thread "main" java.lang.IllegalThreadStateException
	at java.lang.Thread.start(Thread.java:708)
	at com.wmwx.thread.TestState.main(TestState.java:31)

3.8 线程优先级

  • Java 提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行
  • 线程的优先级用数字表示,默认为 5范围从1~10,并设有三个常量:
    • Thread.MIN_PRIORITY = 1
    • Thread.NORM_PRIORITY = 5
    • Thread.MAX_PRIORITY = 10
  • 可以使用以下方法获取线程优先级getPriority();
  • 可以使用以下方法设置除主线程外的线程优先级setPriority(int xxx);

示例:

代码语言:javascript
复制
package com.wmwx.thread;

//测试线程优先级
public class TestPriority {
    public static void main(String[] args) {
        //主线程优先级(无法更改)
        System.out.println(Thread.currentThread().getName()+"==>"+Thread.currentThread().getPriority());
        //设置线程优先级
        MyPriority myPriority = new MyPriority();
        Thread t1 = new Thread(myPriority, "t1");
        Thread t2 = new Thread(myPriority, "t2");
        Thread t3 = new Thread(myPriority, "t3");
        Thread t4 = new Thread(myPriority, "t4");
        Thread t5 = new Thread(myPriority, "t5");

        t1.start();

        t2.setPriority(2);
        t2.start();

        t3.setPriority(Thread.MAX_PRIORITY);
        t3.start();

        t4.setPriority(Thread.MIN_PRIORITY);
        t4.start();

        t5.setPriority(Thread.NORM_PRIORITY);
        t5.start();
    }
}

class MyPriority implements Runnable {
    //测试优先级
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"==>"+Thread.currentThread().getPriority());
    }
}

输出结果:

代码语言:javascript
复制
main==>5
t2==>2
t3==>10
t1==>5
t5==>5
t4==>1

可见,设置优先级不能直接决定线程的执行顺序,只能提高权重

3.9 守护线程

使用 setDaemon(true) 方法可以设置线程为守护线程。守护线程如:后台记录操作日志、监控内存、垃圾回收等等。

  • 线程分为用户线程守护线程
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不必等待守护线程执行完毕

示例:

代码语言:javascript
复制
package com.wmwx.thread;

//测试守护线程
public class TestDaemon {
    public static void main(String[] args) {
        Tao tao = new Tao();
        Thread t1 = new Thread(tao);
        t1.setDaemon(true);     //默认是 false

        t1.start();

        new Thread(new Human()).start();
    }
}

//道
class Tao implements Runnable {
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("道生一,一生二,二生三,三生万物。");
        }
    }
}

//人类
class Human implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("这个人活了"+(i+1)+"岁。");
        }
    }
}

输出结果:

代码语言:javascript
复制
这个人活了1岁。
道生一,一生二,二生三,三生万物。
这个人活了2岁。
... ...
这个人活了99岁。
道生一,一生二,二生三,三生万物。
这个人活了100岁。

可见,当 Human 的线程结束时,原本死循环的 Tao 线程也结束了。

4. 线程同步

在处理多线程问题时,时常会遇到多个线程想要访问同一个对象,并且其中某些线程还想要修改这个对象的情况,此时我们就需要线程同步

线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,然后为正在使用的线程加锁。当一个线程获得对象的排它锁,独占资源时,其他线程必须等待。资源使用完毕后,再将缩释放掉即可。如此,等到前一个线程使用完毕后,下一个线程才会去使用,就可以保证线程的安全性

但是,使用锁机制可能会导致以下问题:

  • 一个线程持有锁会导致其他所有需要此锁的线程挂起
  • 在多线程竞争下,加锁、释放锁会导致比较频繁的上下文切换和调度延时,引起性能问题
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题

4.1 线程不安全问题

还是以 1.4 中的代码为例:

代码语言:javascript
复制
package com.wmwx.thread;

//不安全的买票
public class UnsafeBuyTickets {
    public static void main(String[] args) {
        BuyTickets buyTickets = new BuyTickets();
        Thread t1 = new Thread(buyTickets, "天圣");
        Thread t2 = new Thread(buyTickets, "花泪");
        Thread t3 = new Thread(buyTickets, "雪仙");
        t1.start();
        t2.start();
        t3.start();
    }
}

class BuyTickets implements Runnable {

    //票的数量
    private int ticketNum = 10;
    //外部停止方式的标志位
    private boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            try {
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //买票
    private void buy() throws InterruptedException {
        if (ticketNum>0) {
            Thread.sleep(200);  //模拟延时
            System.out.println(Thread.currentThread().getName()+"拿走了第"+ticketNum--+"张票。");
        } else {
            flag = false;
        }
    }
}

输出结果:

代码语言:javascript
复制
花泪拿走了第10张票。
天圣拿走了第9张票。
雪仙拿走了第10张票。
天圣拿走了第8张票。
雪仙拿走了第7张票。
花泪拿走了第8张票。
花泪拿走了第6张票。
天圣拿走了第6张票。
雪仙拿走了第6张票。
天圣拿走了第5张票。
花泪拿走了第4张票。
雪仙拿走了第3张票。
天圣拿走了第2张票。
花泪拿走了第1张票。
雪仙拿走了第0张票。

同一张票被多个人取走,这显然不是我们想看到的。

下面再举一个例子:

代码语言:javascript
复制
package com.wmwx.thread;

//不安全的取钱
public class UnsafeAccount {
    public static void main(String[] args) {
        //账户
        Account account = new Account(100, "银行卡");
        new Drawing(account, 50, "天圣").start();
        new Drawing(account, 100, "舞泪").start();
    }
}

//账户
class Account {
    private int money;
    private String name;

    public Account() {
    }
    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }

    public int getMoney() {
        return money;
    }
    public void setMoney(int money) {
        this.money = money;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

//银行:模拟取款
class Drawing extends Thread {
    private Account account;    //账户
    private int drawingMoney;   //取钱的数额
    private int nowMoney;       //现在手里钱的数额

    public Drawing(){
    }
    public Drawing(Account account, int drawingMoney, String name) {
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    //取钱
    @Override
    public void run(){
        //判断有没有钱
        if (account.getMoney()<drawingMoney) {
            System.out.println(Thread.currentThread().getName()+"账户余额不足。");
            return;
        }
        account.setMoney(account.getMoney() - drawingMoney);
        nowMoney = nowMoney + drawingMoney;
        System.out.println(account.getName()+"账户的余额为:"+account.getMoney());
        System.out.println(this.getName()+"手里还剩下"+nowMoney);
    }
}

输出结果:

代码语言:javascript
复制
银行卡账户的余额为:50
天圣手里还剩下50
花泪账户余额不足。

可以看到,一个人取出了 50 元后,账户里只剩 50 元,另一个人就没有办法再取出 100 元了,这是正常的。

但是,如果在取钱的代码中加入 sleep() 方法模拟网络延迟,输出结果又会变成怎样呢?

代码语言:javascript
复制
//取钱
@Override
public void run(){
    //判断有没有钱
    if (account.getMoney()<drawingMoney) {
        System.out.println(Thread.currentThread().getName()+"账户余额不足。");
        return;
    }
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    account.setMoney(account.getMoney() - drawingMoney);
    nowMoney = nowMoney + drawingMoney;
    System.out.println(account.getName()+"账户的余额为:"+account.getMoney());
    System.out.println(this.getName()+"手里还剩下"+nowMoney);
}

输出结果:

代码语言:javascript
复制
银行卡账户的余额为:50
银行卡账户的余额为:50
天圣手里还剩下50
花泪手里还剩下100

可以看到,两个人都取出了钱,加起来足有 150 元,超出了原本 100 元的储备。而在实际业务中,网络延迟是必须要考虑的问题。因此,通过使用 sleep() 方法来模拟网络延迟,可以放大问题发生的可能性。

接下来,再来看一看集合线程不安全的例子:

代码语言:javascript
复制
package com.wmwx.thread;

import java.util.ArrayList;

//线程不安全的集合
public class UnsafeList {
    public static void main(String[] args) throws InterruptedException {
        //线程不安全
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        Thread.sleep(3000);
        System.out.println(list.size());
    }
}

输出结果:

代码语言:javascript
复制
9998

按照逻辑,此处本应输出 10000,最后输出的却只有 9998,意味着有两项覆盖了数组前面的内容。

4.2 synchronized

由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制。这套机制就是 synchronized 关键字,它包括两种用法︰synchronized 方法synchronized 块

4.2.1 同步方法

**synchronized 方法控制对对象的访问。**每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞。而方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,并继续执行。

  • 缺陷:将一个较大的方法声明为 synchronized 会影响效率

4.1 中的买票为例,为 buy() 方法添加 synchronized 关键字:

代码语言:javascript
复制
package com.wmwx.thread;
//安全的买票
public class UnsafeBuyTickets {
    public static void main(String[] args) {
        BuyTickets buyTickets = new BuyTickets();
        Thread t1 = new Thread(buyTickets, "天圣");
        Thread t2 = new Thread(buyTickets, "花泪");
        Thread t3 = new Thread(buyTickets, "雪仙");
        t1.start();
        t2.start();
        t3.start();
    }
}
class BuyTickets implements Runnable {
    //票的数量
    private int ticketNum = 10;
    //外部停止方式的标志位
    private boolean flag = true;
    @Override
    public void run() {
        while (flag) {
            try {
                Thread.sleep(200);  //模拟延时
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    //安全的买票方法
    private synchronized void buy() throws InterruptedException {
        if (ticketNum>0) {
            System.out.println(Thread.currentThread().getName()+"拿走了第"+ticketNum--+"张票。");
        } else {
            flag = false;
        }
    }
}

输出结果:

代码语言:javascript
复制
花泪拿走了第10张票。
天圣拿走了第9张票。
雪仙拿走了第8张票。
天圣拿走了第7张票。
花泪拿走了第6张票。
雪仙拿走了第5张票。
天圣拿走了第4张票。
花泪拿走了第3张票。
雪仙拿走了第2张票。
天圣拿走了第1张票。

可以看到,每一张票都只被一个人取走,并且没有人取到第负数张票。

4.2.2 同步块
代码语言:javascript
复制
//同步块
synchronized(Obj) {
    //想要同步执行的代码
}
  • 其中,Obj 称为同步监视器。它可以是任何对象,但是推荐使用共享资源作为同步监视器
  • 同步方法中之所以无需指定同步监视器,是因为同步方法的同步监视器是 this(即这个对象本身),或是 Class

同步监视器的执行过程:

  1. 第一个线程访问,锁定同步监视器,执行其中代码。
  2. 第二个线程访问,发现同步监视器被锁定,无法访问。
  3. 第一个线程访问完毕,解锁同步监视器。
  4. 第二个线程访问,发现同步监视器没有锁,然后锁定并访问。

4.1 中的银行卡为例,将 run() 方法中的代码放入同步块中,并将 account 属性作为同步监视器:

代码语言:javascript
复制
package com.wmwx.thread;
//安全的取钱
public class UnsafeAccount {
    public static void main(String[] args) {
        //账户
        Account account = new Account(100, "银行卡");
        new Drawing(account, 50, "天圣").start();
        new Drawing(account, 100, "花泪").start();
    }
}
//账户
class Account {
    private int money;
    private String name;

    public Account() {
    }
    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }

    public int getMoney() {
        return money;
    }
    public void setMoney(int money) {
        this.money = money;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
//银行:模拟取款
class Drawing extends Thread {
    private Account account;    //账户
    private int drawingMoney;   //取钱的数额
    private int nowMoney;       //现在手里钱的数额

    public Drawing(){
    }
    public Drawing(Account account, int drawingMoney, String name) {
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }
    //线程安全的取钱方法
    @Override
    public void run(){
        synchronized (account){
            //判断有没有钱
            if (account.getMoney()<drawingMoney) {
                System.out.println(Thread.currentThread().getName()+"账户余额不足。");
                return;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            account.setMoney(account.getMoney() - drawingMoney);
            nowMoney = nowMoney + drawingMoney;
            System.out.println(account.getName()+"账户的余额为:"+account.getMoney());
            System.out.println(this.getName()+"手里还剩下"+nowMoney);
        }
    }
}

输出结果:

代码语言:javascript
复制
银行卡账户的余额为:0
花泪手里还剩下100
天圣账户余额不足。

可见,当一个人取走了钱导致余额不足之后,另一个人就不能再取钱了。

同理可以修改 4.1 中集合的例子:

代码语言:javascript
复制
package com.wmwx.thread;

import java.util.ArrayList;

//线程不安全的集合
public class UnsafeList {
    public static void main(String[] args) throws InterruptedException {
        //通过 synchronized 实现线程安全
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                synchronized (list) {
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        Thread.sleep(3000);
        System.out.println(list.size());
    }
}

可以看到正确的输出结果:

代码语言:javascript
复制
10000

事实上,Java 本身提供了一种线程安全的集合 CopyOnWriteArrayList,它位于 JUCjava.util.concurrent)包下。

示例:

代码语言:javascript
复制
package com.wmwx.thread;

import java.util.concurrent.CopyOnWriteArrayList;

//线程安全的集合
public class SafeList {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(list.size());
    }
}

注意代码中不再使用 synchronized 方法或 synchronized 代码块,但输出结果依然正常:

代码语言:javascript
复制
10000

4.3 死锁

多个线程各自占有一些共享资源,并互相等待其他线程占有的资源,导致两个或者多个线程都在等待对方释放资源,从而全都停止执行的情形,就叫做死锁。

某一个同步块同时拥有两个以上对象的锁时,就可能会发生死锁的问题。

产生死锁的四个必要条件

  1. 互斥条件:一个资源每次只能被一个线程使用。
  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源,在未使用完之前,不能强制剥夺。
  4. 循环等待条件:若干线程之间形成一种首尾相连的等待关系。

示例:

代码语言:javascript
复制
package com.wmwx.thread;
//死锁:多个线程互相占有者对方需要的资源,就会形成死锁
public class DeadLock {
    public static void main(String[] args) {
        Makeup m1 = new Makeup(0, "花泪");
        Makeup m2 = new Makeup(1, "雪仙");
        m1.start();
        m2.start();
    }
}
//口红
class Lipstick {
}
//镜子
class Mirror {
}
//化妆
class Makeup extends Thread {
    //用static保证资源只有一份
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();
    int choice;     //选择
    String name;    //人名
    public Makeup(){
    }
    public Makeup(int choice, String name) {
        this.choice = choice;
        this.name = name;
    }
    //化妆
    private void makeUp() throws InterruptedException {
        if (choice==0) {
            synchronized (lipstick) {
                System.out.println(name+"获得了口红的锁。");
                Thread.sleep(1000);
                synchronized (mirror) {
                    System.out.println(name+"获得了镜子的锁。");
                }
            }
        } else {
            synchronized (mirror) {
                System.out.println(name+"获得了镜子的锁。");
                Thread.sleep(1000);
                synchronized (lipstick) {
                    System.out.println(name+"获得了口红的锁。");
                }
            }
        }
    }
    @Override
    public void run(){
        try {
            makeUp();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出结果:

代码语言:javascript
复制
花泪获得了口红的锁。雪仙获得了镜子的锁。

注意,此时程序仍在运行,并没有停止。两个人一个占有了口红资源,一个占有了镜子资源,并且都想要对方的资源,僵持不下,因而陷入了死锁。

4.4 Lock

从 JDK 5.0 开始,Java 提供了更强大的线程同步机制——通过显式定义 Lock 对象充当同步锁来实现同步。

java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁。线程开始访问共享资源之前应先获得 Lock 对象。

ReentrantLock(可重入锁) 类实现了 Lock 接口,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中比较常用。它可以显式地加锁、显式地释放锁。

根据官方的建议,最好将 lock() 方法放在 try 代码块前,而将 unlock() 方法放在 finally 代码块中

示例:

代码语言:javascript
复制
package com.wmwx.thread;

import java.util.concurrent.locks.ReentrantLock;

//测试Lock锁
public class TestLock implements Runnable{
    int ticketNum = 10;

    //定义lock锁
    private final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        TestLock testLock = new TestLock();
        new Thread(testLock, "天圣").start();
        new Thread(testLock, "花泪").start();
        new Thread(testLock, "雪仙").start();
    }
    
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.lock();
            try {
                if (ticketNum>0){
                    System.out.println(Thread.currentThread().getName()+"拿到了第"+ticketNum--+"张票。");
                } else {
                    break;
                }
            } finally {
                lock.unlock();
            }
        }
    }
}

输出结果:

代码语言:javascript
复制
天圣拿到了第10张票。
花泪拿到了第9张票。
雪仙拿到了第8张票。
花泪拿到了第7张票。
天圣拿到了第6张票。
雪仙拿到了第5张票。
天圣拿到了第4张票。
花泪拿到了第3张票。
雪仙拿到了第2张票。
天圣拿到了第1张票。

4.5 Lock 与 synchronized 对比

  • Lock 是显式锁,需手动开启和关闭锁;而 synchronized 是隐式锁,超出作用域会自动释放。
  • Lock 只有代码块锁;而 synchronized 既有代码块锁,也有方法锁。
  • 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)。
  • 推荐优先使用顺序:Lock --> 同步代码块(已经进入了方法体,分配了相应资源) --> 同步方法(在方法体之外)。

5. 线程协作

5.1 生产者消费者问题

  • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费。
  • 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止。
  • 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止。

分析可知,这是一个线程同步问题。生产省和消费者共享同一个资源,并且生产者和消费者之间相互依赖互为条件

  • 对于生产者,没有生产产品之前,要通知消费者等待;而生产了产品之后,又需要马上通知消费者消费。
  • 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费。
  • 在生产者消费者问题中,仅有 synchronized 是不够的。因为 synchronized 虽然可以阻止并发更新同一个共享资源来实现同步,却不能用来实现不同线程之间的消息传递,也就是通信

5.2 线程通信

Java 提供了以下几个方法来解决线程间的通信问题:

方法名

作用

wait()

表示线程一直等待,直到其他线程通知;与 sleep() 不同,wait() 会释放锁

wait(long timeout)

指定等待的毫秒数

notify()

唤醒一个处于等待状态的线程

notifyAll()

唤醒同一个对象上所有调用 wait() 方法的线程,优先级别高的线程优先调度

5.3 管程法

  • 生产者:负责生产数据的模块(可能是方法、对象、线程、进程)
  • 消费者︰负责处理数据的模块(可能是方法、对象、线程、进程)
  • 缓冲区︰消费者不能直接使用生产者的数据,他们之间有个 “缓冲区”。生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据。

示例:

产品类 Product:

代码语言:javascript
复制
//产品
class Product {
    //编号
    int num;
    public Product(int num) {
        this.num = num;
    }
}

缓冲区 SynContainer:

代码语言:javascript
复制
//缓冲区
class SynContainer {
    //容器
    Product[] products = new Product[3];
    //容器计数器
    int productNum = 0;
    //生产者丢入产品
    public synchronized void push(Product product) {
        //如果容器满了,就要等待消费者消费
        while (productNum == products.length){
            //等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //如果还没满,就要丢入产品
        products[productNum++] = product;
        //通知消费者消费
        this.notifyAll();
    }
    //消费者取出产品
    public synchronized Product pop() {
        //如果容器空了,就要等待生产者生产
        while (productNum==0) {
            //等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //如果还没空,就要取出产品
        Product product = products[--productNum];
        //通知生产者生产
        this.notifyAll();
        //返回取出的产品
        return product;
    }

}

生产者 Producer:

代码语言:javascript
复制
//生产者
class Producer extends Thread {
    //缓冲区
    SynContainer container;

    public Producer(SynContainer container) {
        this.container = container;
    }

    //生产
    @Override
    public void run(){
        for (int i = 0; i < 10; i++) {
            System.out.println("生产第"+(i+1)+"只鸡。");
            container.push(new Product(i+1));
        }
    }
}

消费者 Consumer:

代码语言:javascript
复制
//消费者
class Consumer extends Thread {
    //缓冲区
    SynContainer container;

    public Consumer(SynContainer container) {
        this.container = container;
    }

    //消费
    @Override
    public void run(){
        for (int i = 0; i < 10; i++) {
            Product product = container.pop();
            System.out.println("消费第"+product.num+"只鸡。");
        }
    }
}

main 方法:

代码语言:javascript
复制
//测试生产者消费者问题(利用缓冲区解决:管程法)
//需要:生产者、消费者、产品、缓冲区
public class TestPC {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        new Thread(new Producer(container)).start();
        new Thread(new Consumer(container)).start();
    }
}

输出结果:

代码语言:javascript
复制
生产第1只鸡。
生产第2只鸡。
生产第3只鸡。
生产第4只鸡。
生产第5只鸡。
消费第2只鸡。
消费第3只鸡。
生产第6只鸡。
消费第4只鸡。
生产第7只鸡。
消费第5只鸡。
生产第8只鸡。
消费第6只鸡。
生产第9只鸡。
消费第7只鸡。
生产第10只鸡。
消费第8只鸡。
消费第9只鸡。
消费第1只鸡。
消费第0只鸡。

5.4 信号灯法

以演员录制节目和观众观看节目为例:

节目类 TV:

代码语言:javascript
复制
//节目
class TV {
    String program;         //节目名
    boolean flag = true;    //标志位
    //录制
    public synchronized void record(String program) {
        while (!flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("演员录制了:"+program);
        this.program = program;
        //通知观众观看
        this.notifyAll();
        this.flag = !flag;
    }
    //播放
    public synchronized void play() {
        while (flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("观众观看了:"+program);
        //通知演员表演
        this.notifyAll();
        this.flag = !flag;
    }
}

演员类 Player:

代码语言:javascript
复制
//演员
class Player extends Thread {
    TV tv;
    public Player(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run(){
        for (int i = 0; i < 10; i++) {
            if ((i+1)%5==0){
                tv.record("广告");
            } else {
                tv.record("《降温》");
            }
        }
    }
}

观众类 Watcher:

代码语言:javascript
复制
//观众
class Watcher extends Thread {
    TV tv;
    public Watcher(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run(){
        for (int i = 0; i < 10; i++) {
            tv.play();
        }
    }
}

main 方法:

代码语言:javascript
复制
//测试生产者消费者问题(利用标志位解决:信号灯法)
//需要:生产者(演员)、消费者(观众)、产品(录制的节目)
public class TestPC2 {
    public static void main(String[] args) {
        TV tv = new TV();
        new Thread(new Player(tv)).start();
        new Thread(new Watcher(tv)).start();
    }
}

输出结果:

代码语言:javascript
复制
演员录制了:《降温》
观众观看了:《降温》
演员录制了:《降温》
观众观看了:《降温》
演员录制了:《降温》
观众观看了:《降温》
演员录制了:《降温》
观众观看了:《降温》
演员录制了:广告
观众观看了:广告
演员录制了:《降温》
观众观看了:《降温》
演员录制了:《降温》
观众观看了:《降温》
演员录制了:《降温》
观众观看了:《降温》
演员录制了:《降温》
观众观看了:《降温》
演员录制了:广告
观众观看了:广告

5.5 线程池

5.5.1 线程池简介
  • 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
  • 思路:提前创建好多个线程放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现重复利用。
  • 好处:
    • 提高响应速度(减少了创建新线程的时间)
    • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
    • 便于线程管理
      • corePoolSize:核心池的大小
      • maximumPoolSize:最大线程数
      • keepAliveTime:线程没有任务时最多保持多长时间后会终止
5.5.2 使用线程池
  • 自 JDK 5.0 起提供了线程池相关 API:ExecutorServiceExecutors
  • ExecutorService:真正的线程池接口。常见子类有 ThreadPoolExecutor。
    • void execute(Runnable command)∶执行任务/命令,没有返回值,一般用来执行 Runnable。
    • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行 Callable。
    • void shutdown():关闭连接池。
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池。

示例:

代码语言:javascript
复制
package com.wmwx.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

//测试线程池
public class TestPool {
    public static void main(String[] args) {
        //1.创建服务,创建线程池,参数为线程池的大小
        ExecutorService service = Executors.newFixedThreadPool(10);
        //2.执行
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        //3.关闭连接
        service.shutdown();
    }
}

class MyThread implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

输出结果:

代码语言:javascript
复制
pool-1-thread-1:0
pool-1-thread-1:1
pool-1-thread-3:0
pool-1-thread-4:0
pool-1-thread-2:0
pool-1-thread-4:1
pool-1-thread-3:1
pool-1-thread-1:2
pool-1-thread-3:2
pool-1-thread-4:2
pool-1-thread-2:1
pool-1-thread-2:2

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0. 概述
    • 进程与线程
      • 并发与并行
      • 1. 使用多线程
        • 1.1 Thread
          • 1.2 Runnable
            • 1.3 Thread 对比 Runnable
              • 1.4 并发问题
                • 1.5 Callable
                • 2. Lambda 表达式
                  • 2.1 函数式接口
                    • 2.2 推导过程
                      • 2.3 表达式简化
                      • 3. 线程状态
                        • 3.1 线程的五大状态
                          • 3.2 线程方法
                            • 3.3 线程停止
                              • 3.4 线程休眠
                                • 3.5 线程礼让
                                  • 3.6 线程插队
                                    • 3.7 观测线程状态
                                      • 3.8 线程优先级
                                        • 3.9 守护线程
                                        • 4. 线程同步
                                          • 4.1 线程不安全问题
                                            • 4.2 synchronized
                                              • 4.2.1 同步方法
                                              • 4.2.2 同步块
                                            • 4.3 死锁
                                              • 4.4 Lock
                                                • 4.5 Lock 与 synchronized 对比
                                                • 5. 线程协作
                                                  • 5.1 生产者消费者问题
                                                    • 5.2 线程通信
                                                      • 5.3 管程法
                                                        • 5.4 信号灯法
                                                          • 5.5 线程池
                                                            • 5.5.1 线程池简介
                                                            • 5.5.2 使用线程池
                                                        相关产品与服务
                                                        容器服务
                                                        腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                                                        领券
                                                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档