首页 > 编程语言 > 详细

Java 多线程

时间:2021-03-07 22:04:15      阅读:28      评论:0      收藏:0      [点我收藏+]

Java 多线程

一、并发与并行

  • 并发:指两个或多个事件在同一时间段内发生。(交替执行)
  • 并行:指两个或多个事件在同一时刻发生。(同时发生)

即,并发就是你洗完澡后再听歌,并行就是你一边洗澡一边听歌。

二、进程与线程

(线程<进程)

  • 进程:程序的执行过程。(可在任务管理器查看)
  • 线程:进程中的一个执行单元。

一个程序运行后至少有一个进程,一个进程中可以包含多个线程。

举例:Word的使用。
	每次打开一个Word就相当于启动了一个进程,
	在这个进程上又有许多其他程序在执行(例如:拼写检查,自动更正等),这些就是一个个线程。
	如果Word关闭了,这些线程就会全部消失。但是如果这些线程消失了,Word不一定会消失。

线程一定得依附于进程才能够存在。
“同时”执行是线程给人的感觉,在线程之间实际上是轮换执行。

三、线程调度

  1. 分时调度:
    所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。
  2. 抢占式调度:
    优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性)。java使用的为抢占式调度。

设置线程的优先级:
打开任务管理器>选择希望设置优先级的进程>右键>转到详细信息>设置优先级

四、多线程的实现

在Java中要想实现多线程的程序,必须依靠一个线程的主体类,即主线程(执行主方法(main)的线程)。然后此类继承Thread类或实现Runnable接口。

1)继承Thread类

java.lang.Thread是操作线程的类,任何类只需要继承Thread类就可以成为一个线程的主类。

Thread类下的两个重要方法:run()start()方法。

  • 线程执行体:run()。(线程需要完成的任务)
  • 线程的起点:start()。(线程的启动,启动后执行的方法体是run()方法定义的代码)
  • 程序的起点:main()。
//1.创建一个Thread类的子类
public class MyThread extends Thread{
    //2.重写Thread类中的run方法,设置线程任务
    @Override
    public void run(){
        for (int i = 0; i < 20; i++) {
            System.out.println("run:"+i);
        }
    }
}
public class doMain {
    public static void main(String[] args) {
        //3.创建Thread类的子类对象
        MyThread mt = new MyThread();
        //4.调用Thread类中的start(),开启新的线程,执行run()
        mt.start();

        for (int i = 0; i < 20; i++) {
            System.out.println("main:"+i);
        }
    }
}

Thread类的常用方法:

getName() 取得线程名字
setName() 设置线程名字
currentThread() 取得线程名字
sleep(long millitime) 使当前正在执行的程序以指定的毫秒数暂定

2)实现Runnable接口:

为了避免单继承局限的问题,我们可以使用Runnable接口来实现多线程。
要启动多线程,就一定需要通过Thread类中的start()方法,但是Runnable接口中没有提供可以被继承的start()方法。这时就需要借住Thread类中提供的有参构造方法:

**public Thread(Runnable target) ** // 此方法可以接收一个Runnable接口对象

// 创建一个Runnable的实现接口类
public class RunnableImpl implements Runnable{
    // 重写run方法,设置线程任务
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"-->"+"RunnableImpl");
        }
    }
}
public class RunnableDemo {
    public static void main(String[] args) {
        // 创建一个Runnable接口实现类对象
        RunnableImpl run = new RunnableImpl();
        Thread thread = new Thread(run);
        thread.start();

        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+">--"+"a");
        }
    }
}

3)实现Runnable接口的好处:

①避免单继承局限;
②降低程序的耦合性,方便解耦。即把设置线程任务(实现类中重写run())和开启新线程(Thread类对象调用start())进行了分离。

4)多线程的两种实现方式及区别:

●它们的实现都需要一个线程的主类,都必须在子类中覆写run()方法,都必须调用Thread类中的start()方法来开启线程。

Thread类是Runnable接口的子类,而使用Runnable接口可以避免单继承局限,方便解耦,并且可以更加方便地实现数据共享的概念。

public class Thread extends Object implements Runnable

●它们的结构:

Runnable接口 Thread类
class MyThread implements Runnable{} class MyThread extends Thread{}
new Thread(mt).start(); mt.start();

5)使用匿名内部类实现多线程的创建:

ublic class doMain {
    public static void main(String[] args) {
        //1.线程的父类是Thread
        new Thread(){
            @Override
            public void run(){
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+">--"+"a");
                }
            }
        }.start();

        //2.线程的接口Runnable
        new Thread(new Runnable(){
                @Override
                public void run() {
                    for (int i = 0; i < 20; i++) {
                        System.out.println(Thread.currentThread().getName()+"-->"+"b");
                    }
                }
            }).start();
    }
}

五、线程安全问题

先来解释不同步遇到的问题:
如果分成三个窗口卖100张票,假如不同步的话,就有可能出现三个窗口卖重票、错票的情况。(多个线程操作同一资源可能出现的情况,因为前面的线程还没完成操作,其它线程也进来操作车票。(抢占))

实现三个窗口来卖票的程序:

//实现卖票程序
public class RunnableImpl1 implements Runnable{
    //定义一个多线程共享的票源
    private int ticket = 10;
    //设置线程任务:卖票
    @Override
    public void run() {
        //先判断票是否存在
        while(true){
            if (ticket>0) {
                //提高安全问题出现概率
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //票存在,卖票
                System.out.println(Thread.currentThread().getName() + "->>正在卖第" + ticket + "张票");
                ticket--;
            }
        }
    }
}
public class doMain {
    public static void main(String[] args) {
        RunnableImpl1 run = new RunnableImpl1();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }
}

运行结果:

Thread-1->>正在卖第10张票
Thread-0->>正在卖第10张票
Thread-2->>正在卖第10张票
Thread-2->>正在卖第7张票
Thread-1->>正在卖第7张票
Thread-0->>正在卖第7张票
Thread-1->>正在卖第4张票
Thread-0->>正在卖第4张票
Thread-2->>正在卖第4张票
Thread-1->>正在卖第1张票
Thread-2->>正在卖第1张票
Thread-0->>正在卖第1张票

解决方法:通过同步操作来解决

六、同步操作

同步操作:一个代码块中的多个操作在同一时间段内只能由一个线程进行,其他线程要等待此线程完成后才可以继续执行。

1)同步代码块

synchronized(this){
      //需要被同步操作的代码
}

关于this的解释:

  1. 在实现Runnable接口创建多线程的方式中,我们可以使用this充当所,代替手动new一个对象,因为后面我们只创建一个线程的对象。
  2. 在继承Thread类创建多线程的方式中,慎用this,考虑我们的this是不是唯一的。我们可以使用当前类来充当这个是锁。synchronized (类名.class)

使用同步代码块完成同步操作: 主要有变化的在run()方法里,doMain类不变。

注意

  • 通过代码块中的锁对象,可以使用任意的对象
  • 但是必须保证多个线程使用的锁对象是同一个
  • 锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行
//实现卖票程序
public class RunnableImpl1 implements Runnable{
    //定义一个多线程共享的票源
    private int ticket = 100;
    //设置线程任务:卖票
    @Override
    public void run() {
        while(true){
        //先判断票是否存在
            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 doMain {
    public static void main(String[] args) {
        RunnableImpl1 run = new RunnableImpl1();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }
}

2)同步方法

利用synchronized定义的方法。

//实现卖票程序
public class RunnableImpl1 implements Runnable{
    //定义一个多线程共享的票源
    private int ticket = 100;
    //设置线程任务:卖票
    @Override
    public void run() {
        while(true){
            sale();
        }
    }
    public synchronized void sale(){
        //先判断票是否存在
        synchronized (this){
            if (ticket>0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //票存在,卖票
                System.out.println(Thread.currentThread().getName() + "->>正在卖第" + ticket + "张票");
                ticket--;
            }
        }
    }
}

补充:

  1. 非静态同步方法,锁对象:this
  2. 静态同步方法,锁对象:RunnableImpl.class

3)Lock锁

java.util.concurrent.locks.Lock接口
Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。

Lock接口中的方法:

void Lock() 获取锁
void unlock() 释放锁

Lock接口的实现类:ReentrantLock

java.util.concurrent.locks.ReentrantLock implements Lock

使用Lock锁完成同步操作:
三步走。1.创建ReentrantLock对象 2.获取锁 3.释放锁

//实现卖票程序
public class RunnableImpl1 implements Runnable{
    //定义一个多线程共享的票源
    private int ticket = 100;
    //1.创建ReentrantLock对象
    Lock lk = new ReentrantLock();
    //设置线程任务:卖票
    @Override
    public void run() {
        while(true){
            //2.在可能出现线程安全的代码前调用Lock接口中的Lock方法获取锁.
            lk.lock();
            if (ticket>0) {
                try {
                    Thread.sleep(10);
                    //票存在,卖票
                    System.out.println(Thread.currentThread().getName() + "->>正在卖第" + ticket + "张票");
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //3.在可能出现线程安全的代码后释放锁。
                    lk.unlock();
                }
            }
        }
    }
}

4)比较 synchronized 与 Lock:

  1. synchronized是自动释放锁(显示),lock需要手动释放和关闭锁(隐式)。
  2. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。
  3. Lock只有代码块锁,synchronized有代码块锁和方法锁。

5)总结:

加入同步后明显比不加入同步慢许多,所以同步的代码性能低,但是数据安全性高。

6)常见面试题分析

  1. 同步和异步有什么区别。什么情况下使用?
    如果一块数据要在多个线程间共享,则必须进行同步存取。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,那么就应该用异步编程,在很多情况下采用异步途径往往更有效率。

  2. abstract的method是否可以同时是static,是否可以同时是native、synchronized?
    method、static、native、synchronized都不能和“abstract”同时声明方法。

  3. 当一个线程进入一个对象的synchronized方法后,其他线程是否可访问此对象的其他方法?
    不能访问,一个对象操作一个synchronized方法只能由一个线程访问。(其他线程等待)

7)死锁:

死锁就是指两个线程都在等待彼此先完成,造成了程序的停滞状态。(过多的同步操作带来的问题)

举例说明:小张想要小李的画,小李想要小张的书,小张说你把书给我我就给你画,小李说你把画给我我就给你书。这样下去的结果可想而知,谁都得不到画/书。这实际上就是死锁的概念。

七、线程状态

1)线程状态概述

  1. 创建状态
    新线程对象处于新建状态时
  2. 就绪状态
    新建线程对象后,调用该线程的start()方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待CPU服务,这表明它已经具备了运行条件。
  3. 运行状态
    当就绪的线程被调度并获得CPU资源时,便进入运行状态。此时,自动调用该线程对象的run()方法,run()方法定义了线程的操作和功能
  4. 堵塞状态
    在某种特殊情况下,被人为挂起或执行输入输出操作时,将让出 CPU 并临时中止自己的执行,进入阻塞状态。在可执行状态下,如果调用sleep()、suspend()、wait()等方法,线程都将进入堵塞状态。堵塞时,线程不能进入排队队列,只有当引起堵塞的原因被消除后,线程才可以进入就绪状态。
  5. 终止状态
    线程调用stop()或run()方法执行结束后,就处于终止状态。处于终止状态的线程不具有继续运行的功能。
线程状态 导致状态发生条件
NEW(新建) 线程刚被创建,但是并未启动。还没调用start方法。
Runnable(可运行) 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。
Blocked(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
TimedWaiting(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

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

2)唤醒等待机制

  • void wait( )
    • 在其他线程调用此对象的notify() 方法或notifyAll() 方法前,导致当前线程等待。
  • void wait(long time)
    • 等待一段时间以后,自动唤醒线程
  • void notify()
    • 唤醒在此对象监视器上等待的单个线程,会继续执行wait方法之后的代码
    • 只能随机唤醒一个线程
  • void notifyAll()
    • 如果有多个等待的线程,那么就唤醒全部线程
/*
    等待唤醒案例:线程之间的通信
        创建一个顾客线程(消费者):告知老板要的包子的种类和数量调用wait方法,放弃cpu的执行,进入到NWAITING状态(无限等待)
        创建一个老板线程(生产者):花了5秒做包子,做好包子之后,调用notify方法,唤醒顾客吃包子
    注意:
        顾客和老板线程必须使用同步代码块包裏起来,保证等待和唤醒只能有一-个在执行
        同步使用的锁对象必须保证唯一
        只有锁对象才能调用wait和notify方法

    Obejct类中的方法
        void wait( )
            在其他线程调用此对象的notify() 方法或notifyAll() 方法前,导致当前线程等待。
        void notify()
            唤醒在此对象监视器上等待的单个线程。
            会继续执行wait方法之后的代码

 */
public class WaitingAndNotifyDemo {
    public static void main(String[] args) {
        // 创建锁对象,保证唯一
        Object obj = new Object();
        // 创建一个消费者线程
        new Thread() {
            @Override
            public void run() {
                // 保证等待和唤醒的线程只能由一个运行,需要使用同步技术
                synchronized (obj) {
                    System.out.println("告知老板需要的包子种类和数量");
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    // 唤醒之后执行的代码
                    System.out.println("我的包子终于做好了,可以吃了");
                }
            }
        }.start();

        // 创建一个老板线程
        new Thread() {
            @Override
            public void run() {
                // 花费5s做包子
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (obj) {
                    System.out.println("老板做好了包子");
                    // 调用notify()方法,唤醒用户的等待状态,让他吃包子
                    obj.notify();
                }
            }
        }.start();
    }
}

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

  1. wait方法与notify方法必须要由同一一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
  2. wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
  3. wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。

3)生产者消费者问题

① 问题的引出

生产者和消费者是指两个不同的线程类对象,操作同一资源的情况。(即生产一个,取走一个)

由于牵扯到线程的不确定性,所以会存在以下两点问题:

假设生产者线程还没向数据存储空间添加完所有信息,程序就切入到了消费者线程,消费者线程将把该消息的名称和上一个信息的内容联系到一起。(不同步所造成)

生产者放了若干次的线程,消费者才开始取数据,或者是消费者取完一个数据后,还没等到生产者放入新的数据,又重复取出已取过的数据。

八、线程池

1)线程池思想概述

我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?在Java中可以通过线程池来达到这样的效果。今天我们就来详细讲解一下Java的线程池。

2)线程池概念

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

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

技术分享图片

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

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

3)线程池的使用

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

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

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

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

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

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

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

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

  1. 创建线程池对象。
  2. 创建Runnable接口子类对象。(task)
  3. 提交Runnable接口子类对象。(take task)
  4. 关闭线程池(一般不做)。

1.使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
2.创建一个类,实现Runnable接口,重写run方法,设置线程任务
3.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
4.调用ExecutorService中的方法shutdown销毁线程池(不建议执行)

import java.util.concurrent.Executors;

public class Demo01ThreadPool {
    public static void main(String[] args) {
        //1.使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
        ExecutorService es = Executors.newFixedThreadPool(2);
        //3.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
        //线程池会一直开启,使用完了线程,会自动把线程归还给线程池,线程可以继续使用
        es.submit(new RunnableImpl());//pool-1-thread-1创建了一个新的线程执行
        es.submit(new RunnableImpl());//pool-1-thread-1创建了一个新的线程执行
        es.submit(new RunnableImpl());//pool-1-thread-2创建了一个新的线程执行

        //4.调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
        es.shutdown();

        es.submit(new RunnableImpl());//抛异常,线程池都没有了,就不能获取线程了
    }
}
public class RunnableImpl implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"创建了一个新的线程执行");
    }
}

Java 多线程

原文:https://www.cnblogs.com/tiantian152/p/14495362.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!