现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。线程是现代操作系统调度的最小单元,也叫轻量级进程,在一个进程里可以创建多个线程,这些线程都拥有各自的计算器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。今天主要以两个方面让大家更快的了解并发编程!
一、基本概念与方法
二、线程安全问题与解决
(一)、线程与进程
进程是CPU分配资源的最小单位,由一个或多个线程组成。
线程是CPU进行调度的最小单位,被称为轻量级线程。
一个程序至少一个进程,一个进程至少一个线程
(二)、Java中线程的三种创建方式
(1)继承Thread类,并重写run()方法
(2)实现Runnable接口,并重写run()方法
(3)实现Callable接口,并重写call()方法;此种方法有返回值,且需要使用FutureTask类进行封装
实现接口与继承Thread类的比较:
Java中只能单继承,但是可以实现多个接口;使用接口的方法更适合扩展
继承整个Thread类的方法开销过大
若想在线程执行体中(即run方法体中)访问当前线程,继承方式可以直接通过this;而接口方法要通过Thread.currrentThread()
此外实现Runnable接口创建的线程可以处理同一资源,从而实现资源的共享
(三)、线程的状态
(1)新建状态:创建后未启动
(2)就绪状态:调用start()方法后进入该状态,与其他就绪状态线程一起竞争CPU,等待CPU的调度。
(3)运行状态:就绪状态的线程获得CPU时间片,真正的执行run()方法。线程只能从就绪状态进入运行状态
(4)阻塞状态:线程由于如下所示的各种原因进入阻塞,线程挂起
该线程调用Thread.sleep()方法
等待阻塞,线程中的共享变量调用了wait()方法
I/O流方式,如read()方法,receive()方法等待数据
同步阻塞,线程因无法获得目标资源的锁而被挂起
(四)、sleep()方法和wait()方法
sleep()是Thread类中的静态方法,调用Thread.sleep(time)后线程休眠time毫秒,休眠过程中线程不会释放拥有的对象锁。如果该线程睡眠期间其他线程调用了该线程的interrupt()方法中断了该线程,该线程会在调用sleep()方法的地方抛出InterruptedException。
wait()是Object类中的方法,当线程调用一个共享变量的wait()方法是,该线程会被挂起并且释放该对象锁,进入等待此对象的等待锁定池,直到其他线程调用了该共享对象的notify()或者notifyAll()方法。其中,notify()是在等待锁定池中随机唤醒一个线程,notifyAll()是唤醒所有因该对象的wait()方法而挂起的线程。
注意:调用共享变量的wait()、notify()、notifyAll()方法,需要先获得共享变量的对象锁。被唤醒的线程不会立即执行,需要和其他线程一起竞争对象锁(由调用notify()方法的线程所释放的对象锁)。
(五)、join()方法和yield()方法
join()方法,Thread类的成员方法,插队方法,线程A的执行体中调用 B.join(),B代表线程B,则线程A会阻塞,让B线程插队。参数可以传入时间(毫秒),表示允许插队运行的时间长度。
yield()方法,Thread类的静态方法,礼让方法,线程A调用Thread.yield()方法后会让出CPU使用权,进入就绪状态,与其他处于就绪状态的线程一起竞争CPU。(实际上,调用yield()方法之后,线程调度器会从线程就绪队列中获取一个线程优先级最高的线程,而该线程的优先级会变为1)
(六)、线程中断
线程中断是线程间的一种协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。
interrupt()方法,中断线程,将线程的中断标志设置为true。当线程因调用wait()、join()、sleep()等方法进入阻塞时,其他线程调用该线程的interrupt()方法,该线程会抛出InterruptedException并返回。如果调用线程的interrupt()方法后未抛出InterruptedException,则应通过interrupted()方法判断当前线程是否被中断来返回线程(如在执行体中使用该方法作为线程执行前提条件)
(七)、守护线程与用户线程
守护线程是服务于用户线程的,可以通过调用setDaemon(true)方法将用户线程设置为守护线程
两者可以通过JVM是否等待线程结束来区分,JVM只会等待用户线程结束;守护线程不会影响JVM的退出,不管其是否运行结束都会随着JVM的结束而结束。即用户线程全部结束时,程序终止,并杀死所有守护线程。如main函数就是一个用户线程,而垃圾回收线程就是一个守护线程。
(八)、ThreadLocal的使用
ThreadLocal由JDK包提供,它提供了线程本地变量,即每个访问ThreadLocal变量的线程都会有一个该变量的随机副本。线程对该变量进行操作时,实际上是对自己的本地内存里的变量进行操作,从而避免了多线程共享一个变量时的安全问题。如在封装MyBatisUtil工具包时,其中就用到了将SqlSession的实例对象存储在ThreadLocal的实例对象中,每次通过
get获取,使用完后关闭SqlSession实例对象,并set(null)将ThreadLocal清空;tl是ThreadLocal的实例对象。
二。线程安全问题与解决
(一)、Java中的线程安全问题
当多个线程对共享资源进行访问时,只有当至少有一个线程修改共享资源时才会存在线程安全问题。典型的如计数器类实现中的丢失修改问题。
(二)、共享变量的内存可见性问题
Java中所有的变量存放在主存中,而线程使用变量时会把主内存里面的变量复制到自己的工作内存中,线程读写变量时操作的是自己工作变量中的内存,然后将自己工作内存中的变量刷新到主内存中。因此,当线程A和线程B同时处理一个共享变量时,会存在内存不可见的问题。
(三)、锁的概念
(1)乐观锁与悲观锁:是从数据库概念中引入的词。悲观锁指认为数据很容易被其他线程修改,因此会在数据被处理前对数据进行加锁,使得整个处理过程中数据处于锁定状态。乐观锁则是认为数据在一般情况下不会造成冲突,因此在访问数据前不会加排它锁,只有在数据提交更新时,才会正式的对数据冲突与否进行检测。
(2)独占锁与共享锁:根据锁只能被单个线程持有还是能被多个线程持有,分为独占锁(排它锁)和共享锁。独占锁是一种悲观锁,每次访问资源前都先加上互斥锁,只允许同一时间由一个线程读取数据。而共享锁是一种乐观锁,允许多个线程同时进行读操作。
(3)公平锁与非公平锁:根据线程获取锁的抢占机制,可以分为公平锁与非公平锁。公平锁表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,即早到早得。而非公平锁则不一定先到先得。ReentrantLock提供的锁默认是非公平锁。一般来说,在没有公平性需求的前提下,尽量使用非公平锁,因为公平锁会带来性能开销。
(4)可重入锁:一个线程再次获取它自己已经获得的锁时,则称为可重入锁。可重入的原理是在锁内部维护一个线程表示,线程表示来指示该锁目前被哪个线程占有,然后关联一个计数器来表示该锁是否被线程占用,0为未被占用,1为已占用,此后每次重入则计数器+1.
(5)自旋锁:自旋锁是指线程在获取锁失败时不会马上挂起,而是在不放弃CPU使用权的情况下,多次尝试获取该锁(默认10次)。一般而言,当线程获取锁失败后,会切换到内核状态而被挂起;当该线程获取锁后又需要将其切换到内核状态而唤醒该线程,而用户状态切换到内核状态的开销是比较大的,即自旋锁是使用CPU时间换取线程阻塞与调度的开销。
(四、)synchronized的使用
synchronized是Java提供的一种原子性内置锁。是一种排它锁,同时也是非公平的。synchronized可以解决共享变量的内存可见性问题。
进入synchronized块的语义是,把块内使用的变量从线程的工作内存中清除,这样线程就会直接从主内存中去获取块内需要使用的变量。
退出synchronized块的语义是,将synchronized块内对共享变量的修改刷新到主内存中。
(五)、volatile的使用
使用锁的方式解决共享变量内存可见性的问题太过繁琐,开销太大,因此Java提供了一种弱形式的同步,即volatile关键字。
类成员变量或者类静态成员变量被volatile修饰后主要有两个特性
(1)解决不同线程对该变量进行的操作时的可见性问题。因为线程在操作volatile修饰的变量时,不会把值缓存到寄存器或者其他地方,而是直接把值刷新会主内;当其他线程获取该变量时,会从主内存中重新获取最新值,而不是使用当前线程工作内存中的值。
(2)禁止指令重排,一定程度上能保证有序性。具体情况是,写volatile变量时,写之前的操作不会被编译器重排序到volatile写之后。读volatile变量时,读之后的操作不会被编译器重排序到volatile读之前。
(六)、Java中的CAS操作
Java中使用锁来处理并发会产生线程上下文切换和重新调度的开销。而非阻塞的volatile关键字只能保证共享变量的可见性,不能解决读-改-写等原子性问题。因此JDK提供了非阻塞原子性操作,即CAS(Compare
and Swap)操作,它通过硬件保证了比较-更新操作的原子性。
CAS操作有个经典的ABA问题,大概意思是
线程1获取变量X的值(A),然后修改变量X的值为B,这种情况下即使使用CAS操作成,程序也不一定运行正确。因为可能存在线程2在1获取变量X后,使用CAS操作修改了X的值为B,然后又使用CAS操作修改X的值为A,这样线程1修改变量X的值是,已经是此A非彼A了。
ABA问题大概流程:1.CASget(X-A) --->2.CASset(X-B)--->2.CASset(X-A)--->1.CASset(X-B)。
ABA问题的产生是因为变量的状态值产生了环形转换,即变量值从A到B,然后再从B到A。如果规定变量的值只能朝着一个方向转换,则不会出现该问题。因此JDK中的AtomicStampedReference类给每个变量的状态值都配置了一个时间戳死,以避免ABA问题发生。