多线程
进程
进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。
它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
进程状态
进程有三个状态,就绪、运行和阻塞
线程
在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程。
在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”。
特点
线程执行开销小,但不利于资源的管理和保护
多线程
指的是这个程序(一个进程)运行时产生了不止一个线程同时执行。
并行
多个cpu或者多台机器、多核CPU同时执行一段处理逻辑,是真正的同时。
并发
通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。
并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈。
线程安全
一段代码并发的情况之下,经过使用多线程调度顺序不影响任何结果。
这个时候使用多线程,只需要关注系统的内存,cpu是不是够用即可。
线程不安全
一段代码并发的情况之下,经过使用多线程调度顺序会影响最终结果。
这个时候使用多线程,需要关注系统的内存,cpu是不是够用即可、多线程的运行结果。
同步
指的是通过控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。
多线程的三个核心状态
原子性
一个操作要么全部生效,要么全部无效
可见性
当多个线程并发访问共享数据时,一个线程对共享数据进行修改,其它的线程能够看见
顺序性
CPU不保证代码运行顺序
线程的创建和启动
在JAVA里面,JAVA的线程是通过java.lang.Thread类来实现的,每一个Thread对象代表一个新的线程。
Java创建线程两种方式:
1.继承Thread类,并重写该类的run()方法,用该线程对象的start()方法启动线程
2.实现接口runnable,并重写该类的run()方法,创建Runnable实现类的实例,
创建Thread类的对象,并把Runnable的实例交给Thread类的对象,并调用Thread类的对象start()方法启动线程
线程状态(线程的生命周期)
1.新建状态
线程对象创建后的所处的状态,通过调用start方法进入就绪状态。
2、就绪状态
已经具备了运行条件,但还没有分配到CPU,等待系统为其分配CPU,所以也称为等待状态。当操作系统选定一个等待执行的Thread对象后,
它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。
注意:不能对已经启动的线程再次调用start()方法,否则会出现Java.lang.IllegalThreadStateException异常。
3.运行状态
当线程正在被CPU执行,此时的状态就是运行状态。
处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。
如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。
也可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态。
当发生如下情况是,线程会从运行状态变为阻塞状态:
1.线程调用sleep方法主动放弃所占用的系统资源
2.线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
3.线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有
4.线程在等待某个通知(notify)
5.程序调用了线程的suspend方法将线程挂起。该方法容易导致死锁,所以程序应该尽量避免使用该方法。
当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态。
4、阻塞状态
处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,则为阻塞状态。
在阻塞状态的线程不能进入等待状态。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,
线程便转入等待状态,重新等待被系统来执行,系统选中后从原来停止的位置开始继续运行
5、死亡状态
当线程的run()方法执行完,或者被强制性地终止,就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。
线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
线程管理
Java提供了一些便捷的方法对线程状态的控制
1.线程睡眠(sleep)
sleep方法使线程进入阻塞状态
2.线程让步(yield)
yield方法使线程进入就绪状态
3.线程合并(join)
线程的合并的含义就是将几个线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时,
Thread类提供了join方法来完成这个功能.
void join()
当前线程等该加入该线程后面,等待该线程终止。
void join(long millis)
当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
void join(long millis,int nanos)
等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
4.设置线程的优先级
每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。
每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main线程具有普通优先级。
Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,其中setPriority方法的参数是一个整数,
范围是1~10之间,也可以使用Thread类提供的三个静态常量:
MAX_PRIORITY =10
MIN_PRIORITY =1
NORM_PRIORITY =5
5、后台(守护)线程
调用线程对象的方法setDaemon(true),则可以将其设置为守护线程.必须在启动线程前调用.
守护线程的用途为:
1.守护线程通常用于执行一些后台作业
2.Java的垃圾回收也是一个守护线程。守护线的好处就是你不需要关心它的结束问题。
6.正确结束线程
正常执行完run方法,然后结束掉.
线程同步
java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时,将会导致数据不准确,
相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,
从而保证了该变量的唯一性和准确性。
1.在方法上加上同步锁
即有synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。
在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
2.同步代码块
即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
3.使用特殊域变量(volatile)
volatile关键字为域变量的访问提供了一种免锁机制;
使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新;
因此每次使用该域就要重新计算,而不是使用寄存器中的值;
volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。
注:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。用final域,
有锁保护的域和volatile域可以避免非同步的问题。
4、使用重入锁(Lock)
ReentrantLock 类
特点就是在同一个线程中可以重复加锁,只需要解锁同样的次数就能真正解锁即可重入、互斥、实现了Lock接口的锁,
它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。所以灵活性要远远好于synchronized。
ReentrantLock 可以设置等待限时;可中断;可以设置公平锁,默认情况下,锁是非公平;
tryLock()也可以不带参数直接运行,当前线程会尝试获得锁,如果锁并未被其他线程占用,则申请锁会成功,并立即返回true。
如果锁被其他线程占用,则当前线程不会进行等待,而是立即返回false。
ReentrantLock区别
synchronized,如果它在等待锁,那么它就只有两个状态:获得锁继续执行或者保持等待。但是对于重入锁,就有了另外一种可能,
那就是重入锁在等待的时候可以被中断;灵活性要远远好于synchronized
ReentrantLock可以与Condition配合使用
Condition和之前讲过的Object.wait()还有Object.notify()的作用大致相同。
Condition的操作需要在ReentrantLock.lock()和ReentrantLock.unlock()之间进行。
ReentrantLock.newCondition()可以创建一个Condition。
在调用Condition.await()之后,线程占用的锁会被释放。这样在Condition.signal()方法调用的时候才获取到锁。
注意,Condition.signal()方法调用之后,被唤醒的线程因为需要重新获取锁。
所以需要等到调用Condition.signal()的线程释放了锁(调用ReentrantLock.unlock())之后才能继续执行。
死锁
(1)死锁的四个必要条件
互斥条件:资源不能被共享,只能被同一个进程使用
请求与保持条件:已经得到资源的进程可以申请新的资源
非剥夺条件:已经分配的资源不能从相应的进程中被强制剥夺
循环等待条件:系统中若干进程组成环路,该环路中每个进程都在等待相邻进程占用的资源
(2)处理死锁的方法
忽略该问题,也即鸵鸟算法。当发生了什么问题时,不管他,直接跳过,无视它;
检测死锁并恢复;
资源进行动态分配;
破除上面的四种死锁条件之一。
线程通信
1.借助于Object类的wait()、notify()和notifyAll()
线程执行wait()后,就放弃了运行资格,处于等待状态。
notify()执行时唤醒的也是线程池中的线程,有多个线程时唤醒第一个。
notifyall()唤醒所有线程。
注:
(1) wait(), notify(),notifyall()都用在同步里面,因为这3个函数是对持有锁的线程进行操作,而只有同步才有锁,所以要使用在同步中;
(2) wait(),notify(),notifyall(), 在使用时必须标识它们所操作的线程持有的锁,因为等待和唤醒必须是同一锁下的线程;这3个方法都是Object类中的方法。
2.使用Condition
(1)将同步synchronized替换为显式的Lock操作;
(2)将Object类中的wait(), notify(),notifyAll()替换成了Condition对象,该对象可以通过Lock锁对象获取;
(3)一个Lock对象上可以绑定多个Condition对象,这样实现了本方线程只唤醒对方线程,
而jdk1.5之前,一个同步只能有一个锁,不同的同步只能用锁来区分,且锁嵌套时容易死锁。
3.使用阻塞队列(BlockingQueue)
BlockingQueue是一个接口,也是Queue的子接口。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,
如果该队列已满,则线程被阻塞;但消费者线程试图从BlockingQueue中取出元素时,
如果队列已空,则该线程阻塞。程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。
BlockingQueue接口包含如下5个实现类
ArrayBlockingQueue :基于数组实现的BlockingQueue队列。
LinkedBlockingQueue:基于链表实现的BlockingQueue队列。
PriorityBlockingQueue:它并不是保准的阻塞队列,该队列调用remove()、poll()、take()等方法提取出元素时,并不是取出队列中存在时间最长的元素,
而是队列中最小的元素。它判断元素的大小即可根据元素(实现Comparable接口)的本身大小来自然排序,也可使用Comparator进行定制排序。
SynchronousQueue:同步队列。对该队列的存、取操作必须交替进行。
DelayQueue:它是一个特殊的BlockingQueue,底层基于PriorityBlockingQueue实现.
BlockingQueue提供如下两个支持阻塞的方法
(1)put(E e):尝试把Eu元素放如BlockingQueue中,如果该队列的元素已满,则阻塞该线程。
(2)take():尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。
ThreadLocal类
用处:保存线程的独立变量。对一个线程类
原子类 AtomicInteger、AtomicBoolean……
线程池
合理利用线程池能够带来三个好处。
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
Java JDK中的线程池接口是ExecutorService,ExecutorService还继承了Executor接口
ExecutorService接口有两个实现类 ThreadPoolExecutor和ScheduledThreadPoolExecutor
线程池创建
线程池创建方式
用Executors工厂类产生线程池
用Executors工厂类产生线程池步骤:
(1)调用Executors类的静态工厂方法创建一个ExecutorService对象.
(2)创建Runnable实现类或Callable实现类的实例,作为线程执行任务.
(3)调用ExecutorService对象的submit()方法来提交Runnable实例或Callable实例
(4)当不想提交任务时,调用ExecutorService对象的shutdown()方法来关闭线程池
Executors工厂类产生线程池类型和特点
(1) newFixedThreadPool
特点:
1.固定线程数量,该线程池中的线程数量始终不变,即不会再创建新的线程,也不会销毁已经创建好的线程,
自始自终都是那几个固定的线程在工作
2.采用队列的存放,不能及时处理任务。
说明:假如有一个新任务提交时,线程池中如果有空闲的线程则立即使用空闲线程来处理任务,
如果没有,则会把这个新任务存在一个任务队列中,一旦有线程空闲了,则按FIFO方式处理任务队列中的任务。
(2)newCachedThreadPool
特点:
1.线程数量不确定,是根据实际情况动态调整
2.线程池中的线程都有一个“保持活动时间”的参数,如果线程池中的空闲线程的空闲时间超过该“保存活动时间”则立刻停止该线程,
默认的“保持活动时间”为60s。
说明:假如该线程池中的所有线程都正在工作,而此时有新任务提交,那么将会创建新的线程去处理该任务,
而此时假如之前有一些线程完成了任务,现在又有新任务提交,那么将不会创建新线程去处理,而是复用空闲的线程去处理新任务。
(3)newSingleThreadExecutor
特点:
1.只有一个线程
2.采用队列的存放,不能及时处理任务。
说明:每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待这一个线程空闲,
当这个线程空闲了再按FIFO方式顺序执行任务队列中的任务。
(4)newScheduledThreadPool
特点:
1.可以控制线程池内线程定时或周期性执行某任务
(5)newSingleThreadScheduledExecutor
特点:
1.只有一个线程
2.可以控制线程池内线程定时或周期性执行某任务
ExecutorService接口方法
提交异步执行
execute(Runnable)
submit(Runnable)
submit(Callable)
submit(Runnable)和execute(Runnable)区别是前者可以返回一个Future对象,通过返回的Future对象,可以检查提交的任务是否执行完毕。
如果任务执行完成,future.get()方法会返回一个null。
submit(Callable)和submit(Runnable)类似,也会返回一个Future对象,但是除此之外,submit(Callable)接收的是一个Callable的实现,
Callable接口中的call()方法有一个返回值,可以返回任务的执行结果,而Runnable接口中的run()方法是void的,没有返回值。
如果任务执行完成,future.get()方法会返回Callable任务的执行结果。
invokeAny(...)
invokeAll(...)
invokeAny(...)方法接收的是一个Callable的集合,执行这个方法不会返回Future,但是会返回所有Callable任务中其中一个任务的执行结果。
这个方法也无法保证返回的是哪个任务的执行结果,反正是其中的某一个。
invokeAll(...)与 invokeAny(...)类似也是接收一个Callable集合,但是前者执行之后会返回一个Future的List,
其中对应着每个Callable任务执行后的Future对象。
关闭方法
shutdown()
shutdownNow()
在调用shutdown()方法之后,线程池不会立即关闭,但是它不再接收新的任务,直到当前所有线程执行完成才会关闭,
所有在shutdown()执行之前提交的任务都会被执行。
shutdownNow() 将跳过所有正在执行的任务和被提交还没有执行的任务。但是它并不对正在执行的任务做任何保证,有可能它们都会停止,也有可能执行完成。