Java多线程基础

本文深入探讨了Java中的多线程概念,包括任务、线程与进程的关系、线程创建(通过继承Thread类和实现Runnable接口)、线程安全问题以及并发控制。介绍了线程状态、线程同步方法(如synchronized和Lock)、线程通信(生产者消费者模式)以及线程池的使用。此外,还讨论了线程礼让、线程优先级和守护线程等概念,强调了正确管理和控制线程的重要性。
摘要由CSDN通过智能技术生成

线程简介

1. 任务
什么是多任务,最简单的例子,边吃饭边玩手机就是多任务,宏观上看起来是这样。但是把这件事细分出来,单独抓出某一个瞬间,大脑是在做一件事情
2. 多线程
单条道路上容易阻塞,单向单车道容易堵车,这个时候我多一条车道出来组成双向四车道就可以一定程度上缓解这样的问题
3.普通方法去调用多线程
显然,左边的执行效率要低一些,右边的执行效率两条线程同时运行效率更高~在这里插入图片描述
4.进程(Process)和线程(Thread)

写好的程序是静态的,让他跑起来就变成了动态的进程
进程是一个巨大的保护伞,保护伞下有多个真正干活的线程
在这里插入图片描述

一个进程中有多个线程,比如b站刷视频,这个视频播放的时候作为一个进程来看待
那么这个视频播放的时候(进程活跃时),包含了声音、画面、弹幕…多个功能,那么这些功能每一项都是一个单独的线程,因此可以说明,一个进程里有多个线程
再举一个例子就是任务管理器
在这里插入图片描述
章节总结
在这里插入图片描述

补充个面试题:JDK在启动的时候最少有几个线程同时启动
		 答:两个,分别是main主线程和GC垃圾回收线程

线程的创建

1.继承Thread类(重点)
子类继承完Thread类之后,这个子类要重写run类的方法Thread,然后开启它。
new子类对象,调用start方法执行(开启线程要用start方法)

测试代码

public class TestThread extends Thread{
    /**
     * 养成习惯,马上重写run方法
     */
    @Override
    public void run() {
        //run方法线程体
        for (int i=0;i<10;i++){
            System.out.println("第"+i+"次输出");
        }
    }
    /**
     * 主线程
     */
    public static void main(String[] args) {
        //调用上面创建的线程
        TestThread testThread = new TestThread();
        testThread.start();
        for (int i=0;i<10;i++) {
            System.out.println("学习多线程"+i);
        }
    }
}
学习多线程0
第0次输出
学习多线程1
第1次输出
学习多线程2
第2次输出
学习多线程3
第3次输出
第4次输出
第5次输出
第6次输出
第7次输出
第8次输出
第9次输出
学习多线程4
学习多线程5
学习多线程6
学习多线程7
学习多线程8
学习多线程9

可以看到,并不是先执行的我们自己重写的run方法内的内容,而是两个线程去交替或者不定时的输出内容(换句话说,类似并行)。线程不一定什么时候执行,执行的这个时间看CPU来调度,具体的优先级就涉及到了线程的优先级设置。一句话总结:线程开启不一定立即执行,由CPU调度。
总结具体流程就是
在这里插入图片描述
2.实现Runnable接口(重点)
通过实现Runnable的接口,实现run方法
在这里插入图片描述

//定义一个类去实现Runnable接口
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        //实现Run方法,编写方法体
        for (int i=0;i<10;i++){
            System.out.println("第"+i+"次输出");
        }
    }
    /**
     * 测试方法
     */
    public static void main(String[] args) {
        //创建Runnable接口的实现类对象
        MyRunnable myRunnable = new MyRunnable();
        //把这个多线程Runnable接口的实现对象传入new Thread方法中
        //myRunnable对象没办法直接启动start,需要传入代理Thread对象才可以
        /**
        源码:
         public Thread(Runnable target) {
            init(null, target, "Thread-" + nextThreadNum(), 0);
         }
         */
        Thread thread = new Thread(myRunnable);
        //启动线程
        thread.start();
        /**
        或者new Thread(myRunnable).start();也可以达到一样的启动线程效果
        */
        for (int i=0;i<10;i++){
            System.out.println("学习多线程"+i);
        }
    }
}

运行代码,结果和上面继承Thread类的多线程实现方法输出结果类似,证实了两个线程在并行,这里就不过多赘述了。

整体总结
在这里插入图片描述
3.实现Callable接口
在这里插入图片描述
4.初识并发问题
多个线程操作同一个对象的时候,线程不安全,数据紊乱(线程安全问题)
举例:

//多个线程同一时间操作同一个对象
//买火车票的例子
public class MuiltThreadController implements Runnable{
    private int ticketNums=10;
    @Override
    public void run() {
        while (true){
            if (ticketNums<=0){
                break;
            }
            //延时
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //Thread.currentThread().getName()是获取当前线程的名称
            System.out.println(Thread.currentThread().getName()+"拿到了第"+ticketNums--+"张票");
        }
    }
    public static void main(String[] args) {
        MuiltThreadController ticket = new MuiltThreadController();
        new Thread(ticket,"线程1").start();
        new Thread(ticket,"线程2").start();
        new Thread(ticket,"线程3").start();
    }
}

输出结果**可以很明显的看到票的顺序是紊乱的,不是按顺序递减**

线程2拿到了第10张票
线程1拿到了第8张票
线程3拿到了第9张票
线程1拿到了第6张票
线程2拿到了第5张票
线程3拿到了第7张票
线程3拿到了第4张票
线程2拿到了第3张票
线程1拿到了第4张票
线程3拿到了第2张票
线程1拿到了第1张票
线程2拿到了第0张票

5.龟兔赛跑例子

/**龟兔赛跑练习
 1.首先来个赛道距离,然后要离终点越来越近
 2.判断比赛是否结束
 3. 打印出胜利者
 4.龟兔赛跑开始
 5.故事中是乌龟赢的,兔子需要睡觉,所以我们来模拟兔子睡觉
 6. 终于,乌龟赢得比赛
 */
public class GuiTuSaiPao implements Runnable{
    //胜利者
    private static String winner;
    //跑道长度
    int TrackLong=100;
    @Override
    public void run() {
        for (int i=1;i<=TrackLong;i++){
            //让兔子睡觉
            if ((Thread.currentThread().getName()).equals("兔子") && i>= 10) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //判断比赛是否结束
            boolean flag = isRaceOver(i);
            if (flag==true){
                break;
            }
            System.out.println(Thread.currentThread().getName()+"跑了第"+i+"步");
        }
    }

    //判断是否完成比赛
    private boolean isRaceOver(int steps){
        if (steps>=100){
            winner = Thread.currentThread().getName();
            System.out.println("胜利者"+winner);
        }
        //判断是否有胜利者
        if (winner!=null){
            return true;
        }
        //没有胜利者
        return false;
    }

    public static void main(String[] args) {
        GuiTuSaiPao guiTuSaiPao = new GuiTuSaiPao();
        new Thread(guiTuSaiPao, "乌龟").start();
        new Thread(guiTuSaiPao, "兔子").start();
    }
}

运行结果,前十步的时候两个线程交替输出,十步以后乌龟线程单独输出
在这里插入图片描述
总结一下,因为有两个new Thread(guiTuSaiPao,name).strat();
启动了两个线程,各跑各的,互不干扰

6.总结一下run和start方法的区别
系统调用start()方法启动一个线程,该线程处于就绪状态而非运行状态,也就意味着这个线程可以被JVM来调度执行,在调度过程中,JVM通过线程类的run()方法来完成实际的操作,当run()方法结束后就会终止;

如果直接调用run()方法,这就相当于一个普通函数的调用,是同步的,无法达到多线程的目的,start()方法能后异步的调用run()方法,真正达到多线程的目的

Lambda表达式

在这里插入图片描述
Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。
Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。
使用 Lambda 表达式可以使代码变的更加简洁紧凑。
注意:接口有且仅有一个抽象方法才能使用lambad表达式
只有一个抽象接口,因为唯一,所以就可以简化掉重写、new对象时的关键字
lambda 表达式的语法格式如下:

(parameters) -> expression
或
(parameters) ->{ statements; }

以下是lambda表达式的重要特征:

可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定表达式返回了一个数值。

Lambda 表达式的简单例子:

// 1. 不需要参数,返回值为 5  
() -> 5  
// 2. 接收一个参数(数字类型),返回其2倍的值  
x -> 2 * x  
// 3. 接受2个参数(数字),并返回他们的差值  
(x, y) -> x – y  
// 4. 接收2个int型整数,返回他们的和  
(int x, int y) -> x + y  
// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)  
(String s) -> System.out.print(s)

最后lambda表达式含义:
接口 对象 = (参数数据类型(接口中函数的参数)) -> {具体实现};

注意
1.有一个参数的时候,可以省去小括号,也可以省去参数类型
2.有两个参数的时候,小括号不可以省略,但是参数类型两个必须是都有或者都没有
3.方法体只有一句语句时可以省略大括号,但是多条语句不可以省略
到最后可以简化成
接口=(参数)-> {具体实现}
应用于多线程中的简单小例子

new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("内容输出...");
            }
        }).start();

你看反正我只是要输出,而且本身,Runnable接口里面只有一个run方法,那么这个时候就满足了Lamdba表达式的使用条件,简化后就变成了

//空括号内代表无参数,因为只有一个方法,就可以省略掉方法名字的内容
//如果没有参数那么参数也可以省略掉,很方便的完成了重写的内容
        new Thread(()->{
            System.out.println("简化后的代理");
        }).start();

静态代理(代理帮你做些事情)

代码演示

public class StaticProxy {
    //静态代理演示
    public static void main(String[] args) {
        //测试,传参,调方法
        company company = new company(new you());
        company.marry();
    }
}
//定义一个接口代表目标事件
interface Marry{
    void marry();
}
//角色1,自己结婚
class you implements Marry{
    @Override
    public void marry() {
        System.out.println("你结婚了");
    }
}
//角色2,婚庆公司帮你结婚
class company implements Marry{
    //结婚代理对象的变量
    private Marry target;
    //构造方法
    public company(Marry target) {
        this.target = target;
    }
    @Override
    public void marry() {
        //定义结婚之前的方法
        before();
        //这就是真实对象
        this.target.marry();System.out.println("代理结婚");
        //定义结婚之后的方法
        after();
    }
    private void before() {
        System.out.println("进行准备");
    }
    private void after() {
        System.out.println("完成结婚");
    }
}

总结:真实对象和代理对象都要实现同一个接口,代理对象要代理真实角色,帮助角色完成内容,专注其他任务

线程状态

线程状态(经典内容了属于是)
在这里插入图片描述
线程方法
在这里插入图片描述
上面的线程方法里,interrupt()并不建议使用,jdk官方的停止线程方法也不建议使用,这里建议用标志位来停止线程
线程停止在这里插入图片描述
通过标志位来停止线程的测试代码

public class ThreadStop implements Runnable{
    private boolean flag = true;
    @Override
    public void run() {
        //通过标志位来启停
        while (flag){
            System.out.println("Thread run...");
        }
    }
    //设置一个暴露在外的方法来改变flag
    public boolean stop(){
        return this.flag = false;
    }
    public static void main(String[] args) {
        ThreadStop threadStop = new ThreadStop();
        //启动线程
        new Thread(threadStop).start();
        for (int i=0;i<=1000;i++){
            System.out.println("主线程运行");
            if (i==900){
                //调用方法切换标志位
                threadStop.stop();
                System.out.println("线程停止了");
            }
        }
    }
}

输出结果

主线程运行
主线程运行
线程停止了
主线程运行
主线程运行
...主线程把剩下的跑完

更推荐让线程自己停下来,而不是依靠外部标志位

线程休眠

在这里插入图片描述
举例倒数代码

//模拟网络延时:放大问题的发生性,完成倒数功能
public class ThreadSleep {
    public static void main(String[] args) {
        try {
            countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void countDown() throws InterruptedException {
        int num = 10;
        while (true){
            //每次处理间隔0.5s
            Thread.sleep(500);
            System.out.println(num--);;
            if (num<=0){
                break;
            }
        }
    }
}

输出就是倒数从10—>0
例2:
线程睡眠来控制打印间隔时间

创建日期对象
	Date startTime = new Date(System.currentTimeMillis());
        while (true){
            try {
                //每次睡眠1s
                Thread.sleep(1000);
                //控制台打印格式化时间
                System.out.println(new SimpleDateFormat("HH:mm:SS").format(startTime));
                //更新时间对象
                startTime=new Date(System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

线程礼让(yield)

在这里插入图片描述
打个比方,A、B两个线程,A线程已经进入CPU里面进行处理了
这个时候,突然触发yield了,A线程就从CPU出来了(礼让嘛),这个时候A与B重新回到起跑线上,等待CPU的重新调度。这个时候还是看CPU的心情,有可能再次调A起来也有可能调B,不一定。
插入内容:看一下源码:
native关键字
在这里插入图片描述
代码举例

public class TestYield {
    //礼让未必成功,调度主要看CPU心情
    public static void main(String[] args) {
        //创建两个线程并启动
        new Thread(new MyYield(),"A线程").start();
        new Thread(new 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()+"线程停止执行");
    }
}

预期结果,A线程先启动,执行后礼让了,理想状态下两个线程重新回到起点,等待CPU调度,CPU调起B,B开始执行。之前的A执行结束,B也跟着执行结束。
结果输出

A线程线程开始执行
B线程线程开始执行
A线程线程停止执行
B线程线程停止执行

线程强制执行(插队join)

代码演示

public class TestJoin implements Runnable{
    //测试join线程插队
    @Override
    public void run() {
        for (int i=0;i<=100;i++){
            System.out.println("插队线程join"+i);
        }
    }
    public static void main(String[] args) throws InterruptedException {
        TestJoin testJoin = new TestJoin();
        Thread thread = new Thread(testJoin);
        //启动重写出来的run方法
        thread.start();
        //主线程
        for (int i = 0; i < 1000; i++) {
            if (i==600){
                //当主线程跑到600次的时候
                thread.join();//进行插队,其他线程进行等待
            }
            System.out.println("main"+i);
        }
    }
}

在这里插入图片描述

线程状态观测

线程的几种状态
在这里插入图片描述
获取线程状态,得到枚举出来的状态

Thread.State state = thread.getState();

示例代码

public class ThreadState {
    //观测线程状态
    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(i+"....");
            }
        });
        //观察状态
        Thread.State state = thread.getState();
        System.out.println(state);//这时候的状态为new
        //启动线程
        thread.start();
        state=thread.getState();//更新线程状态--->RUNNABLE,跑起来了
        System.out.println(state);
        //Thread.State.TERMINATED枚举出来的状态,为线程终止
        while (state!= Thread.State.TERMINATED){//只要线程不终止就一直跑
            Thread.sleep(1000);
            state = thread.getState();//更新线程状态--->TERMINATED
        }
    }
}

输出结果

NEW
RUNNABLE
0....
1....
2....
3....
4....
TERMINATED

线程优先级

更多的是权重的概念,因为线程真正被调起是等CPU下命令,加了优先级也只是让他被调起的概率更大一些
在这里插入图片描述
示例代码

import sun.management.ThreadInfoCompositeData;

public class TestPriority {
    //测试线程优先级
    public static void main(String[] args) {
        //主线程默认优先级
        System.out.println(Thread.currentThread().getName()+"优先级:"+Thread.currentThread().getPriority());
        //创建Runnable线程对象
        MyPriority myPriority = new MyPriority();
        //将Runnable对象以不同身份运行线程
        Thread threadA = new Thread(myPriority,"A");
        Thread threadB = new Thread(myPriority,"B");
        Thread threadC = new Thread(myPriority,"C");
        Thread threadD = new Thread(myPriority,"D");
        //A线程设置最低优先级
        threadA.setPriority(Thread.MIN_PRIORITY);
        threadA.start();
        //B线程设置7优先级
        threadB.setPriority(7);
        threadB.start();
        //C线程设置最高优先级
        threadC.setPriority(Thread.MAX_PRIORITY);
        threadC.start();
        //D线程设置最高优先级10
        threadD.setPriority(10);
        threadD.start();
    }
}

class MyPriority implements Runnable{
    @Override
    public void run() {
        //输出调用线程的优先级
        System.out.println(Thread.currentThread().getName()+"优先级:"+Thread.currentThread().getPriority());
    }
}

输出结果

main优先级:5
B优先级:7
C优先级:10
D优先级:10
A优先级:1

结果不难看出,理想状态下,main给了默认优先级5,剩下的线程根据优先级依次输出,优先级相当的情况下按自上到下的顺序依次输出。
注意:优先级高也只是被调用的概率高,不一定给了高优先级就一定先输出

守护线程

在这里插入图片描述
先举例:
用户线程:main线程(JVM必须等他执行完毕)
守护线程:GC线程(JVM不用等他执行完毕)
语法:thread线程对象.setDaemon(布尔标记); true为开启守护线程,false为关闭守护线程

Thread thread = new Thread(守护线程);
thread.setDaemon(true);//默认是false
thread.start();//守护线程启动,持续输出直到JVM关闭

测试代码

public class TestDeamon {
    //守护线程测试类
    //场景:上帝(守护线程)守护你(用户线程)
    public static void main(String[] args) {
        God god = new God();
        U u = new U();
        Thread thread = new Thread(god);
        thread.setDaemon(true);
        thread.start();//守护线程启动,持续输出直到JVM关闭
        new Thread(u).start();//用户线程启动
    }
}
//上帝
class God implements Runnable{
    @Override
    public void run() {
        while (true){
            System.out.println("上帝守护着你");
        }
    }
}
//你
class U implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 36500; i++) {
            System.out.println("一生都开心的活着");
        }
    }
}

输出结果:U的内容持续输出,和God守护线程交替输出,最后U结束了,守护线程还在持续输出。直到JVM结束。

线程同步(多个线程同时操作一个资源对象)

并发:同一个对象被多个线程同时操作
在这里插入图片描述
线程同步的实现=队列(排队)+锁(处理数据的时候上锁)
线程同步
在这里插入图片描述
线程不安全问题1
参考上面的初识并发问题,不排队,三个角色同时拿到内存里10张余票,面对的都是十张,票数就会不安全。
线程不安全问题2(不安全的集合)

public class UnSafeList {
    public static void main(String[] args) {
        //众所周知,ArrayList不是线程安全的
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(() -> {
                list.add(Thread.currentThread().getName());
            }).start();
        }
        //添加完毕输出集合大小
        System.out.println(list.size());
    }
}

因为循环了1w次,这里理想就是输出1w,但是结果不是这样

输出结果:9683

原因:线程不安全,多个线程同一瞬间把两个数组添加进了同一个位置上造成了覆盖,这样系统就默认已经加上去了,可是事实却是一个位置被多个线程反复添加覆盖,就造成了缺失
总结:多个线程进入内存,同时看见了内存是100,就操作了100,实际上大家同时去操作就会造成错误,不去排队操作就会产生超出预期的数据,因此要加锁。

同步方法以及同步块

同步方法
说白了synchronize也是一个修饰符,加在方法上
在这里插入图片描述
读方法不需要加锁,写入方法才需要加锁

同步监视器和同步方法是一个东西,同步块可以锁任何对象
锁住想要增删改的对象,保证并发安全
在这里插入图片描述
举例代码:线程不安全问题2(不安全的集合)加锁解决版
这里为什么加了睡眠,因为输出System.out.println(list.size());属于主线程的范畴,而run里面的又算是另一个线程。两个线程都各跑各的。主线程没等run的线程运行完就先输出了,所以要加一个sleep

public class UnSafeList {
    public static  void main(String[] args) {
        //众所周知,ArrayList不是线程安全的
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(() -> {
                synchronized (list){//加同步块,锁上目标list对象
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        try {
            Thread.sleep(3000);//加个延迟放大问题,让线程睡一会防止主线程先跑完
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //添加完毕输出线程大小
        System.out.println(list.size());
    }
}

最终输出答案:10000

有没有安全类型的集合呢?答案是有的:在JUC包(java.util.concurrent)下有一个方法,CopyOnWriteArrayList。安全类型的集合,就可以很好的解决上面的问题,就不需要再用同步块锁一下list对象了
示例代码如下

public class SafeList {
    public static void main(String[] args) {
        CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{//还是同样的用lamdba表达式,这里就不用上锁了
                copyOnWriteArrayList.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(copyOnWriteArrayList.size());
    }
}

最终结果:10000

死锁

这就是两个线程想用对方的资源,互相等待对方放锁的情况,我等你,你等我,那你俩就一直僵着吧
在这里插入图片描述
把两个synchronize块拿出来分开放,不要synchronize套synchronize
示例代码

package syncThread;
//死锁:多个线程互相抱着对方需要的资源
public class DeadLock {
    public static void main(String[] args) {
        Makeup m1 = new Makeup(0,"用户A");
        Makeup m2 = new Makeup(1,"用户B");
        //分别启动两个线程
        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(int choice, String name) {
        this.choice = choice;
        this.name = name;
    }

    @Override
    public void run() {
        //化妆
        try {
            makeup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //化妆,互相持有对方的锁,就是需要拿到对方的资源
    private  void makeup() throws InterruptedException {
        if(choice ==0){
            synchronized (lipstick){  //获得口红的锁
                System.out.println(this.name+"获得口红的锁");
                Thread.sleep(1000);
                //注意这里,同步块套了同步块
                synchronized (mirror){  //一秒钟后想获得镜子的锁
                    System.out.println(this.name+"获得镜子的锁");
                }
            }
        }else{
            synchronized (mirror){  //获得镜子的锁
                System.out.println(this.name+"获得镜子的锁");
                Thread.sleep(2000);
                //注意这里,同步块套了同步块
                synchronized (lipstick) {  //两秒钟后想获得口红的锁
                    System.out.println(this.name + "获得口红的锁");
                }
            }
        }
    }
}

这个时候输出内容,都在等对方放锁

用户A获得口红的锁
用户B获得镜子的锁

修正代码,把两个嵌套的同步块拿出来,分开放锁
修改后的部分代码:

//化妆,不再互相持有对方的锁,依次拿资源
    private  void makeup() throws InterruptedException {
        if(choice ==0){
            synchronized (lipstick){  //获得口红的锁
                System.out.println(this.name+"获得口红的锁");
                Thread.sleep(1000);
            }
            synchronized (mirror){  //一秒钟后想获得镜子的锁
                System.out.println(this.name+"获得镜子的锁");
            }
        }else{
            synchronized (mirror){  //获得镜子的锁
                System.out.println(this.name+"获得镜子的锁");
                Thread.sleep(2000);
            }
            synchronized (lipstick) {  //两秒钟后想获得口红的锁
                System.out.println(this.name + "获得口红的锁");
            }
        }
    }

输出结果,避免了死锁的情况

用户A获得口红的锁
用户B获得镜子的锁
用户A获得镜子的锁
用户B获得口红的锁

编码过程还是避免下列条件的发生
在这里插入图片描述

Lock锁

注意,lock是个接口
在这里插入图片描述
语法格式
在这里插入图片描述
不安全的买票例子说明:

class TestLock2 implements Runnable {
    int ticketNums = 10;
    //定义Lock锁
    private final ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true) {
            try {
                lock.lock();  //加锁
                if (ticketNums > 0) {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums-- + "票");
                } else {
                    break;
                }
            }finally {
                //解锁
                lock.unlock();
            }
        }
    }
}

正常有加锁就有解锁
注意:如果需要抛出异常,解锁unlock()需要写在finally{}块中

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

线程通信(生产者消费者模式)

从之前的线程与线程之间互不干扰,到互相交流做些什么(并非设计模式,只是一个问题)
举例:两个线程,一个线程生产者,另一个线程消费者。生产者产出消费者需要的东西,某一时间生产好了,就叫消费者过来拿东西
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
解决线程通信问题主要用wait和notify来实现

解决方式1:管程法
消费者是不可以直接拿生产者的东西的,需要一个缓冲区来作为一个仓库
生产者把生产好的数据放入缓冲区,消费者从缓冲区拿出数据
实例代码,不是很严谨,主要看这种模式

package ThreadCommuication;
//测试生产者消费者模型,用缓冲区解决(管程法)
//生产者,消费者,产品,缓冲区
public class TestPc {
    public static void main(String[] args) {
        //测试方法
        SynContainer container = new SynContainer();//启动测试
        new Thread(new Productor(container)).start();
        new Thread(new Consumer(container)).start();
    }
}
//生产者
class Productor extends Thread{
    SynContainer container;//容器
    public Productor(SynContainer container){
        this.container = container;//默认构造
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            container.pushChicken(new Chicken(i));
            System.out.println("生产了"+i+"只鸡");
        }
    }
}
//消费者
class Consumer extends Thread{
    SynContainer container;//容器
    public Consumer(SynContainer container){
        this.container = container;
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("消费了第"+container.getChicken().id+"只鸡");
        }
    }
}
//产品
class Chicken{
     int id;
     public Chicken(int id){
         this.id = id;//默认构造
     }
}
//缓冲区
class SynContainer{
    Chicken[] chickens = new Chicken[10];//容器大小
    int count=0;//容器计数器
    //生产者往仓库放入产品
    public synchronized void pushChicken(Chicken chicken){
        //如果容器满了,就需要等待消费者消费
        if (count==chickens.length){
            //通知消费者可以消费了
            this.notifyAll();//唤醒线程
        }
        //如果没有满了,就要放入产品了
        chickens[count] = chicken;
        count++;
        //可以通知生产者生产了
        this.notifyAll();
    }
    //消费者从仓库拿走产品
    public synchronized Chicken getChicken(){
        //判断能否消费了
        if (count==0){
            //等待生产者生产
            try {
                this.wait();//令线程等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //可以消费了
        count--;
        Chicken chicken=chickens[count];
        //可以通知消费者消费了
        this.notifyAll();
        return chicken;
    }
}

解决方式2:信号灯法
用一个标志位来判断,true就等待,false就通知放行去唤醒线程
示例代码

//生产者--演员
class Player extends Thread{
	public static void main(String[] args) {
        //测试方法
        Tv tv=new Tv();
        new Player(tv).start();
        new Watcher(tv).start();
    }

    TV tv;
    public Player(TV tv) {
        this.tv = tv;
    }
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if(i%2==0){
                this.tv.play("快乐大本营");
            }else{
                this.tv.play("天天向上");
            }
        }
    }
}
//消费者--观众
class Watcher extends Thread{
    TV tv;
    public Watcher(TV tv) {
        this.tv = tv;
    }
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            tv.watch();
        }
    }
}
//产品--节目
class TV{
    //演员表演,观众等待  T
    //观众观看,演员等待  F
    String voice;   //表演节目
    boolean flag = true;
    //表演
    public synchronized void play(String voice){
        if(!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("演员表演了:"+voice);
        //通知观众观看
        this.notifyAll();   //通知唤醒
        this.voice = voice;
        //更新标记位
        this.flag = !this.flag;
    }

    //观看
    public synchronized void watch(){
        if(flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("观看了:"+voice);
        //通知演员表演
        this.notifyAll();
        //更新标记位
        this.flag = !this.flag;
    }

}

线程池

使用线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。
可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处:
提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理…
corePoolSize: 核心池的大小
maximumPoolSize:最大线程数
keepAliveTime: 线程没有任务时最多保持多长时间后会终止

JDK 5.0起提供了线程池相关API: ExecutorService 和Executors
ExecutorService: 真正的线程池接口。常见子类ThreadPoolExecutor
void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执Runnable
Future submit(Callable task):执行任务,有返回值,一般用来执行Callable
void shutdown() :关闭连接池
Executors: 工具类、线程池的工厂类,用于创建并返回不同类型的线程池

示例代码

public class TestPool {

    public static void main(String[] args) {
        //1.创建服务,创建线程池
        ExecutorService service = Executors.newFixedThreadPool(线程池大小); //线程池大小(数字)
        //执行,execute(传入Runnable接口的实现类)
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        //2.关闭连接
        service.shutdown();
    }
}
class MyThread implements Runnable{
    @Override
    public void run() {
            System.out.println(Thread.currentThread().getName());
    }
}

线程池参数
1.corePoolSize 核心线程数
2.maximumPoolSize 最大线程池参数
3.keepAliveTime 任务结束后,线程存活此处指定时间后才会被释放
4.TimeUnit 上一个参数的单位,常用s,ms
5.BlockingQueue 队列,当核心线程用完时,任务放进队列
6.ThreadFactory 线程工厂
7.丢弃策略 默认Abort,直接丢弃,并抛出异常

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值