什么是线程,与进程的区别
- 什么是进程?
- 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
- 什么是线程?
- 线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
进程和线程的关系?
继承Thread,重写run()方法
实现Runable接口
线程安全概念:当多个线程访问某一个类(对象或方法)时,这个类始终能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。这里的加锁机制常见的如:synchronized
synchronized修饰符
synchronized:可以在任意对象及方法上加锁,而加锁的这段代码称为“互斥区”或“临界区”。
- 说明如下:
- 当多个线程访问thread 的run方法的时候,如果使用了synchronized修饰,那个多线程就会以排队的方式进行处理(这里排队是按照CPU分配的先后顺序而定的),一个线程想要执行synchronized修饰的方法里的代码,首先是尝试获得锁,如果拿到锁,执行synchronized代码体的内容,如果拿不到锁的话,这个线程就会不断的尝试获得这把锁,直到拿到为止,而且多个线程同时去竞争这把锁,也就是会出现锁竞争的问题。
- 一个对象一把锁,多个线程多个锁!
synchronized底层原理
- 关于这两条指令的作用,我们直接参考JVM规范中描述:
- monitorenter :
- Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
- • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
- • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
- • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.
- 每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
- 1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
- 2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
- 3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
- monitorexit:
- The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
- The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
- 执行monitorexit的线程必须是objectref所对应的monitor的所有者。
- 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
- Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
- 通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
- monitorenter :
总结
Synchronized是Java并发编程中最常用的用于保证线程安全的方式,其使用相对也比较简单。但是如果能够深入了解其原理,对监视器锁等底层知识有所了解,一方面可以帮助我们正确的使用Synchronized关键字,另一方面也能够帮助我们更好的理解并发编程机制,有助我们在不同的情况下选择更优的并发策略来完成任务。对平时遇到的各种并发问题,也能够从容的应对。
对象锁的同步和异步
同步:synchronized
- 同步的概念就是共享,我们要知道“共享”这两个字,如果不是共享的资源,就没有必要进行同步,也就是没有必要进行加锁;
- 同步的目的就是为了线程的安全,其实对于线程的安全,需要满足两个最基本的特性:原子性和可见性;
- 异步:asynchronized
- 异步的概念就是独立,相互之间不受到任何制约,两者之间没有任何关系。
- 示例代码:
关键字Synchronized拥有锁重入的功能,也就是在使用Synchronized的时候,当一个线程得到一个对象的锁后,在该锁里执行代码的时候可以再次请求该对象的锁时可以再次得到该对象的锁。
- 也就是说,当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。
- 一个简单的例子就是:在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的,示例代码A如下:
- 结果
- 结果
为什么要引入可重入锁这种机制哪?
出现异常时,锁自动释放
- 就是说,当一个线程执行的代码出现异常的时候,其所持有的锁会自动释放
- 将任意对象作为监视器
- 单利模式-双重校验锁:
- ![image.png](https://cdn.nlark.com/yuque/0/2019/png/244997/1568103213143-cc34ff7f-0581-4d84-b102-3b101588aea7.png#height=392&id=QvLmA&name=image.png&originHeight=538&originWidth=623&originalType=binary&size=160191&status=done&style=none&width=454)
- 为什么volatile关键字可以实现禁止指令的重排序优化以及什么是指令重排序优化哪?
- 在Java内存模型中我们都是围绕着原子性、有序性和可见性进行讨论的。为了确保线程间的原子性、有序性和可见性,Java中使用了一些特殊的关键字申明或者是特殊的操作来告诉虚拟机,在这个地方,要注意一下,不能随意变动优化目标指令。关键字volatile就是其中之一。
- 指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度(比如:将多条指定并行执行或者是调整指令的执行顺序)。编译器、处理器也遵循这样一个目标。注意是单线程。可显而知,多线程的情况下指令重排序就会给程序员带来问题。
- 最重要的一个问题就是程序执行的顺序可能会被调整,另一个问题是对修改的属性无法及时的通知其他线程,已达到所有线程操作该属性的可见性。
- 根据编译器的优化规则,如果不使用volatile关键字对变量进行修饰的,那么这个变量被修改后,其他线程可能并不会被通知到,甚至在别的线程中,看到变量修改顺序都会是反的。一旦使用volatile关键字进行修饰的话,虚拟机就会特别小心的处理这种情况。
- 因此,如何正确的使用双重校验锁,以及为什么使用关键字volatile这里我们应该很清楚了。
- volatile关键字的作用就是强制从公共堆栈中取得变量的值,而不是线程私有的数据栈中取得变量的值。
volatile与synchronized的区别
ThreadLocal简介
- 我们知道变量值的共享可以使用
public static
变量的形式,所有的线程都使用同一个被public static
修饰的变量。 - 那么如果我们想实现每一个线程都有自己的共享变量该如何解决哪?JDK提供的ThreadLocal正是为了解决这样的问题的。
- 类ThreadLocal主要解决的就是每个线程绑定自己的值,可以将ThreadLocal类比喻成全局存放数据的盒子,盒子中可以存储每个线程的私有变量。
- 我们知道变量值的共享可以使用
- 先举个例子
- 可以,看出虽然多个线程对同一个变量进行访问,但是由于
threadLocal
变量由ThreadLocal
修饰,则不同的线程访问的就是该线程设置的值,这里也就体现出来ThreadLocal的作用。 - 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
- 可以,看出虽然多个线程对同一个变量进行访问,但是由于
- ThreadLocal与synchronized同步机制的比较
- 在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
- ThreadLocal是线程局部变量,是一种多线程间并发访问变量的解决方案。和synchronized等加锁的方式不同,ThreadLocal完全不提供锁,而使用以空间换时间的方式,为每个线程提供变量的独立副本,以保证线程的安全。
线程间通信wait()和notify()/notifyAll()
- 线程间通信简介
- 我们知道线程是操作系统中独立的个体,但是这个单独的个体之间没有一种特殊的处理方式使之成为一个整体,线程之间没有任何交流和沟通的话,他就是一个个单独的个体,不足以形成一个强大的交互性较强的整体。
- 为了提高CPU的利用率和各线程之间相互协作,Java的一种实现线程间通信的机制是:wait/notify线程间通信,下边就一起学习一下这种线程间的通信机制。
- 不使用等待/通知机制实现线程间通信
- 可以看出,当List集合中的数据为5个的时候线程B退出,虽然两个线程之间实现了通信,但是代码中我们的线程B是一直执行着
while(true)
循环的,直到长度为5才终止执行,显然这种方式是很消耗资源的。所以,就需要一种机制能避免上述的操作又能实现多个线程之间的通信,这就是接下来需要学习的“wait/notify线程间通信”。 - 什么是等待/通知机制
- 道理很简单,就像我们去银行办业务,进门之后取票号,等到达的时候会广播通知我们办业务一样,这就是很实际的一个场景,我们取了票号就需要等待,等业务员轮到票号的时候就会广播通知。
Java中等待/通知机制的实现
- 之所以会是超类Object中的方法,我们可以简单的理解:上几篇文章中我们知道任何对象都可以作为锁,而wait()/notify()是由锁调用的,想到这里自然可以体会到这里设计的巧妙之处。
wait方法
- 方法wait()的作用是使当前执行代码的线程进行等待,该方法会将该线程放入”预执行队列“中,并且在wait()所在的代码处停止执行,直到接到通知或被中断为止。
- 在调用wait()之前,线程必须获得该对象级别锁,这是一个很重要的地方,很多时候我们可能会忘记这一点,即只能在同步方法或同步块中调用wait()方法。
- 还需要注意的是wait()是释放锁的,即在执行到wait()方法之后,当前线程会释放锁,当从wait()方法返回前,线程与其他线程竞争重新获得锁。
notify方法
- 和wait()方法一样,notify()方法也要在同步块或同步方法中调用,即在调用前,线程也必须获得该对象的对象级别锁。
- 该方法是用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选出其中一个呈wait状态的线程,对其发出通知notify,并使它等待获取该对象的对象锁。
- 这里需要注意的是,执行notify方法之后,当前线程不会立即释放其拥有的该对象锁,而是执行完之后才会释放该对象锁,被通知的线程也不会立即获得对象锁,而是等待notify方法执行完之后,释放了该对象锁,才可以获得该对象锁。
- notifyAll()通知所有等待同一共享资源的全部线程从等待状态退出,进入可运行状态,重新竞争获得对象锁。
wait()/notify()方法总结
- wait()/notify()要集合synchronized关键字一起使用,因为他们都需要首先获取该对象的对象锁;
- wait方法是释放锁,notify方法是不释放锁的;
- 线程的五种状态
- 其他注意事项
- wait()和notify()方法要在同步块或同步方法中调用,即在调用前,线程也必须获得该对象的对象级别锁。
- wait方法是释放锁,notify方法是不释放锁的;
- notify每次唤醒wait等待状态的线程都是随机的,且每次只唤醒一个;
- notifAll每次唤醒wait等待状态的线程使之重新竞争获取对象锁,优先级最高的那个线程会最先执行;
- 当线程处于wait()状态时,调用线程对象的interrupt()方法会出现InterruptedException异常;
Lock
- Lock对象简介
- 这里为什么说Lock对象哪?Lock其实是一个接口,在JDK1.5以后开始提供,其实现类常用的有ReentrantLock,这里所说的Lock对象即是只Lock接口的实现类,为了方便记忆或理解,都简称为Lock对象。
- 我们知道synchronized关键字可以实现线程间的同步互斥,从JDK1.5开始新增的ReentrantLock类能够达到同样的效果,并且在此基础上还扩展了很多实用的功能,比使用synchronized更佳的灵活。
ReentrantLock实现线程同步
- 为了进一步说明使用ReentrantLock可以实现线程之间同步
- 可以看出,在sleep指定的时间内,当调用了lock.lock()方法线程就持有了”对象监视器”,其他线程只能等待锁被释放后再次争抢,效果和使用synchronized关键字是一样的
使用Lock对象实现线程间通信
- 在前文中我们已经知道可以使用关键字synchronized与wait()方法和notify()方式结合实现线程间通信,也就是等待/通知模式。在ReentrantLock中,是借助Condition对象进行实现的。
- Condition的创建方式如下:
- Condition按字面意思理解就是条件,当然,我们也可以将其认为是条件进行使用,这样的话我们可以通过上述的代码创建多个Condition条件,我们就可以根据不同的条件来控制现成的等待和通知。而我们还知道,在使用关键字synchronized与wait()方法和notify()方式结合实现线程间通信的时候,notify/notifyAll的通知等待的线程时是随机的,显然使用Condition相对灵活很多,可以实现”选择性通知”。
- 这是因为,synchronized关键字相当于整个Lock对象只有一个单一的Condition对象,所有的线程都注册到这个对象上。线程开始notifAll的时候,需要通知所有等待的线程,让他们开始竞争获得锁对象,没有选择权,这种方式相对于Condition条件的方式在效率上肯定Condition较高一些。
使用Lock对象和Condition实现等待/通知实例
- 主要方法对比如下:
- Object的wait()方法相当于Condition类中的await()方法;
- Object的notify()方法相当于Condition类中的signal()方法;
- Object的notifyAll()方法相当于Condition类中的signalAll()方法;
- 首先,使用Lock的时候,都需要先获取锁。
- 主要方法对比如下:
- 使用Lock对象和多个Condition实现等待/通知实例
- 可以看出实现了分别通知。因此,我们可以使用Condition进行分组,可以单独的通知某一个分组,另外还可以使用signalAll()方法实现通知某一个分组的所有等待的线程。
- 公平锁和非公平锁
- 概念很好理解,公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配,即先进先出,那么他就是公平的;非公平是一种抢占机制,是随机获得锁,并不是先来的一定能先得到锁,结果就是不公平的。
- ReentrantLock提供了一个构造方法,可以很简单的实现公平锁或非公平锁,源代码构造函数如下:
- 参数:fair为true表示是公平锁,反之为非公平锁,这里不再写代码测试。
使用ReentrantReadWriteLock实现Lock并发
ReentrantLock具有完全互斥排他的效果,即同一时间只能有一个线程在执行ReentrantLock.lock()之后的任务。
类似于我们集合中有同步类容器 和 并发类容器,HashMap也是完全排他的,即使是读也只能同步执行,而ConcurrentHashMap就可以实现同一时刻多个线程之间并发。为了提高效率,ReentrantLock的升级版ReentrantReadWriteLock就可以实现效率的提升。
ReentrantReadWriteLock有两个锁:一个是与读相关的锁,称为“共享锁”;另一个是与写相关的锁,称为“排它锁”。也就是多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。
在没有线程进行写操作时,进行读操作的多个线程都可以获取到读锁,而写操作的线程只有获取写锁后才能进行写入操作。即:多个线程可以同时进行读操作,但是同一时刻只允许一个线程进行写操作。
ReentrantReadWriteLock锁的特性
- (1)读读共享;
- (2)写写互斥;
- (3)读写互斥;
- (4)写读互斥;
读读共享
- 可以看出两个线程之间,获取锁的时间几乎同时,说明
lock.readLock().lock();
允许多个线程同时执行lock()方法后面的代码
- 可以看出两个线程之间,获取锁的时间几乎同时,说明
- 写写互斥
- 可以看出执行结果大致差了5秒的时间,可以说明多个写线程是互斥的
- 读写互斥或写读互斥
- 可以看出执行结果大致差了5秒的时间,可以说明读写线程是互斥的
线程计数器CountDownLatch
CountDownLatch是一个非常实用的多线程控制工具类,称之为“倒计时器”,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。
举了例子:
- 注意:
- CountDownLatch的构造函数
- 7表示需要等待执行完毕的线程数量。
- 在每一个线程执行完毕之后,都需要执行
countDownLatch.countDown()
方法,不然计数器就不会准确; - 只有所有的线程执行完毕之后,才会执行
countDownLatch.await()
之后的代码; - 可以看出上述代码中CountDownLatch 阻塞的是主线程;
- 那么,假如我们不是用计数器CountDownLatch的话,结果可想而知,示例如下:
- CountDownLatch的构造函数
官方解释
- CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
- CountDownLatch.java类中定义的构造函数:
- 构造器中的计数值(count)实际上就是闭锁需要等待的线程数量。这个值只能被设置一次,而且CountDownLatch没有提供任何机制去重新设置这个计数值。
- 与CountDownLatch的第一次交互是主线程等待其他线程。主线程必须在启动其他线程后立即调用CountDownLatch.await()方法。这样主线程的操作就会在这个方法上阻塞,直到其他线程完成各自的任务。
- 其他N 个线程必须引用闭锁对象,因为他们需要通知CountDownLatch对象,他们已经完成了各自的任务。这种通知机制是通过 CountDownLatch.countDown()方法来完成的;每调用一次这个方法,在构造函数中初始化的count值就减1。所以当N个线程都调用了这个方法,count的值等于0,然后主线程就能通过await()方法,恢复执行自己的任务。
CountDownLatch在实时系统中的使用场景
让我们尝试罗列出在java实时系统中CountDownLatch都有哪些使用场景。
- 实现最大的并行性:有时我们想同时启动多个线程,实现最大程度的并行性。例如,我们想测试一个单例类。如果我们创建一个初始计数为1的CountDownLatch,并让所有线程都在这个锁上等待,那么我们可以很轻松地完成测试。我们只需调用 一次countDown()方法就可以让所有的等待线程同时恢复执行。
- 开始执行前等待n个线程完成各自任务:例如应用程序启动类要确保在处理用户请求前,所有N个外部系统已经启动和运行了。
- 死锁检测:一个非常方便的使用场景是,你可以使用n个线程访问共享资源,在每次测试阶段的线程数目是不同的,并尝试产生死锁。
线程计数器循环屏障CyclicBarrier
CyclicBarrier是另一种多线程并发控制使用工具,和CountDownLatch非常类似,他也可以实现线程间的计数等待,但他的功能要比CountDownLatch更加强大一些。
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
CyclicBarrier默认的构造方法是CyclicBarrier(int parties)
,其参数表示屏障拦截的线程数量,每个线程调用await
方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
CyclicBarrier强调的是n个线程,大家相互等待,只要有一个没完成,所有人都得等着。
示例代码如下:
- CyclicBarrier和CountDownLatch的区别
- CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。
- CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。比如以下代码执行完之后会返回true。
- CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程。