多线程

多线程

1.并行与并发概念

​ 并发是指cpu在一段之间内交替执行多个任务.

​ 并行是指cpu在一个时刻同时执行多个任务

2.进程与线程概念

进程:是指在内存中运行的应用程序,每一个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程.

线程:是指进程中的一个执行单元,负责当前线程中程序的执行,一个进程至少有一个线程.

cpu:中央处理器,对数据进行计算,只会电脑中软件和硬件干活.例如:电脑管家点击运行就会进入内存中,就是一个进程.点击不同的功能,就会开启一条应用程序到cpu的执行路径中,就叫线程.

线程是属于进程的,是进程中的一个执行单元,负责程序的执行.

线程调度:

​ 分时调度:轮流使用cpu,平均分配使用时间.

抢占式调度:优先让优先级高的线程使用cpu,优先级相同则随机选择一个.java用的就是抢占式的方式.

3.单线程演示

主线程:执行(main)方法的线程  
单线程程序:java程序中只有一个线程,从main方法开始,从上到下依次执行
public class demoThread01 { 
    public static void main(String[] args) {   
        Person person = new Person("小强");    
        person.run();       
        Person person2 = new Person("小哥");    
        person2.run();   
    }
}
public class Person {   
    private String name;  
    public void run() {  
        for (int i = 0; i < 20; i++) { 
            System.out.println(name+i);  
        }  
    }    
    public String getName() {  
        return name; 
    }   
    public void setName(String name) {
        this.name = name;  
    }    
    public Person() {
    }  
    public Person(String name) { 
        this.name = name;
    }
}

单线程弊端,一旦抛出异常后续任务就不会再执行了.

4.创建多线程程序

4.1 实现Thread类

 /**
 * 创建多线程程序的第一种方式:
 *      创建实现java.lang.Thread类的子类
 */
public class demoThread02 extends Thread{
    //1.继承Thread类
    //2.重写run()的执行方法
    //3.创建对象
    //4.调用start()方法开启新的线程,执行run的代码
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(i);
        }
    }

    public static void main(String[] args) {
        demoThread02 demoThread02 = new demoThread02();
        demoThread02.start();
        for (int i = 0; i < 20; i++) {
            System.out.println("main"+i);
        }
    }
}

4.2 实现Runable接口,重写run方法

public class demoThread04 implements Runnable{

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

    public static void main(String[] args) {
        demoThread04 thread04 = new demoThread04();
        new Thread(thread04).start();
    }
}

步骤: 1.实现runable接口,重写run方法

​ 2.创建类对象,再创建Thread对象将runable对象传入,再调用start()开启.

4.3 匿名内部类方式创建

/**
 * 匿名内部类方式创建多线程
 */
public class demoThread05 {
    public static void main(String[] args) {
        //方式1
        new Thread(){
            @Override
            public void run() {
                System.out.println("哈哈");
            }
        }.start();
		//方式2
        new Thread( new Runnable(){
            @Override
            public void run() {
                System.out.println("嘿嘿");
            }
        }).start();
    }
}

区别:

​ 实现runable接口创建的好处,避免了单继承的局限性(一个类只能继承一个类),实现了runable之后还可以继承或者实现其他类.

​ 降低了耦合性,将设置线程和开启线程进行分离.传递不同的实现类,实现不同的任务.

5.多线程内存原理

主方法和run方法都会压栈执行,但是调用start()方法则不同,会开辟一个新的栈空间,这样就可以让cpu自己选择执行的线程.

6.Thread类API

6.1 getName()

​ 返回该线程的名称

6.2 currentThread()

​ 获取当前正在执行的线程

6.3 设置线程名称

6.3.1 setName()

6.3.2 创建一个带参的构造方法,调用父类的构造方法,将参数传递过去

6.4 sleep()

public static void main(String[] args) throws InterruptedException { 
    for (int i = 0; i <=60; i++) { 
        System.out.println(i); 
        Thread.sleep(1000L);
    }
}

设置线程休眠时间.静态方法,可直接类名.调用

7.线程安全

安全问题: 多线程访问了共享的数据,会产生线程安全问题.

7.1解决问题:

方式一:同步代码块

synchronized(同步锁){
    需要同步操作的代码
}

同步锁: 只是一个概念,可以认为在对象上标记了一个锁,锁对象可以是任何类型

任何时候最多允许一个线程拥有同步锁进入代码块,其他的线程只能等待.

/**
 * 卖票安全问题
 *  同步代码块解决
 */
public class demoThread06 implements Runnable{
        Object obj =new Object();
        private int ticket = 100;
        @Override
        public void run () {
        while (true) {
           synchronized (obj){
               if (ticket > 0) {
                   try {
                       Thread.sleep(10);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   System.out.println(Thread.currentThread().getName() + "-->正在卖第" + ticket + "张票");
                   ticket--;
               }
           }
        }
    }
}

程序频繁的判断锁和调用锁,执行效率会变低.

方式二:同步方法

1.把访问了共享数据的代码抽取出来,放到一个方法中*  
2.在方法上增加synchronized修饰符
public class demoThread07 implements Runnable{
    private int ticket = 100;
    @Override
    public void run () {
        while (true) {
                payTicked();
            }
    }

    /*
    定义同步方法
     */
    public synchronized void payTicked() {
        if (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "-->正在卖第" + ticket + "张票");
            ticket--;
        }
    }
}

同步方法的锁对象其实就是runable也就是this.

方式三:LOCK锁

jdk1.5之后有了LOCK接口,提供了比synchronized方法和语句更广泛的锁定操作.

API:

​ lock() 获取锁

​ unlock() 释放锁

使用步骤:

​ 1.在成员位置创建一个lock接口的的实现类reentrantLock类对象

​ 2.在可能出现安全问题的代码钱调用lock()方法,获取锁

​ 3.在代码后调用unlock()方法释放锁

public class demoThread08 implements Runnable{
    private int ticket = 100;

   Lock l= new ReentrantLock();
    @Override
    public void run () {
        while (true) {
            l.lock();
                if (ticket > 0) {
                    try {
                        Thread.sleep(10);
                        System.out.println(Thread.currentThread().getName() + "-->正在卖第" + ticket + "张票");
                        ticket--;
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        //无论怎样都会蒋锁释放
                        l.unlock();
                    }
                }

            }
    }
}

8.线程状态

1569333446887

8.1 线程状态概述

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,

有几种状态呢?在API中 java.lang.Thread.State 这个枚举中给出了六种线程状态:

这里先列出各个线程状态发生的条件,下面将会对每种状态进行详细解析

1569333470606

我们不需要去研究这几种状态的实现原理,我们只需知道在做线程操作中存在这样的状态。那我们怎么去理解这几个状态呢,新建与被终止还是很容易理解的,我们就研究一下线程从Runnable(可运行)状态与非运行状态之间 的转换问题。

8.2 Timed Waiting(计时等待)

Timed Waiting在API中的描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。单独的去理解这句话,真是玄之又玄,其实我们在之前的操作中已经接触过这个状态了,在哪里呢?

在我们写卖票的案例中,为了减少线程执行太快,现象不明显等问题,我们在run方法中添加了sleep语句,这样就 强制当前正在执行的线程休眠(暂停执行),以“减慢线程”。

其实当我们调用了sleep方法之后,当前执行的线程就进入到“休眠状态”,其实就是所谓的Timed Waiting(计时等 待),那么我们通过一个案例加深对该状态的一个理解。

实现一个计数器,计数到100,在每个数字之间暂停1秒,每隔10个数字输出一个字符串

代码:

public class MyThread extends Thread {
public void run() {
for (int i = 0; i < 100; i++) {
if ((i) % 10 == 0) {
System.out.println("‐‐‐‐‐‐‐" + i);
    }
System.out.print(i);
try {
Thread.sleep(1000);
System.out.print(" 线程睡眠1秒!\n");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new MyThread().start();
}
}

通过案例可以发现,sleep方法的使用还是很简单的。我们需要记住下面几点:

  1. 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协作关系。

  2. 为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程 中会睡眠

  3. sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态。

小提示:sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就开始立刻执行。

8.3 BLOCKED(锁阻塞)

Blocked状态在API中的介绍为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。

我们已经学完同步机制,那么这个状态是非常好理解的了。比如,线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。

这是由Runnable状态进入Blocked状态。除此Waiting以及Time Waiting状态也会在某种情况下进入阻塞状态,而这部分内容作为扩充知识点带领大家了解一下。

8.4 Waiting(无限等待)

Wating状态在API中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。 那么我们之前遇到过这种状态吗?答案是并没有,但并不妨碍我们进行一个简单深入的了解。我们通过一段代码来 学习一下:

/**
 * 等待唤醒案例,
 *  创建一个顾客线程(消费者),告知老板要的的包子的种类和数量,调用wait方法,fangqicpu
 *  执行,进入无线等待状态,
 *  创建老板进程(生产者):花了5秒做包子,做好之后调用notify方法,唤醒顾客吃包子
 *
 *  注意:顾客和老板 要用同步代码块包裹,保证等待和唤醒只能有一个再执行
 *  同步使用的所对象必须保证唯一
 *  只有锁对象才能调用wait和notify方法
 */
public class demoThread09 {
    public static void main(String[] args) {
        //创建锁对象
        Object obj=new Object();
        new Thread(){
            @Override
            public void run() {
                synchronized (obj){
                    try {
                    System.out.println("告知老板要的的包子的种类和数量");
                        obj.wait();
                        System.out.println("包子已经做好了");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj) {
                    System.out.println("老板5秒之后做好包子,告知顾客可以吃包子了");
                    obj.notify();
                }
            }
        }.start();
    }
}

通过上述案例我们会发现,一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的Object.notify()方法 或 Object.notifyAll()方法。

其实waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,

多个线程会争取锁,同时相互之间又存在协作关系。就好比在公司里你和你的同事们,你们可能存在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。

当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法那么A线程就进入 了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了notify()方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入

Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。

9.等待唤醒机制

9.1 线程间通信

概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。

比如:线程A用来生成包子的,线程B用来吃包子的,包子可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题。

为什么要处理线程间通信:

多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。

如何保证线程间通信有效利用资源:

多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。

9.2 等待唤醒机制

什么是等待唤醒机制

这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race,比如去争夺锁,但这并不是 故事的全部,线程间也会有协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时 候你们更多是一起合作以完成某些任务。

就是在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。

wait/notify 就是线程间的一种协作机制。

等待唤醒中的方法

等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:

  1. wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify”在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中

  2. notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。

  3. notifyAll:则释放所通知对象的 wait set 上的全部线程。

注意:

哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而 此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。

总结如下:

如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;

否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态

调用wait和notify方法需要注意的细节

  1. wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。

  2. wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。

  3. wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。

案例:

package com.hr.demo01.Thread;
//包子类
public class Baozi {
        String pi;
        String xian;
        Boolean status=false;

}

package com.hr.demo01.Thread;

/**
 * 包子铺类
 */
public class BaoZiShop extends Thread{
    private Baozi bz;

    public BaoZiShop(Baozi bz) {
        this.bz = bz;
    }
    public void run() {
        int count = 0;
        while (true) {
            //包子铺线程
            synchronized (bz) {
                if (bz.status == true) {
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if (count % 2 == 0) {
                    //生产薄皮三鲜馅包子
                    bz.pi = "薄皮";
                    bz.xian = "三鲜馅";
                } else {
                    //生产冰皮牛肉馅包子
                    bz.pi = "冰皮";
                    bz.xian = "牛肉馅";
                }
                count++;
                System.out.println("包子铺正在生产:" + bz.pi + bz.xian + "包子");
                //生产包子需要3秒
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                bz.status = true;
                //唤醒吃包子
                bz.notify();
                System.out.println("包子铺已经生产好了:" + bz.pi + bz.xian + "包子,可以开吃了");
            }
        }
    }
}

package com.hr.demo01.Thread;

/**
 * 消费者
 * 吃包子类
 */
public class Chibaozi extends Thread {
    private Baozi bz;

    public Chibaozi(Baozi bz) {
        this.bz = bz;
    }

    @Override
    public void run() {
        //吃线程
        while (true){
            synchronized (bz) {
                if (bz.status==false){
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                //被唤醒之后
                System.out.println("吃货正在吃:"+bz.pi+bz.xian+"包子");
                bz.status=false;
                bz.notify();
                System.out.println("吃货已经把:"+bz.pi+bz.xian+"的包子吃完了,包子铺开始生产包子");
                System.out.println("---------------------------------------------------------------------");
            }
        }
    }
}

package com.hr.demo01.Thread;

/**
 * 包子案例测试类
 */
public class BaoziMain {
    public static void main(String[] args) {
        Baozi bz =new Baozi();
        new BaoZiShop(bz).start();
        new Chibaozi(bz).start();

    }
}

10.线程池

10.1 线程池思想概述

我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间。 那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务? 在Java中可以通过线程池来达到这样的效果。今天我们就来详细讲解一下Java的线程池。

10.2 线程池概念

线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作, 无需反复创建线程而消耗过多资源。

由于线程池中有很多操作都是与优化资源相关的,我们在这里就不多赘述。我们通过一张图来了解线程池的工作原 理:

1569333495593

合理利用线程池能够带来三个好处:

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

10.3 线程池的使用

Java里面线程池的顶级接口是 java.util.concurrent.Executor ,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 java.util.concurrent.ExecutorService。

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。

Executors类中有个创建线程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int Threads) :返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:

  • public Future submit(Runnable task) :获取线程池中的某一个线程对象,并执行

​ Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。

使用线程池中线程对象的步骤:

  1. 创建线程池对象。

  2. 创建Runnable接口子类对象。(task)

  3. 提交Runnable接口子类对象。(take task)

  4. 关闭线程池(一般不做)。

Runnable实现类代码:

public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("我要一个教练");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("教练来了: " + Thread.currentThread().getName());
System.out.println("教我游泳,交完后,教练回到了游泳池");
}
}

线程池测试类:

public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
// 创建Runnable实例对象
MyRunnable r = new MyRunnable();
//自己创建线程对象的方式
// Thread t = new Thread(r);
// t.start(); ‐‐‐> 调用MyRunnable中的run()
// 从线程池中获取线程对象,然后调用MyRunnable中的run()
service.submit(r);
// 再获取个线程对象,调用MyRunnable中的run()
service.submit(r);
service.submit(r);
// 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
// 将使用完的线程又归还到了线程池中
// 关闭线程池
//service.shutdown();
}
}

HuangRui

Every man dies, not every man really lives.

HaungRui, China suixinblog.cn