Java多线程复习笔记

Java多线程复习笔记

看完狂神的多线程教程后为了避免遗忘写的复习笔记:
原视频链接

进程(Process)与线程(Thread)

  • 程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念;
  • 进程是执行程序的一次执行过程,是一个动态的概念。是系统资源分配的单位
  • 通常在一个进程中可以包含若干个线程,一个进程中至少有一个线程。线程是CPU调度和执行的单位
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的;
  • 线程会带来额外的开销,如CPU调度时间,并发控制开销;
  • 每个线程在自己的工作内存交互,内存控制不当造成数据不一致。

创建线程的方式

创建线程一共有三种方式,其中继承Thread类和实现Runnable接口更常用一些,推荐使用方式二,这样可以避免了单继承局限的问题。

方式一:继承Thread类

/**
 * 创建线程方式一:继承Thread类,覆写run()方法,调用start()开启线程
 * 线程开启不一定立即执行,由CPU调度执行
 */

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

    public static void main(String[] args) {
        // 创建线程对象
        TestThread1 testThread1 = new TestThread1();
        // 调用start()方法开启线程
        testThread1.start();
        // main线程
        for (int i = 0; i < 100; i++) {
            System.out.println("主线程" + i);
        }
    }
    
}

方式二:实现Runnable接口*

/**
 * 创建线程方式二:实现Runnable接口,重写run方法,执行线程需要丢入runnable接口实现类,调用start()方法
 */
public class TestThread2 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("线程一" + i);
        }
    }

    public static void main(String[] args) {
        // 创建Runnable接口的实现类对象
        TestThread2 testThread2 = new TestThread2();

        // 创建线程对象,通过线程对象来开启我们的线程,代理
        Thread thread = new Thread(testThread2);
        thread.start();

        for (int i = 0; i < 100; i++) {
            System.out.println("线程二" + i);
        }
    }
    
}

方式三:实现Callable

import java.util.concurrent.*;

/**
 * 创建线程方法三:实现Callable接口
 * 1. 可以定义返回值
 * 2. 可以抛出异常
 */
public class TestThread4 implements Callable<Boolean> {
    @Override
    public Boolean call() throws Exception {
        for (int i = 0; i < 50; i++) {
            System.out.println(Thread.currentThread().getName() + i);
        }
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        TestThread4 testThread1 = new TestThread4();
        TestThread4 testThread2 = new TestThread4();
        TestThread4 testThread3 = new TestThread4();

        // 创建执行服务
        ExecutorService ser = Executors.newFixedThreadPool(3);

        // 提交执行
        Future<Boolean> r1 = ser.submit(testThread1);
        Future<Boolean> r2 = ser.submit(testThread2);
        Future<Boolean> r3 = ser.submit(testThread3);

        // 获取结果
        boolean rs1 = r1.get();
        boolean rs2 = r2.get();
        boolean rs3 = r3.get();

        ser.shutdownNow();
    }
}

静态代理模式

我们发现方式一继承的Thread类的内部其实也实现了Runnable接口,这里用到了静态代理模式。

以上面方式二中的代码为例,我们自己写的类TestThread2和Thread类都实现了Runnable接口,并且在Thread类内部对Runnable对象进行了接收

在这里插入图片描述

此时我们在main方法中就可以通过代理对象(Thread)去代替我们的真实对象(TestThread2)去执行,一方面可以在代理类中自定义许多其他特殊的方法,比如Thread类中的start(),让真实对象更专注于自己本身的职责,另一方面也可以避免对真实对象的直接访问,在一定程度上起到了保护作用。

以下也是一个静态代理模式的简易代码实现:

interface Marry {
    void happyMarry();
}

class You implements Marry {
    @Override
    public void happyMarry() {
        System.out.println("Get marry!");
    }
}

class WeddingCompany implements Marry {

    private Marry client;

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

    @Override
    public void happyMarry() {
        before();
        this.client.happyMarry(); // 真实对象
        after();
    }

    private void before() {
        System.out.println("prepare");
    }

    private void after() {
        System.out.println("packaging");
    }

}

public class StaticProxy {
    public static void main(String[] args) {
        You you = new You();
        WeddingCompany weddingCompany = new WeddingCompany(you);
        weddingCompany.happyMarry();
    }
}

线程状态

在这里插入图片描述

在这里插入图片描述

public class TestState {
    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("");
        });

        // 观察状态
        Thread.State state = thread.getState();
        System.out.println(state); // NEW

        // 观察启动后
        thread.start(); // 启动线程
        state = thread.getState();
        System.out.println(state); // RUN

        while (state != Thread.State.TERMINATED) {
            Thread.sleep(100);
            state = thread.getState(); // 更新线程状态
            System.out.println(state);
        }
    }
}

线程优先级

  • Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行;

  • 线程优先级用数字表示,范围1 - 10;

    • Thread.MIN_PRIORITY = 1;
    • Thread.MAX_PRIORITY = 10;
    • Thread.NORM_PRIORITY = 5; // 主线程默认优先级
  • 通过以下方式改变或获取优先级

    • getPriority()
    • setPriority(int n)
  • 优先级的设置应该在start()调度之前。

守护线程

  • 线程分为用户线程和守护线程;
  • JVM必须确保用户线程执行完毕;
  • JVM不用等待守护线程执行完毕;
  • 设置方法:setDaemon(true); // 默认为false
  • 常见的守护线程:内存监控,垃圾回收,ms word中的拼写检查。

常用的Thread类方法

线程休眠 sleep()

public class TestSleep {
    public static void main(String[] args) {
        Date startTime = new Date(System.currentTimeMillis()); // 获取系统当前时间
        while (true) {
            try {
                Thread.sleep(1000);
                System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
                startTime = new Date(System.currentTimeMillis()); // 更新当前时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

线程礼让 yield()

/**
 * 测试礼让进程
 * 礼让不一定成功
 */

class MyYield implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "开始执行");
        Thread.yield(); // 线程礼让
        System.out.println(Thread.currentThread().getName() + "结束执行");
    }

}


public class TestYield {
    public static void main(String[] args) {
        MyYield myYield = new MyYield();
        new Thread(myYield, "A").start();
        new Thread(myYield, "B").start();
    }
}

线程强制执行 join()

/**
 * 测试join
 * 可以理解为插队
 */
public class TestJoin {

    public static void main(String[] args) throws InterruptedException {
        Runnable thread1 = null;
        thread1 = () -> {
            for (int i = 0; i < 1000; i++) {
                System.out.println("vip" + i);
            }
        };
        Thread test = new Thread(thread1);
        test.start();

        // 主线程
        for (int i = 0; i < 500; i++) {
            if (i == 200) {
                test.join();
            }
            System.out.println("main" + i);
        }
    }
}

线程同步

Java提供了synchronized关键字(本质上是悲观锁)来解决基础的并发问题,对应有同步代码块同步方法两种方式。

每个对象都对应一把锁,synchronized只有获得一个对象的锁才能去执行操作该对象的方法,一个锁被获得后,其他的线程需要等待,直到一个线程释放这个锁后才能去获得。下面是两种方法的一些实际运用:

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;
    }

    /**
     * synchronized默认锁的是this
     */
    @Override
    public void run() {

        // 锁的对象就是变化的量,需要增删改的对象
        synchronized (account) {
            if (account.money - drawingMoney < 0) {
                System.out.println(Thread.currentThread().getName() + "钱不够,取不了");
                return;
            }

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

            // 卡内余额
            account.money = account.money - drawingMoney;

            // 手里的钱
            nowMoney += drawingMoney;

            System.out.println(account.name + "余额为:" + account.money);
            System.out.println(this.getName() + "手里的钱" + nowMoney);
        }

    }
}



public class SafeBank {
    public static void main(String[] args) {
        Account account = new Account(100, "结婚基金");
        Drawing you = new Drawing(account, 50, "你");
        Drawing npy = new Drawing(account, 100, "NPY");

        you.start();
        npy.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();
            }
        }
    }

    // synchronized 同步方法,锁得是this : BuyTicket这个类本身 类是对象的模板,对象是类的实例
    private synchronized void buy() throws InterruptedException {
        if (ticketNum <= 0) {
            flag = false;
            return;
        }
        Thread.sleep(500);
        System.out.println(Thread.currentThread().getName() + "买到" + ticketNum--);
    }
}

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

        Thread A = new Thread(buyTicket, "A");
        A.setPriority(Thread.MIN_PRIORITY);
        A.start();
        Thread B = new Thread(buyTicket, "B");
        B.setPriority(Thread.MAX_PRIORITY);
        B.start();
        Thread C = new Thread(buyTicket, "C");
        C.setPriority(Thread.MAX_PRIORITY);
        C.start();
    }
}
public class SafeList {
    public static void main(String[] args) throws InterruptedException {
        List<String> arrayList = new ArrayList<>();

        for (int i = 0; i < 10000; i++) {
            new Thread(() -> {
                synchronized (arrayList) {
                    arrayList.add(Thread.currentThread().getName());
                }
            }).start();
        }

        Thread.sleep(3000);

        System.out.println(arrayList.size());
    }
}

  • 死锁

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

  • 通过JUC的Lock来实现线程同步

    import java.util.concurrent.locks.ReentrantLock;
    
    class Station implements Runnable {
    
        private int ticketNum = 10;
    
        private final ReentrantLock reentrantLock = new ReentrantLock(); // 本质也是悲观锁
    
        @Override
        public void run() {
            while (true) {
                try {
                    reentrantLock.lock(); // 锁的是当前这个类
                    if (ticketNum > 0) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(ticketNum--);
                    } else {
                        break;
                    }
                } finally {
                    reentrantLock.unlock(); // 释放锁
                }
            }
        }
    }
    
    public class TestLock {
        public static void main(String[] args) {
            Station station = new Station();
            new Thread(station, "A").start();
            new Thread(station, "B").start();
            new Thread(station, "C").start();
        }
    }
    
  • synchronized和Lock的对比

    • Lock是显式锁需要手动释放,synchronized是隐式锁,出了作用域自动释放;
    • Lock只有代码块锁;
    • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类);
    • 优先使用顺序
      • Lock > 同步代码块 > 同步方法

生产者消费者问题(有限缓冲问题 bounded-buffer problem)

存在一个公共且有限的缓冲区,使用这些的进程被分为生产者和消费者,生产者只有在缓冲区未满时生产,消费者只有在缓冲区非空时消费。

管程法

/**
 * 生产者消费者模型,利用缓冲区解决:管程法
 */

// 生产者
class Producer extends Thread {
    SynContainer container;

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

    // 生产
    @Override
    public void run() {
        for (int i = 0; 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 = 0; 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;

    // 生产者放入产品
    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;
    }

}

public class TestPC {
    public static void main(String[] args) {
        SynContainer synContainer = new SynContainer();

        new Producer(synContainer).start();
        new Consumer(synContainer).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("bilibili");
            }
        }
    }
}

// 消费者
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("观看了:" + this.voice);
        // 通知演员表演
        this.notifyAll();
        this.flag = !this.flag;
    }

}

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

临界区问题(Critical Section Problem)

  • 临界区即是会访问共享资源的代码区(改变共同变量,读写文件等),我们需要控制程序进入这段代码的时机。

    • 进入区:控制进入临界区

    • 临界区:这之内的代码会访问共享资源

    • 退出区:告诉其他进程该进程退出了邻接区

在这里插入图片描述

  • 临界区三个原则

    • 互斥(Mutual Exclusion):当一个进程/线程在其临界区中执行时,其他进程/线程不能在其临界区中执行;
    • 前进(Progress):如果没有进程/线程在它的临界区执行,如果有一些进程/线程希望进入它们的临界区,那么这些进程/线程中的一个将进入临界区。必须有可能协商下一个进入CS的人是谁;
    • 有界等待(Bounded Waiting):没有进程/线程应该永远等待进入临界区。临界区外的进程/线程的等待时间应该是有限的(否则进程/线程可能会遭受饥饿。
  • 解决办法

    • Peterson’s solution

      进程可以共享一些共同的变量来同步它们的行动

在这里插入图片描述

​ 需要turn和flag[2]来保证互斥、有界等待,以及前进。

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值