多线程


文章目录

  • 多线程
  • 1:基本概念
  • 1.1:多线程的实现
  • 1:继承Thread类
  • 2:实现Runnable接口
  • 3:线程池
  • 3.1:Executors类
  • 3.2:ThreadPoolExecutor
  • 3.3:线程池执行
  • 1.2:线程常用API
  • 2:线程使用
  • 2.1:线程安全和线程非安全
  • 2.2:线程非安全解决
  • 1:synchronized同步锁
  • 2:volatile关键字:可见性和有序性
  • 3:线程安全的集合详解
  • 4:乐观锁和悲观锁
  • 4.1:悲观锁
  • 4.2:乐观锁


1:基本概念

串行:一个线程执行到底,相当于单线程。
并发:多个线程交替执行,抢占cpu的时间片,但是速度很快,在宏观角度看来就像是多个线程同时执行。
并行:多个线程在不同的cpu中同时执行。

1.1:多线程的实现

1.继承Thread类,重写run方法
2.实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target
3.通过Callable和FutureTask创建线程(没用过)
4.通过线程池创建线程

1:继承Thread类

继承thread类,重写run方法

public class ThreadTest extends Thread{
    @Override
    public void run() {   }
}

线程启动:创建线程对象调用start方法

new ThreadTest().start();

2:实现Runnable接口

public class runableTest implements Runnable{
    @Override
    public void run() {}
}

线程启动:创建线程对象最为参数传递给Thread类的构造函数,调用start方法

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

实现Runnable接口,避免了继承Thread类的单继承局限性,较多使用。

3:线程池

线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。

线程池的创建方式

  • 1:Java线程池的工厂类:Executors类,初始化4种类型的线程池:
newFixedThreadPool()
说明:初始化一个指定线程数的线程池,其中corePoolSize == maxiPoolSize,使用LinkedBlockingQuene作为阻塞队列
特点:即使当线程池没有可执行任务时,也不会释放线程。
newCachedThreadPool()
说明:初始化一个可以缓存线程的线程池,默认缓存60s,线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue作为阻塞队列;
newSingleThreadExecutor()

主要创建参数如下:

corePoolSize:核心线程数,核心线程会一直存活,即使没有任务需要执行,也就是一直运行的线程数
maxPoolSize: 最大线程数,也就是核心线程数任务满了才会创建的线程。
	          当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
  • 2:使用ThreadPoolExecutor创建线程池
3.1:Executors类

1:newCacheThreadPool可缓存线程池
先查看池中有没有以前建立的线程,如果有,就直接使用。如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务
该线程池中没有核心线程,需要就创建

ExecutorService e = Executors.newCachedThreadPool();  
运行:e.execute(new MyRunnablel());
	 e.submit(() -> {})

2:newFixedThreadPool固定大小的线程池
实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。也是较常用的一个线程池,比如连接redis。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
对于newFixedThreadPool核心线程的即为最大的线程数量

创建:ExecutorService e = Executors.newFixedThreadPool(3);  // 固定大小线程池。
运行:e.execute(new MyRunnablel());
	 e.submit(() -> {})
3.2:ThreadPoolExecutor

构造器中各个参数的含义:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
  • 1:corePoolSize:核心池的大小
    这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
  • 2:maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
  • 3:keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止。直到线程池中的线程数不超过corePoolSize。
    但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
  • 4:unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
TimeUnit.DAYS;               //天

TimeUnit.HOURS;             //小时

TimeUnit.MINUTES;           //分钟

TimeUnit.SECONDS;           //秒

TimeUnit.MILLISECONDS;      //毫秒

TimeUnit.MICROSECONDS;      //微妙

TimeUnit.NANOSECONDS;       //纳秒
  • 5:rkQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:
    ArrayBlockingQueue;
    LinkedBlockingQueue;
    SynchronousQueue;

线程池的排队策略与BlockingQueue有关。

  • 6:threadFactory:线程工厂,主要用来创建线程;
  • 7:handler:表示当拒绝处理任务时的策略,有以下四种取值:
    ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
    ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
    ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
    ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

示例: ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 20, TimeUnit.MINUTES, new ArrayBlockingQueue(10));

3.3:线程池执行

包括execute()和submit()方法

  • 1、execute(),只能执行 Runnable 类型的任务,没有返回值。
  • 2、submit(),可以执行 Runnable 和 Callable 类型的任务,有返回值。

1.2:线程常用API

在线程运行中,我们可以获取线程的执行状态,线程名等属性

获取执行该段代码的线程属性:Thread.currentThread()
	如:Thread.currentThread().getName();
线程休眠指定时间ms:Thread.sleep();
线程是否存活判断:Thread.interrupted()

线程启动:线程创建实例.start()
线程停止:线程创建实例.interrupt();

2:线程使用

2.1:线程安全和线程非安全

1、线程安全

  • 指多个线程在执行同一段代码的时候采用加锁机制,使每次的执行结果和单线程执行的结果都是一样的,不存在执行程序时出现意外结果。

2、线程非安全

  • 是指不提供加锁机制保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。

线程非安全存在于实例变量(全局变量)中,不会存在于局部变量中

静态变量,实例变量以及局部变量的线程安全性

  • 全局变量:也叫成员变量,是指在类中定义的变量;它在整个类中都有效。
    全局变量又可分为:类变量和实例变量
    类变量:线程不安全:又叫静态变量 ,用static修饰 ,它可以直接用类名调用 ,也可以用对象调用。
    所有对象的同一个类变量都是共享 一块内存空间的,类加载时只加载一次,被线程改变时,对其他线程可见所以线程不安全
    实例变量:单例时线程不安全,非单例模式安全。不用static修饰 它只能通过对象调用。实例变量归对象私有,所以非单例模式每个线程拥有自己私有自己的变量。单例模式时系统只存在一个实例对象,则在多线程环境下,如果值改变后,则其它对象均可见,故线程非安全
  • 局部变量:线程安全。是指那些在方法体中定义的变量以及方法的参数 它只在定义它的方法内有效

2.2:线程非安全解决

1:利用同步锁synchronized和Lock锁,同步代码块(syn(this)),方法
2:可见性—volatile关键字修饰变量(不适用于++操作)等。
3:可利用各个数据结构的特性进行使用,比如线程安全的map等

1:synchronized同步锁

synchronized锁包括对象锁和类锁,表示锁的部分只有一个线程在执行,也就是由并发执行变为了顺序执行。保证了变量的同步,避免脏读。锁的是对象而不是某块代码

多用在同步代码块或者方法上,表示对变量改变的操作代码只有一个线程在执行(串行)保证该变量的线程安全

使用方法

对象锁:
	    syn(this){}:锁的是创建的实例对象
		syn(obj){}:锁的是同步代码块,同步代码块比锁方法性能更高,缩短等待时间
类锁:修饰static静态方法:锁该类的所有实例!也就是 .class字节码文件

2:volatile关键字:可见性和有序性

  • 可见性:在多线程环境下,某个共享变量如果被其中一个线程给修改了,其他线程读取的时候必须等其他线程操作完再去读取,避免了读取到变量的中间状态(比如变量初始化中、申请空间中、未被赋值时等现象)。一个变量被声明为volatile,保证变量的可见性,但是volatile修饰的变量并不保证线程安全,因为java运算不是原子操作比如a=b+1涉及三次操作。
  • 保证有序性:保证jvm对变量操作的过程中不会发生乱序现象。(也是单例模式中使用的原因)

3:线程安全的集合详解

1:Collections包装方法

Vector和HashTable被弃用后,它们被ArrayList和HashMap代替,但它们不是线程安全的,所以Collections工具类中提供了相应的包装方法把它们包装成线程安全的集合

List<E> synArrayList = Collections.synchronizedList(new ArrayList<E>());
Set<E> synHashSet = Collections.synchronizedSet(new HashSet<E>());
Map<K,V> synHashMap = Collections.synchronizedMap(new HashMap<K,V>());

Collections针对每种集合都声明了一个线程安全的包装类,在原集合的基础上添加了锁对象,集合中的每个方法都通过这个锁对象实现同步

2:java.util.concurrent包中的集合

1.ConcurrentHashMap:
ConcurrentHashMap和HashTable都是线程安全的集合,它们的不同主要是加锁粒度上的不同。HashTable的加锁方法是给每个方法加上synchronized关键字,这样锁住的是整个Table对象。而ConcurrentHashMap是更细粒度的加锁 

2.CopyOnWriteArrayList和CopyOnWriteArraySet
它们是加了写锁的ArrayList和ArraySet,锁住的是整个对象,但读操作可以并发执行

3.ConcurrentLinkedQueue
多用于存放文件或线程间数据唯一性处理等。

除此之外还有ConcurrentSkipListMap、ConcurrentSkipListSet、、ConcurrentLinkedDeque等,至于为什么
没有ConcurrentArrayList,原因是无法设计一个通用的而且可以规避ArrayList的并发瓶颈的线程安全的集合类,
只能锁住整个list,这用Collections里的包装类就能办到

4:乐观锁和悲观锁

Java 按照锁的实现分为乐观锁和悲观锁,乐观锁和悲观锁并不是一种真实存在的锁,而是一种设计思想。

4.1:悲观锁

悲观锁是一种悲观思想,它总认为最坏的情况可能会出现,它认为数据很可能会被其他人所修改,所以悲观锁在持有数据的时候总会把资源 或者 数据 锁住,这样其他线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止。
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。悲观锁的实现往往依靠数据库本身的锁功能实现。

Java 中的 Synchronized 和 Lock 等独占锁(排他锁)也是一种悲观锁思想的实现,比如单例模式,因为 Synchronzied 和 Lock 不管是否持有资源,它都会尝试去加锁,生怕自己心爱的宝贝被别人拿走。

4.2:乐观锁

乐观锁的思想与悲观锁的思想相反,它总认为资源和数据不会被别人所修改,所以读取不会上锁,但是乐观锁在进行写入操作的时候会判断当前数据是否被修改过(具体如何判断我们下面再说)。乐观锁的实现方案一般来说有两种: 版本号机制 和 CAS算法实现 。乐观锁多适用于多度的应用类型,这样可以提高吞吐量。

版本号机制是在数据表中加上一个 version 字段来实现的,表示数据被修改的次数,当执行写操作并且写入成功后,version = version + 1,当线程A要更新数据时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
在大数据架构中:elasticsearch的doc文档就是基于版本号实现的乐观锁,我们在get查询时即回返回该文档的version版本号,每次对文档的改变version+1