Java之多线程(一)

天涯浪子 提交于 2019-11-29 08:16:33

一,前言

​ 今天总结一些关于线程方面的知识,说到线程可谓是无人不知,毕竟这东西不管是在工作开发中,还是实际生活中都时时存在着。关于线程方面的内容非常多,从简单的单线程,多线程,线程安全以及到高并发等等,当然也包括信息通信。

​ 当然这次从线程的基本开始,后面也会慢慢的补充线程的高级使用,这也算是让自己再复习一次了(哈哈)。

​ 以下内容包括:

  • 二,线程介绍
  • 三,线程的创建
  • 四,线程安全
  • 五,线程池

二,线程介绍

​ 先来介绍几个关于线程方面的概念。

2.1,并行与并发

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

​ 在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的,CPU在多个程序之间高速切换。

​ 而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。

注意:

单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度

2.2 ,线程与进程

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

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

​ 我们可以再电脑底部任务栏,右键----->打开任务管理器,可以查看当前任务的进程:

线程调度:

  • 分时调度

    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度

    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

三,线程的创建

​ 创建线程有两种方式:

  • 继承java.lang.Thread类,重写run方法实现线程创建。
  • 实现java.lang.Runnable接口,实例化其实现类对象创建线程。

3.1,Thread

​ 先来看看API文档的说明:

Thread是一个类,但同时也实现了Runnable接口。

接着使用Thread创建线程。

public class ThreadMain {
    public static void main(String[] args) {
        // 1,实例化ThreadMode对象
        ThreadMode thread = new ThreadMode();
        // 2,调用start()方法启动线程
        thread.start();
    }
}
/**
 * 继承Thread类
 */
class ThreadMode extends Thread {
    // 1,重写父类的run方法。
    @Override
    public void run() {
        System.out.println("使用Thread创建线程!");
    }
}

3.2,构造方法

public Thread():分配新的 Thread 对象。

public Thread(Runnable target):分配一个带有指定目标的新线程。

public Thread(String name):分配一个带有名字的新线程。

public Thread(Runnable target, String name):分配一个带有指定目标的新线程,并指定线程的名字。

3.3,Runnable

java.lang.Runnable:Runnable 接口该由那些打算通过某一线程其实例的类来实现。类必须定义一个称为run的无参方法。

实现步骤:
​ 1,创建一个Runnable接口的实现类。
​ 2,在实现类中重写Runnable接口中的run方法,设置线程的任务。
​ 3,创建一个Runnable实现类的对象。
​ 4,创建Thread类对象,构造方法中传递Runnable接口的实现类对象。
​ 5,调用Thread类中start方法,开启线程执行run方法。

public class ThreadInterface implements Runnable{
    @Override
    public void run() {
        System.out.println("使用Runnable创建线程!");
    }
}
 // 在main方法中,创建实现类对象并开启线程
ThreadInterface thread = new ThreadInterface();
 new Thread(thread,"runnable").start();

​ 说到这里Thread类和Runnable接口都可以创建新的线程,那么它们之间又有什么区别呢?

使用Runnable接口的好处:

  • 避免了单继承的局限性一个类只能继承一个父类,如果创建线程选择继承Thread类,那么就不能再继承别的父类。而采用实现Runnable接口,则还可以再继承,且再实现别的接口。
  • 增强了程序的扩展性,降低了程序的耦合性(解耦)实现Runnable接口的方式,把设置线程任务和开启线程的任务进行了分离。

3.4,匿名内部类方式创建线程

作用:
​ 1,简化代码
​ 2,把子类继承父类,重写父类的方法,创建子类对象一步合成。
​ 3,把实现类接口,重写接口中的方法,创建实现类对象一步合成。
格式:
​ new 父类/接口(){}

Thread

new Thread(){
            @Override
            public void run() {
                super.run();
            }
        }.start();

​ 使用lambda表达式写法(JDK8特性,后面会分享该特性):

new Thread(() -> System.out.println("Thread匿名内部类")).start();

Runnable

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("Runnable匿名内部类");
    }
};
// 开启线程
 new Thread(runnable).start();

​ 使用lambda表达式:

Runnable runnable = () -> System.out.println("Runnable匿名内部类");
// 开启线程的第一种方式
new Thread(runnable).start();

四,线程安全

​ 线程安全通常有3种解决方式:

​ 1,同步代码块

​ 2,同步方法

​ 3,锁机制(lock)

​ 我们以卖车票为案例,用三种方式去解决车票的重复售卖,超卖情况。

4.1,同步代码块

格式:
​ synchronized(锁对象){
​ 可能会出现线程安全问题的代码(访问了共享数据的代码)
​ }

注意:
1.通过代码块中的锁对象,可以使用任意的对象。
2.但是必须保证多个线程使用的锁对象是同一个。
3.锁对象作用: 把同步代码块锁住,只让一个线程在同步代码块中执行。

public class RunnableImpl implements Runnable{
    //定义一个多个线程共享的票源
    private  int ticket = 100;
    //创建一个锁对象
    Object obj = new Object();
    //设置线程任务:卖票
    @Override
    public void run() {
        //使用死循环,让卖票操作重复执行
        while(true){
           //同步代码块
            synchronized (obj){
                //先判断票是否存在
                if(ticket>0){
                    //提高安全问题出现的概率,让程序睡眠
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //票存在,卖票 ticket--
              System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                    ticket--;
                }
            }
        }
    }
}

​ 在main方法中调用线程,并模拟三个窗口同时售票。

public class Demo01Ticket {
    public static void main(String[] args) {
        //创建Runnable接口的实现类对象
        RunnableImpl run = new RunnableImpl();
        //创建Thread类对象,构造方法中传递Runnable接口的实现类对象
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        //调用start方法开启多线程
        t0.start();
        t1.start();
        t2.start();
    }
}

4.2,同步方法

使用步骤:
​ 1.把访问了共享数据的代码抽取出来,放到一个方法中
​ 2.在方法上添加synchronized修饰符

格式:定义方法的格式
​ 修饰符 synchronized 返回值类型 方法名(参数列表){
可能会出现线程安全问题的代码(访问了共享数据的代码)

​ }

public class RunnableImpl implements Runnable{
    //定义一个多个线程共享的票源
    private static int ticket = 100;
    //设置线程任务:卖票
    @Override
    public void run() {
        System.out.println("this:"+this);
        //使用死循环,让卖票操作重复执行
        while(true){
            payTicketStatic();
        }
    }
    /*
        静态的同步方法
        锁对象应该是谁?
        不能是this
        this是创建对象之后产生的,静态方法优先于对象
        静态方法的锁对象是本类的class属性-->class文件对象(反射)
     */
    public static /*synchronized*/ void payTicketStatic(){
        synchronized (RunnableImpl.class){
            //先判断票是否存在
            if(ticket>0){
                //提高安全问题出现的概率,让程序睡眠
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //票存在,卖票 ticket--
                System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                ticket--;
            }
        }
    }
    /*
        定义一个同步方法
        同步方法也会把方法内部的代码锁住
        只让一个线程执行
        同步方法的锁对象是谁?
        就是实现类对象 new RunnableImpl()
        也是就是this
     */
    public /*synchronized*/ void payTicket(){
        synchronized (this){
            //先判断票是否存在
            if(ticket>0){
                //提高安全问题出现的概率,让程序睡眠
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //票存在,卖票 ticket--
                System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                ticket--;
            }
        }
    }
}

​ 在main函数中调用该线程,其代码与上述一样。

4.3,同步锁(Lock)

java.util.concurrent.locks.Lock接口
Lock 实现提供了比使用 synchronized方法和语句可获得的更广泛的锁定操作。
Lock接口中的方法:
​ void lock()获取锁。
​ void unlock() 释放锁。

使用步骤:
​ 1.在成员位置创建一个ReentrantLock对象。
​ 2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁。
​ 3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁 。

​ 请看如下API说明:

public class RunnableImpl implements Runnable{
    //定义一个多个线程共享的票源
    private  int ticket = 100;
    //1.在成员位置创建一个ReentrantLock对象
    Lock l = new ReentrantLock();
    //设置线程任务:卖票
    @Override
    public void run() {
        //使用死循环,让卖票操作重复执行
        while(true){
            //2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
            l.lock();
            //先判断票是否存在
            if(ticket>0){
                try {
                    //票存在,卖票 ticket--
                    System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
                    l.unlock();//无论程序是否异常,都会把锁释放
                }
            }
        }
    }
}

五,线程池

​ 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程是需要时间的。

​ 在JDK5之前,对于线程池的使用是需要程序员用集合来自己进行创建。在JDK5之后就不再需要手动去创建,JDK已经帮我们封装好了。

5.1,线程池概念

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

​ 用一张简单的图来理解下线程池工作的原理。

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

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

5.2,使用方式

​ 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. 关闭线程池(一般不做,因为再次使用的时候线程池中就没有线程了)。
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程: " + Thread.currentThread().getName());
    }
}
public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
        // 创建Runnable实例对象
        MyRunnable r = new MyRunnable();
        // 从线程池中获取线程对象,然后调用MyRunnable中的run()
        service.submit(r);
        // 再获取个线程对象,调用MyRunnable中的run()
        service.submit(r);
        service.submit(r);
        // 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
        // 将使用完的线程又归还到了线程池中
    }
}

六,sleep()与wait()

​ 1,所属分类不同,sleep属于Thread类中,而wait属于Object类中。

​ 2,锁控制不同,sleep不会释放锁,而wait会释放锁且不会影响其他线程进入同步代码块或同步方法中。也就是说sleep会占用资源,wait不会占用资源。

​ 3,sleep可以在任意地方使用,而wait需在同步代码块或者同步方法中使用。

七,volatile与synchronized

​ 1,volatile性能比synchronized要好,因为volatile是线程同步的轻量级实现。

​ 2,volatile只能修饰变量,synchronized可以修饰方法以及代码块。

​ 3,多线程访问volatile不会发生阻塞,synchronized会出现阻塞。

​ 4,volatile能保证数据的可见性,但不能保证原子性。synchronized可以保证原子性,也可以间接的保证可见性。

​ 5,volatile解决的是变量在多线程之间的可见性。synchronized解决的是多线程之间访问资源的同步性。

​ 6,volatile防止指令重排。

八,总结

​ 似乎觉得本人的每一篇博客的篇幅都好长,可能是因为都是一些很基础的知识点吧,所以就会涉及到很多方方面面,写着写着就很多了。不过这样记下来时间久了还可以再回来看看,多少也算有点印象(哈哈)。

​ 如果你阅读到此,很感谢您的耐心。以上总结的内容,如有不适之处,欢迎留言指正。

感谢阅读!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!