多线程

进程和线程

概念

程序是机器上安装的软件,是一个静止的内容。当程序被启动,就会产生(至少)一个进程。一般情况下,一个程序产生一个进程,但是有些特殊用途的程序运行时可能会产生多个进程。

在一个进程中,可以创建多个任务来同时进行,这些任务称为线程,是一种轻量级的进程。当这些线程同时执行时(交替执行),称为多线程。

理解

线程是同时执行还是交替执行?

线程是利用CPU的空闲时间交替执行,由于交替执行的时间较短,看起来像是同时执行。

在现在的电脑上是交替执行,还是同时执行?

现在的电脑并非单核,单核CPU执行的多线程都是交替执行执行的,但是多核意味着有多个CPU,也就是说可以做到同时执行。

区别

进程和线程的区别:

  • 一个程序运行后至少包含一个进程。
  • 一个进程至少包含一个线程,可以包含多个线程。
  • 进程是系统分配资源的基本单位,而线程是CPU调度的单位。
  • 进程之间一般不能共享数据,但是线程之间可以共享数据。

线程的创建

线程的组成

  • CPU的时间片。每一个线程在执行时都需要CPU分配时间。
  • 运行数据。

    • 堆空间数据,共享数据。
    • 栈空间数据,一般是临时变量,线程中有独立空间来保存。
  • 逻辑代码。

线程创建

线程创建有两种方式:

  • 继承Thread类
  • 实现Runnable接口

继承Thread类

继承Thread类,并重写run方法。

  1. public class TestMain {
  2. public static void main(String[] args) {
  3. // 创建两个线程
  4. MyThread1 t1 = new MyThread1();
  5. MyThread2 t2 = new MyThread2();
  6. // 启动线程
  7. t1.start();
  8. t2.start();
  9. }
  10. }
  11. // 定义线程
  12. class MyThread1 extends Thread{
  13. @Override
  14. public void run() {
  15. for (int i = 0; i < 100; i++) {
  16. System.out.println("=====" + i);
  17. }
  18. }
  19. }
  20. class MyThread2 extends Thread{
  21. @Override
  22. public void run() {
  23. for (int i = 0; i < 100; i++) {
  24. System.out.println("-----" + i);
  25. }
  26. }
  27. }

实现Runnable接口

实现Runnable接口,重写run方法。在使用时还是要创建Thread类的对象。

继承Thread类和实现Runnable接口的区别:

  • 继承Thread类后使用简单,而实现接口后还是要创建Thread类的对象,使用相对复杂。
  • 继承类后不能再继承其他类,而实现接口还可以继承其他类,或者实现其他接口,使用更灵活。
  • 继承类后,中间逻辑代码不能复用,而实现接口后,逻辑代码还可以复用。
  1. public class TestMain1 {
  2. public static void main(String[] args) {
  3. // 创建两个线程
  4. MyRunnable1 r1 = new MyRunnable1();
  5. Thread t1 = new Thread(r1);
  6. MyRunnable2 r2 = new MyRunnable2();
  7. Thread t2 = new Thread(r2);
  8. // 启动线程
  9. t1.start();
  10. t2.start();
  11. }
  12. }
  13. // 定义线程
  14. class MyRunnable1 implements Runnable{
  15. @Override
  16. public void run() {
  17. for (int i = 0; i < 100; i++) {
  18. System.out.println("=====" + i);
  19. }
  20. }
  21. }
  22. class MyRunnable2 implements Runnable{
  23. @Override
  24. public void run() {
  25. for (int i = 0; i < 100; i++) {
  26. System.out.println("-----" + i);
  27. }
  28. }
  29. }

经典面试题:

start()与run()的区别:

  • 直接调用run方法是直接将线程类中的业务逻辑代码执行。等同于定义一个类,创建该类的对象,调用其方法。根本没有使用线程相关的内容,没有创建多的线程。
  • 当调用start方法时,会先创建线程,进入就绪状态,等待抢占CPU的执行时间,进行执行run方法。

当程序启动时, 会自动创建一个进程,该进程中会有一个默认线程,此线程名称为main(主线程)。

线程的状态

基本状态

新建:创建Thread对象,与普通对象没有区别。

就绪:调用start后,进入就绪状态,此时会等待操作系统分配时间片。

运行:当被操作系统选中后,开始调用run方法执行相应的业务,如果业务没有执行完毕,但是时间片到期,会进入就绪状态,等待下一次的时间片。

终止:业务执行完毕或者main结束。

线程饿死:是指一个线程一直没有被分配到时间片,无法执行。

常用方法

sleep休眠

sleep:指让线程进入休眠状态,直到休眠的时间结束,进入就绪状态。

一旦线程进入休眠状态,其他的线程就会优先抢占时间片。

  1. public class TestMain2 {
  2. public static void main(String[] args) {
  3. // 创建两个线程
  4. MyThread3 t1 = new MyThread3();
  5. MyThread4 t2 = new MyThread4();
  6. // 启动线程
  7. t1.start();
  8. t2.start();
  9. }
  10. }
  11. // 定义线程
  12. class MyThread3 extends Thread{
  13. @Override
  14. public void run() {
  15. // 得到当前正在运行的线程名称
  16. String name = Thread.currentThread().getName();
  17. for (int i = 0; i < 100; i++) {
  18. System.out.println(name + "=====" + i);
  19. if(i == 20) {
  20. // 单位是毫秒
  21. try {
  22. Thread.sleep(5000);
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. }
  26. }
  27. }
  28. }
  29. }
  30. class MyThread4 extends Thread{
  31. @Override
  32. public void run() {
  33. String name = Thread.currentThread().getName();
  34. for (int i = 0; i < 100; i++) {
  35. System.out.println(name + "-----" + i);
  36. }
  37. }
  38. }

yield让渡

yield:让线程放弃当前的时间片,进入就绪状态。

注意:当一个线程让渡时,并不一定另一个线程就能抢到时间片。也可能让渡完又抢到时间片。

public class TestMain2 {
    public static void main(String[] args) {
        // 创建两个线程
        MyThread3 t1 = new MyThread3();
        MyThread4 t2 = new MyThread4();
        // 启动线程
        t1.start();
        t2.start();
//        String name = Thread.currentThread().getName();
//        System.out.println(name);
    }
}

// 定义线程
class MyThread3 extends Thread{
    @Override
    public void run() {
        // 得到当前正在运行的线程名称
        String name = Thread.currentThread().getName();
        for (int i = 0; i < 100; i++) {
            System.out.println(name + "=====" + i);
            if(i == 20) {
                // 让渡
                Thread.yield();
            }
        }
    }
}

class MyThread4 extends Thread{
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        for (int i = 0; i < 100; i++) {
            System.out.println(name + "-----" + i);
        }
    }
}

join合并

有一个线程A正在运行,此时如果join了线程B,那么线程A会等待线程B执行完毕后才继续执行。(插队)

public class TestMain4 {
    // 合并
    public static void main(String[] args) {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("=======" + i);
                }
            }
        };

        Thread t1 = new Thread(r1);

        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("-----" + i);
                    if(i == 20) {
                        try {
                            // 会让t1执行完毕后,再执行t2剩下内容
                            t1.join();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        };

        Thread t2 = new Thread(r2);

        t1.start();
        t2.start();
    }
}

线程等待

等待:线程进入了等待状态,等待结束后进入就绪状态。

  • 当线程中使用了sleep后,进入了限时等待,时间结束进入就绪状态。
  • 当线程中使用join后,进入不限时等待,直到join进来的线程执行完毕才进入就绪状态。
  • 当线程中使用了wait后,进入了等待,直到被唤醒或者等待超时,才进入就绪状态。

线程安全

当多线程同时访问共享资源时,如果破坏了原子操作,可能会出现线程不安全问题。

解决方式:线程同步(加锁)。

语法:使用synchronized关键字。

  • synchronized同步代码块
  • synchronized同步方法
public class TestMain5 {
    public static void main(String[] args) {
        Seller s1 = new Seller("张三");
        Seller s2 = new Seller("李四");
        Seller s3 = new Seller("王五");
        s1.start();
        s2.start();
        s3.start();
    }
}


// 售票员卖票问题
class Seller extends Thread{
    public static Integer ticket = 10;
    private String sellName;

    public Seller(String sellName) {
        super();
        this.sellName = sellName;
    }

    @Override
    public void run() {
        while(ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 加锁在此处必须是唯一的锁,而且需要引用数据类型
            synchronized (A.obj) { // 同步代码块
                // 双重检测锁
                if(ticket > 0) {
                    ticket--;
                    System.out.println(sellName + "卖出一张票,还剩下" + ticket + "张");
                }
            }
        }
    }
}

class A{
    public static final Object obj = new Object();
}

线程阻塞

阻塞:当线程运行过程中,遇到了加锁的代码,需要去获取锁,在没有获取锁时,进入阻塞状态,需要等待持有锁的线程将加锁的代码执行完毕后,才能继续执行。

死锁

当一个线程持有锁A,等待锁B,另一个线程持有锁B,等待锁A,两个线程都不会释放锁,此时也无法继续获得另一把锁,产生死锁。

public class TestMain6 {
    public static void main(String[] args) {
        Boy boy = new Boy();
        Girl girl = new Girl();
        boy.start();
        girl.start();
    }
}

class Boy extends Thread{
    @Override
    public void run() {
        synchronized (A1.a) {
            System.out.println("男孩抢到了筷子A");
            synchronized (A1.b) {
                System.out.println("男孩抢到了筷子B");
                System.out.println("男孩吃东西");
            }
        }
    }
}

class Girl extends Thread{
    @Override
    public void run() {
        synchronized (A1.b) {
            System.out.println("女孩抢到了筷子B");
            synchronized (A1.a) {
                System.out.println("女孩抢到了筷子A");
                System.out.println("女孩吃东西");
            }
        }
    }
}

class A1{
    public static final Object a = new Object(); // 筷子A
    public static final Object b = new Object(); // 筷子B
}

线程通信

使用wait方法让线程进入等待状态。

使用notify或者notifyAll唤醒线程,进入就绪状态。

注意:使用wait需要在synchronized中使用。

使用wait等待是无限期等待,需要唤醒或者超时。

  • 唤醒,指使用notify或者notifyAll
  • 超时,是指在指定时间内,如果没有唤醒,那么就放弃等待。
public class TestMain7 {
    public static void main(String[] args) {
        MyThread5 t1 = new MyThread5();
        MyThread6 t2 = new MyThread6();
        t1.start();
        t2.start();
    }
}

class MyThread5 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("===========" + i);
            if(i == 20) {
                synchronized (MyObj.obj) {
                    try {
                        MyObj.obj.wait(10000);// 指定超时时间,如果不唤醒,时间过了会自动醒来
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

class MyThread6 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("----------" + i);
        }
    }
}

class MyObj{
    public static final Object obj = new Object();
}

注意:唤醒时也需要在synchronized中使用。唤醒后线程进行就绪状态。

public class TestMain7 {
    public static void main(String[] args) {
        MyThread5 t1 = new MyThread5();
        MyThread6 t2 = new MyThread6();
        t1.start();
        t2.start();
    }
}

class MyThread5 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("===========" + i);
            if(i == 20) {
                synchronized (MyObj.obj) {
                    try {
                        MyObj.obj.wait(10000);// 指定超时时间,如果不唤醒,时间过了会自动醒来
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

class MyThread6 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("----------" + i);
            if(i == 800) {
                // 唤醒
                synchronized (MyObj.obj) {
                    MyObj.obj.notify();
                }
            }
        }
    }
}

class MyObj{
    public static final Object obj = new Object();
}

注意:如果有多个线程同时使用某个锁对象进行wait状态,那么一次notify方法调用只会随机唤醒一个,需要多次调用notify方法,此时,可以使用notifyAll一次唤醒所有的进入wait状态的线程。

经典面试题:

sleep和wait的区别:

1、sleep需要指定时间,时间到了会自动醒来。而wait如果没有指定超时时间,会无限等待,直到被唤醒为止。

2、sleep是一个静态方法,而且不需要在同步时调用,而wait是一个对象方法,需要在同步时使用。

3、sleep在休眠时不会释放锁。而wait会释放锁。

public class TestMain8 {
    public static void main(String[] args) {
        MyThread7 t1 = new MyThread7();
        MyThread8 t2 = new MyThread8();
        t1.start();
        t2.start();
    }
}

class MyThread7 extends Thread{
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        for (int i = 0; i < 100; i++) {
            System.out.println(name + "===========" + i);
            if(i == 20) {
                synchronized (MyObj.obj) {
                    System.out.println("MyThread7获得锁");
                    try {
                        // sleep不释放锁,下面的线程需要等待锁
                        Thread.sleep(10000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

class MyThread8 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("----------" + i);
            if(i == 200) {
                // 加锁
                synchronized (MyObj.obj) {
                    System.out.println("MyThread8获得锁");
                }
            }
        }
    }
}
public class TestMain8 {
    public static void main(String[] args) {
        MyThread7 t1 = new MyThread7();
        MyThread8 t2 = new MyThread8();
        t1.start();
        t2.start();
    }
}

class MyThread7 extends Thread{
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        for (int i = 0; i < 100; i++) {
            System.out.println(name + "===========" + i);
            if(i == 20) {
                synchronized (MyObj.obj) {
                    System.out.println("MyThread7获得锁");
                    try {
                        // wait会释放锁
                        MyObj.obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

class MyThread8 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("----------" + i);
            if(i == 200) {
                // 加锁
                synchronized (MyObj.obj) {
                    System.out.println("MyThread8获得锁");
                }
            }
        }
    }
}

生产消费模式

设计模式

设计模式是指前人经验的总结,并且经过长期的实践验证行之有效的方案。将在项目中可能遇到的问题进行分类总结,并找到其对应的解决方案。

二十三种设计模式:常见的二十三种问题对应的解决方案。

分为三大类:

  • 创建型模式:创建对象的不同方式。(5种)单例模式、工厂模式、原型模式等
  • 结构型模式:两个对象形成一种新的结构。(7种)桥接模式、适配器模式、装饰模式
  • 行为型模式:多个对象之间相互作用。(11种)命令模式、监听者模式、调停者模式、迭代模式

注意:软件发展到现在,遇到的问题远不止23种,所以现在有很多新的设计模式出现,不在23种设计模式中。例如:MVC模式等。

生产消费模式

并发和并行:

并行是指多个程序同时执行。

并发是指多线程同时执行多个任务。如果在执行过程中涉及到临界资源的修改,会引发线程安全问题。

生产消费模式:多个生产者生产产品,多个消费者消费产品,要使得消费者和生产者并发执行。使用一个缓冲区来进行缓冲,将生产者生产的产品放入到缓冲区中,但是一旦缓冲满,生产者应该等待,等待消费者消费后唤醒生产者。消费者在缓冲区中取得产品进行消费,但是一旦缓冲区空,消费者应该等待,等待生产者生产后唤醒消费者。

public class Car {
    private String id;
    private String name;
    public Car(String id, String name) {
        super();
        this.id = id;
        this.name = name;
    }
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "Car [id=" + id + ", name=" + name + "]";
    }
}
public class Storage {
    private Car [] cars = new Car[6];
    private int size = 0;

    // 1、同步方法表示当前方法的所有代码全部放入同步块
    // 2、同步方法加锁的对象是调用该方法的对象,所以静态方法锁对象是类,即类名.class,非静态方法锁对象是this
    public synchronized void add(Car car) {
        while(size >= 6) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        cars[size++] = car;
        System.out.println(car + "放入仓库");
        this.notifyAll();
    }

    public synchronized Car out() {
        while(size <= 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        Car car = cars[size - 1];
        cars[size - 1] = null;
        size--;
        System.out.println(car + "取出仓库");
        this.notifyAll();
        return car;
    }
}
public class Producer extends Thread{
    private Storage storage;

    public Producer(Storage storage) {
        super();
        this.storage = storage;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        for (int i = 0; i < 15; i++) {
            Car car = new Car("汽车" + i, name);
            storage.add(car);
            System.out.println("生产了一辆汽车:" + car);
        }
    }
}
public class Customer extends Thread{
    private Storage storage;

    public Customer(Storage storage) {
        super();
        this.storage = storage;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        for (int i = 0; i < 10; i++) {
            Car car = storage.out();
            System.out.println(name + "消费了一辆汽车:" + car);
        }
    }
}
public class TestMain {
    public static void main(String[] args) {
        Storage storage = new Storage();
        Producer p1 = new Producer(storage);
        Producer p2 = new Producer(storage);
        Customer c1 = new Customer(storage);
        Customer c2 = new Customer(storage);
        Customer c3 = new Customer(storage);
        p1.start();
        p2.start();
        c1.start();
        c2.start();
        c3.start();
    }
}

线程的终止

让正在执行的线程停止。

一般有3种方法:

使用stop

1、使用stop,此方法已被废弃,不允许使用。stop是强行终止线程,相当于电脑在运行时断电,会导致一些隐患。

public class TestMain1 {
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; ; i++) {
                    System.out.println(name + "======" + i);
                }
            }
        });
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 100; i++) {
                    System.out.println(name + "-----" + i);
                    if(i == 80) {
                        // 不能使用,会出现未知问题
                        t1.stop();
                    }
                }
            }
        });
        t2.start();
    }
}

自定义标识

在线程运行过程种,定义一个线程运行时需要满足的标识,需要线程停止时,修改该标识,线程就会执行完毕。

public class TestMain1 {
    public static void main(String[] args) {
        T1 t1 = new T1();

        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 100; i++) {
                    System.out.println(name + "-----" + i);
                    if(i == 80) {
                        t1.b = true;
                    }
                }
            }
        });
        t2.start();
    }
}

class T1 extends Thread{
    public volatile boolean b;
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        for (int i = 0; ; i++) {
            System.out.println(name + "======" + i);
            if(b) {
                break;
            }
        }
    }
}

使用interrupt

系统对线程种定义的标识,通过改变该标识的值,来停止线程。

注意:如果线程正在休眠,通过interrupt想去终止线程,会出现异常。

public class TestMain1 {
    public static void main(String[] args) {
        T1 t1 = new T1();

        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 100; i++) {
                    System.out.println(name + "-----" + i);
                    if(i == 80) {
                        t1.interrupt();
                    }
                }
            }
        });
        t2.start();
    }
}

class T1 extends Thread{
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        for (int i = 0; ; i++) {
            System.out.println(name + "======" + i);
            if(i == 10) {
                try {
                    Thread.sleep(10000);
                    // 休眠时不能被interrupt,会出现异常
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    // 出现异常也停止循环
                    break;
                }
            }
            // 如果被interrupt,则停止循环
            if(Thread.interrupted()) {
                break;
            }
        }
    }
}

线程的优先级

优先级越高,被CPU选中分配时间片的概率越高。

优先级最高为10,最低为1,默认为5。可以通过常量设置。

public class TestMain2 {
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 100; i++) {
                    System.out.println(name + "-----" + i);
                }
            }
        });
        t1.setPriority(Thread.MAX_PRIORITY); // 设置为最高的优先级
        // 如果不设置优先级,默认为普通优先级
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 100; i++) {
                    System.out.println(name + "-----" + i);
                }
            }
        });
        t2.setPriority(Thread.MIN_PRIORITY); // 设置为最低的优先级
        t2.start();
    }
}

守护线程

守护线程是一个特殊的线程,如果没有其他的线程在运行,守护线程会自动停止。JVM就是一个守护线程,当程序执行结束时,JVM也自动停止。

public class TestMain3 {
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0;; i++) {
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(name + "======" + i);
                }
            }
        });
        t1.setDaemon(true); // 设置为守护线程
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 500; i++) {
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(name + "----------" + i);
                }
            }
        });
        t2.start();
    }
}

volatile关键字的作用

synchronized:

  • 可见性:执行到同步代码块中时,如果要访问临界资源,会去获取最新的值。
  • 互斥性:执行到同步代码块时,会持有锁,其他线程如果也要执行同步代码块时,会等待前一个线程执行完毕同步代码块后释放锁才能去持有锁执行。

volatile修饰属性时,表示该属性具备有可见性。注意:该关键字并不能解决线程安全。

package com.qf.day19;

public class TestMain4 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();

        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 500; i++) {
//                    try {
//                        Thread.sleep(2);
//                    } catch (InterruptedException e) {
//                        e.printStackTrace();
//                    }
                    System.out.println(name + "----------" + i);
                    if(i == 400) {
                        t1.n = "b";
                    }
                }
            }
        });
        t2.start();
    }
}

class MyThread extends Thread{
    public volatile String n = "a";

    @Override
    public void run() {
        System.out.println("程序开始执行====");
        while(true) {
//            try {
//                Thread.sleep(2);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//            System.out.println("===");
            if(n.equals("b")) {
                break;
            }
        }
        System.out.println("程序执行结束====");
    }
}

当没有使用volatile关键字,也没有在循环中打印或者休眠时,发现即使在别的线程中修改了变量n的值,该线程也不会停止。原因是循环中的判断并没有去获取n的最新的值,一直使用a在做判断,所以无法停止。

但是加了volatile关键字后,每次使用变量n都会去获取最新的值,所以线程能够正常停止。

但是即使没有使用volatile关键字,如果在循环中休眠或者打印后,一样可以实现线程停止的效果。注意:原因是循环时CPU相对较忙,没有空闲时间进行变量值的更新,但是系统会尽量完成值的更新,所以一旦休眠或者打印,对于CPU来说,会比较闲,也就有足够的时间来进行变量n的值更新。但是不推荐这样写。应该使用volatile关键字。

线程池

池就是一个集合,在集合中会放入一定量的对象,对这些对象进行管理和重用,避免多次创建和销毁对象的时间浪费。

线程池

线程池是一个集合,里面保存了一定量的线程对象,对这些对象进行管理和重用,避免多次创建和销毁线程带来的时间开销。

public class TestMain5 {
    public static void main(String[] args) {
        // 线程池帮助类
        // 创建线程数量固定的线程池
        ExecutorService service = Executors.newFixedThreadPool(2);
        // 创建线程数量无上限的线程池
//        ExecutorService service = Executors.newCachedThreadPool();
        // 创建单线程的线程池
//        ExecutorService service = Executors.newSingleThreadExecutor();

        // 创建任务
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 200; i++) {
                    System.out.println(name + "====" + i);
                }
            }
        };
        // 让线程池中线程执行任务
        // 当把任务交给线程池去执行时,只要有空闲的线程,会自动启动
        service.submit(r1);
        service.submit(r1);
        service.submit(r1);
        service.submit(r1);

        // 关闭线程池
        // 等待线程池中所有任务执行完毕后才关闭
        service.shutdown();
    }
}

Callable和Future

Callable在JDK1.5时候加入,与Runnable都是定义多线程任务的,但是Runnable没有返回值,而Callable有返回值。

Callable有返回值,通过Future接口去获取返回值,

// 使用两个线程分别求1~50、51~100的和,并求最终的和
public class TestMain6 {
    public static void main(String[] args) {
        // 线程池帮助类
        // 创建线程数量固定的线程池
        ExecutorService service = Executors.newFixedThreadPool(2);
        // 泛型是返回值类型
        // 第一个任务,计算1~50的和
        Callable<Integer> c1 = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1; i <= 50; i++) {
                    sum += i;
                }
                return sum;
            }
        };

        // 第二个任务,计算51~100的和
        Callable<Integer> c2 = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 51; i <= 100; i++) {
                    Thread.sleep(100);
                    sum += i;
                }
                return sum;
            }
        };

        // 获得结果
        Future<Integer> f1 = service.submit(c1);
        Future<Integer> f2 = service.submit(c2);

        // 求和
        try {
            int sum1 = f1.get();
            System.out.println("第一个和为:" + sum1);
            int sum2 = f2.get();
            System.out.println("第二个和为:" + sum2);
            int result = sum1 + sum2;
            System.out.println("最终结果为:" + result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        // 关闭线程池
        // 等待线程池中所有任务执行完毕后才关闭
        service.shutdown();
    }
}

经典面试题:

同步和异步:

同步是指在一个线程中顺序执行,需要等待第一个执行完成后继续执行第二个。

异步是指多个线程同时执行。

Lock锁

Lock接口

与synchronized作用一致。但是比同步方法粒度更细,比同步代码块使用更灵活。功能更强大。

lock():加锁开始,相当于同步代码块的{

unlock():加锁结束,相当于同步代码块的}

boolean tryLock():尝试获取锁

重入锁(公平锁)

ReentrantLock:Lock接口的常用实现类。

在创建锁对象时,使用无参构造方法,或者传入参数false,表示普通的重入锁。

在创建锁对象时,如果传入参数true,表示创建一个公平锁,先等待的先获得锁,按照等待抢锁的顺序获得锁,需要额外的资源来维持顺序,性能会下降。

public class TestMain {
    public static void main(String[] args) {
        Seller s1 = new Seller("张三");
        Seller s2 = new Seller("李四");
        Seller s3 = new Seller("王五");
        s1.start();
        s2.start();
        s3.start();
    }
}


// 售票员卖票问题
class Seller extends Thread{
    public static Integer ticket = 10;
    private String sellName;
    private static final ReentrantLock lock = new ReentrantLock();
    public Seller(String sellName) {
        super();
        this.sellName = sellName;
    }

    @Override
    public void run() {
        while(ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 加锁在此处必须是唯一的锁,而且需要引用数据类型
            lock.lock();
            try {
                // 双重检测锁
                if(ticket > 0) {
                    ticket--;
                    System.out.println(sellName + "卖出一张票,还剩下" + ticket + "张");
                }
            } finally {
                lock.unlock();
            }
        }
    }
}

class A{
    public static final Object obj = new Object();
}

读写锁

支持读和写的锁,如果多个线程都是读,没有写,则共享,如果有一个线程在写,那么其他线程需要等待。

下面是一个简单的读写锁案例:

public class MyClass {
    private int value;
    private ReentrantLock lock = new ReentrantLock();

    public int getValue() throws InterruptedException {
        lock.lock();
        try {
            Thread.sleep(1000);
            return value;
        } finally {
            lock.unlock();
        }
    }

    public void setValue(int value) throws InterruptedException {
        lock.lock();
        try {
            Thread.sleep(1000);
            this.value = value;
        } finally {
            lock.unlock();
        }
    }
}
public class TestMain1 {
    public static void main(String[] args) {
        final MyClass c = new MyClass();
        // 写任务
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                try {
                    c.setValue(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        // 读任务
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                try {
                    c.getValue();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        // 创建一个20任务的线程池
        ExecutorService service = Executors.newFixedThreadPool(20);
        // 当前时间
        long startTime = System.currentTimeMillis();
        // 添加2个写任务
        for (int i = 0; i < 2; i++) {
            service.submit(r1);
        }
        // 添加18个读任务
        for (int i = 0; i < 18; i++) {
            service.submit(r2);
        }
        // 关闭线程池
        service.shutdown();
        // 如果线程池中的线程没有执行完毕,就卡住,不继续向下执行代码
        while(!service.isTerminated()) {}
        long endTime = System.currentTimeMillis();
        System.out.println("执行时间为:" + (endTime - startTime));
    }
}

以上代码没有使用读写锁,大概需要时间为20秒。(20131毫秒)

修改为读写锁后:

public class MyClass {
    private int value;
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private ReadLock readLock = lock.readLock(); // 得到读锁
    private WriteLock writeLock = lock.writeLock(); // 得到写锁

    public int getValue() throws InterruptedException {
        readLock.lock();
        try {
            Thread.sleep(1000);
            return value;
        } finally {
            readLock.unlock();
        }
    }

    public void setValue(int value) throws InterruptedException {
        writeLock.lock();
        try {
            Thread.sleep(1000);
            this.value = value;
        } finally {
            writeLock.unlock();
        }
    }
}

以上代码修改成读写锁后,两次写需要2秒,所有的读共享一次锁,需要1秒,共计3秒。(3027毫秒)

线程安全的集合

Collections类中提供的线程安全的集合获得方法

Collections类中提供了一些线程安全的集合获得方法(这些方法以synchronized开头),但是这些方法都是jdk1.2时提供,线程安全,但是性能不高,不推荐使用。

CopyOnWriteArrayList

写有锁,读无锁。

写时会复制一份,写入后替换掉原来的地址。

优先:线程安全,缺点:耗费内存。

与ArrayList用法一样。

CopyOnWriteArraySet

底层使用CopyOnWriteArrayList实现,

区别在于添加时应该使用addIfAbsent()方法,该方法在添加时会遍历集合,如果发现有相同元素,则放弃添加。

ConcurrentHashMap

使用方式与HashMap相同。

JDK1.7:

  • 使用分段锁segment。
  • 初始时采用16段,对应16把锁,只有当添加到同一个段时,才需要互斥等待,如果不是同一个段,不需要等待。
  • 最理想状态是分别添加到16个段,理论可以16个线程同时添加。

JDK1.8:

  • 采用CAS(compare and swap比较交换算法)机制
  • 有三个核心变量,V、E、N,V是要更新的变量,E是预期值,当V==E时,认为没有其他线程修改过,则进行修改V=N,否则就认为有别的线程修改过,则取消操作。

悲观锁和乐观锁:

悲观锁就是一种互斥锁,要求只能有一个线程在操作,其他的线程需等待。

乐观锁一般需要一个状态(版本),每次操作时先下载版本号,再进行修改,修改完后提交时先比较版本号,如果版本号一致,则没有人在你修改时进行了修改,则可以正常提交修改,并将版本号一同修改。

Queue接口

队列,遵循FIFO(First In First Out先进先出)原则。

ConcurrentLinkedQueue

线程安全,高并发时性能最好的队列。

采用CAS原则。

BlockingQueue

线程阻塞的队列,是Queue的子接口,增加两个无限期等待的方法。

put(E e); 向队列中添加元素,如果队列中没有空间,则等待。

E take(); 在队列中获取元素,如果队列中没有元素,则等待。

生产消费模式。

ArrayBlockingQueue

使用数组实现的有界队列,需要固定大小。

LinkedBlockingQueue

使用链表实现的无界队列,默认大小Integer.MAX_VALUE。

public class TestMain2 {
    public static void main(String[] args) {
        // 创建一个6个大小的有界队列
        ArrayBlockingQueue<Car> queue = new ArrayBlockingQueue<Car>(6);

        // 创建一个生产任务
        Runnable r1 = new Runnable() {
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 15; i++) {
                    try {
                        Car car = new Car("汽车" + i, name);
                        queue.put(car);
                        System.out.println("生产一辆汽车,准备放入仓库" + car);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        // 创建一个消费任务
        Runnable r2 = new Runnable() {
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 10; i++) {
                    try {
                        Car car = queue.take();
                        System.out.println(name + "消费一辆汽车," + car);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        // 创建一个线程池
        ExecutorService service = Executors.newFixedThreadPool(5);
        // 提交任务
        service.submit(r1);
        service.submit(r1);
        service.submit(r2);
        service.submit(r2);
        service.submit(r2);
        // 关闭线程池
        service.shutdown();
    }
}