欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java中的多线程

程序员文章站 2022-05-05 09:59:07
...

多线程

线程,进程,多线程

Process(进程)与 Thread(线程)

普通方法调用,只有主线程一条执行路径

多线程则是多条执行路径,主线程和子线程并行交替执行

一个进程可以有多个线程。

  • 程序:程序是指令和数据的集合,其本身没有任何运行的含义,是一个静态的概念
  • 进程:是执行程序的一次执行过程,它是一个动态的概念,是系统资源分配的单位
  • 线程:线程是CPU调度和执行的单位,一个进程中至少有一个线程,可以有若干个线程

多线程:***真正的多线程是指有多个CPU,即多核,如服务器。***如果是模拟出来的多线程,即在一个CPU的情况下,在同一时间点,CPU只能执行一个代码,因为切换的很快,所以就有同时执行的错觉

注意点:

  • 线程就是独立的执行路径
  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,比如main线程,gc线程
  • main() 称之为主线程,是系统的入口,用于执行整个程序
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
  • 线程会带来额外开销,如CPU调度时间,并发控制开销
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

线程创建

三种创建方式:

  • Thread class:继承 Thread 类(重点)
  • Runnable接口:实现 Runnable 接口(重点)
  • Callable接口:实现 Callable 接口(仅作了解)

Thread

继承Thread类创建线程

使用 Thread 类创建线程的方法:

  • 自定义线程类继承Thread类
  • 重写 run() 方法,编写线程执行体
  • 创建线程对象,调用 start() 方法启动线程

调用了start()方法不一定立即执行,因为线程需要CPU安排调度

public static void main(String[] args){
        Demo thread = new Demo();
        // 启动线程
        thread.start();
        for (int i = 0; i < 200; i++) {
            System.out.println("这是main线程"+i);
        }

    }
    @Override
    public void run() {
        // run方法线程体
        for (int i = 0; i < 20; i++) {
            System.out.println("这是run线程"+i);
        }
    }
// run方法中的输出语句,可能在main方法中的输出语句之后,也可能夹杂在其中,是因为CPU需要安排调度

main方法与run方法是交替执行的

例题:多线程下载网络图片

package test;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;

public class TestThread extends Thread {
    private String url;
    private String name;

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

    public void run() {
        WebDownloadr webDownloadr = new WebDownloadr();
        webDownloadr.downloader(url, name);
        System.out.println("下载了文件,名为" + name);
    }

    public static void main(String[] args) {
        TestThread t1 = new TestThread("https://i0.hdslb.com/bfs/article/aaa@qq.com_690h.webp", "1.jpg");
        TestThread t2 = new TestThread("https://i0.hdslb.com/bfs/article/aaa@qq.com_690h.webp", "2.jpg");
        TestThread t3 = new TestThread("https://i0.hdslb.com/bfs/article/aaa@qq.com_690h.webp", "3.jpg");
        t1.start();
        t2.start();
        t3.start();
        // 输出不一定为 1,2,3 。先下载完的会先显示
    }
}

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

在 WebDownload 方法中使用的 FileUtils.copyURLToFile 方法,为封装好的工具包,

commons-io-2.8.0下载地址

实现Runnable接口创建线程

  • 定义 MyRunnable 类实现 Runnable 接口
  • 实现 run() 方法,编写线程执行体
  • 创建线程对线,调用 start() 方法启动线程

推荐使用 Runnable 接口来创建线程,这样可以避免程序被单继承限制

public class TestThread implements Runnable {
    public static void main(String[] args){
        // 创建实现了Runnable接口的对象
        TestThread thread = new TestThread();
        // 创建线程对象,通过线程对象来启动线程
        new Thread(thread).start();
        for (int i = 0; i < 200; i++) {
            System.out.println("这是main线程"+i);
        }

    }
    @Override
    public void run() {
        // run方法线程体
        for (int i = 0; i < 20; i++) {
            System.out.println("这是run线程"+i);
        }
    }
}

多个线程操作同一个对象

获得当前线程名字的方法

  • Thread.currentThread().getName()

多个线程操作同一个资源数据时,数据可能出错,

// 代码中存在数据错误问题,在并发执行笔记中会进行解决
public class TestThread implements Runnable {
    private int ticketNums = 10;

    public static void main(String[] args) {
        TestThread ticket = new TestThread();
        new Thread(ticket, "小王").start();
        new Thread(ticket, "小李").start();
        new Thread(ticket, "小张").start();

    }

    @Override
    public void run() {
        while (true) {
            if (ticketNums <= 0) {
                break;
            }
            try {
                // 设置延时
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "买到了" + ticketNums-- + "号票");
        }
    }
}

实现Callable接口创建线程

  • 实现Callable接口,需要返回值类型
  • 重写call方法,需要抛出异常
  • 创建目标对象
  • 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool( 1 );
  • 提交执行:Future< Boolean > result1 = ser.submit( t1 );
  • 获取结果:boolean r1 = result1.get()
  • 关闭服务:ser.shutdownNow();

Callable的好处:

  • 可以设置返回值
  • 可以抛出异常
package test;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;

public class TestCallable implements Callable<Boolean> {
    private String url;
    private String name;

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

    public Boolean call() {
        WebDownloadr webDownloadr = new WebDownloadr();
        webDownloadr.downloader(url, name);
        System.out.println("下载了文件,名为" + name);
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        TestCallable t1 = new TestCallable("https://i0.hdslb.com/bfs/article/aaa@qq.com_690h.webp", "1.jpg");
        TestCallable t2 = new TestCallable("https://i0.hdslb.com/bfs/article/aaa@qq.com_690h.webp", "2.jpg");
        TestCallable t3 = new TestCallable("https://i0.hdslb.com/bfs/article/aaa@qq.com_690h.webp", "3.jpg");

        //创建执行服务,池子中有三个线程:
        ExecutorService ser = Executors.newFixedThreadPool( 3 );
        //提交执行:
        Future< Boolean > r1 = ser.submit( t1 );
        Future< Boolean > r2 = ser.submit( t2 );
        Future< Boolean > r3 = ser.submit( t3 );
        //获取call()方法返回的结果:
        boolean rs1 = r1.get();
        boolean rs2 = r2.get();
        boolean rs3 = r3.get();
        //关闭服务:
        ser.shutdownNow();
    }
}

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

静态代理

静态代理模式的注意事项

  • 真实对象与代理对象都要实现同一个接口
  • 代理对象要代理真实角色

静态代理模式的好处

  • 代理对象可以做许多真实对象所不能做的事
  • 真实对象只需要做自己的事即可
package test;

public class StaticProxy {
    public static void main(String[] args) {

        // 代理角色接收真实角色
        Chauffeur chauffeur = new Chauffeur(new Boss());
        //new Thread(()-> System.out.println("A")).start();
        //new Chauffeur(new Boss()).drive();   实际上,Thread也是代理
        chauffeur.drive();
    }
}
// 定义接口
interface Drive{
    void drive();
}

// 真实角色:Boss,只负责坐车,自己不开车
class Boss implements Drive{
    @Override
    public void drive() {
        System.out.println("我是老板,我不开车");
    }
}
// 代理角色:司机,帮你开车
class Chauffeur implements Drive{
    private Drive target;

    // 定义有参构造,接收 真实对象
    public Chauffeur(Drive target) {
        this.target = target;
    }

    @Override
    public void drive() {
        System.out.println("我是司机,我来开车");
        talk();
        // 真实对象实现的接口方法
        this.target.drive();
        arrive();
    }

    private void arrive() {
        System.out.println("老板,已经到了目的地");
    }

    private void talk() {
        System.out.println("老板,您想去哪里");
    }
}

线程的状态与方法

Java中的多线程
Java中的多线程

线程中的方法

  • setPriority( int newPriority ):更改线程的优先级
  • static void sleep( long millis ) :在指定的毫秒数内让线程休眠
  • void join():等待线程终止
  • static void yield():暂停当前正在执行的线程,并执行其他线程
  • void interrupt():中断线程,一般不会使用这个方法
  • boolean isAlive():判断线程是否处于活动状态

线程停止

终止线程,建议使用标志位方式。stop()和destory()方法已经是过时方法

package test;

public class TestThread implements Runnable {
    // 设置标志位
    private boolean flag = true;

    // 自行设置停止方法
    public void stop() {
        this.flag = false;
    }

    @Override
    public void run() {
        int i = 0;
        while (flag) {
            System.out.println("线程运行" + i++);
        }
    }

    public static void main(String[] args) {
        TestThread thread = new TestThread();
        new Thread(thread).start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("main运行" + i);
            if (i == 800) {
                // 调用stop方法切换标志位,让线程停止
                thread.stop();
                System.out.println("线程停止了");
            }
        }
    }
}

线程休眠

  • sleep方法中的参数表示指定当前线程阻塞的毫秒数
  • sleep存在 InterruptedException 异常
  • sleep的休眠时间达到后,线程会进入就绪状态
  • sleep可以用来模拟网络延时,倒计时等操作
  • 每一个对象都有一个锁,sleep并不会释放锁

模拟网络延时的作用:放大问题的发生性

public class TestSleep {
    public static void main(String[] args) throws InterruptedException {
        // 获取当前时间
        Date date = new Date(System.currentTimeMillis());
        // 每隔一秒输出当前时间方法
        while (true) {
            // 休眠一秒
            Thread.sleep(1000);
            // 设置时间格式,并且输出时间
            System.out.println(new SimpleDateFormat("HH:mm:ss").format(date));
            // 更新时间
            date = new Date(System.currentTimeMillis());
        }
//        countTime();

    }

    // 倒计时方法
    public static void countTime() throws InterruptedException {
        int num = 10;
        while (true) {
            Thread.sleep(1000);
            System.out.println(num);
            num--;
            if (num <= 0) {
                break;
            }

        }
    }
}

线程礼让

  • 礼让线程:让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转换为就绪状态
  • 让CPU重新调度,礼让不一定成功,这取决于CPU的调度顺序
public class TestYield {
    public static void main(String[] args){
        MyYield myYield = new MyYield();
        new Thread(myYield,"ThreadOne").start();
        new Thread(myYield,"ThreadTwo").start();

    }
}
class MyYield implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"线程开始执行");
        // 进行礼让
        // 正常情况下为 ThreadOne开始执行,ThreadOne停止执行,然后是Two
        // 可能礼让成功,也可能礼让不成功
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+"线程停止执行");
    }
}

join

  • join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
  • 相当于插队,尽量少使用,避免造成线程阻塞
public class TestJoin implements Runnable {
    public static void main(String[] args) throws InterruptedException {
        TestJoin testJoin = new TestJoin();
        Thread thread = new Thread(testJoin);
        thread.start();
        for (int i = 0; i < 100; i++) {
            if (i == 70) {
                thread.join();
            }
            System.out.println("main" + i);
        }
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println("VIP" + i);
        }
    }
}

观测线程状态

Thread.getState():获取指定线程当前状态

Thread.State state = thread.getState();
  • NEW:尚未启动状态
  • RUNNABLE:执行状态
  • BLOCKED:阻塞状态
  • WAITING:等待状态,正在等待另一个线程执行特定动作
  • TIMED_WAITING:等待状态,正在等待另一个线程执行动作达到指定等待时间
  • TERMINATED:已经退出

一个线程可以在给定时间点处于一个状态,这些状态是不反映任何操作系统线程状态的虚拟机状态

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);
        thread.start();
        // 启动后状态
        // 每次输出前,都要更新一次state
        state = thread.getState();
        System.out.println(state);
        // 如果线程不是终止状态
        while (state != Thread.State.TERMINATED) {
            // 每200毫秒输出一次线程状态
            Thread.sleep(200);
            // 更新状态,先更新,再输出
            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 xxx )

优先级越高,也不一定先执行,主要看CPU的调度,只不过优先级高了,先执行的几率更大

public class TestPriority {
    public static void main(String[] args){
        MyPriority myPriority = new MyPriority();
        Thread t1 = new Thread(myPriority, "t1");
        Thread t2 = new Thread(myPriority, "t2");
        Thread t3 = new Thread(myPriority, "t3");
        Thread t4 = new Thread(myPriority, "t4");
        Thread t5 = new Thread(myPriority, "t5");
        // 输出main方法的优先级,他只有默认优先级,默认为5
        System.out.println("我是:" + Thread.currentThread().getName() + "\t我的优先级为:" + Thread.currentThread().getPriority());

        // 设置优先级
        t1.setPriority(Thread.MIN_PRIORITY);
        t2.setPriority(7);
        t3.setPriority(2);
        t4.setPriority(Thread.MAX_PRIORITY);

        // 测试错误的优先级,小于0或者大于10,都是错误的
//        t5.setPriority(-2);

        // 启动线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
//        t5.start();

        /*我是:main	我的优先级为:5
        我是:t4	我的优先级为:10
        我是:t2	我的优先级为:7
        我是:t3	我的优先级为:2
        我是:t1	我的优先级为:1*/
    }
}

class MyPriority implements Runnable {
    @Override
    public void run() {
        System.out.println("我是:" + Thread.currentThread().getName() + "\t我的优先级为:" + Thread.currentThread().getPriority());
    }
}

守护线程

  • 线程分为用户线程和守护线程
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕
  • 例如监控内存,垃圾回收等,就是守护线程

设置守护线程的方法

  • Thread.setDaemon( boolean on ):设置为true则为守护线程,默认为false
public class TestDaemon {
    public static void main(String[] args){
        Thread thread = new Thread(new God());
        // 设置为God为守护线程
        thread.setDaemon(true);
        thread.start();

        // 启动Person线程
        new Thread(new Person()).start();
    }
}

class God implements Runnable {
    @Override
    public void run() {
        // 虽然条件一直为真,但是守护线程并不会一直执行,用户线程结束之后,守护线程也会随之结束
        while(true){
            System.out.println("God always be with you");
        }
    }
}

class Person implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("I am " + i + "years old");
        }
        System.out.println("Now i'm dead");
    }
}

Lamda表达式

lamda 表达式是JDK8的新特性。为什么要使用 lamda 表达式

  • 避免匿名内部类定义过多
  • 让代码看起来更简洁
  • 去除一部分无意义代码

Functional Interface ( 函数式接口 )是学习 lamda 表达式的关键所在

函数式接口的定义:

任何***只包含唯一 一个抽象方法***的接口,如下

public interface Runnable{
    public abstract void run();
}

就是函数式接口对于函数式接口,我们可以通过 lamda 表达式来创建该接口的对象

package test;

// Lamda表达式的推导
public class TestLamda {
    // 3.定义静态内部类
    static class Study2 implements Lamda {
        @Override
        public void lamda() {
            System.out.println("Lamda lesson 2");
        }
    }

    public static void main(String[] args) {

        new Study().lamda();

        new Study2().lamda();

        // 4.定义局部内部类
        class Study3 implements Lamda {
            @Override
            public void lamda() {
                System.out.println("Lamda lesson 3");
            }
        }

        new Study3().lamda();

        // 5.定义匿名内部类
        Lamda study = new Lamda() {
            @Override
            public void lamda() {
                System.out.println("Lamda lesson 4");
            }
        };

        study.lamda();


        // 6.用Lamda简化
        study = () -> {
            System.out.println("Lamda lesson 5");
        };
        study.lamda();
    }
}

// 1.定义 函数式接口
interface Lamda {
    void lamda();
}

// 2.定义外部类
class Study implements Lamda {
    @Override
    public void lamda() {
        System.out.println("Lamda lesson 1");
    }
}

Lamda表达式的直观使用

package test;

public class TestLamda {

    public static void main(String[] args) {
        Lamda person = (name) -> {
            System.out.println(name + "喜欢lamda表达式");
        };
        person.lamda("张三");
        // 张三喜欢lamda表达式
    }
}

interface Lamda {
    void lamda(String name);
}

lamda表达式的简化

  • lamda表达式可以将参数类型去除

  • 当函数式接口中的方法只有一个参数时,lamda表达式可以将小括号去除,当方法中只有一句代码时,可以将花括号去除,如下:

    public class TestLamda {
    
        public static void main(String[] args) {
            Lamda person = name -> System.out.println(name + "喜欢lamda表达式");
            person.lamda("张三");
        }
    }
    
    interface Lamda {
        void lamda(String name);
    }
    
  • 当方法中有两个参数时,可以去除参数类型,但是不能去除小括号,如果方法中的代码语句不止一条,那么需要加上花括号,如下:

    public class TestLamda {
    
        public static void main(String[] args) {
            Lamda person = (name, age) -> {
                System.out.println("我今年" + age + "岁");
                System.out.println(name + "喜欢lamda表达式");
            };
            person.lamda("张三", 20);
        }
    }
    
    interface Lamda {
        void lamda(String name, int age);
    }
    

因为Runnaable是一个函数式接口,只有一个run方法,所以可以用lamda表达式来简化他的实现类。

线程同步

并发:同一个对象被多个线程同时操作,例如抢票

处理多线程问题时,如果有多个进程访问同一个对象,并且还存在修改对象的行为,那么就需要线程同步。

线程同步实际上是一种排队机制,让需要访问同一个对象的线程进入对象的等待池,形成队列,排队访问资源

队列和锁

线程同步需要队列加锁来实现

为了保证数据的安全性,多线程访问数据需要加入锁机制,synchronized,当一个线程获得对象的排他锁时,它可以独享对象,其他线程必须等待,在使用完资源之后 ,需要释放锁。

锁机制带来的问题:

  • 一个线程持有锁会导致其他所有访问该资源的线程挂起
  • 较多线程访问统一资源时,频繁的加锁,释放锁会导致较多的上下文切换,调度延时,引起性能问题
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题

例题:银行中不安全的取钱问题

package test;

public class TestSyn {
    public static void main(String[] args) {
        Account account = new Account(100, "存款");
        DrawMoney boy = new DrawMoney(account, 50, 0, "男孩");
        DrawMoney girl = new DrawMoney(account, 100, 0, "女孩");
        new Thread(boy).start();
        new Thread(girl).start();

    }
}

class Account {
    // 卡里的钱
    int money;
    // 卡号
    String name;

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

class DrawMoney implements Runnable {
    // 实例化一个账号对象
    Account account;
    // 取走的钱
    private int drawMoney;
    // 手里的钱
    private int nowMoney;
    String name;

    public DrawMoney(Account account, int drawMoney, int nowMoney, String name) {
        this.name = name;
        this.account = account;
        this.drawMoney = drawMoney;
        this.nowMoney = nowMoney;
    }

    @Override
    public void run() {
        // 让线程休眠一秒,增大问题发生性

        // 如果取的钱大于余额,则跳出方法
        if ((account.money - drawMoney) < 0) {
            System.out.println(this.name + "无法取钱,余额不足");
            return;
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        account.money = account.money - drawMoney;
        nowMoney = nowMoney + drawMoney;
        System.out.println("余额为" + account.money + this.name + "手上有" + nowMoney);

    }
}

同步

synchronized关键字,它有两种用法,synchronized方法和synchronized块

同步方法

  • 同步方法:public synchronized void method( int args ){ }

synchronized方法控制对对象的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。

缺点:如果将一个大的方法申明为synchronized ,会影响他的效率

一般只在方法中需要修改的资源加锁,锁的内容太多,会浪费资源

package test;

public class TestThread implements Runnable {
    private int ticketNums = 10;
    boolean flag = true;

    public static void main(String[] args) {
        TestThread ticket = new TestThread();
        new Thread(ticket, "小王").start();
        new Thread(ticket, "小李").start();
        new Thread(ticket, "小张").start();

    }

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

    }
    // 增加 sychronized 修饰符,设置同步方法
    public synchronized void buy(){
            if (ticketNums <= 0) {
                flag =false;
                return;
            }
            try {
                // 设置延时
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "买到了" + ticketNums-- + "号票");
    }
}

synchronized 锁的是this对象(this所在的当前类)

同步静态方法的锁对象是 类名.class

同步块

synchronized ( Obj ) { }

Obj称为同步监视器

  • Obj 可以是任何对象,但是推荐使用共享资源作为同步监视器
  • 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class

一般挑选的加锁对象,为数据发生变化的对象(进行了增删改)

package test;

public class TestSyn {
    public static void main(String[] args) {
        Account account = new Account(100, "存款");
        DrawMoney boy = new DrawMoney(account, 50, 0, "男孩");
        DrawMoney girl = new DrawMoney(account, 100, 0, "女孩");
        new Thread(boy).start();
        new Thread(girl).start();

    }
}

class Account {
    // 卡里的钱
    int money;
    // 卡号
    String name;

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

class DrawMoney implements Runnable {
    // 实例化一个账号对象
    Account account;
    // 取走的钱
    private int drawMoney;
    // 手里的钱
    private int nowMoney;
    String name;

    public DrawMoney(Account account, int drawMoney, int nowMoney, String name) {
        this.name = name;
        this.account = account;
        this.drawMoney = drawMoney;
        this.nowMoney = nowMoney;
    }

    @Override
    public void run() {
        // 同步代码块,对象为 account
        synchronized (account){
            // 如果取的钱大于余额,则跳出方法
            if ((account.money - drawMoney) < 0) {
                System.out.println(this.name + "无法取钱,余额不足");
                return;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            account.money = account.money - drawMoney;
            nowMoney = nowMoney + drawMoney;
            System.out.println("余额为" + account.money + this.name + "手上有" + nowMoney);
        }
    }
}

如果一段代码分成两段,两次锁的对象不同,则无法保证安全性。

同步监视器的执行过程

  • 第一个线程访问,锁定同步监视器,执行其中代码
  • 第二个线程访问,发现同步监视器被锁定,无法访问
  • 第一个线程访问完毕,解锁同步监视器
  • 第二个线程访问,发现同步监视器没上锁,锁定同步监视器并且访问

死锁

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

简单来说,就是 多个线程互相拥有者对方所需要的资源,形成僵持状态

package test;

public class TestLock {
    public static void main(String[] args) {
        Eat lbxx = new Eat(0, "蜡笔小新");
        Eat fjc = new Eat(1, "风间彻");
        new Thread(lbxx).start();
        new Thread(fjc).start();
    }
}

class Eat implements Runnable {
    int choice;
    String name;
    static Dish dish = new Dish();
    static Chopsticks chopsticks = new Chopsticks();

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

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

    public void eat() throws InterruptedException {
        if (choice == 0) {
            // 获得dish锁
            synchronized (dish) {
                System.out.println(this.name + "获得了盘子");
                // 设置延时,放大问题发生性
                Thread.sleep(1000);
                // 想要获得chopsticks锁
                synchronized (chopsticks) {
                    System.out.println(this.name + "想获得筷子");
                }
            }
        } else {
            synchronized (chopsticks) {
                System.out.println(this.name + "获得了筷子");
                Thread.sleep(2000);
                synchronized (dish) {
                    System.out.println(this.name + "想获得盘子");
                }
            }
        }
    }
}

// 模拟两个人,一个人拿到了盘子,一个人拿到了筷子,都想要吃饭

// 盘子
class Dish {

}

// 筷子
class Chopsticks {

}

最终,会产生死锁,因为各自拥有对方想要的资源,互相僵持

产生死锁的四个必要条件

  • 互斥条件:一个资源每次只能被一个进程使用
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

只要其中任意一个条件或多个条件能被打破,那么死锁就不会产生

Lock

Lock接口是控制多个线程对共享资源进行访问的工具,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前先获得Lock对象

ReentrantLock ( 可重入锁 ) 类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁

public class TestLock {
    public static void main(String[] args) {
        sellTickets sellTickets = new sellTickets();
        new Thread(sellTickets).start();
        new Thread(sellTickets).start();
        new Thread(sellTickets).start();
    }
}

class sellTickets implements Runnable {
    int tickNums = 10;
    // 定义 可重入 锁,使用 private final 修饰,保证安全性
    private final ReentrantLock lock = new ReentrantLock();

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

一般解锁都写在finally里面

synchronized与Lock的对比

  • Lock是显式锁(手动开启和关闭锁),synchronized是隐式锁,出了作用域则自动释放
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且有更好的扩展性(子类更多)

锁的优先使用顺序:

Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)

线程协作

生产者消费者问题

生产者消费者问题场景:

假设一个仓库中只能存放一件商品,生产者将生产出的产品放入仓库,消费者从仓库中取走产品。如果仓库中没产品,则由生产者生产产品放入仓库,消费者等待;如果仓库中有产品,则消费者取走产品,生产者停止生产并且等待,直到产品被取走,才继续生产。消费者等到仓库中有产品,才能再次取走,如此循环。

实际上,这是一个线程同步的问题,生产者和消费者共享同一个资源,并且生产者和消费者之间互相依赖,互为条件

在生产者消费者问题中,仅有synchronized是不够的,

  • synchronized 可阻止并发更新同一个共享资源,实现了同步
  • synchronized 不能用来实现不同线程之间的消息传递(通信)

线程通信

线程通信之间的常用方法

  • wait():表示线程一直等待,直到其他线程通知,与sleep不同的是,它会释放锁
  • wait( long timeout ):指定等待的毫秒数
  • notify():唤醒一个处于等待状态的线程
  • notifyAll():唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度

注意:以上都是Object 类的方法,只能在同步方法或者同步代码块中使用,否则会抛出 IIIegalMonitorStateException 异常


解决生产者消费者问题


管程法

利用缓冲区解决问题

package test;

// 管程法解决生产者消费者问题
public class BoxTest {
    public static void main(String[] args) {
        Box box = new Box();
        Producer producer = new Producer(box);
        Customer customer = new Customer(box);
        Customer customer1 = new Customer(box);
        Customer customer2 = new Customer(box);
        new Thread(producer).start();
        new Thread(customer,"蜡笔小新").start();
        new Thread(customer1,"樱桃小丸子").start();
        new Thread(customer2,"犬夜叉").start();
    }
}

// 牛奶,是共享资源
class Milk {
    // 牛奶编号
    int id;

    Milk(int id) {
        this.id = id;
    }
}

// 奶箱,是缓冲区
class Box {
    // 设置缓冲区大小为10,意味着同一时间内,缓冲区中最多有10个产品,
    // 因为Milk[9]表示长度为9,但是在存入的时候,是从milk[0]开始存入,所以设置 Milk[9]来让缓冲区为10
    Milk[] milks = new Milk[9];
    // 容器计数器
    int count = 0;

    // 生产者生产产品
    public synchronized void push(Milk milk,int i) {
        // 如果容器满了,则需要等待消费者进行消费
        while (count == milks.length) {
            // 通知消费者消费,生产者等待
            try {
                // 让生产者进行等待
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 如果没有满,则生产产品,从0开始放入
        milks[count] = milk;
        // 生产一个产品,计数器增加一次
        count++;
        // 此时输出语句,则不会在控制台出现抢先消费的问题
        System.out.println("生产了第" + (i) + "瓶牛奶");
        // 已经生产了,有产品,就可以通知消费者进行消费
        // 唤醒因为产品为空而进入等待的消费者
        this.notifyAll();
    }

    // 消费者消费产品,返回一个产品对象
    public synchronized Milk pop() {
        // 判断是否有产品
        while (count == 0) {
            // 没有产品,则等待生产者进行生产,消费者等待
            try {
                // 让消费者进行等待
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 如果有数据,则进行消费
        count--;
        // 取出产品
        Milk milk = milks[count];
        // 消费完,通知生产者生产
        // 唤醒因为产品满了而进入等待的生产者
        this.notifyAll();
        return milk;
    }
}

// 生产者
class Producer implements Runnable {
    // 获取奶箱对象
    Box b;

    Producer(Box b) {
        this.b = b;
    }

    // 生产牛奶方法
    @Override
    public void run() {
        // 生产100瓶牛奶,奶箱只有10的大小,所以到了10,应该通知消费者进行消费
        for (int i = 0; i < 100; i++) {
            // 把i作为牛奶的ID传入,生产牛奶
            // 传入一个(i+1)作为输出参数,由于下标从0开始,但是输出不能从0开始,所以i需要加1
            b.push(new Milk(i),(i+1));
        }
    }
}

// 消费者
class Customer implements Runnable {
    // 获取奶箱对象
    Box b;

    Customer(Box b) {
        this.b = b;
    }

    // 消费牛奶方法
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            // 获得消费方法中返回的产品对象的id
            // 由于在生产者方法中,生产时存入的下标为从0开始,所以输出要加1
            int id = b.pop().id;
            System.out.println(Thread.currentThread().getName()+"消费了第" + (id + 1) + "瓶牛奶");
        }
    }
}

在最开始的情况下,push()方法中的 System.out.println(“生产了第” + (i) + “瓶牛奶”); 是写在生产者的run方法中的,原来的方法如下:

public void run() {
        // 生产100瓶牛奶,奶箱只有10的大小,所以到了10,应该通知消费者进行消费
        for (int i = 0; i < 100; i++) {
            // 把i作为牛奶的ID传入,生产牛奶
            // 传入一个(i+1)作为输出参数,由于下标从0开始,但是输出不能从0开始,所以i需要加1
            b.push(new Milk(i));
            System.out.println("生产了第" + (i+1) + "瓶牛奶");
        }
    }

在这种情况下,控制台的输出可能会出现超前消费的现象(消费未生产的产品)

原因如下:

由于线程通过start()启动,生产者先调用push()方法进行生产产品,然而同时,消费者也调用了pop()方法,但是他发现没有产品,只能进行等待,此时生产者在push()方法中,创建了一个新对象,由于push()方法和pop()方法都存在于共享缓冲区里面,而各自的输出语句都在各自的对象run()方法中,此时我们可以认为,线程都在同一个方法中,消费者在等待,此时由于先执行了count++,消费者立马认为缓冲区中是存在产品的,迅速进行了消费,先行输出了消费语句,生产语句才随之输出。

所以将输出语句放入到push方法中,即可避免出现超前消费的现象。

同理,消费者的run方法中也存在着输出语句,所以会出现超前生产的现象,原理同上,由于pop方法中不方便传入id参数,(会导致连续调用两次pop方法)所以此处并没有对这个问题进行解决


信号灯法

通过设置标志位来通知线程何时等待,何时唤醒

package test;

import java.nio.file.Path;

// 管程法解决生产者消费者问题
public class BoxTest {
    public static void main(String[] args) {
        DriveCar driveCar = new DriveCar();
        Teacher teacher = new Teacher(driveCar);
        Student student = new Student(driveCar);
        new Thread(teacher).start();
        new Thread(student).start();
    }
}

// 驾校教练
class Teacher implements Runnable {
    DriveCar driveCar;

    Teacher(DriveCar driveCar) {
        this.driveCar = driveCar;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {
                this.driveCar.TeacherDrive("AE86");
            } else {
                this.driveCar.TeacherDrive("五菱宏光");
            }
        }
    }
}

// 学员
class Student implements Runnable {
    DriveCar driveCar;

    Student(DriveCar driveCar) {
        this.driveCar = driveCar;
    }

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

// 车
class DriveCar {
    // 教练开车,学员等待;学员开车,教练等待
    // 开的车
    String car;
    // 标志位
    boolean flag = true;

    // 教练开车
    public synchronized void TeacherDrive(String car) {
        // 如果标志位为假,说明学员在开车
        if (!flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("学员没在开" + car);
        System.out.println("教练开了" + car);
        // 教练开完之后,让学员开
        this.notifyAll();
        this.car = car;
        this.flag = !this.flag;
    }

    // 学员开车
    public synchronized void StudentDrive() {
        // 如果标志位为真,说明教练在开车
        if (flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("教练没在开" + car);
        System.out.println("学员开了" + car);
        // 让教练来开车
        this.notifyAll();
        this.flag = !this.flag;
    }

}

线程池

由于多线程中经常创建和销毁、使用量特别大的资源,如并发情况下的线程,对性能的影响很大,所以需要使用线程池来提升性能。

线程池的思路

提前创建好多个线程,放入线程池中,需要使用时直接获取,使用完放回线程池,这样可以避免频繁的创建和销毁,实现反复利用

线程池的好处

  • 提高相应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中的线程,不需要每次都重新创建)
  • 便于线程管理

线程池的接口和工具类

ExecutorService:真正的线程池接口,常见子类 ThreadPoolExecutor

  • void execute( Runnable command ):执行任务/命令,没有返回值 ,一般用来执行Runnable
  • < T > Future < T > submit( Callable < T > task ):执行任务,有返回值,一般用来执行 Callable
  • void shutdown():关闭连接池

Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

package test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestPool {
    public static void main(String[] args) {
        // 1.创建服务,创建线程池,线程池大小为10
        ExecutorService service = Executors.newFixedThreadPool(10);

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

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

class MyThread implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + "运行到" + i);
        }
    }
}