多线程基础复习总结(吐血总结附代码版)

一、程序,进程,线程?

程序:指令和数据的有序集合,其本身没有任何运行意义,是一个静态的概念。
进程:程序的一次执行过程,是动态的概念,是系统资源分配的单位
线程:一个进程有至少一个到多个线程,是CPU调度和执行的最小单位

二、实现多线程的方法?

1.继承Thread类、重写run方法、创建线程对象,调用start方法

public class Thread1 extends Thread {

    public void run(){
        //run方法线程体
        for(int i = 0;i<=20;i++){
            System.out.println("小黄"+"----->跑了"+i+"次");
        }
    }

    public static void main(String[] args) {
        //main线程主线程

        //创建一个线程对象
        Thread1 thread1 = new Thread1();
        //start方法启动该线程
        thread1.start();
        //run方法启动该线程
        //thread1.run();

        for(int i = 0;i<=20;i++){
            System.out.println("小林"+"----->跑了"+i+"次");
        }
    }

}

我们选取一些代表性的结果来看看,可以看到如果调用的是start方法的话,主线程和子线程是交替执行的,是并发执行的(严格意义上只有多个CPU或多个服务器处理的时候才是并发,很多多线程是模拟出来的,由于每个片段占用的cpu的时间非常短,所以我们一位它们是并行执行,但其实仍然是交替执行的)
在这里插入图片描述

那么,如果我们调用的是run方法呢?
在这里插入图片描述
可以看到,如果调用的是run方法的话,可以明显的看出一个执行的先后顺序的,这样就引出我们以下的这张图:
在这里插入图片描述
调用run方法执行是有顺序的,只有一条执行路径,结果是唯一的;而如果调用的是start方法,结果是不唯一的,因为CPU的调度是随机的,是没有先后顺序的。

2.实现runnable接口、重写run方法、创建runnable接口实现类对象、创建线程对象代理runnable接口实现类对象

//模拟龟兔赛跑
public class ThreadTest implements Runnable{

    //胜利者
    private static String winner;

    public void run() {
        for (int i = 0;i<=100;i++){
            //模拟兔子睡觉
            if(Thread.currentThread().getName().equals("兔子")&&i%10==0){
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //判断比赛是否结束
            boolean flag = gameover(i);
            if(flag){
                break;
            }
                System.out.println(Thread.currentThread().getName()+"---->跑了"+i+"步");
        }

    }

    //判断比赛是否结束
    private boolean gameover(int steps){
        if(winner!=null){
            return true;
        }
        if(steps>=100){
            winner = Thread.currentThread().getName();
            System.out.println("winner is---->"+winner);
            return true;
        }
        return false;
    }

    public static void main(String[] args) {
    	//runnable接口实现类对象
       ThreadTest race = new ThreadTest();
       //Thread代理对象
       new Thread(race,"兔子").start();
       new Thread(race, "乌龟").start();
    }

}

可以看到其实无论是继承自Thread类或者实现runnable接口,他们的整体思路都是大同小异的,运行结果也是交替执行,运行结果如下:
在这里插入图片描述

但是,在开发中,我们还是优先选择实现Runable接口的方式,因为在java规范中明确指出一个类只能单继承于一个类,局限性比较高,如果使用runnable接口的话可以避免单继承局限性,灵活方便,并且如果我们需要的话,我们可以实现一个对象,多个代理来启动多个线程。

3.实现callable接口,并与Future、线程池结合使用

//1.实现callable接口,泛型填写call方法的返回值类型
//2.重写call方法,需要抛出异常
//3.创建目标对象
//4.创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
//5.提交执行:Future<Boolean> result1 = ser.submit(t1);
//6.获取结果: boolean r1 = result1.get();
//7.关闭服务: ser.shutdownNow();
/**
 * 练习使用Callable实现Thread网络下载图片
 * */
public class Thread4 implements Callable<Boolean> {

    private String url;//网络图片地址
    private String name;//输出的要保存的文件名

    public Thread4(String url,String name){
        this.url = url;
        this.name = name;
    }

    //下载图片执行体
    public Boolean call(){
        webDownLoader webDownLoader = new webDownLoader();
        webDownLoader.downloader(url,name);
        System.out.println("下载了名为:"+name);
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Thread4 threadA = new Thread4("https://img-blog.csdnimg.cn/20201002174925882.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3B0R3JhcGVz,size_16,color_FFFFFF,t_70#pic_center","1.jpg");
        Thread4 threadB = new Thread4("https://img-blog.csdnimg.cn/20200923110318259.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3B0R3JhcGVz,size_16,color_FFFFFF,t_70#pic_center","2.jpg");
       //创建执行服务,创建线程池newFixedThreadPool 参数为池的大小
        ExecutorService ser = Executors.newFixedThreadPool(1);
        //提交执行
        Future<Boolean> result1 = ser.submit(threadA);
        Future<Boolean> result2 = ser.submit(threadB);
         //获取返回结果
        boolean r1 = result1.get();
        boolean r2 = result2.get();
        //关闭服务
        ser.shutdownNow();
    }

}

class webDownLoader{
    public void downloader(String url,String name){
        try {
            FileUtils.copyURLToFile(new URL(url),new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO异常,downloader出现异常");
        }
    }
        }

三、初识多线程的并发问题

直接上代码:

//买火车票的例子
public class Thread3 implements Runnable {

    //定义一个全局的常量表示火车票
    private int tickets = 10;
    boolean flag = true;//外部停止方式
    public void run() {
        while (flag) {
            try {
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public  void buy() throws InterruptedException {
        if(tickets<=0){
            flag = false;
            return;
        }
        //模拟延时
        Thread.sleep(200);
        //买票
        System.out.println(Thread.currentThread().getName()+"拿到了第------>"+tickets--+"张票");
    }


    public static void main(String[] args) {
        Thread3 thread3 = new Thread3();
        new Thread(thread3,"老师").start();
        new Thread(thread3,"黄牛党").start();
        new Thread(thread3,"学生").start();
    }



}

运行结果:
在这里插入图片描述

可以发现问题,多个线程操作同一个资源的情况下,线程不安全,出现了多个线程拿到同一张票的结果。那么我们应该怎么解决呢?这里我们就需要用到线程的同步机制:

线程同步:是一种等待机制,多个需要同时访问该对象的线程同时进入对象等待池形成队列,等待前一个线程执行完毕,后一个线程再继续执行。但是光有队列是不够的,我们还需要锁来保证安全。举个不恰当的例子,在一个旅游景点,只有一个厕所,这个厕所有很多人排队等待上厕所,假如说前一个人上厕所没有锁,后面的人等不及了有可能就会冲进去抢占该厕所,造成不安全的现象,因此我们得给厕所加上一把锁。这里的厕所我们可以理解为一个对象,一个资源,很多排队上厕所的人可以理解为线程。
那么我们如何实现线程同步呢?因此我们在这里引入了的概念。
synchronized锁:可以解决线程的安全问题,但是同时也带来了一些问题,例如:
性能降低:一个线程获得该对象的排他锁,会导致其需要此锁的线程挂起。
性能倒置:优先级高的线程等待优先级低的线程,导致性能导致,从而降低性能。

讲了这么多理论的东西,那么我们如何具解决线程不安全的问题呢?其实这里的例子很简单,我们只需要加上一个synchronized锁即可,如:

//买火车票的例子
public class Thread3 implements Runnable {

    //定义一个全局的常量表示火车票
    private int tickets = 10;
    boolean flag = true;//外部停止方式
    public void run() {
        while (flag) {
            try {
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    //synchronized同步方法,用这个方法的线程都会去请求对象的锁
    public synchronized void buy() throws InterruptedException {
        if(tickets<=0){
            flag = false;
            return;
        }
        //模拟延时
        Thread.sleep(200);
        //买票
        System.out.println(Thread.currentThread().getName()+"拿到了第------>"+tickets--+"张票");
    }


    public static void main(String[] args) {
        Thread3 thread3 = new Thread3();
        new Thread(thread3,"老师").start();
        new Thread(thread3,"黄牛党").start();
        new Thread(thread3,"学生").start();
    }

}

我们来看看加了synchronized锁之后的运行结果:很明显的看到,线程不安全的问题解决了。
在这里插入图片描述
我们来看看第二个不安全的例子–模拟两个线程同时取钱:

//两个对象操作一个资源
public class unsafeBank {
    public static void main(String[] args) {
        Account account = new Account(100,"结婚基金");
        //模拟一个名为你的线程去取钱
        Drawing you = new Drawing(account,50,"你");
        //模拟一个名为黑客的线程去取钱
        Drawing hacker = new Drawing(account,100,"hacker");

        //启动线程
        you.start();
        hacker.start();
    }

    //模拟账户信息
    static class Account{

        int money;//余额
        String name;//卡名

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

    //模拟银行取钱
    //synchronized默认锁的是this类本身--unsafeBank
    static class Drawing extends Thread{
        Account account;
        int drawMoney;//取了多少钱
        int nowMoney;//剩余多少钱

        public Drawing(Account account,int drawMoney,String name){
            //调用父类构造参数传递父类名字
            super(name);
            this.account = account;
            this.drawMoney = drawMoney;
        }
        //取钱操作
        @Override
        public void run() {
                //判断有没有钱
                if(account.money-drawMoney<0){
                    System.out.println(Thread.currentThread().getName()+"钱不够取不了");
                    return;
                }
                try {
                    //延时操作,假设所有人都可以同时取钱来到这一步,不存在前一个人取钱导致后一个人取不了
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //取钱之后账户所剩的钱
                account.money = account.money - drawMoney;
                //你手里的钱
                nowMoney = nowMoney + drawMoney;
                System.out.println(Thread.currentThread().getName()+"取了"+drawMoney+"钱后基金余额剩下:"+account.money);
                System.out.println(Thread.currentThread().getName()+"手里的钱还有"+nowMoney);

        }
    }

}

结果:出现了余额是负数的情况,这是因为有两个线程一个是hacker,一个是你,假设结婚基金原本有100块,你看到有100块,hacker看到也有100块,这个时候同一时刻大家都去取钱,你取了50之后,你认为还有50,而hacker认为原本还是有100块的,所以他取走100,导致最终余额只剩下-50。
在这里插入图片描述
那我们该怎么解决呢?同样,我们还是加synchronized锁,不过这次我们锁的就不是在void方法里面加,详细如图:

//两个对象操作一个资源
public class unsafeBank {
    public static void main(String[] args) {
        Account account = new Account(100,"结婚基金");
        //模拟一个名为你的线程去取钱
        Drawing you = new Drawing(account,50,"你");
        //模拟一个名为黑客的线程去取钱
        Drawing hacker = new Drawing(account,100,"hacker");

        //启动线程
        you.start();
        hacker.start();
    }

    //模拟账户信息
    static class Account{

        int money;//余额
        String name;//卡名

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

    //模拟银行取钱
    //unsafeBank
    static class Drawing extends Thread{
        Account account;
        int drawMoney;//取了多少钱
        int nowMoney;//剩余多少钱

        public Drawing(Account account,int drawMoney,String name){
            //调用父类构造参数传递父类名字
            super(name);
            this.account = account;
            this.drawMoney = drawMoney;
        }
        //取钱操作
        @Override
        public void run() {
            //synchronized同步块锁的是变化的量,需要增删改的地方,如果像上个演示那样在void前面添加synchronized锁,
            //那么synchronized默认锁的是this类本身,即Drawing,而涉及到增删改查的部分是account
            //因此我们通过synchronized(obj){}同步块的方式来锁住具体要涉及到的增删改的对象。
            synchronized (account){
                //判断有没有钱
                if(account.money-drawMoney<0){
                    System.out.println(Thread.currentThread().getName()+"钱不够取不了");
                    return;
                }
                try {
                    //延时操作,假设所有人都可以同时取钱来到这一步
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //取钱之后账户所剩的钱
                account.money = account.money - drawMoney;
                //你手里的钱
                nowMoney = nowMoney + drawMoney;
                System.out.println(Thread.currentThread().getName()+"取了"+drawMoney+"钱后基金余额剩下:"+account.money);
                System.out.println(Thread.currentThread().getName()+"手里的钱还有"+nowMoney);
            }
        }
    }

}

结果如下:
在这里插入图片描述

四、线程的五种状态

1.新建----当New出一个新的线程的时候,该线程就进入了新建状态。
2.就绪----当线程调用了start方法之后线程就进入了就绪状态,而什么时候运行是由CPU来调度的。
3.运行----当CPU调度了该线程,该线程进入运行状态。
4.阻塞----当调用了sleep,wait等方法锁定线程的时候,进入阻塞状态,阻塞状态解除后重新进入就绪状态
5.死亡----线程正常结束或者中断后,进入死亡状态,不能再次启动。总体过程如下图所示
在这里插入图片描述
在这里插入图片描述

五、Thread的几种常用的调用方法

在这里插入图片描述

sleep:每一个对象都有一个锁,sleep不会释放锁。
yield:线程礼让,让当前线程暂停,但不阻塞,将线程从运行态转变为就绪态,然后重新等待CPU调度,yield不一定能礼让成功,取决于CPU
join:可以理解为线程插队,当线程A插队了线程B之后,线程B会一直等待线程A执行完之后才会继续执行线程B,如:
在这里插入图片描述
结果如下:
可以看到在主线程到200之前,线程是交替执行的,它的调度是由CPU来安排的,当main线程i到了200之后,我们让test线程join了,可以看到,test线程插队到200之后,我们的主线程才继续开始执行
在这里插入图片描述
在这里插入图片描述
Thread.state:在这里插入图片描述

测试代码如下:

public class testState implements Runnable{

    @Override
    public void run() {

    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            for (int i = 0;i <= 5; i++){
                try {
                    Thread.sleep(1000);//手动阻塞,这里应该是TIMED_WAITING阻塞状态
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("第------->"+i+"次");
            }
        });


        //观测NEW状态
        Thread.State state = thread.getState();
        System.out.println(state);//此时还没Start线程,因此这里的状态应为NEW状态

        //观察Start状态
        thread.start();
        state = thread.getState();
        System.out.println(state);//这个时候start了,应该处于Runnable状态

        while (state!=Thread.State.TERMINATED){
            //只要线程不中止就一直输出状态
            Thread.sleep(100);
            state = thread.getState();
            System.out.println(state);

        }
    }
}

输出结果:

NEW
RUNNABLE
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
RUNNABLE
第------->0次
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
第------->1次
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
第------->2次
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
第------->3次
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
第------->4次
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
第------->5次
TERMINATED

setPriority:设计线程优先级,最小的优先级为Thread.MIN_PRIORITY=1,最大的优先级为Thread.MAX_PRIORITY=10,默认的优先级为Thread.NORM_PRIORITY=5;但是值得注意的是,优先级大并不意味着一定先执行,只是说有这么一个比较大的概率先执行,因为CPU的调度是随机的。测试代码如下:

public class testPriority implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"---->优先级:"+Thread.currentThread().getPriority());
    }

    public static void main(String[] args) {
        //主线程默认优先级为5
        System.out.println(Thread.currentThread().getName()+"---->优先级:"+Thread.currentThread().getPriority());
        testPriority test = new testPriority();
        Thread t1 = new Thread(test);
        Thread t2 = new Thread(test);
        Thread t3 = new Thread(test);
        Thread t4 = new Thread(test);

        //设置优先级
        t1.start();

        t2.setPriority(1);
        t2.start();

        t3.setPriority(4);
        t3.start();

        t4.setPriority(Thread.MAX_PRIORITY);
        t4.start();
    }
}

结果如下:
可以验证我们上面的猜想,优先级大并不一定会最先执行。
在这里插入图片描述
在这里插入图片描述

六、守护线程

1.守护线程是程序运行的时候在后台提供一种通用服务的线程。所有用户线程停止,进程会停掉所有守护线程,退出程序。
2.Java中把线程设置为守护线程的方法:在 start 线程之前调用线程的 setDaemon(true) 方法。值得注意的是,setDaemon(true) 必须在 start() 之前设置,否则会抛出IllegalThreadStateException异常,该线程仍默认为用户线程,继续执行。
3.虚拟机必须保障用户线程执行完毕,虚拟机不用等待守护线程直线完毕。

测试代码:

public class testDaemon {


    public static void main(String[] args) {
        You you = new You();
        God god = new God();

        Thread thread = new Thread(god);
        thread.setDaemon(true);
        thread.start();//启动上帝这个守护线程

        new Thread(you).start();//启动 你 线程

    }


    //你
    static class You implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i<=36500; i++){
                System.out.println("你度过了你人生快乐的"+i+"天");
            }
            System.out.println("================GoodBye java==================");
        }
    }

    //上帝
    static class God implements Runnable{
        @Override
        public void run() {
           while (true){
               System.out.println("上帝保佑着你每一天");
           }
        }
    }
}

结果:
可以看到我们并没有关闭守护线程,但是在用户线程 你 结束之后,守护线程并不会马上结束,而是等待一会再结束。
在这里插入图片描述

七、死锁

两个或多个以上的线程各自占有一些资源,并且互相等待对方或其他的线程所占有的资源才能运行,从而导致彼此都等待对方释放资源,从而造成死锁现象。就好比如两个小朋友A,B。A拿着玩具C,B拿着玩具D,但是A想要B的玩具,B也想要A的玩具,假设他们不懂得让出自己的玩具,那么他们就一直这样僵持下去。这便是死锁。

代码演示:

//死锁:多个线程互相抱着对方需要的资源
public class deadLock {

    public static void main(String[] args) {
        MakeUp LiHua = new MakeUp(0,"李华");//获得口红的锁,1秒后企图获得镜子
        MakeUp HuaLi = new MakeUp(1,"王雷");//获得镜子的锁,1秒后企图获得口红

        LiHua.start();
        HuaLi.start();
    }

    //口红
    static class Lipstick{

    }

    //镜子
    static class Mirror{

    }

    //模拟化妆进程
    public static class MakeUp extends Thread{

        int choice;//表示选择,0表示一开始货的口红的锁,其他表示获得镜子的锁
        String name;//表示化妆的人

        MakeUp(int choice, String name){
            this.choice = choice;
            this.name = name;
        }

       static Lipstick lipstick = new Lipstick();//static表示该资源只会创建一次,为共享资源
       static Mirror mirror = new Mirror();//

        public void run(){
            try {
                makeup();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        public void makeup() throws InterruptedException {
            if(choice==0){
                synchronized (lipstick){//如果为0表示获得口红的锁
                    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(1000);//休眠一秒后企图获得别人的口红的锁,同时让其他人获得别的锁
                    synchronized (lipstick){
                        System.out.println(this.name+"获得了口红的锁");//尝试获取第二个人的锁
                    }
                }
            }
        }

    }

运行结果:
可以看到我们并没有停止程序,他还一直跑下去,但是就是没有输出获取别人的锁,说明发生了死锁现象。这说明某一个同步块如果同时持有“两个以上的对象的锁”时,就有可能发生死锁。即:锁中锁。或者换种说法,持有锁的同时等待锁,就有可能造成死锁。
在这里插入图片描述
那么我们怎么才能避免死锁的出现呢?在上面的这种情况?其实很简单,我们其实只要在makeup方法里面,想办法让同步块不同时抱两把锁即可,一个同步块抱一把锁。因为同步块的锁,在同步块结束之后就会释放锁。如下代码所示:

 public void makeup() throws InterruptedException {
            if(choice==0){
                synchronized (lipstick){//如果为0表示获得口红的锁
                    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(1000);//休眠一秒后企图获得别人的口红的锁,同时让其他人获得别的锁
                }
                synchronized (lipstick){
                    System.out.println(this.name+"获得了口红的锁");//尝试获取第二个人的锁
                }
            }
        }

改造后的运行结果:
在这里插入图片描述

产生死锁的四个必要条件
1 互斥条件:一个资源每次只能被一个进程使用。
2 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
4 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
上面列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件 就可以避免死锁发生

八、Lock锁

Lock(锁) :从JDK 5.0开始, Java提供了更强大的线程同步机制-—通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当 java.uti concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。 锁提供了对共享资源的独占访问, 每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象 ,ReentrantLock类实现了Lock, 它拥有与synchronized相同的并发性和内存语义, 在实现线程安全的控制中,比较常用的是Reentrant Lock, 可以显式加锁、释放锁。
Lock锁和synchronized的区别**:

locksynchronized
显式加锁,手动开锁关锁隐式加锁 ,自动加锁解锁,出了作用域解锁
只能锁代码块能锁方法和代码块

测试lock锁:

//测试lock锁
public class Lock{
    public static void main(String[] args) {
        testLock testLock = new testLock();
        new Thread(testLock,"小林").start();
        new Thread(testLock,"小黄").start();
        new Thread(testLock,"小李").start();

    }
}

class testLock implements Runnable{
    static int tickets = 1000;
    //定义一个可重入锁ReentrantLock 
    private final ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
       while (true){
           lock.lock();//加锁操作一般放在try外
           try {
           //try{放需要保证线程安全的代码}
               if(tickets>0){
                   System.out.println(Thread.currentThread().getName()+"拿到了-------->"+tickets--+"票");
               }else {
                   break;
                  }
             }finally {
               lock.unlock();//一般解锁操作都放在finally执行
           }
       }
    }
}

测试结果:
可以看到已经保证了线程的安全

小林拿到了-------->16票
小李拿到了-------->15票
小李拿到了-------->14票
小黄拿到了-------->13票
小黄拿到了-------->12票
小黄拿到了-------->11票
小黄拿到了-------->10票
小黄拿到了-------->9票
小黄拿到了-------->8票
小黄拿到了-------->7票
小黄拿到了-------->6票
小黄拿到了-------->5票
小黄拿到了-------->4票
小黄拿到了-------->3票
小黄拿到了-------->2票
小黄拿到了-------->1

九、生产者消费者问题

1.什么是生产者消费者问题?
生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是如何保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。这便是生产者消费者问题
2.如何解决?
为了解决生产者消费者问题,光有synchronized锁是不够的,他只能保证线程的同步,不能用来实现线程之间的通信,因此java提供了几个解决线程之间通信问题的方法:

方法名作用
wait线程一直等待直到其他线程通知,不同于sleep,会释放锁
wait(long timeout)指定等待毫秒数
notify()唤醒一个除于等待状态的线程
notifyall()唤醒同一个对象上所有调用wait方法的线程,优先级高的优先调度

解决方法一:管程法

public class guancheng {

    public static void main(String[] args) {
        //提供容器
        ContainerCache container = new ContainerCache();

        //生产者线程启动
        new Producer(container).start();

        //消费者线程启动
        new Consumer(container).start();
    }


    //产品
    static class Chicken{
        int id;
        Chicken(int id){
            this.id = id;
        }
    }

    //生产者线程
    static class Producer extends Thread{
        ContainerCache container;

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

        //生产鸡
        public void run(){
            for (int i = 0; i < 100; i++) {
                container.push(new Chicken(i));
                System.out.println("生产了"+i+"只鸡");
            }
        }

    }

    //消费者线程
    static class Consumer extends Thread{
        ContainerCache container;

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

        public void run(){
            for (int i = 0; i < 100; i++) {
                System.out.println("消费者消费了第"+container.pop().id+"只鸡");
            }
        }
        
    }

    //自定义缓冲区容器,相当于肯德基的餐柜,存放好了已经准备好的食物
    static class ContainerCache{
        //需要一个容器大小模拟一次能缓存多少只鸡----好比如肯德基餐柜一下子能放多少成品
        //假设定义一个能存放10只鸡的数组
        Chicken[] chickens = new Chicken[10];

        //容器计数器,用来判断餐柜还有多少成品
         int count = 0;

        //生产者放入产品
        public synchronized void push(Chicken chicken){
            //容器满了,等待消费者消费
            while (count==chickens.length){
                //通知消费者消费,生产者停止生产
                try {
                    this.wait();//生产者停止生产产品
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //容器没满,继续补数量
            chickens[count] = chicken;
            count++;
            //可以通知消费者消费
            this.notifyAll();
        }


        //生产者消费产品
        public synchronized Chicken pop(){
            //判断是否有库存
            while (count==0){
                //如果没有库存,消费者等待,等待生产者生产
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //如果可以消费
            count--;
            Chicken chicken = chickens[count];

            //吃完了通知生产者生产
            this.notifyAll();

            //看看吃了那只鸡
            return chicken;
        }

    }


}

解决方法二:信号灯法

//信号灯法解决生产者消费者问题--通过标志位
public class xinhaodeng {

    public static void main(String[] args) {
        Tv tv = new Tv();
        new Player(tv).start();
        new Watcher(tv).start();
    }


    //生产者--演员
    static class Player extends Thread{
        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("广告播放中");
                }
            }
        }
    }

    //消费者--观众
    static class Watcher extends Thread{
        Tv tv;
        public Watcher(Tv tv){
            this.tv = tv;
        }

        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                    this.tv.watch();
            }
        }

    }

    //产品--节目
    static class Tv{
       //演员表演,观众等待--True
        //观众观看,演员等待--False
        String singing;//表演唱歌
        boolean flag = true;//标志位

        //表演
        public synchronized void play(String singing){
            //标志位为false的时候,等待观众观看
            if(!flag){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //否则演员表演,通知观众观看
            System.out.println("演员表演了"+singing);
            //通知观众观看
            this.notifyAll();
            this.singing = singing;
            this.flag = !this.flag;
        }

        //观看
        public synchronized void watch(){
           //观众观看,判断演员表演完没,如果为true则没表演完,观众等待
            if(flag){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("观众观看了"+singing);
            //看完之后通知演员表演
            this.notifyAll();
            //切换标志位
            this.flag = !this.flag;
        }

    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值