Java 并发(一)简介
目录
3.1 Java内存模型—— JMM Java Memory Model
六 join()方法和interrupt()方法以及线程终止方法
一 进程和多线程简介
1.1 概念
并行:多个CPU实例或者多台机器同时执行一段处理逻辑,真正的同时。
并发:通过CPU调度算法,不是真的同时。 通常是提高运行在单处理器上的程序的性能
线程:线程是比进程更小的执行单位。是进程中执行运算的最小单位。是种轻量级进程。包含堆栈,寄存器,优先权。
进程:进程是程序的一次执行过程,是系统运行程序的基本单位。是动态的,一旦程序被载入内存中并准备执行是就是一个进程。
进程是表示资源分配的基本概念,是调度运行的基本单位,是系统中并发执行的单位。
区别:一个线程只属于一个进程,但一个进程可以拥有多个线程。各进程是独立的。
所有的线程共享进程的内存和资源。
同一进程的多个线程共享代码段(代码和常量),数据段(全局和静态变量),扩展段(堆存储)。
1.2 线程的状态
线程状态转换
Blocked 阻塞(se)态
等待阻塞:通过调用线程的wait()方法
同步阻塞:线程获取synchronized同步锁失败
其他阻塞:sleep()、join()等
1.3 多线程
多线程就是几乎同时执行多个线程。
优点:更好滴利用CPU资源。
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("MyRunnable");
}
}
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("MyRunnable");
}
}
线程状态切换的代价:
java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络, CPU划分出两个权限等级 :用户态 和 内核态
内核态
CPU可以访问内存所有数据, 包括外围设备, 例如硬盘, 网卡. CPU也可以将自己从一个程序切换到另一个程序
用户态
只能受限的访问内存, 且不允许访问外围设备. 占用CPU的能力被剥夺, CPU资源可以被其他程序获取
1.4 一些常用方法
currentThread() : 返回执行线程对象的引用;
getId() : 返回线程的标识符;
getName(): 返回线程名称;
getPriority() :返回线程优先级;
isAlive(): 测试线程是否处于活动状态;
interrupt(): 中断线程;但实际上只是给线程设置一个中断标志,线程仍会继续运行。
setName(String name);
interrupted() isinterrupted() : 测试是否处于中断状态;interrupted()还会清楚中断标记
isDaemon() : 测试是否守护线程;
用户线程:Java虚拟机在它所有非守护线程已经离开后自动离开。
守护线程:守护线程则是用来服务用户线程的,如果没有其他用户线程在运行,那么就没有可服务对象,也就没有理由继续下去。 例如:垃圾回收线程,一些检测线程
setDaemon(boolean on);
join(): 等待该线程终止,指的是主线程等待子线程的终止。
yield():放弃当前CPU资源;
setPriority(int newPriority): 更改优先级;
1.5 线程的优先级
1. 线程优先级具有继承性;
2. 线程优先级具有随机性。
Thread.MIN_PRIORITY(常数1)
Thread.NORM_PRIORITY(常数5)-默认优先级
Thread.MAX_PRIORITY(常数10)
二 synchronized关键词
解决共享资源竞争的问题;
共享资源:一般是以对象形式存在的内存片段,但也可以是文件、输入/输出端口,或者是打印机。
每个对象有且仅有一个同步锁。同步锁是依赖对象而存在的。不同线程对同步锁的访问时互斥的,这种机制称为互斥量(mutex)
2.1 synchronized基本原则
第一条:当一个线程访问某对象的synchronized方法或者synchronized代码块时,其他线程对该对象的该synchronized方法或者synchronized代码块的访问将被阻塞。
第二条:当一个线程访问某对象的synchronized方法或者synchronized代码块时,其他线程仍然可以访问该对象的非同步代码块。
第三条:当一个线程访问某对象的synchronized方法或者synchronized代码块时,其他线程对该对象的其他的synchronized方法或者synchronized代码块的访问将被阻塞。
2.2 synchronized方法和代码块
synchronized方法是修饰方法,这是一种粗粒度锁; public synchronized void method(){ }
synchronized代码块是修饰代码块,这是一种细粒度锁; synchronized(this){ }
2.3 实例锁和全局锁
实例锁:锁在某个实例对象上;
全局锁:该锁针对的是类,无论实例多少个对象,那么线程都共享该锁。 static synchronized
三 volatile关键字
- adj. [化学] 挥发性的;不稳定的;爆炸性的;反复无常的
- n. 挥发物;有翅的动物
3.1 Java内存模型—— JMM Java Memory Model
所有的变量都是存储在主内存中,每个线程都是独立的工作内存,里面保存该线程使用到的变量的副本。线程对共享共享变量的所有操作必须在自己的工作内存,不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值传递需要通过主内存来完成。例如,线程1对共享变量的修改,要想被线程2及时看到,必须经历如下两个过程:
(1)把工作内存1中更新过的变量刷新到主内存中。
(2)将主内存中最新的共享变量的值更新到线程2中。
3.2 基本概念
可见性: 线程之间的可见性,一个线程修改状态对另一个线程是可见的。
原子性 :JVM执行的最小单位,具有不可分割性。
有序性:线程之间操作的有序性。
3.3 Volatile原理
当变量声明为volatile类型时, 在读取volatile变量时总会返回最新写入的值。
四 线程等待和唤醒
wait() 让当前线程进入等待状态;并且让当前线程释放持有的锁;
notify() 唤醒当前对象上的等待线程;
notifyAll() 唤醒所有线程;
- notify vt. 通告,通知;公布
API接口 API说明 notify() 唤醒在此对象监视器上等待的单个线程 notifyAll() 唤醒在此对象监视器上等待的所有线程 wait() 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的notify()方法或是notifyAll()方法”,当前线程被唤醒(进入“就绪状态”) wait(long timeout) 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的notify()方法或notifyAll()方法,或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”) wait(long timeout,int nanos) 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量”,当前线程被唤醒(进入“就绪状态”)
为什么notify()、wait()等函数定义在Object类中而不是Thread类中?
notify()、wait()依赖于“同步锁”,而同步锁是该对象持有,并且每个对象有且只有一个。所以,你可以把wait()方法放进任何同步控制方法里,而不用考虑这个类是继承自Thread还是实现了Runnable接口。这就是为什么notify()、wait()等函数定义在Object类,而不是Thread类中的原因。实际上,只能在同步控制方法或同步控制块里调用wait()、notify()和notifyAll()(因为不用操作锁,所以sleep()可以在非同步控制方法里调用)。如果在非同步控制方法里调用这些方法,程序可以通过编译,但运行时将得到IllegalMonitorStateException异常,异常的大概是当前线程不是拥有者。意思是,调用wait()、notify()和notifyAll()的任务在调用这些方法前必须获取对象的锁。
说明:
1、wait()、notify/notifyAll() 方法是Object的本地final方法,无法被重写。
2、wait()使当前线程阻塞,前提是 必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。
3、 由于 wait()、notify/notifyAll() 在synchronized 代码块执行,说明当前线程一定是获取了锁的。
当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。
只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。
也就是说,notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程
4、wait() 需要被try catch包围,中断也可以使wait等待的线程唤醒。
5、notify 和wait 的顺序不能错,如果A线程先执行notify方法,B线程在执行wait方法,那么B线程是无法被唤醒的。
6、notify 和 notifyAll的区别
notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。
7、在多线程中要测试某个条件的变化,使用if 还是while?
要注意,notify唤醒沉睡的线程后,线程会接着上次的执行继续往下执行。所以在进行条件判断时候,可以先把 wait 语句忽略不计来进行考虑,显然,要确保程序一定要执行,并且要保证程序直到满足一定的条件再执行,要使用while来执行,以确保条件满足和一定执行。
五 线程让步和休眠
5.1 线程让步
yield vt. 屈服;出产,产生;放弃 vi. 屈服,投降 n. 产量;收益
在Java线程中,yield()方法的作用是让步,能让当前线程由“运行状态”进入“就绪状态”,从而让其他具有相同优先级的等待线程获取执行权,但是,并不能保证在当前线程调用yield()之后,其他具有相同优先级的线程就一定能获得执行权;也有可能是当前线程有进入到“运行状态”继续运行。
wait()的作用是让当前线程由“运行状态”进入到“等待(阻塞)”的同时,也会释放同步锁。而yield()的作用是让步,它也是让当前线程离开“运行状态”。区别是:
(1)wait()是让线程由“运行状态”进入到“等待(阻塞)状态”,而yield()是让线程由“运行状态”进入到“就绪状态”。
(2)wait()是会让线程释放它所持有的对象的同步锁,而yield()方法不会释放对象的同步锁。
5.2 线程休眠
sleep()方法 : 该方法定义在Thread类中,作用是让当前线程休眠,即当前线程会从“远程状态”进入到“休眠(阻塞)状态”。sleep()会指定休眠时间,线程休眠的时间会大于/等于该休眠时间;在线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”,从而等待CPU的调度执行。不会释放同步锁。
六 join()方法和interrupt()方法以及线程终止方法
6.1 join()
一个线程可以在其他线程之上调用join()方法,其效果是等待一段时间直到第二个线程结束结束才继续执行。如果某个线程在另一个线程t上调用t.join(),此线程将被挂起,直到目标线程 t 结束才恢复(即 t.isAlive()返回为假)。
也可以在调用join()时带上一个超时参数(单位可以是毫秒,或者纳秒),这样如果目标线程在这段时间到期时还没有结束的话,join()方法总能返回。
对join()方法的调用可以被中断,做法是在调用线程上调用interrupt()方法,这时需要用到try_catch子句。
6.2 interrupt()
interrupt() 的作用是中断本线程。本线程中断自己是被允许的;其他线程调用本线程的interrupt()方法时,会通过checkAccess()检查权限。这有可能抛出SecurityException异常。
但实际上只是给线程设置一个中断标志,线程仍会继续运行。
interrupt()是给线程设置中断标志;
interrupted()是检测中断并清除中断状态;
isInterrupted()只检测中断。
还有重要的一点就是interrupted()作用于当前线程,interrupt()和isInterrupted()作用于此线程,即代码中调用此方法的实例所代表的线程。
6.3 线程终止的方法
Thread中的stop()和suspend()方法,由于固有的不安全性,已经不建议使用。
6.3.1 终止处于 阻塞状态 的线程
通常,我们通过“中断”方式终止处于“阻塞状态”的线程。当线程由于被调用了sleep(),wait(),join()等方法而进入阻塞状态;若此时调用线程的interrupt()将线程的中断标记设为true。由于处于阻塞状态,中断标记会被清除,同时产生InterruptedException异常。将InterruptedException放在适当的为止就能终止线程
@Override public void run() { try { while (true) { // 执行任务... } } catch (InterruptedException ie) { // 由于产生InterruptedException异常,退出while(true)循环,线程终止! } }
说明:在while(true)中不断的执行任务,当线程处于阻塞状态时,调用线程的interrupt()产生InterruptedException异常中断。中断的捕获在while(true)之外,这样就退出while(true)循环。
6.3.2 终止处于 运行状态 的线程
通常,我们通过“标记”方式终止处于“运行状态”的线程。其中包括“中断标记”和“额外添加标记”。
(1)通过“中断标记”终止线程
@Override public void run() { while (!isInterrupted()) { // 执行任务... } }
说明:
isInterrupted()来判断线程中的中断标记是不是为true。当线程处于运行状态,并且我们需要终止它时,可以调用线程的interrupt()方法,使用线程的中断标记为true,即isInterrupted()方法会返回true。此时,就会退出while循环。
注意:interrupt()并不会终止处于“运行状态”的线程,它会将线程的中断标记设为true。
(1)通过“额外添加标记”
private volatile boolean flag= true; protected void stopTask() { flag = false; } @Override public void run() { while (flag) { // 执行任务... } }
说明:线程中有一个flag标记,它的默认值是true;并且我们提供stopTask()来设置flag标记。当我们需要终止该线程时,调用该线程的stopTask()方法就可以让线程退出while循环。
注意:将flag定义为volatile类型,是为了保证flag的可见性。即其它线程通过stopTask()修改了flag之后,本线程能看到修改后的flag的值。
总结:通用的终止线程的形式:
@Override public void run() { try { // 1. isInterrupted()保证,只要中断标记为true就终止线程。 while (!isInterrupted()) { // 执行任务... } } catch (InterruptedException ie) { // 2. InterruptedException异常保证,当InterruptedException异常产生时,线程被终止。 } }
Object.wait, Thread.sleep方法,会不断的轮询监听 interrupted 标志位,发现其设置为true后,会停止阻塞并抛出 InterruptedException异常。
七 线程优先级和守护线程
Java中线程的优先级的范围是1~10,默认的优先级为5。“高优先级线程”会优先于“低优先级线程”执行。而且Java中有两种线程:用户线程和守护线程。可以通过isDeamon()方法来区别它们:如果返回false,则说明该线程是“用户线程”;否则就是“守护线程”。用户线程一般用户执行用户级任务,而守护线程也就是“后台线程”,一般用来执行后台任务。需要注意的是:JVM在“用户线程”都结束后会退出。
每个线程都有一个优先级。“高优先级线程”会优先于“低优先级线程”执行。每个线程都可以被标记为一个守护进程或非守护进程。在一些运行的主线程中创建新的子线程时,子线程的优先级被设置为等于“创建它的主线程的优先级”,当且仅当“创建它的主线程是守护线程”时“子线程才会是守护线程”。
当Java虚拟机启动时,通常有一个单一的非守护线程(该线程通过是通过main()方法启动)。JVM会一直运行直到下面的任意一个条件发生,JVM就会终止运行:
(1) 调用了exit()方法,并且exit()有权限被正常执行。
(2) 所有的“非守护线程”都死了(即JVM中仅仅只有“守护线程”)。