带你了解多线程

@[toc]

多线程

一、程序、进程、线程

1、程序

  • 程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。

2、进程

  • 是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。

    1. 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
    2. 程序是静态的,进程是动态的

3、线程

  • 进程可进一步细化为线程,是一个程序内部的一条执行路径

    1. 若一个进程同一时间 并行执行多个线程,就是支持多线程的
    2. 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
    3. 一个进程中的多个线程共享相同的内存单元/内存地址空间---->它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。

4、并行与并发

  • 区别:

    并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。

    并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。

二、线程的创建和使用

1、Thread类

1.1、Thread类的特性

  • 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体
  • 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()

1.2、构造器

  • Thread():创建新的Thread对象
  • Thread(String threadname):创建线程并指定线程实例名
  • Thread(Runnable target) :指定创建线程的目标对象,它实现了Runnable接口中的run方法
  • Thread(Runnable target, String name) :创建新的Thread对象

2、创建线程的第一种方式(继承Thread类)

  • JDK1.5之前创建新执行线程有两种方法:

    继承Thread类的方式

    实现Runnable接口的方式

  • 继承Thread类

    定义子类继承Thread类。

    子类中重写Thread类中的run方法。

    创建Thread子类对象,即创建了线程对象。

    调用线程对象start方法:启动线程,调用run方法。

  • mt子线程的创建和启动过程

image
image

3、创建线程和使用的注意点

  • 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式
  • run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
  • 想要启动多线程,必须调用start方法
  • 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常IllegalThreadStateException

创建线程代码如下:

package com.shsxt.thread;

/**
 * 多线程的创建,方式一:继承于Thread类
 */
//1. 创建一个继承于Thread类的子类
class MyThread extends Thread {
    //2. 重写Thread类的run()
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

public class ThreadTest {
    public static void main(String[] args) {
        //3. 创建Thread类的子类的对象
        MyThread mt = new MyThread();
        //4.通过此对象调用start():①启动当前线程 ② 调用当前线程的run()
        mt.start();
        //问题一:我们不能通过直接调用run()的方式启动线程。
        //mt.run();

        //问题二:再启动一个线程,遍历100以内的偶数。不可以还让已经start()的线程去执行。会报IllegalThreadStateException
        //t1.start();

        //我们需要重新创建一个线程的对象
        MyThread mt2 = new MyThread();
        mt2.start();

        //如下操作仍然是在main线程中执行的。
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i + "******main*******");
            }
        }

    }
}

public class ThreadDemo {

    public static void main(String[] args) {

        //创建Thread类的匿名子类的方式
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 100 ; i++) {
                    if (i%2==0){
                        System.out.println(Thread.currentThread().getName()+":"+i);
                    }
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if(i % 2 != 0){
                        System.out.println(Thread.currentThread().getName() + ":" + i);

                    }
                }
            }
        }.start();
    }
}

4、Thread类有关方法

  • void start(): 启动线程,并执行对象的run()方法

  • run(): 线程在被调度时执行的操作

  • String getName(): 返回线程的名称

  • void setName(String name):设置该线程名称

  • static Thread currentThread(): 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类

  • static void yield():线程让步

    暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程

  • join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。

  • static void sleep(long millis) :(指定时间:毫秒);让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。

  • boolean isAlive():返回boolean,判断线程是否还活着

5、线程调度

  • 调度策略

    1. 时间片:
      image
    2. 抢占式: 高优先级的线程抢占CPU
  • Java的调度方法

    1. 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
    2. 对高优先级,使用优先调度的抢占式策略

6、线程的优先级

  • 线程的优先级等级:

    MAX_PRIORITY :10
    MIN _PRIORITY :1
    NORM_PRIORITY :5

  • 涉及的方法

    getPriority() :返回线程优先值
    setPriority(int newPriority) :改变线程的优先级

  • 注意点:

    线程创建时继承父线程的优先级

    低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用

关于方法的一些使用,代码如下:

package com.shsxt.thread;

class HelloThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(getName() + ":" + getPriority() + ":" + i);
            }

            //yield()方法的使用(礼让线程)
//            if (i % 20 == 0) {
//                yield();
//            }
        }
    }

    public HelloThread(String name) {
        //给子线程赋名字
        super(name);
    }
}

public class ThreadMethod {
    public static void main(String[] args) {
        //第一种方式:给子线程赋名字
        HelloThread h1 = new HelloThread("Thread:1");
        //第二种方式:给子线程赋名字
        //h1.setName("线程一");

        //给子线程设置优先级
        //h1.setPriority(Thread.MAX_PRIORITY);
        //启动子线程
        h1.start();

        //给主线程命名
        Thread.currentThread().setName("主线程");
        //给主线程设置优先级
        //Thread.currentThread().setPriority(Thread.MIN_PRIORITY);

        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
            }
            //调用join()方法,当i==20时主线程阻塞,子线程运行完后,主线程才运行
            if (i == 20){
                try {
                    h1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        //判断线程是否还存活着
        System.out.println(h1.isAlive());
    }
}

使用继承Thread类,写一个窗口卖票的练习

class Window extends Thread{
    //使用static关键字是防止new Window()每个线程都有100张票
    //不使用static关键字的话,需要用到创建线程的第二种方式实现Runnable接口
    private static int tickets = 100;
    @Override
    public void run() {
        while (true){
            if (tickets>0){
                System.out.println(getName()+":卖票,票号为"+tickets);
                tickets--;
            }else {
                break;
            }
        }
    }
}

public class WindowTest {
    public static void main(String[] args) {
        Window w = new Window();
        Window w1 = new Window();
        Window w2 = new Window();

        w.setName("窗口一");
        w1.setName("窗口二");
        w2.setName("窗口三");

        w.start();
        w1.start();
        w2.start();

    }
}

7、创建线程的第二种方式(实现Runnable接口)

  • 实现Runnable接口

    1. 创建一个实现了Runnable接口的类
    2. 实现类去实现Runnable中的抽象方法:run()
    3. 创建实现类的对象
    4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
    5. 通过Thread类的对象调用start()

线程创建,代码如下:

package com.shsxt.thread;

//1. 创建一个实现了Runnable接口的类
class MyThread1 implements Runnable {
    //2、实现类去实现Runnable中的抽象方法:run()
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }

        }
    }
}

public class ThreadTest1 {

    public static void main(String[] args) {
        //3. 创建实现类的对象
        MyThread1 myThread1 = new MyThread1();

        //4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1 = new Thread(myThread1);
        t1.setName("线程1");
        //5. 通过Thread类的对象调用start():① 启动线程 ②调用当前线程的run()-->调用了Runnable类型的target的run()
        t1.start();

        //再启动一个线程,遍历100以内的偶数
        Thread t2 = new Thread(myThread1);
        t2.setName("线程2");
        t2.start();

    }

}

8、创建线程(继承Thread类和实现Runnable接口)的两种方式的异同

相同点:

两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。

不同点:

开发中:优先选择:实现Runnable接口的方式

原因:1、实现了Runnable接口的方式解决了类的单继承性的局限性

2、实现Runnable接口的方式更适合来处理多个线程有共享数据的情况。

使用继承Thread类,写一个窗口卖票的练习:

package com.shsxt.thread;

/**
 * 创建三个窗口卖票,总票数为100张.使用实现Runnable接口的方式
 * @author Rainbow
 * @date 2020/7/15 10:31
 */
class Window1 implements Runnable {
    private int tickets = 100;

    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + tickets);
                tickets--;
            } else {
                break;
            }
        }
    }
}

public class WindowTest1 {

    public static void main(String[] args) {
        Window1 w1 = new Window1();

        Thread t1 = new Thread(w1);
        Thread t2 = new Thread(w1);
        Thread t3 = new Thread(w1);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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

三、线程的生命周期

  • JDK 中用Thread.State 类定义了 线程的几种状态

要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的 五种状态:

  • <u>新建</u>:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
  • <u>就绪</u>:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
  • <u>运行</u>:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
  • <u>阻塞</u>:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
  • <u>死亡</u>**:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

1、线程生命周期图

image
image

四、线程的同步

首先举个例子看下:

package com.shsxt.day;

/**
 * @author Rainbow
 * @date 2020/7/15 9:15
 */

class Window extends Thread {

    private static int tickets = 100;

    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                System.out.println(getName() + ":卖票,票号为" + tickets);
                tickets--;
            } else {
                break;
            }
        }
    }
}

/**
 * @author Rainbow
 */
public class WindowTest {
    public static void main(String[] args) {
        Window w = new Window();
        Window w1 = new Window();
        Window w2 = new Window();

        w.setName("窗口一");
        w1.setName("窗口二");
        w2.setName("窗口三");

        w.start();
        w1.start();
        w2.start();

    }
}

由此代码看出,发现有线程安全问题:理想状态下

image

极端状态:

image
image
  • 由上述代码可以看出出现了线程安全问题

  • 问题的原因:

    当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。

  • 解决办法:

    对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。即使有条线程发生了阻塞,也不能改变,也得等这条线程执行完毕,其他线程才能执行

1、Synchronized的使用方法

  • Java 对于多线程的安全问题提供了专业的解决方式 : 同步机制

    1. 同步代码块

      synchronized(同步监视器){

      ​ //需要被同步的代码

      }

    2. synchronized 还可以放在方法声明中,表示整个方法为同步方法

      public synchronized void show (){
      ….
      }

  • 关于以上名词的说明:

    <u>同步的代码</u>:操作共享数据的代码 ----------->(同步的范围)同步的代码不能被同步代码块包含多了,也不能包含少了**

    <u>同步监视器(俗称:锁)</u>:任何一个类的对象,都可以充当锁。要求:多个线程必须共同拥有一把锁

  • 同步机制中的锁和注意事项:

    1、任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)

    2、同步方法的锁:静态方法(类名.class)、非静态方法(this)

    3、同步代码块:自己指定,很多时候也是指定为this或类名.class

    注意事项:

    1、必须确保使用同一个资源的 多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全

    2、 一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)

  • 使用同步方式的优缺点:

    优点:解决了线程的安全问题。

    缺点:操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低

2、使用同步代码块的方式解决实现Runnable接口的线程安全问题

  • 代码如下:
package com.shsxt.thread;

/**
 * 在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
 * @author Rainbow
 * @date 2020/7/15 16:22
 */
class Window1 implements Runnable {
    private int ticket = 100;
    //使用同步代码块的第一种解决方式
    Object obj = new Object();

    @Override
    public void run() {
        while (true) {
//            synchronized (obj) {
           //使用同步代码块的第二种解决方式
            synchronized (this) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
//            }

        }
    }
}

public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w = new Window1();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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

3、使用同步代码块解决继承Thread类的方式的线程安全问题

package com.shsxt.thread;

/**
 * 在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
 *
 * @author Rainbow
 * @date 2020/7/15 16:33
 */
class Window2 extends Thread {
    private static int ticket = 100;
    private static Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            //正确的方式:
//            synchronized (obj) {
            synchronized (Window2.class) { //Class clazz = Window2.class,Window2.class只会加载一次
                //错误的方式:
//            synchronized (this) {//此时的this代表着t1,t2,t3三个对象
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
//            }
            }
//            }
        }
    }
}

public class WindowTest2 {
    public static void main(String[] args) {
        Window2 t1 = new Window2();
        Window2 t2 = new Window2();
        Window2 t3 = new Window2();


        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();

    }
}

4、使用同步方法解决实现Runnable接口的线程安全问题

package com.shsxt.thread;

/**
 * @author Rainbow
 * @date 2020/7/15 16:40
 */
class Window3 implements Runnable {
    private int ticket = 100;
    boolean flag = true;

    @Override
    public void run() {

        while (flag) {
            show();
        }
    }

    private synchronized void show() {//同步监视器:this
        //相当于下面的方式
//        synchronized (this) {
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        } else {
            flag = false;
        }
//        }
    }
}

public class WindowTest3 {
    public static void main(String[] args) {
        Window3 w = new Window3();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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

5、使用同步方法处理继承Thread类的方式中的线程安全问题

package com.shsxt.thread;


/**
 * 使用同步方法处理继承Thread类的方式中的线程安全问题
 * @author Rainbow
 * @date 2020/7/15 16:59
 */
class Window4 extends Thread {
    static boolean flag = true;
    private static int ticket = 100;


    @Override
    public void run() {

        while (flag) {
            show();
        }
    }

    private static synchronized void show() {//同步监视器:Window4.class
        //没有static关键字修饰,此时的同步监视器为:t1,t2,t3;此种解决方式是错误的
//        private synchronized void show(){
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        } else {
            flag = false;
        }
//        }

    }
}

public class WindowTest4 {
    public static void main(String[] args) {
        Window4 t1 = new Window4();
        Window4 t2 = new Window4();
        Window4 t3 = new Window4();


        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();

    }
}

6、单例设计模式之懒汉式( 线程安全)

package com.shsxt.thread;

/**
 * 单例线程安全的懒汉模式
 * @author Rainbow
 * @date 2020/7/15 17:08
 */
class Singleton {
    private static Singleton instance = null;

    private Singleton() {

    }

    public static Singleton getInstance() {

        //方式一、效率低
//        synchronized (Singleton.class) {
//            if (instance == null) {
//                instance = new Singleton();
//            }
//            return instance;
//        }

        //方式二、效率高
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

public class SingletonTest {
    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        Singleton instance3 = Singleton.getInstance();

        System.out.println(instance);
        System.out.println(instance1);
        System.out.println(instance2);
        System.out.println(instance3);

    }

}

五、线程的死锁问题

  • 死锁问题的产生

    1. 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
    2. 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
  • 解决方法

    用专门的算法、原则

    尽量减少同步资源的定义

    尽量避免嵌套同步

  • 案例

    package com.shnsxt.thread;
    
    /**
     * @author Rainbow
     * @date 2020/7/15 21:28
     */
    public class ThreadTest {
        public static void main(String[] args) {
            StringBuffer s1 = new StringBuffer();
            StringBuffer s2 = new StringBuffer();
    
            new Thread(){
                @Override
                public void run() {
                    synchronized (s1){
                        s1.append("a");
                        s2.append("1");
    
                        //增大产生死锁的几率
                        try {
                            sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                        synchronized (s2){
                            s1.append("b");
                            s2.append("2");
    
                            System.out.println(s1);
                            System.out.println(s2);
                        }
                    }
    
                }
            }.start();
    
    
            new Thread(){
                @Override
                public void run() {
                    synchronized (s2){
                        s1.append("c");
                        s2.append("3");
                    }
    
                    try {
                        sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                    synchronized (s1){
                        s1.append("d");
                        s2.append("4");
    
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }.start();
    
        }
    }
    

六、创建线程的第三种方式:Lock锁(JDK5.0提供)

  • 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
  • ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

1、Synchronized与Lock的异同?

  • 相同点:

    二者都可以解决线程安全问题

  • 不同点:

    1、synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器

    2、Lock需要手动的启动同步(lock()方法)紧跟try代码块同时结束同步也需要手动的实现(unlock()方法)且必须放在finally的首行

  • 优先使用顺序

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

使用Lock锁的案例:

package com.shnsxt.thread;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Rainbow
 * @date 2020/7/15 21:41
 */
class Window implements Runnable {
    private int ticket = 100;
    //1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                //2.调用锁定方法lock()
                lock.lock();

                if (ticket > 0) {

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

                    System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }

            } finally {
                //3.调用解锁方法:unlock(),且必须放在finally的首行
                lock.unlock();

            }
        }
    }
}

public class LockTest {
    public static void main(String[] args) {
        Window w = new Window();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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

七、对以上知识点的一个小练习

package com.shnsxt.thread;

/**
 *  银行有一个账户。
 *  有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
 * @author Rainbow
 * @date 2020/7/15 21:52
 */

class Account{
    private double balance;

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

    //存钱
    public synchronized void deposit(double AMB) {//同步监视器:this;虽然说使用继承Thread类慎用this,但是此处的this不代表Customer,而是Account
        if (AMB>0){
            balance+=AMB;

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

            System.out.println(Thread.currentThread().getName() + ":存钱成功。余额为:" + balance);
        }
    }
}
//储户
class Customer extends Thread{
    private Account account;

    public Customer(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        for (int i = 0; i < 3 ; i++) {
            account.deposit(1000);
        }
    }
}
public class AccountTest {
    public static void main(String[] args) {
        Account account = new Account(0);
        Customer customer = new Customer(account);
        Customer customer1 = new Customer(account);

        customer.setName("甲");
        customer1.setName("已");

        customer.start();
        customer1.start();

    }
}

package com.shnsxt.thread;

/**
 * @author Rainbow
 * @date 2020/7/16 9:37
 */

class Blank {
    private String accountId;
    private double balance;

    public Blank(String accountId, double balance) {
        this.accountId = accountId;
        this.balance = balance;
    }

    public double getBalance() {
        return balance;
    }

    public String getAccountId() {
        return accountId;
    }

    public void setAccountId(String accountId) {
        this.accountId = accountId;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    @Override
    public String toString() {
        return "Blank{" +
                "accountId='" + accountId + '\'' +
                ", balance=" + balance +
                '}';
    }
}

class DrawThread extends Thread {
    private Blank blank;
    //取款额度
    private double money;

    public DrawThread(String name, Blank blank, double money) {
        super(name);
        this.blank = blank;
        this.money = money;
    }

    @Override
    public void run() {
        synchronized (blank) {
            if (blank.getBalance() > money) {
                System.out.println(Thread.currentThread().getName() + ":取款成功," + "取现的金额为" + money);

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

                blank.setBalance(blank.getBalance() - money);

            } else {
                System.out.println("取现额度超过账户余额,取款失败");
            }
        }
        System.out.println(blank.getAccountId() + "账户的余额为:" + blank.getBalance());
    }
}

public class DrawThreadTest {

    public static void main(String[] args) {
        Blank blank = new Blank("中国银行", 100000.00);

        DrawThread d1 = new DrawThread("张三", blank, 4000);
        DrawThread d2 = new DrawThread("李四", blank, 5000);
        DrawThread d3 = new DrawThread("王五", blank, 8000);

        d1.start();
        d2.start();
        d3.start();
    }

}

八、线程的通信

  • 首先先给个案例,先理解下:使用两个线程印 打印 1-100 。线程1, 线程2 交替打印
package com.shnsxt.thread;

/**
 * @author Rainbow
 * @date 2020/7/15 22:07
 */

class Number implements Runnable {
    private int number = 1;
    private Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (number <= 100) {

                    //唤醒被wait的一个线程。
                    obj.notify();

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

                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;

                    try {
                        //使得调用如下wait()方法的线程进入阻塞状态
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    break;
                }
            }
        }
    }
}

public class CommunicationTest {
    public static void main(String[] args) {
        Number number = new Number();
        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);

        t1.setName("线程1");
        t2.setName("线程2");

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

  • 图解:
image

1、关于上述代码中涉及到三个方法:wait() 与 notify() 和 notifyAll()

  • wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器(锁)。
  • notify():一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级高的那个。
  • notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。

2、关于这三个方法的注意点:

  • wait()notify()notifyAll()三个方法必须使用在同步代码块或同步方法中
  • wait()notify()notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器(锁)。否则,会出现IllegalMonitorStateException异常
  • wait()notify()notifyAll()三个方法是定义在java.lang.Object类中。

3、sleep()和wait()方法的异同(面试题)

  • 相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。

  • 不同点:

    1、两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()

    2、调用的要求(范围)不同:sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块同步方法中

    3、关于是否释放同步监视器(锁):如果两个方法都使用在同步代码块或同步方法中,<u>sleep()不会释放锁</u><u>wait()会释放锁</u>**。

九、关于线程通信问题总结小练习(消费者与生产者)

package com.shnsxt.thread;

/**
 * 经典例题:生产者/消费者的问题
 *
 * @author Rainbow
 * @date 2020/7/16 9:07
 */
class Clerk {
    private int productCount = 0;

    /**
     * 生产产品
     */
    public synchronized void produceProduct() {
        if (productCount < 20) {
            productCount++;
            System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "产品");

            //线程唤醒
            this.notify();

        } else {

            //线程等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 消费产品
     */
    public synchronized void consumerProduct() {
        if (productCount > 0) {
            System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "产品");
            productCount--;

            //线程唤醒
            this.notify();

        } else {
            //线程等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * 生产者
 */
class Producer extends Thread {
    private Clerk clerk;

    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(getName() + ": 开始生产产品......");
        while (true) {

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

            //生产产品
            clerk.produceProduct();
        }

    }
}

/**
 * 消费者
 */
class Consumer extends Thread {
    private Clerk clerk;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(getName() + ": 开始消费产品......");

        while (true) {

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

            //消费产品
            clerk.consumerProduct();
        }
    }
}

public class ProducetTest {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();

        //生产者
        Producer p1 = new Producer(clerk);
        p1.setName("生产者");
        //消费者
        Consumer c1 = new Consumer(clerk);
        c1.setName("消费者1");
        Consumer c2 = new Consumer(clerk);
        c2.setName("消费者2");

        p1.start();
        c1.start();
        c2.start();
    }

}

十、创建线程的第四种方式之一:实现Callable接口(JDK5.0新增)

  • 与使用Runnable相比, Callable功能更强大些

    1、 相比run()方法,可以有返回值

    2、 方法可以抛出异常

    3、支持泛型的返回值

    4、需要借助FutureTask类,比如获取返回结果

  • Future接口

    1、 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。

    2、FutrueTask是Futrue接口的唯一的实现类

    3、 FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值

代码理解,如下所示:

package com.shnsxt.thread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * 创建线程的方式三:实现Callable接口。 --- JDK 5.0新增
 * 1. call()可以有返回值的。
 * 2. call()可以抛出异常,被外面的操作捕获,获取异常的信息
 * 3. Callable是支持泛型的
 *
 * @author Rainbow
 * @date 2020/7/16 10:31
 */
//1.创建一个实现Callable的实现类
class NumThread implements Callable<Integer> {

    //2.实现call方法,将此线程需要执行的操作声明在call()方法中
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                sum += i;
            }
        }
        return sum;
    }
}

public class ThreadCallable {
    public static void main(String[] args) {
        //3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();

        //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask<Integer> futureTask = new FutureTask(numThread);

        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        new Thread(futureTask).start();

        try {
            //6.获取Callable中call方法的返回值
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
            Integer sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

十一、创建线程的第四种方式之二:使用线程池(JDK5.0新增)

  • 为什么要用线程池?

    经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

  • 使用线程池的好处

    1、提高响应速度(减少了创建新线程的时间)
    2、降低资源消耗(重复利用线程池中线程,不需要每次都创建)

    便于线程的管理:

    1、corePoolSize:核心池的大小

    2、maximumPoolSize:最大线程数

    3、keepAliveTime:线程没有任务时最多保持多长时间后会终止

  • 线程池相关的API

    JDK 5.0起提供了线程池相关API:ExecutorServiceExecutors

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

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

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

    1. Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池,<u>主要的问题是线程数最大数Integer.MAX_VALUE,可能会创建数量非常多的线程</u>
    2. Executors.newFixedThreadPool(n): 创建一个可重用固定线程数的线程池,主要问题是堆积的请求处理队列可能会损耗非常大的内存
    3. Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池,主要问题是堆积的请求处理队列可能会损耗非常大的内存
    4. Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。<u>主要的问题是线程数最大数Integer.MAX_VALUE,可能会创建数量非常多的线程</u>

代码理解,如下所示:

package com.shnsxt.thread;

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


/**
 * 创建线程的方式四之二:使用线程池
 * 好处:
 * 1.提高响应速度(减少了创建新线程的时间)
 * 2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
 * 3.便于线程管理
 * corePoolSize:核心池的大小
 * maximumPoolSize:最大线程数
 * keepAliveTime:线程没有任务时最多保持多长时间后会终止
 *
 * @author Rainbow
 * @date 2020/7/16 11:08
 */
class NumberThread implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

class NumberThread1 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

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

        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);//这里可能会出现上面写出的问题
        ThreadPoolExecutor pool = (ThreadPoolExecutor) service;
        //设置线程池的属性
//        pool.setCorePoolSize(15);
//        pool.setKeepAliveTime(60,MINUTES);

        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于Runnable
        service.execute(new NumberThread1());//适合适用于Runnable

//        service.submit(Callable callable);//适合使用于Callable

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

<u>如果,此文章对你有所帮助,请帮忙点个赞呗! 谢谢!!!</u>

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