笔记 对应视频 B站遇见狂神说–多线程详解
线程简介
多任务
线程创建
三种创建方法
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
1. 继承Thread
类
- 定义类继承Thread类
- 重写run方法
- 调用start()方法,开启线程
注: 不建议使用 避免oop单继承局限性
public class ThreadDemo extends Thread {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("thread-------------");
}
}
public static void main(String[] args) {
ThreadDemo tt = new ThreadDemo();
tt.start();
for (int i = 0; i < 200; i++) {
System.out.println("main++++++++++++++++");
}
}
}
2. 实现Runnable
接口
- 定义类实现Runnable接口
- 实现run()方法,编写线程执行体
- 创建线程对象,调用start()方法启动线程
注: 推荐使用,避免单继承局限性(仍然可以继承或者实现其他类或接口),灵活方便,方便同一个对象被多个线程使用
public class RunnableDemo implements Runnable{
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("thread--------------"+i);
}
}
public static void main(String[] args) {
// 创建Runnable接口的实现类对象
Runnable rr= new RunnableDemo();
// 创建线程对象,通过线程对象开启自定义线程,代理
Thread thread = new Thread(rr);
// 通过线程代理开启自定义线程
thread.start();
for (int i = 0; i < 200; i++) {
System.out.println("main++++++++++++++++"+i);
}
}
}
3. 实现Callable
接口
- 实现Callable接口,需要返回值类型
- 重写call方法,需要抛出异常
- 创建目标对象
- 创建执行服务:
ExecutorService ser = Executors.newFixedThreadPool(1);
- 提交执行:
Future<Boolean> result1 = ser.submit(1)
- 获取结果:
boolean r1 = result1.get()
- 关闭服务:
ser.shutdownNow()
public class CallableDemo implements Callable<Boolean> {
private String name;
public CallableDemo(String name) {
this.name = name;
}
@Override
public Boolean call() throws Exception {
for (int i = 0; i < 200; i++) {
System.out.println(name+"\tthread--------------"+i);
}
return true;
}
public static void main(String[] args) {
CallableDemo c1 = new CallableDemo("AA");
CallableDemo c2 = new CallableDemo("BB");
CallableDemo c3 = new CallableDemo("CC");
// 创建执行服务
ExecutorService ser = Executors.newFixedThreadPool(2);
// 提交执行
Future<Boolean> f1 = ser.submit(c1);
Future<Boolean> f2 = ser.submit(c2);
Future<Boolean> f3 = ser.submit(c3);
// 获取结果
try {
boolean r1 = f1.get();
boolean r2 = f2.get();
boolean r3 = f3.get();
System.out.println(r1+":"+r2+":"+r3+":");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
// 关闭服务
ser.shutdown();
}
}
实际案例
模拟售票系统
public class Test01 implements Runnable{
/**
* 模拟售票系统
*/
private int ticketNum = 20;
@Override
public void run() {
while(true){
if (ticketNum<=0){
break;
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t========\t"+this.ticketNum--);
}
}
public static void main(String[] args) {
Test01 t1 = new Test01();
new Thread(t1,"小明").start();
new Thread(t1,"黄牛1").start();
new Thread(t1,"黄牛2").start();
}
}
小明 ======== 20
黄牛1 ======== 19
黄牛2 ======== 18
黄牛2 ======== 17
黄牛1 ======== 16
小明 ======== 15
黄牛2 ======== 14
黄牛1 ======== 13
小明 ======== 12
黄牛2 ======== 11
黄牛1 ======== 10
小明 ======== 9
黄牛2 ======== 8
小明 ======== 7
黄牛1 ======== 6
小明 ======== 5
黄牛2 ======== 4
黄牛1 ======== 3
黄牛2 ======== 2
小明 ======== 1
黄牛1 ======== 0
黄牛2 ======== -1
发现问题: 可能会出现变量不受控制,线程不安全的可能
龟兔赛跑
public class Race implements Runnable {
private String winner = null;
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if ("兔子".equals(Thread.currentThread().getName())){
try {
Thread.sleep(10 );
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"\t=====\t"+i+"步");
if (gameOver(i)){
System.out.println(winner+"胜利了");
break;
}
}
}
private boolean gameOver(int foot){
if (winner!=null){
return true;
}
if (foot>=100){
winner=Thread.currentThread().getName();
return true;
}
return false;
}
public static void main(String[] args) {
Race race = new Race();
new Thread(race,"兔子").start();
new Thread(race,"乌龟").start();
}
}
静态代理
要求:
1. 代理对象和真实对象都要实现同一个接口
2. 代理对象要代理真实角色
优点:
1. 代理对象可以做很多真实对象做不了的事情
2. 真实对象专注做自己的事情
public class StaticProxy {
public static void main(String[] args) {
MarryCompany marry = new MarryCompany(new You());
marry.happyMarry();
}
}
interface Marry {
void happyMarry();
}
class You implements Marry {
@Override
public void happyMarry() {
System.out.println("你即将结婚");
}
}
class MarryCompany implements Marry {
// 真实目标角色
private Marry target;
public MarryCompany(Marry target) {
this.target = target;
}
@Override
public void happyMarry() {
before();
target.happyMarry();
after();
}
private void after() {
System.out.println("after+++++++");
}
private void before() {
System.out.println("before------");
}
}
通过这里我们可以关联: Tread类中也有target
lambda表达式
例: new Thread (()-> System.out.println("多线程学习")).start()
形如:
- (params) -> expression[表达式]
- (params) -> statement[语句]
- (params) -> {statement}
为什么使用Lambda表达式
- 避免匿名内部类定义过多
- 简化代码
- 省略无意义的代码,只留下核心逻辑
如何使用Lambda表达式
-
理解使用Functional Inteface (函数式接口)是学习Java8 lambda表达式的关键所在
函数式接口的定义:
-
任何接口如果只包含唯一一个抽象方法,那么它就是一个函数式接口
- 对于函数式接口,我们可以使用lambda表达式来创建该接口的对象
-
-
实现Lambda表达式的要求
-
所指向的对象实现的接口有且只有一个方法
-
例如:
public class LambdaDemo { public static void main(String[] args) { TestIntegerFace test = ()->{ System.out.println("实现了testInterface接口,运行的是function方法"); }; test.function(); } } interface TestIntegerFace{ void function(); }
-
lambda表达式的简化
public class LamdbaDemo02 {
public static void main(String[] args) {
TestInterface2 test ;
test = (String s)->{
System.out.println(s);
};
test.say("123");
test = (s)->{
System.out.println(s);
};
System.out.println("4");
test = s -> {
System.out.println(s);
};
System.out.println("5");
// 只能在一行代码的情况下这么定义
test = s-> System.out.println(s);
test.say("6");
}
}
interface TestInterface2 {
void say(String s);
}
线程状态
- 创建状态
- 就绪状态
- 阻塞状态
- 运行转态
- 死亡状态
停止线程
不推荐使用jdk的stop(),destory()方法
推荐使用线程自己停下来
建议使用标志位来控制程序中的循环
public class StopDemo implements Runnable {
private boolean flag;
@Override
public void run() {
flag = true;
int i=0;
while(flag){
System.out.println("RUN----\t "+i++);
}
}
public void stop(){
this.flag = false;
}
public static void main(String[] args) {
StopDemo stopDemo = new StopDemo();
new Thread(stopDemo,"停止线程的测试").start();
for (int i = 0; i < 200; i++) {
System.out.println("main-----\t"+i);
}
System.out.println("=======停止=======");
stopDemo.stop();
}
}
线程休眠
- sleep(时间)指定当前线程阻塞的毫秒数
- sleep存在异常InterruptedException;
- sleep时间达到后,线程进入就绪状态
- sleep可以模拟网络延时,倒计时等
- 每个对象都有一个锁,sleep不会释放锁
public class SleepDemo {
public static void main(String[] args) {
TimeDown time=new TimeDown(10);
new Thread(time).start();
}
}
class TimeDown implements Runnable {
private int flag;
public TimeDown(int flag) {
this.flag = flag;
}
@Override
public void run() {
// 模拟倒计时
while(flag>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(flag--);
}
}
}
线程礼让
-
礼让线程,让当前正在执行的线程暂停,但不阻塞
-
将线程从运行状态转为就绪状态
-
让CPU重新调度,礼让不一定成功,看CPU心情
语句:Thread.yield()
public class YieldDemo {
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()+"线程结束");
}
}
- 礼让情况
- 未礼让情况
合并线程
join合并线程,待此线程执行完成后
,在执行其他线程,其他线程阻塞.
public class JoinDemo implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("Join--插队"+i);
}
}
public static void main(String[] args) {
JoinDemo joinDemo = new JoinDemo();
Thread t = new Thread(joinDemo);
t.start();
for (int i = 0; i < 100; i++) {
if (i==20){
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("main"+i);
}
}
}
状态测试:
public class ShowStateDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
});
Thread.State state = thread.getState();
System.out.println(state);
thread.start();
while((state=thread.getState())!=Thread.State.TERMINATED){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (state!=thread.getState()){
System.out.println(thread.getState());
}
}
}
}
线程优先级
-
java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度那个线程来执行.
-
线程的优先级用数字来表示,范围1~10
-
使用g/s Priority(),获取/设置优先级.
public class PriorityDemo implements Runnable {
public static void main(String[] args) {
// System.out.println(Thread.currentThread().getName()+":"+
// Thread.currentThread().getState()+":"+
// Thread.currentThread().getPriority());
PriorityDemo demo = new PriorityDemo();
Thread t1 = new Thread(demo,"A");
t1.setPriority(1);
t1.start();
Thread t2 = new Thread(demo,"B");
t2.setPriority(3);
t2.start();
Thread t3 = new Thread(demo,"C");
t3.setPriority(5);
t3.start();
Thread t4 = new Thread(demo,"D");
t4.setPriority(7);
t4.start();
Thread t5 = new Thread(demo,"E");
t5.setPriority(10);
t5.start();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"==>"+Thread.currentThread().getPriority());
}
}
守护线程
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕
守护线程:后台记录操作日志,监控内存,垃圾回收等待
public class DaemonDemo {
public static void main(String[] args) {
You2 you = new You2();
God god = new God();
Thread youT = new Thread(you);
Thread godT = new Thread(god);
// 设置为守护线程,使得main方法不会因为它而一直执行
godT.setDaemon(true);
youT.start();
godT.start();
}
}
class You2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 36500; i++) {
System.out.println("====你还在活着====");
}
}
}
class God implements Runnable{
@Override
public void run() {
while(true){
System.out.println("上帝永远活着");
}
}
}
线程同步
处理线程问题时,多个线程访问同一个对象,并且某些线程还需要修改.这个时候就需要线程同步,线程同步其实是一种等待机制,多个需要同时访问此对象的线程进入这个对象的线程池形成队列等待前面的线程使用完毕,下个线程再使用
并发
形成条件:队列+锁 👥👥👥👥+ 🔒
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排他锁,独占资源,其他线程必须等待,使用后释放锁即可,存在以下问题:
- 一个线程持有锁的时候会导致其他需要这个锁的线程挂起
- 在多线程竞争下,加锁,释放锁会造成较多的上下文切换,和调度延迟,引起性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级导致,引起性能问题
同步块
synchronized(Obj){}
-
Obj 称之为同步监视器
- Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class;
-
同步监视器的执行过程
-
第一个线程访问,锁定同步监视器,执行其中代码
-
第二个线程访问,发现同步监视器被锁定,无法访问
-
第一个线程访问完毕,解锁同步监视器
-
第二个线程访问,发现同步监视器没有锁,然后锁定并访问
-
死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,从而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形,某一个同步代码块同时拥有"两个以上对象的锁",就可能会发生"死锁"的问题.
- 死锁示例
public class DeadLock {
public static void main(String[] args) {
Girl girl1 = new Girl(1, "公主");
Girl girl2 = new Girl(0, "孩子");
new Thread(girl1).start();
new Thread(girl2).start();
}
}
class LipStick{
// 口红
}
class Mirror{
// 镜子
}
class Girl implements Runnable{
int choose;
String girlName;
public static LipStick lipStick= new LipStick();
public static Mirror mirror = new Mirror();
public Girl(int choose,String girlName){
this.choose = choose;
this.girlName = girlName;
}
@Override
public void run() {
try {
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void makeup() throws InterruptedException {
if (choose==0){
synchronized (lipStick){
System.out.println(this.girlName+"取得了口红");
Thread.sleep(1000);
synchronized (mirror){
System.out.println(this.girlName+"取得了镜子");
}
}
}else {
synchronized (mirror){
System.out.println(this.girlName+"取得了镜子");
Thread.sleep(2000);
synchronized (lipStick){
System.out.println(this.girlName+"取得了口红");
}
}
}
}
}
- 解决死锁问题
private void makeup() throws InterruptedException {
if (choose==0){
synchronized (lipStick){
System.out.println(this.girlName+"取得了口红");
}
Thread.sleep(1000);
synchronized (mirror){
System.out.println(this.girlName+"取得了镜子");
}
}else {
synchronized (mirror){
System.out.println(this.girlName+"取得了镜子");
}
Thread.sleep(2000);
synchronized (lipStick){
System.out.println(this.girlName+"取得了口红");
}
}
}
产生死锁的四个必要条件
- 互斥条件: 一个资源每次只能被一个进程使用
- 请求与保持条件: 一个进程因请求资源二阻塞时,对已获得的资源保持不放
- 不剥夺条件: 进程已获得的资源,在未使用完之前不能强行剥夺
- 循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系
上述的代码是打破了请求与保持条件,从而打破了死锁
锁🔒
-
从jdk5.0开始,java提供了更强大的线程同步机制,通过显式定义同步锁对象来实现同步.同步锁使用Lock对象充当
-
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
-
ReentrantLock类(可重入锁)实现了Lock,他拥有与synchronize相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁,释放锁.
public class LockDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket).start();
new Thread(ticket).start();
new Thread(ticket).start();
}
}
class Ticket implements Runnable{
int ticketnum=10;
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
lock.lock();
try{
while(true){
if(ticketnum>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketnum--);
}else{
break;
}
}
}finally {
lock.unlock();
}
}
}
lock()语句后必须紧跟try代码块,且finally{}块中第一句为unlock()语句
synchronnized与lock的对比:
-
lock是显式锁(手动开启和关闭锁);synchronize是隐式锁,出了作用域自动释放
-
lock只有代码块锁,synchronized有代码块锁和方法锁
-
使用Lock锁,JVM将花费更少的时间来调度线程,性能更好.并且具有更好的扩展性(提供更多的子类)
-
优先使用顺序
Lock>同步代码块>同步方法体
线程协作
生产者消费者问题
这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件
-
对于生产者,没有生产产品之前,要通知消费者等待.而生产了产品之后,又需要马上通知消费者消费
-
对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费.
-
生产者消费者问题中,仅有 synchronized是不够的
- synchronized可阻止并发更新同一个共享资源,实现了同步
- synchronized不能用来实现不同线程之间的消息传递(通信)
java中提供了几个解决线程之间的通信问题
方法名 | 作用 |
---|---|
wait() | 表示线程一直等待,直到其他线程通知,与sleep不同会释放锁 |
wait(long timeout) | 指定等待的亳秒数 |
notify() | 唤醒一个处于等待状态的线程 |
notifyAll() | 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度 |
注:均是Object类的方法,都只能在同步方法和同步代码块中使用,否则hi抛出异常IllegalMonitorStateException;
解决方法
- 并发协作模型"生产者/消费者模式"–>管程法
- 生产者:负责生产数据的模块
- 消费者:负责处理数据的模块
- 缓冲区:消费者不能直接使用生产者的数据,他们之间有个缓冲区
- 生产者将生产号的数据放在缓冲区中,消费者从缓冲区拿出数据
- 并发协作模型 “生产者/消费者模式”–>信号灯法
管程法
模拟快餐店生产烤鸡供用户食用
public class PCDemo {
public static void main(String[] args) {
Buffer buffer = new Buffer();
new Productor(buffer).start();
new Customer(buffer).start();
}
}
// 生产者
class Productor extends Thread {
Buffer buffer;
public Productor(Buffer buffer){
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
Chicken chicken = new Chicken(i);
System.out.println("生产了第"+chicken.i+"只鸡 ++");
buffer.push(chicken);
}
}
}
// 消费者
class Customer extends Thread {
Buffer buffer;
public Customer(Buffer buffer){
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
Chicken chicken = buffer.pop();
System.out.println("获得了第"+chicken.i+"只鸡--");
}
}
}
// 消费物品
class Chicken {
int i;
public Chicken(int i) {
this.i = i;
}
}
// 缓冲区
class Buffer {
// 指针
int point = 0;
// 容量
int size = 10;
// 存储位置
Chicken[] chickens = new Chicken[size];
// 生产者生产
public synchronized void push(Chicken chicken) {
// 已经满了,通知消费者消费
if (point == size) {
//等待消费者消费
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 未满,将产品放在容器中
chickens[point] = chicken;
point++;
// 通知消费者可以消费
this.notifyAll();
}
public synchronized Chicken pop(){
// 如果没有产品,等待生产者生产
if(point==0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 进行消费
point--;
Chicken chicken = chickens[point];
// 消费完了,等待生产者生产
this.notifyAll();
return chicken;
}
}
信号灯法
模拟演员在剧场表演给观众观看
package multithreading;
// 信号灯法解决生产者消费者问题
public class PCDemo2 {
public static void main(String[] args) {
Theater theater = new Theater();
new Actor(theater).start();
new Spectator(theater).start();
}
}
// 演员 提供表演
class Actor extends Thread {
Theater theater;
public Actor(Theater theater) {
this.theater = theater;
}
@Override
public void run(){
for (int i = 0; i < 100; i++) {
theater.play(String.valueOf(i));
}
}
}
// 观众 观看表演
class Spectator extends Thread {
private Theater theater;
public Spectator(Theater theater) {
this.theater = theater;
}
@Override
public void run(){
for (int i = 0; i < 100; i++) {
String watch = theater.watch();
}
}
}
// theater 剧场
class Theater {
// 演员是否表演
boolean flag = false;
String program;
// 演员表演
public synchronized void play(String program) {
// 当前正在表演节目
if (flag == true) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.program = program;
System.out.println("演员表演"+program+"节目++");
flag = true;
this.notifyAll();
}
// 观众观看
public synchronized String watch() {
if (flag == false) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
flag = false;
System.out.println("观众观看"+program+"节目 --");
this.notifyAll();
return this.program;
}
}
使用flag变量作为信号灯对线程的执行进行控制
线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建〕
- 便于线程管理
- core Poolsize:核心池的大小
- maximum Poo|size:最大线程数
- keepAlive Time:线程没有任务时最多保持多长时间后会终止
使用线程池
package multithreading;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class PoolDemo {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.shutdown();
}
}
class MyThread implements Runnable{
@Override
public void run() {
// for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName());
// }
}
}