Java基础(3)-多线程

1、多线程简介

1.1、进程

进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。

进程的特征:
1)独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间 。 在没有经过进程本身允许的情况下, 一个用户进程不可以直接访问其他进程的地址空间 。
2)动态性:
3)并发性:

1.2、线程

1.2.1、多线程和普通方法

多线程执行和普通方法执行之间的差别.png

1.2.2、线程与进程

线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。

(1)一个进程可以包含若干个线程;
(2)线程就是独立的执行路径;
(3)在程序运行时,即使没有自己创建线程、后台也会有多个线程,如主线程,gc线程;
(4)同一个进程的线程之间会进行内存资源的交互,但不同进程之间独享各自分配的内存资源;
(5)main()也是线程,称之为主线程,为系统的入口,用于执行整个程序。

2、线程创建的方法

线程的创建方法有三种分别是继承Thread类、实现Runnable接口以及实现Callable接口,但是第三种再生产开发中几乎见不到,所以主要描述前两种线程创建方法。


image.png

2.1、继承Thread类创建线程

public class TestThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("我在学习线程"+i);
        }
    }
    public static void main(String[] args) {

        TestThread testThread = new TestThread();
        testThread.start();

        for (int i = 0; i < 20; i++) {
            System.out.println("我在看代码"+i);
        }
    }
}

类继承Thread,重写run(),编写线程执行体;在main方法中使用start方法启动线程。
小案例:使用三个线程同时下载网上图片:

public class TestThread2 extends Thread {
    private String url;
    private String name;
    public TestThread2(String url, String name){
        this.url = url;
        this.name = name;
    }
    @Override
    public void run() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url,name);
    }


    public static void main(String[] args) {
        TestThread2 t1 = new TestThread2("https://imagepphcloud.thepaper.cn/pph/image/126/215/304.jpg","1.jpg");
        TestThread2 t2 = new TestThread2("https://imagepphcloud.thepaper.cn/pph/image/126/215/307.jpg","2.jpg");
        TestThread2 t3 = new TestThread2("https://imagepphcloud.thepaper.cn/pph/image/126/215/307.jpg","3.jpg");
        t1.start();
        t2.start();
        t3.start();
    }
}

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方法出现问题");
        }
    }
}

2.2、实现Runnable接口创建线程

public class TestThread3 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("Runnable线程");
        }
    }
    public static void main(String[] args) {

        TestThread3 testThread3 = new TestThread3();
        new Thread(testThread3).start();
        for (int i = 0; i < 100; i++) {
            System.out.println("主线程....");
        }
    }
}

3、必要知识点

3.1、静态代理

静态代理:使用一个代理对象来实现真实对象的方法,同时在不修改目标对象的前提下扩展目标对象的功能。

使用一个简单的例子来理解静态代理:假如对象You想要结婚,但是You只能做结婚的动作,那些结婚时候的其他动作,比如办婚礼,装扮现场气氛组You不能做也不想做,就可以找一个婚庆公司来代理这件事。

package ThreadDemo;

/**
 * @author : HaiLiang Huang
 * @date : 2021年4月16日14:51:52
 */
public class StaticProxy  {
    public static void main(String[] args) {
        You you = new You();
        MarryCompany marryCompany = new MarryCompany(you);
        marryCompany.marry();
    }
}

interface Marry{
    void marry();
}

class You implements Marry{

    @Override
    public void marry() {
        System.out.println("秦老师要结婚了,超开心");
    }
}
class MarryCompany implements Marry{
    private You target;

    public MarryCompany(You target){
        this.target = target;
    }

    @Override
    public void marry() {
        before();
        target.marry();
        after();
    }
    private void after() {
        System.out.println("完事收钱");
    }
    private void before() {
        System.out.println("婚期公司布置婚礼现场");
    }
}

3.2、Lambda表达式:

Lambda表达式形成的步骤(以电影为例子):
1、创建出函数式编程:

interface  Move {
    void move();
}

2、可以采用类来实现这个接口完成接口动作:

class Move2 implements Move {
    @Override
    public void move() {
        System.out.println("我在看电影1");
    }
}

3、可以使用静态内部类来完成接口动作:

public class LambdaTest {

    static class Move3 implements Move {
        @Override
        public void move() {
            System.out.println("我在看电影2");
        }
    }

4、局部内部类完成接口动作:

    public static void main(String[] args) {
        Move move2 = new Move2();
        move2.move();



        move2 = new Move3();
        move2.move();

        class Move4 implements Move {
            @Override
            public void move() {
                System.out.println("我在看电影2");
            }
        }
        Move move4 = new Move4();
        move4.move();

5、匿名内部类完场接口方法:

Move move = new Move() {
            @Override
            public void move() {
                System.out.println("我在看电影3");
            }
        };
        move.move();

6、Lambda表达式:

Move move1 = () -> {
            System.out.println("我在看电影3");
        };
        move1.move();

总结:
上述完整代码:

/**
 * @author : HaiLiang Huang
 * @date : 2021年4月16日15:13:58
 */
public class LambdaTest {

    static class Move3 implements Move {
        @Override
        public void move() {
            System.out.println("我在看电影2");
        }
    }

    public static void main(String[] args) {
        Move move2 = new Move2();
        move2.move();
        move2 = new Move3();
        move2.move();

        class Move4 implements Move {
            @Override
            public void move() {
                System.out.println("我在看电影2");
            }
        }
        Move move4 = new Move4();
        move4.move();

        Move move = new Move() {
            @Override
            public void move() {
                System.out.println("我在看电影3");
            }
        };
        move.move();
        Move move1 = () -> {
            System.out.println("我在看电影3");
        };
        move1.move();
    }
}

interface  Move {
    void move();
}

class Move2 implements Move {
    @Override
    public void move() {
        System.out.println("我在看电影1");
    }
}

4、线程状态

线程五大状态:创建、就绪、阻塞、运行以及死亡

线程状态之间的转换.png

操作线程的一些方法:


image.png

4.1、线程停止

(1)线程的停止建议使用正常停止 --> 利用次数来控制线程停止,但是不建议使用死循环。
(2)建议使用标志位来停止线程   --> 使用flag,来设置一个标志位。
(3)不要使用Stop或者destroy等过时或者JDK不建议使用的方法。

4.2、线程休眠

(1)Thread.sleep(毫秒)指定当前线程阻塞毫秒数;
(2)sleep时间达到后线程会进入就绪状态。

4.3、线程礼让

(1)礼让线程:让当前正在执行的线程暂停,但不阻塞;
(2)礼让不一定能成功;
(3)Thread.yield()。

4.4、Join线程

(1)Join线程,指的是合并线程,需要先执行此线程,将其他线程阻塞,可以想象成插队;

4.5、线程优先级

image.png

4.6、守护线程

现成分为用户线程和守护线程,Java虚拟机一定会确保用户线程执行完毕,但并不会保证守护线程执行完毕,这里面的守护线程有:后台记录操作日志、监控内存、垃圾回收等待等等。

5、线程同步

处理多线程问题时,多个对象访问同一个对象,并且某些线程还想修改这个对象时,就需要线程同步来避免数据紊乱;线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完之后,下一个线程使用。

5.1、线程不同步引起的三大不安全案例

(1)多线程买票案例
public class UnsafeBuyTicket {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();
        new Thread(buyTicket, "用户校长").start();
        new Thread(buyTicket, "黄牛党").start();
        new Thread(buyTicket, "VIP校长").start();
    }
}

class BuyTicket implements Runnable {
    private int ticketNum = 10;
    private boolean flag = true;

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

    public  void buy() throws InterruptedException {
        if (ticketNum <= 0) {
            flag = false;
            return;
        }

        Thread.sleep(1000);
        System.out.println(Thread.currentThread().getName() + "-->购买到了第" + ticketNum-- + "张票");
    }
}
(2)银行取钱问题
package ThreadDemo.syn;

/**
 * @author : HaiLiang Huang
 * @date : 2021年4月19日11:09:50
 */
public class UnsafeBank {
    public static void main(String[] args) {
        Account account =  new Account(100,"结婚基金");
        Drawing you = new Drawing(account,50,"我自己");
        Drawing friend = new Drawing(account,100,"对象");
        you.start();
        friend.start();
    }
}

class Account {
    int money;
    String name;

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

class Drawing extends Thread {

    Account account;
    int drawingMoney;
    int nowMoney;

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

    }

    @Override
    public void run() {

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

        if (account.money - drawingMoney < 0) {
            System.out.println("钱不够");
            return;
        }

        account.money = account.money - drawingMoney;
        nowMoney = nowMoney + drawingMoney;
        System.out.println(account.name+"余额为:"+account.money);
        System.out.println(this.getName()+"手里面有多少钱:"+nowMoney);

    }
}

(3)list不安全问题
public class UnsafeList {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(list.size());
    }
}

5.2、线程同步方法

为了解决上面的方案,引出三种实现线程同步的方法:

5.2.1、同步方法

image.png

只需要在可能会被修改数据的地方加上,比如上面的买票不安全问题,可以直接在买票的动作出直接加上synchronized

public synchronized void buy() throws InterruptedException {
        if (ticketNum <= 0) {
            flag = false;
            return;
        }

        Thread.sleep(1000);
        System.out.println(Thread.currentThread().getName() + "-->购买到了第" + ticketNum-- + "张票");
    }

5.2.2、同步块

image.png

和上面的方法一样,只有在需要修改的地方加上synchronized

public void run() {

        synchronized (account){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if (account.money - drawingMoney < 0) {
                System.out.println("钱不够");
                return;
            }

            account.money = account.money - drawingMoney;
            nowMoney = nowMoney + drawingMoney;
            System.out.println(account.name+"余额为:"+account.money);
            System.out.println(this.getName()+"手里面有多少钱:"+nowMoney);
        }

    }

5.2.3、死锁

多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形,某一个同步块同时拥有"两个以上的锁"时,就可能发生死锁的问题。

1、产生死锁的四个条件:
(1)互斥条件
(2)请求与保持条件
(3)不剥夺条件
(4)循环等待条件

image.png

2、锁的使用方法:


image.png

6、线程协作

6.1、线程通信

线程之间存在相互依赖,互为条件,比如:生产者和消费者
(1)对于生产者,没有生产产品之前,需要通知消费者等待,而生产了产品之后还需要通知消费者启动;
(2)对于消费者,在消费之后,需要通知生产者生产出新的产品消费。
那么以上问题,仅仅使用线程同步方法是不够的,所以就有了线程之间的通信方法:


image.png

6.2、线程通信的解决方案

6.2.1、生产者消费者模式

image.png

在具体的实现中,生产者就只有生产的方法,消费就只有消费的方法,其中生产者与消费者之间的通信关系在缓存池中定义,比如容器满了,通知生产者等待;容器空了,通知消费者等待。

package ThreadDemo;

/**
 * @author : HaiLiang Huang
 * @date :  13:51
 */

// 生产者  消费者,产品 缓存区

public class PCTest {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        Productor productor = new Productor(container);
        Consumer consumer = new Consumer(container);
        productor.start();
        consumer.start();
    }
}
//生产者
class Productor extends Thread{
    SynContainer container;
    public Productor(SynContainer container){
        this.container = container;
    }

    //生产
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            container.push(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 = 1; i <= 100; i++) {
            System.out.println("消费了"+container.pop().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 push(Chicken chicken){
        //如果容器满了,就需要等待消费消费
        while(count==chickens.length){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        chickens[count++] = chicken;
        this.notifyAll();

    }
    //消费者消费产品
    public synchronized Chicken pop(){

        while (count == 0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        Chicken chicken = chickens[--count];
        this.notifyAll();
        return chicken;
    }
}

6.2.2、信号灯模式

信号灯法,就是采用一个Boolean变量开控制生产者等待还是消费者等待:

public class PCTest2 {
    public static void main(String[] args) {
        TV tv = new TV();
        Player player = new Player(tv);
        Watcher watcher = new Watcher(tv);
        player.start();
        watcher.start();
    }
}
// 演员
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("抖音:记录美好生活");
            }
        }
    }
}
// 观众
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();
        }
    }
}
// 产品-->节目
class TV{
    String voice;
    boolean flag = true;

    //如果在演员表演的时候,观众休息 T
    public synchronized void play(String voice){
        if (!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.notifyAll();
        this.voice=voice;
        this.flag = !this.flag;
        System.out.println("演员表演了"+voice);
    }
    //如果在观众观看的时候,演员休息 F
    public synchronized void watch(){
        if (flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.notifyAll();
        System.out.println("观众观看了"+voice);
        this.flag = !this.flag;
    }
}

7、线程池

1、思路:考虑到经常创建和销毁线程会导致资源占用比较大,考虑可以提前创建好几个线程,放入线程池中,使用时直接获取,不适用的时候放回池中。

2、线程池的使用:
image.png

3、测试代码:
public class ThreadPoolsTest implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        executorService.execute(new ThreadPoolsTest());
        executorService.execute(new ThreadPoolsTest());
        executorService.execute(new ThreadPoolsTest());
        executorService.execute(new ThreadPoolsTest());
        executorService.execute(new ThreadPoolsTest());
        executorService.execute(new ThreadPoolsTest());
        executorService.execute(new ThreadPoolsTest());

        executorService.shutdown();
    }
}

参考:

[01] 【狂神说Java】多线程详解_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容