【JavaSE总结】多线程

一、进程和线程

在这里插入图片描述
像这样每一个应用就可以理解为一个进程。
在一个进程里可能会有多条逻辑同时进行,以保证满足这个进程需要完成的任务。那么这些不同的逻辑就是多个线程。
在这里插入图片描述
可以看到线程数是远大于进程的,说明每个进程里都有一个或多个线程。

二、线程创建的方式

1、Thread

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

public class TestThread1 extends Thread {
    @Override
    public void run() {
        //run()方法线程体
        for (int i = 0; i < 10; i++) {
            System.out.println("run--" + i);
        }
    }

    public static void main(String[] args) {
        //创建线程对象
        TestThread1 testThread1 = new TestThread1();
        //调用start()方法,开启线程
        testThread1.start();

        //main()线程
        for (int i = 0; i < 1000; i++) {
            System.out.println("main--" + i);
        }
    }
}

2、Runnable

创建线程方式2:实现runnable接口,重写run方法,执行线程需要丢入runnable的实现类,调用start方法

public class TestRunnable1 implements Runnable{
    @Override
    public void run() {
        //run()方法线程体
        for (int i = 0; i < 10; i++) {
            System.out.println("run--" + i);
        }
    }

    public static void main(String[] args) {
        //创建runnable接口的实现类对象
        TestRunnable1 testRunnable1 = new TestRunnable1();
        //创建线程对象,通过线程对象开启线程,代理Proxy
        new Thread(testRunnable1).start();

        //main()线程
        for (int i = 0; i < 1000; i++) {
            System.out.println("main--" + i);
        }
    }
}

由于Runable接口是函数式接口,所以可以用lambda表达式简化。

public class DemoRunable implements Runnable{
    @Override
    public void run() {}
    public static void main(String[] args) {
        Runnable r = new DemoRunable();
        r = ()->{ for(int i=0;i<10;i++){ System.out.println("run--"+i); } };
        new Thread(r).start();
    }
}

三、静态代理

静态代理模式
真实对象和代理对象都要实现同一个接口
代理对象要代理真实角色
代理的好处:可以做真实对象额外的事情
这里以结婚作为例子,自己结婚只会输出"要结婚了,超开心",使用代理会输出"结婚之前,布置现场"“要结婚了,超开心”“结婚之后,收尾款”。代理就相当于婚庆公司的作用。

public class StaticProxy {
    public static void main(String[] args) {
        //自己结婚
        System.out.println("--不使用Proxy--");
        You you = new You();
        you.HappyMarry();

        System.out.println("\n\n\n");//分割
        //婚庆公司代理
        System.out.println("--使用Proxy--");
        WeddingCompany weddingCompany = new WeddingCompany(new You());
        weddingCompany.HappyMarry();
    }
}

interface Marry{
    void HappyMarry();
}

//真实角色
class You implements Marry{
    @Override
    public void HappyMarry() {
        System.out.println("要结婚了,超开心");
    }
}

//代理角色
class WeddingCompany implements Marry{
    private Marry targrt;

    public WeddingCompany(Marry targrt) {
        this.targrt = targrt;
    }

    @Override
    public void HappyMarry() {
        before();
        this.targrt.HappyMarry();//真实对象结婚
        after();
    }

    private void after() {
        System.out.println("结婚之后,收尾款");
    }

    private void before() {
        System.out.println("结婚之前,布置现场");
    }
}

四、Lambda表达式

避免内部类定义过多降低代码的可读性。
lambda表达式的产生是一个逐步简化的过程,一个函数式接口调用的简化过程可以大致记作:1、使用实现类调用 2、使用静态内部类调用 3、使用局部内部类调用 4、使用匿名内部类调用 5、使用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) {
        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();
    }
}
//1.定义一个函数式接口
interface ILike{
    void lambda();
}

//2.实现类
class Like implements ILike{
    @Override
    public void lambda() {
        System.out.println("I like lambda");
    }
}

五、线程的状态

1、新建(NEW)

一个线程对象被new出来了

2、可运行(RUNNABLE)

线程对象的start()方法被调用,但是还没被cpu执行

3、运行(RUNNING)

线程对象被cpu执行着

4、阻塞(BLOCKED)

线程在运行状态下,由于某种原因又回到了可运行状态

5、死亡(DEAD)

线程执行完,或因为某种原因退出了run()方法,无法再次运行了

六、常用方法

方法用处
sleep(long millis)线程休眠一定的毫秒
join()所有线程阻塞,让调用该方法的线程先执行完
yield()线程退出cpu的占用,让cpu重新选择调用(相当于两个线程又回到同一起跑线)
isAlive()判断线程是否还活着
start()启用线程
setPriority(int newPriority)设置某个线程的优先级1~10
interrupt()给线程一个中断的标记,并不会真的中断线程

七、线程同步

线程同步就是让线程按照预定的先后顺序运行

1、synchronized

先看一个在线程里使用List的例子

public class UnsafeList {
    public static void main(String[] args) {
        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());
    }
}

由于未实现线程同步,导致这个list在执行一次add()方法的同时,有可能又被再次调用,而再次调用的时候,因为list已经被占用了,所以这个再次调用就是无效的。
这样,最后的输出结果就不是10000。
现在给list对象上个线程锁:

public class SafeList {
    public static void main(String[] args) {
        List<String>  list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                synchronized (list){
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

这样,list在线程里就是个安全的List了,只有上一次调用完成,才会进行下一次调用。输出的结果一定是10000。
类似的,synchronized关键字不仅可以给对象上锁,还可以给方法上锁。
用一个买票的过程做示例:

public class UnsafeBuyTicket {
    public static void main(String[] args) {
        BuyTicket station = new BuyTicket();

        new Thread(station,"苦逼的我").start();
        new Thread(station,"牛逼的你").start();
        new Thread(station,"可恶的黄牛").start();
    }
}

class BuyTicket implements Runnable {
    //票
    private int ticketNums = 10;
    boolean flag = true;

    @Override
    public void run() {
        //买票
        while (flag){
            buy();
        }
    }

    private void buy() {
        //判断是否有票
        if (ticketNums <= 0) {
            flag = false;
            return;
        }

        //模拟延时
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //买票
        System.out.println(Thread.currentThread().getName() + "买到" + ticketNums--);
    }
}

买票的方法没有实现线程同步就会产生类似这样的结果:
在这里插入图片描述
因为前一个人买票、剩余票-1张的过程还没有结束,另一个人就开始买票了,这时候他们看到的票的张数是一样的,他们就会买到同一张票。而实际生活中,两个人买票一定是会有一个先后买票顺序的。所以将买票的方法上锁,即可实现线程同步:

public class SafeBuyTicket {
    public static void main(String[] args) {
        BuyTicket2 station = new BuyTicket2();

        new Thread(station,"苦逼的我").start();
        new Thread(station,"牛逼的你").start();
        new Thread(station,"可恶的黄牛").start();
    }
}

class BuyTicket2 implements Runnable {
    //票
    private int ticketNums = 10;
    boolean flag = true;

    @Override
    public void run() {
        //买票
        while (flag){
            buy();
        }
    }

    //同步方法(上锁,锁的是this)
    private synchronized void buy() {
        //判断是否有票
        if (ticketNums <= 0) {
            flag = false;
            return;
        }

        //买票
        System.out.println(Thread.currentThread().getName() + "买到" + ticketNums--);
    }
}

输出结果类似于:
在这里插入图片描述

死锁(deadlock)

进程a和进程b都上了锁处于阻塞状态,这时a需要拿到b的锁才可以解除阻塞状态,b需要拿到a的锁才可以解除阻塞状态,这样的情况就叫死锁。
在编程的过程中一定要避免这样的情况。

2、Lock

优先级高
ReentrantLock类
使用示例:

public class TestLock {
    public static void main(String[] args) {
        TestLock2 testLock2 = new TestLock2();

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

class TestLock2 implements Runnable{
    int tickNums = 10;

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

    @Override
    public void run() {
        while (true){
            try {
                lock.lock();//加锁
                if(tickNums>0){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(tickNums--);
                }else {
                    break;
                }
            } finally {
                lock.unlock();
            }

        }
    }
}

解释一下:

private final ReentrantLock lock = new ReentrantLock();

是利用ReentrantLock类定义了一个锁,这个锁是通过Java代码实现的,与synchronized不同,synchronized是提交给JVM实现的。
在lock.lock()和lock.unlock()之间的部分是不可以同时被多个线程调用的,如果没有这个锁,三个线程会同时进行tickNums- -,那么tickNums三个三个的减就会出现0和-1被输出出来。
加了锁的结果是:10 9 8 7 6 5 4 3 2 1
不加锁的结果是:10 9 8 7 6 5 4 3 2 1 0 -1(结果是三个三个出来的)

八、线程通信

wait()使线程进入阻塞
notify()唤醒线程
notifyAll()唤醒所有线程

1、管程法(利用缓冲区)

举个例子:
一个生产鸡肉餐厅有厨师和顾客,这是两个毫不相关的人(线程),人专门吃鸡,厨师专门做鸡。那如何知道是否需要做鸡(调用厨师线程)或者有鸡可吃(调用顾客线程)呢?我们可以设置一个缓冲区——前台。当一只鸡都没有的时候,让顾客等着(阻塞),当鸡放不下的时候,让厨师等着(阻塞)。一旦厨师做了鸡,就通知顾客有鸡可吃(唤醒)。一旦顾客吃了鸡,就通知厨师做鸡(唤醒)。当然,吃鸡和做鸡的效率是由cpu选择的,吃鸡的选多了便会产生无鸡可吃的情况,反之亦然。
下面放上源码理解一下:

public class PC_buffer {
    public static void main(String[] args) {
        SynBuffer buffer = new SynBuffer();
        new Productor(buffer).start();
        new Consumer(buffer).start();
    }
}

class Productor extends Thread {
    SynBuffer buffer;

    public Productor(SynBuffer buffer) {
        this.buffer = buffer;
    }

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

class Consumer extends Thread {
    SynBuffer buffer;

    public Consumer(SynBuffer buffer) {
        this.buffer = buffer;
    }

    //消费
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("消费了编号为" + buffer.pop().id + "的鸡");
        }
    }
}

class Chicken {
    int id;//产品编号

    public Chicken(int id) {
        this.id = id;
    }
}

class SynBuffer {
    //需要一个容器大小
    Chicken[] chickens = new Chicken[10];

    //容器计数器
    int count = 0;

    //生产者放入产品
    public synchronized void push(Chicken chicken) {
        //如果容器满了,就需要等待消费者消费
        if (count == chickens.length) {
            //通知消费者消费,生产者等待
            try {
                this.wait();//释放锁,等待进程
            }catch (InterruptedException e){
                e.printStackTrace();
            }

        }

        //如果未满,就需要丢入产品
        chickens[count] = chicken;
        count++;

        //可以通知消费者消费
        this.notifyAll();
    }

    //消费者消费产品
    public synchronized Chicken pop() {
        //判断能否消费
        if (count == 0) {
            //消费者等待生产
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

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

        //通知生产者生产
        this.notifyAll();
        return chicken;
    }
}

2、信号灯法(利用标志位)

将管程法的缓冲区变成了一个标志位。
举个例子:演员表演节目的电视上。一表演,标志位就变成了可观看,就会通知观众观看(唤醒),如果观众还没观看,演员就会等待(阻塞)。观众看了节目后,标志位又会变为不可观看,通知演员表演(唤醒),观众等待(阻塞)。
上源码理解一下:

public class PC_flag {
    public static void main(String[] args) {
        TV tv = new TV();
        new viewer(tv).start();
        new actor(tv).start();
    }
}

//生产者-->演员
class actor extends Thread {
    TV tv;
    public actor(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if(i%2==0){
                this.tv.perform("斗罗大陆");
            }else {
                this.tv.perform("广告");
            }
        }
    }
}

//消费者-->观众
class viewer extends Thread {
    TV tv;
    public viewer(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 perform(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;
    }
}

九、线程池

(线程池我只会简单的调用,相关知识需要自行搜索)
线程池的简单使用示例:

//1.创建服务,创建线程池,参数为池子大小
ExecutorService service = Executors.newFixedThreadPool(10);

//执行execute
service.execute(new MyThread());
service.execute(new MyThread());

//2.关闭连接
service.shutdown();

线程池的几个参数:

  1. 池的大小
  2. 最大连接数
  3. 连接保持时间(一定时间某个线程连接了并未操作就自动断开)

上一篇 IO流
下一篇 网络编程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

新手且笨蛋37

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值