Java多线程总结笔记

2016/12/6 posted in  Java

多线程的概述

进程和线程

  • 进程:是一个正在运行的程序,每个进程执行,都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元
  • 线程:进程中的一个独立控制单元,线程在控制着进程的执行,一个进程中至少有一个线程(控制单元)

简单实现多线程

常用的实现多线程的两种方法,继承Thread类,实现Runnable接口

继承javalangThread类

  • 步骤:
    • 1、首先,定义类继承Thread;
    • 2、复写Thread类中的run方法;目的:将自定义代码存储在run方法中,让线程执行
    • 3、调用线程的start方法(该方法有两个作用:启动线程和调用run()方法)

代码示例:

public class BasicThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("Thread ID: "+i);
        }
    }
}

public class MainFunction {
    public static void main(String[] args) {
        BasicThread basicThread = new BasicThread();
        basicThread.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("Main Function ID: "+i);
        }
    }
}

要点:继承Thread类,重写run方法.

实现javalangRunnable接口

  • 步骤:
    • 1、定义类实现Runnable接口
    • 2、复写接口中的run方法
    • 3、创建实现Runnable接口对象
    • 4、创建Thread类对象,将实现Runnable接口的对象作为实际参数传递给Thread类的构造函数
    • 5、调用Thread类的start方法,开启接口

代码示例:

public class BasicRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+" ---> "+i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

public class MainRunnable {
    public static void main(String[] args) {
        Thread thread = new Thread(new BasicRunnable());
        thread.setName("Child Thread");
        thread.start();
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread()+" ---> "+i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

为什么要重写run方法

因为Thread类定义了一个用于存储线程要运行的代码的方法,该方法就是run方法.也就是说Thread类中run方法,用于
存储线程要运行的代码.主线程中的代码存储于main方法中.

继承Thread类和实现Runnable接口有什么区别?

  • 继承Thread类:线程代码存放在Thread子类的run方法中
  • 实现Runnable接口:线程代码存放在Runnable实现类的run方法中
  • 实现Runnable接口避免了单继承的局限性,在定义线程时建议使用实现方式来完成

线程数据共享问题---锁

问题: 当多个线程共享同一个数据时,导致共享数据出现错误
解决方案: 当一个线程执行时,其他的线程不能参与执行.
锁: 对象如图锁,持有锁的线程才能在同步中执行,没有锁的线程,即使获取了CPU的执行权,也进不去.
+ 同步的前提:
+ 必须要有两个或两个以上的线程访问共享数据
+ 必须是多个线程使用同一个锁
+ 好处:解决了多线程安全问题
+ 弊端:多个线程都需要判断锁,较为消耗资源

伪代码:

synchronized(监视器对象){
//需要被同步的代码
}

synchronized同步代码块

虽然Java允许使用任何对象作为监视器对象.但想一下同步的目的:阻止多个线程同时对同一个共享资源并发访问,
因此通常使用可能被并发访问的资源作为同步监视器对象.

经典案例:模拟银行取钱操作

账户对象:

public class Account {
    private double balance;//账户余额

    public Account(double balance) {
        this.balance = balance;
    }

    /**
     * 取钱
     * @param drawBalance
     * @return
     */
    public double drawMoney(double drawBalance) {
        balance = balance - drawBalance;
        return balance;
    }

    /**
     * 查询余额
     * @return
     */
    public double getBalance() {
        return balance;
    }
}

取钱线程:

public class DrawThread extends Thread {
    private Account account;
    private double drawBalance;

    public DrawThread(String name, Account account, double drawBalance) {
        super(name);
        this.account=account;
        this.drawBalance=drawBalance;
    }
    @Override
    public void run() {
        while (true) {
//            同步代码块,将共享访问的Account对象作为锁
            synchronized (account) {
                if (account.getBalance()<drawBalance) {
                    System.out.println(Thread.currentThread()+" : 余额不足,当前余额: "+account.getBalance());
                    break;
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                account.drawMoney(drawBalance);
                System.out.println(Thread.currentThread()+"取钱成功,取走金额: "+drawBalance+" ,当前余额: "+account.getBalance());
            }
        }
    }
}

测试取钱:

public class DrawTest {
    public static void main(String[] args) {
        Account account = new Account(1000);
        new DrawThread("A账户: ",account,500).start();
        new DrawThread("B账户: ",account,300).start();
        new DrawThread("C账户: ",account,100).start();
    }
}

结果输出:
Thread[B账户: ,5,main]取钱成功,取走金额: 300.0 ,当前余额: 700.0
Thread[B账户: ,5,main]取钱成功,取走金额: 300.0 ,当前余额: 400.0
Thread[B账户: ,5,main]取钱成功,取走金额: 300.0 ,当前余额: 100.0
Thread[B账户: ,5,main] : 余额不足,当前余额: 100.0
Thread[C账户: ,5,main]取钱成功,取走金额: 100.0 ,当前余额: 0.0
Thread[C账户: ,5,main] : 余额不足,当前余额: 0.0
Thread[A账户: ,5,main] : 余额不足,当前余额: 0.0

synchronized同步方法

同步方法是使用synchronized关键字来修饰某个方法,则该方法就被称为同步方法.

  • 伪代码:
public synchronized void disPlay(){}
  • 特点:
    • 1.对于同步方法而言,无需指定同步监视器对象.同步方法的同步监视器对象是this.
    • 2.一个对象的一个同步方法被调用,这个对象的其他同步方法都用不了.

代码说明:

业务类:

public class Working {
    public synchronized void tom() {
        System.out.println("tom in..."+"\n"+"tom say: Hello Jerry ");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("tom out...");
    }

    public synchronized void jerry() {
        System.out.println("jerry in..."+"\n"+"jerry say: Hello Tom");
        System.out.println("jerry out...");

    }
}

线程类:

public class JerryThread extends Thread {
    private Working working;

    public JerryThread(Working working) {
        this.working = working;
    }
    @Override
    public void run() {
        working.jerry();
    }
}

public class TomThread extends Thread {
    private Working working;

    public TomThread(Working working) {
        this.working = working;
    }

    @Override
    public void run() {
        working.tom();
    }
}

测试类:

public class SynFunction {
    public static void main(String[] args) {
        Working working = new Working();
        TomThread tomThread = new TomThread(working);
        JerryThread jerryThread = new JerryThread(working);
        tomThread.start();
        jerryThread.start();
    }
}

输出结果:
jerry in...
jerry say: Hello Tom
jerry out...
tom in...
tom say: Hello Jerry
tom out...

从输出可以印证之前说的同步方法的特点:当一个对象的同步方法被调用,它的其他同步方法都用不了.

死锁

死锁现象: 当两个线程互相等待对方释放同步监视器时就会发生死锁.Java虚拟机没有检测,也没有采取措施来处理死锁情况,
所以多线程应该避免死锁的出现.一旦程序出现死锁,整个程序既不会发生异常,也不会有任何提示.只是所有线程都处于阻塞状态.

一般这种情况发生在:同步中嵌套同步,但锁不同.
比如:a线程锁定一个资源,同时想获取b线程的资源.b线程锁定一个资源,同时想获取a线程的资源.

代码示例:

线程1:

public class Thread1 extends Thread {
    @Override
    public void run() {
        synchronized (Object.class) {
            System.out.println(Thread.currentThread().getName()+"...外层锁");
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (String.class) {
                System.out.println("haha,锁主了");

            }
        }
    }
}

线程2:

public class Thread2 extends Thread {
    @Override
    public void run() {
        synchronized (String.class) {
            System.out.println(Thread.currentThread().getName()+"...外层锁");
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (Object.class) {
                System.out.println("haha,我也锁主了");

            }
        }
    }
}

测试类:

public class LockFunction {
    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        Thread2 thread2 = new Thread2();
        thread1.start();
        thread2.start();
    }
}

输出:
Thread-0...外层锁
Thread-1...外层锁

线程通信

当多个线程访问同一个资源,每个线程的实现功能不一样,而每个线程之间需要协调发展(两种线程交替运行)

例如:

    老师:我们开始上课;
    学生A:老师等一下,我要去厕所;
    老师:OK,你快点,那我wait()了,等你回来notify我一下。

线程通信的前提:同步环境

代码说明:
加入了存钱的业务,现在分别有两个不同的业务了.

银行类:

public class Bank {
    private double money;
    boolean flag = false;

    public Bank(double money) {
        this.money = money;
    }

    /**
     * 取钱方法
     * @param drawMoney
     * @return
     */
    public double drawMoney(double drawMoney) {
        money = money - drawMoney;
        return money;
    }

    /**
     * 存钱
     * @param saveMoney
     * @return
     */
    public double saveMoney(double saveMoney) {
        money = money + saveMoney;
        return money;
    }

    /**
     * 查询余额
     * @return
     */
    public double getMoney() {
        return money;
    }
}

存钱类:

public class SaveThread extends Thread {
    private Bank bank;
    private double saveMoney;

    public SaveThread(String threadName, Bank bank, double saveMoney) {
        super(threadName);
        this.bank=bank;
        this.saveMoney = saveMoney;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (bank) {
                if (bank.flag) {
                    try {
                        bank.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                bank.saveMoney(saveMoney);
                System.out.println(Thread.currentThread().getName() + " 存款成功,存款金额: " + saveMoney + ",账户余额:" + bank.getMoney());
                bank.flag = true;
                bank.notify();
            }
        }
    }
}

取钱类:

public class DrawThread extends Thread {
    private Bank bank;
    private double drawMoney;

    public DrawThread(String threadName, Bank bank, double drawMoney) {
        super(threadName);
        this.bank = bank;
        this.drawMoney = drawMoney;
    }
    @Override
    public void run() {
        while (true) {
            synchronized (bank) {
                if (!bank.flag) {
                    try {
                        bank.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if (bank.getMoney() < drawMoney) {
                    System.out.println("余额不足,当前余额是: " + bank.getMoney());
                    break;
                }
                bank.drawMoney(drawMoney);
                System.out.println(Thread.currentThread().getName()+" 取款成功,取款金额是: "+drawMoney+",账户余额: "+bank.getMoney());
                bank.flag = false;
                bank.notify();
            }
        }
    }
}

测试类:

public class BankTest {
    public static void main(String[] args) {
        Bank bank = new Bank(5000);
        new DrawThread("取钱线程",bank,500).start();
        new SaveThread("存钱线程",bank,200).start();
    }
}

输出:
存钱线程存款成功,存款金额: 200.0,账户余额:5200.0
取钱线程 取款成功,取款金额是: 500.0,账户余额: 4700.0
存钱线程存款成功,存款金额: 200.0,账户余额:4900.0
取钱线程 取款成功,取款金额是: 500.0,账户余额: 4400.0
存钱线程存款成功,存款金额: 200.0,账户余额:4600.0
取钱线程 取款成功,取款金额是: 500.0,账户余额: 4100.0
存钱线程存款成功,存款金额: 200.0,账户余额:4300.0
取钱线程 取款成功,取款金额是: 500.0,账户余额: 3800.0
存钱线程存款成功,存款金额: 200.0,账户余额:4000.0
取钱线程 取款成功,取款金额是: 500.0,账户余额: 3500.0
存钱线程存款成功,存款金额: 200.0,账户余额:3700.0
取钱线程 取款成功,取款金额是: 500.0,账户余额: 3200.0
存钱线程存款成功,存款金额: 200.0,账户余额:3400.0
取钱线程 取款成功,取款金额是: 500.0,账户余额: 2900.0
存钱线程存款成功,存款金额: 200.0,账户余额:3100.0
取钱线程 取款成功,取款金额是: 500.0,账户余额: 2600.0
存钱线程存款成功,存款金额: 200.0,账户余额:2800.0
取钱线程 取款成功,取款金额是: 500.0,账户余额: 2300.0
存钱线程存款成功,存款金额: 200.0,账户余额:2500.0
取钱线程 取款成功,取款金额是: 500.0,账户余额: 2000.0
存钱线程存款成功,存款金额: 200.0,账户余额:2200.0
取钱线程 取款成功,取款金额是: 500.0,账户余额: 1700.0
存钱线程存款成功,存款金额: 200.0,账户余额:1900.0
取钱线程 取款成功,取款金额是: 500.0,账户余额: 1400.0
存钱线程存款成功,存款金额: 200.0,账户余额:1600.0
取钱线程 取款成功,取款金额是: 500.0,账户余额: 1100.0
存钱线程存款成功,存款金额: 200.0,账户余额:1300.0
取钱线程 取款成功,取款金额是: 500.0,账户余额: 800.0
存钱线程存款成功,存款金额: 200.0,账户余额:1000.0
取钱线程 取款成功,取款金额是: 500.0,账户余额: 500.0
存钱线程存款成功,存款金额: 200.0,账户余额:700.0
取钱线程 取款成功,取款金额是: 500.0,账户余额: 200.0
存钱线程存款成功,存款金额: 200.0,账户余额:400.0
余额不足,当前余额是: 400.0

线程学习的经典案例-生产者消费者案例

大多数设计模式中,都会有一个第三者来进行解耦.在生产者和消费者模式中,我们需要一个第三者来对此进行解耦

为什么要用生产者和消费者模式?

在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,
如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,
才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待
生产者。为了解决这个问题于是引入了生产者和消费者模式。

什么是生产者消费者模式

生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者
彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待
消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,
阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

这个阻塞队列就是用来给生产者和消费者解耦的。纵观大多数设计模式,都会找一个第三者出
来进行解耦,如工厂模式的第三者是工厂类,模板模式的第三者是模板类。在学习一些设计模
式的过程中,如果先找到这个模式的第三者,能帮助我们快速熟悉一个设计模式。

单生产者单消费者

一个生产者线程,一个消费者线程.过程看代码注释,非常详细了.希望我以后看见我的注释就能回想起一切.

资源类:

public class Resource {
    private int num = 0;
    boolean flag = false;

    public Resource(int num) {
        this.num = num;
    }

//    生产资源
    public synchronized void createNum() {
//        flag为true表示有资源。那么生产者就不能生产了,生产者线程就开始wait等待
        if (flag) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
//        如果到了这里,表示:生产者已经被唤醒了,那么意味着资源已经被消费了,生产者要开始生产了
        num++;
        System.out.println(Thread.currentThread().getName()+" 生产者线程: ---> "+num);
//        生产者生产完资源后,把flag标记为true.表示有资源了
        flag = true;
//        有资源后,生产者去唤醒消费者去消费资源
        notify();
    }

    //    消费资源
    public synchronized void destory() {
//        因为flag为true时候有资源,那么!flag表示没用资源了,因此消费者线程就需要等待了,等待生产者线程去生产资源
        if (!flag) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
//        到了这里,说明消费者线程被生产者线程唤醒了,那么也意味着有资源了
        System.out.println(Thread.currentThread().getName()+" 消费者线程: ---> "+num);
//        消费了数据,那么消费者就得把flag标记为false,表示没用数据了,提醒生产者得去生产数据了
        flag = false;
//        接下来就得去唤醒生产者去生产数据了
        notify();
    }
}

生产者线程:

public class Producer implements Runnable {
    private Resource resource;

    public Producer(Resource resource) {
        this.resource = resource;
    }
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            resource.createNum();
        }
    }
}

消费者线程:

public class Consumer implements Runnable {
    private Resource resource;

    public Consumer(Resource resource) {
        this.resource = resource;
    }
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            resource.destory();
        }
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        Resource resource = new Resource(50);
        new Thread(new Producer(resource)).start();
        new Thread(new Consumer(resource)).start();
    }
}

输出结果:
Thread-0 生产者线程: ---> 51
Thread-1 消费者线程: ---> 51
Thread-0 生产者线程: ---> 52
Thread-1 消费者线程: ---> 52
Thread-0 生产者线程: ---> 53
Thread-1 消费者线程: ---> 53
Thread-0 生产者线程: ---> 54
Thread-1 消费者线程: ---> 54
Thread-0 生产者线程: ---> 55
Thread-1 消费者线程: ---> 55
Thread-0 生产者线程: ---> 56
Thread-1 消费者线程: ---> 56

单生产者和单消费者没有问题.

多生产者多消费者

两个生产者两个消费者.生产者生产一个,消费者消费一个.

测试代码:

public class Test {
    public static void main(String[] args) {
        Resource resource = new Resource(0);
        new Thread(new Producer(resource)).start();
        new Thread(new Producer(resource)).start();
        new Thread(new Consumer(resource)).start();
        new Thread(new Consumer(resource)).start();
    }
}
输出:

Thread-1 生产者线程: ---> 105
Thread-3 消费者线程: -----------> 105
Thread-2 消费者线程: -----------> 105

这里出现了问题.105生产了一次,消费了两次.
为了在做实验的时候,看出问题.我们不能把线程的等待时间设置得过长,不然可能没法暴露出问题.然后输出打印的时候,让消费者和生产者的输出格式不一样,是为了更快的定位到问题.
所以,在这里我修改了消费者的输出.所以会和上面的不太一样.

  • 原因分析
    • 当两个线程同时操作生产者生产或者消费者消费时,如果有生产者或者消费者的两个线程都wait()时,再次notify(),由于其中一个线程已经改变了标记而另外一个线程再次往下直接执行的时候没有判断标记而导致的。
    • if判断标记,只有一次,会导致不该运行的线程运行了。出现了数据错误的情况。
  • 解决方案 +while判断标记,解决了线程获取执行权后,是否要运行!也就是每次wait()后再notify()时先再次判断标记

注意:
+ 把单消费者单生产者改为多消费者和多生产者时候.
+ 1.把if改为while
+ 2.把notify改为notifyAll(不然会出现锁死)

代码:

public class Resource {
    private int num = 0;
    boolean flag = false;

    public Resource(int num) {
        this.num = num;
    }

//    生产资源
    public synchronized void createNum() {
//        flag为true表示有资源。那么生产者就不能生产了,生产者线程就开始wait等待
        while (flag) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
//        如果到了这里,表示:生产者已经被唤醒了,那么意味着资源已经被消费了,生产者要开始生产了
        num++;
        System.out.println(Thread.currentThread().getName()+" 生产者线程: ---> "+num);
//        生产者生产完资源后,把flag标记为true.表示有资源了
        flag = true;
//        有资源后,生产者去唤醒消费者去消费资源
//        notify();
        notifyAll();
    }

    //    消费资源
    public synchronized void destory() {
//        因为flag为true时候有资源,那么!flag表示没用资源了,因此消费者线程就需要等待了,等待生产者线程去生产资源
        while (!flag) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
//        到了这里,说明消费者线程被生产者线程唤醒了,那么也意味着有资源了
        System.out.println(Thread.currentThread().getName()+" 消费者线程: -----------> "+num);
//        消费了数据,那么消费者就得把flag标记为false,表示没用数据了,提醒生产者得去生产数据了
        flag = false;
//        接下来就得去唤醒生产者去生产数据了
//        notify();
        notifyAll();
    }
}