1 概念
多进程是指系统能同时运行多个任务(程序)。多线程是指在同一程序中有多个顺序流在执行。例如:在编辑或下载邮件的同时可以打印文件。
1.1 进程与线程区别
进程和线程都是一个时间段的描述,是CPU工作时间段的描述,不过是颗粒大小不同。
- 本质的区别在于每个进程拥有独立的内存单元;而线程则共享内存,共享内存使线程之间的通信比进程之间通信更有效、容易;
- 进程是操作系统中应用程序的抽象概念;线程是应用程序调度的最小单位;
- 一个程序至少有一个进程;一个进程至少有一个线程;
- 线程在执行过程中与进程还是有区别的。每个独立的进程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制;
- 从逻辑角度来看,多线程在于:一个应用程序中有多个执行部分可以同时执行。进程:实现进程的调度和管理以及资源分配。
1.2 什么是线程安全?
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
或者说:一个类或者程序所提供的接口对于线程来说是原子操作,或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题,那就是线程安全的。
1.3 多线程同步问题
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没执行完,另一个线程参与进来执行,导致共享数据的错误。
3 多线程同步方法
3.1 synchronized关键字
public class SynchronizedThread {
class Bank {
private int account = 100;
public int getAccount() {
return account;
}
// 1.synchronized methods(){}
public synchronized void save(int money) {
account += money;
}
// 2.synchronized(this){}
public void save(int money) {
synchronized (this) {
account += money;
}
}
}
class NewThread implements Runnable {
private Bank bank;
public NewThread(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// bank.save1(10);
bank.save(10);
System.out.println(i + "账户余额为:" + bank.getAccount());
}
}
}
// 建立线程,调用内部类
public void useThread() {
Bank bank = new Bank();
NewThread new_thread = new NewThread(bank);
System.out.println("线程1");
Thread thread1 = new Thread(new_thread);
thread1.start();
System.out.println("线程2");
Thread thread2 = new Thread(new_thread);
thread2.start();
}
public static void main(String[] args) {
SynchronizedThread st = new SynchronizedThread();
st.useThread();
}
}
3.2 使用特殊域变量(volatile)实现线程同步
3.2.1 volatile关键字的作用
在当前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
要解决这个问题,只需要把该变量声明为volatile,这就指示JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。一般,多任务环境下各任务间共享的标志都应该加volatile修饰。
3.2.2 volatile关键字的两层语义
(1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
(2)禁止进行指令重排序。
3.2.3 volatile的原理和实现机制——volatile到底如何保证可见性和禁止指令重排序的?
下面这段话摘自《深入理解Java虚拟机》:“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”。lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
(1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
(2)它会强制将对缓存的修改操作立即写入主存;
(3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
3.2.4 volatile保证可见性
当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在主内存的变量的值,然后把堆内存变量的具体值读到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前)强制将线程变量副本的值立即写入主存。
Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
保证可见性:当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
3.2.5 volatile保证原子性吗?
volatile不能完全确保线程安全:对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的。
volatile不能保证原子性,也就是在读写之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样。例子如下:
public class ThreadTest2 {
//产品
static class ProductObject{
//线程操作变量可见
//volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新
public volatile static String value;
}
// 生产者线程
static class Producer extends Thread {
Object lock;
public Producer(Object lock) {
this.lock = lock;
}
@Override
public void run() {
// 不断生产产品
for (int i = 1; i < 10; i++) {
// 产品已经消费完成,生产新的产品
ProductObject.value = "NO:" + System.currentTimeMillis();
System.out.println("生产产品:" + ProductObject.value);
}
}
}
// 消费者线程
static class Consumer extends Thread {
Object lock;
public Consumer(Object lock) {
this.lock = lock;
}
@Override
public void run() {
for (int i = 1; i < 10; i++) {
// 产品已经消费完成,生产新的产品
System.out.println("消费产品:" + ProductObject.value);
ProductObject.value = null;
}
}
}
public static void main(String[] args) {
Object lock = new Object();
new Producer(lock).start();
new Consumer(lock).start();
}
}
用final域,有锁保护的域和volatile域可以避免非同步的问题,详解如 4 线程间的通信:ThreadTest3。
3.2.6 volatile能保证有序性吗?
在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。
//x、y为非volatile变量
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
volatile flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。
3.2.7 synchronized和volatile变量对比
(1)volatile变量是一种稍弱的同步机制在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。
(2)从内存可见性的角度看,写入volatile变量相当于退出同步代码块,而读取volatile变量相当于进入同步代码块。
(3)在代码中如果过度依赖volatile变量来控制状态的可见性,通常会比使用锁的代码更脆弱,也更难以理解。仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它。
(4)synchronized机制(即同步机制)既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。原因是声明为volatile的简单变量如果当前值与该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作:“count++”、“count = count+1”。
3.2.8 使用volatile关键字的场景
通常来说,使用volatile必须具备以下2个条件:
(1)对变量的写操作不依赖于当前值,或者你能确保只有单个线程更新变量的值。比如:i++、i+=1这种。但是可以改为num=i+1,如果i是一个 volatile 类型,那么num就是安全的,总之就是不能作用于自身。
(2)该变量没有包含在具有其他变量的不变式中。
这些条件表明:可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
以下为2个场景例子:
//(1)状态标记量
volatile boolean inited = false;
Thead1 {
context = loadContext();
inited = true;
}
Thead2 {
while(!inited ) {
sleep()
}
doSomething(context);
}
//(2)DCL(Double CheckLock)实现单例(常用)
3.2.9 总结
需要同步的时候,第一选择应该是synchronized关键字,这是最安全的方式,尝试其他任何方式都是有风险的。尤其在、jdK1.5之后,对synchronized同步机制做了很多优化,如:自适应的自旋锁、锁粗化、锁消除、轻量级锁等,使得它的性能明显有了很大的提升。
3.3 使用重入锁实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。ReenreantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用。
class Bank {
private int account = 100;
//需要声明这个锁
private Lock myLock = new ReentrantLock();
public int getAccount() {
return account;
}
public void save(int money) {
myLock.lock(); //锁住
try{
account += money;
} finally {
myLock.unlock(); //如果在临界区的代码抛出异常,锁必须被释放。否则,其他线程将永远阻塞。
}
}
}
4 线程间的通信
4.1 概念
等待唤醒机制----wait及notify方法。注意:Adater的notifyDatasetChanged()方法通知ListView更新显示,基于此原理。
- wait()方法使得当前线程必须要等待,等到另外一个线程调用notify()或者notifyAll()方法。
- notify()方法会唤醒一个等待当前对象的锁的线程。
4.2 线程阻塞
阻塞状态的线程的特点是:该线程放弃CPU的使用,暂停运行,只有等到导致阻塞的原因消除之后才恢复运行。或者是被其他的线程中断,该线程也会退出阻塞状态,同时抛出InterruptedException。以下为线程阻塞条件:
- 线程执行了Thread.sleep(int millsecond);方法,当前线程放弃CPU,睡眠一段时间,然后再恢复执行
- 线程执行一段同步代码,但是尚且无法获得相关的同步锁,只能进入阻塞状态,等到获取了同步锁,才能回复执行。
- 线程执行了一个对象的wait()方法,直接进入阻塞状态,等待其他线程执行notify()或者notifyAll()方法。
- 线程执行某些IO操作,因为等待相关的资源而进入了阻塞状态。比如说监听system.in,但是尚且没有收到键盘的输入,则进入阻塞状态。
4.3 生产者消费者例子
public class ThreadTest3 {
// 产品
static class ProductObject {
// 线程操作变量可见
// volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新
public volatile static String value;
}
// 生产者线程
static class Producer extends Thread {
Object lock;
public Producer(Object lock) {
this.lock = lock;
}
@Override
public void run() {
// 不断生产产品
for (int i = 1; i < 10; i++) {
synchronized (lock) { // 互斥锁
// 产品还没有被消费,等待
if (ProductObject.value != null) {
try {
lock.wait();// 等待,阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 产品已经生产完成
ProductObject.value = "NO:" + System.currentTimeMillis();
System.out.println("生产产品:" + ProductObject.value);
lock.notify(); // 生产完成,通知消费者消费(唤醒一个等待当前对象的锁的线程)
}
}
}
}
// 消费者线程
static class Consumer extends Thread {
Object lock;
public Consumer(Object lock) {
this.lock = lock;
}
@Override
public void run() {
for (int i = 1; i < 10; i++) {
synchronized (lock) {
// 没有产品可以消费
if (ProductObject.value == null) {
try {
lock.wait();// 等待,阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 产品已经消费完成
System.out.println("消费产品:" + ProductObject.value);
ProductObject.value = null;
lock.notify(); // 消费完成,通知生产者,继续生产(唤醒一个等待当前对象的锁的线程)
}
}
}
}
public static void main(String[] args) {
Object lock = new Object();
new Producer(lock).start();
new Consumer(lock).start();
}
}
5 线程死锁
5.1 死锁的四个必要条件
- 互斥条件,即某个资源在一段时间内只能由一个线程占有,不能同时被两个或两个以上的线程占有
- 不可抢占条件,线程所获得的资源在未使用完毕之前,资源申请者不能强行地从资源占有者手中夺取资源,而只能由该资源的占有者线程自行释放
- 占有且申请条件,线程至少已经占有一个资源,但又申请新的资源;由于该资源已被另外线程占有,此时该线程阻塞;但是,它在等待新资源之时,仍继续占用已占有的资源。
- 循环等待条件,存在一个线程等待序列{P1,P2,…,Pn},其中P1等待P2所占有的某一资源,P2等待P3所占有的某一源,…,而Pn等待P1所占有的的某一资源,形成一个线程循环等待环。
5.2 死锁的四个必要条件
public class MyTask implements Runnable {
// //字符串常量是存放在常量池,new两个Task,两个字符串常量"obj1"是同一个对象。
// //两个Task引用的是同一个"obj1"对象,起不到两份数据的效果。
// private final Object obj1 = "obj1"; //资源1
// private final Object obj2 = "obj2"; //资源2
private final Object obj1 = new String("obj1"); //资源1
private final Object obj2 = new String("obj2"); //资源2
private int flag;
public void setFlag(int flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag == 1) {
synchronized (obj1) {
Log.e("Task", "锁住:" + obj1); //占用obj1
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2) {
Log.e("Task", "使用顺序obj1->obj2");
}
}
} else if (flag == 2) {
synchronized (obj2) {
Log.e("Task", "锁住:" + obj2); //占用obj2
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj1) {
Log.e("Task", "使用顺序obj2->obj1");
}
}
}
}
}
public class DeadLockTest {
/**
* 死锁
*/
public static void deadLock() {
MyTask task = new MyTask();
task.setFlag(1);
Thread t1 = new Thread(task);
t1.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//改变条件
task.setFlag(2);
Thread t2 = new Thread(task);
t2.start();
}
/**
* 解决死锁的实现方法
*/
public static void removeDeadLock() {
MyTask task1 = new MyTask();
task1.setFlag(1);
Thread t1 = new Thread(task1);
t1.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//新创建一个MyTask
MyTask task2 = new MyTask();
task2.setFlag(2);
Thread t2 = new Thread(task2);
t2.start();
}
}
//死锁结果
//Task: 锁住:obj1
//Task: 锁住:obj2
//解决死锁后结果
//Task: 锁住:obj1
//Task: 锁住:obj2
//Task: 使用顺序obj1->obj2
//Task: 使用顺序obj2->obj1
6 如何优雅的结束线程
线程对象属于一次性消耗品,一般线程执行完run方法之后,线程就正常结束了,线程结束之后就报废了,不能再次start,只能新建一个线程对象。但有时run方法是永远不会结束的。有三种方法可以结束线程:
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止;
- 使用interrupt()方法中断线程;
- 使用stop方法强行终止线程(不推荐使用,可能发生不可预料的结果)
6.1 使用退出标志终止线程
使用一个变量来控制循环,例如最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出。代码如下:
public class ThreadSafe extends Thread {
public volatile boolean exit = false;
public void run() {
while (!exit){
//do something
}
}
}
定义了一个退出标志exit,当exit为true时,while循环退出,exit的默认值为false.在定义exit时,使用了一个Java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值。
6.2 使用interrupt()方法终止线程
使用interrupt()方法来终端线程可分为两种情况:
(1)线程处于阻塞状态,如使用了sleep,同步锁的wait,socket的receiver,accept等方法时,会使线程处于阻塞状态。当调用线程的interrupt()方法时,系统会抛出一个InterruptedException异常,代码中通过捕获异常,然后break跳出循环状态,使线程正常结束。通常很多人认为只要调用interrupt方法线程就会结束,实际上是错的,一定要先捕获InterruptedException异常之后通过break来跳出循环,才能正常结束run方法。
public class ThreadSafe extends Thread {
public void run() {
while (true){
try{
Thread.sleep(5*1000);阻塞5妙
}catch(InterruptedException e){
e.printStackTrace();
break;//捕获到异常之后,执行break跳出循环。
}
}
}
}
(2)线程未进入阻塞状态,使用isInterrupted()判断线程的中断标志来退出循环,当使用interrupt()方法时,中断标志就会置true,和使用自定义的标志来控制循环是一样的道理。
public class ThreadSafe extends Thread {
public void run() {
while (!isInterrupted()){
//do something, but no tthrow InterruptedException
}
}
}
(3)为什么要区分进入阻塞状态和和非阻塞状态两种情况了,是因为当阻塞状态时,如果有interrupt()发生,系统除了会抛出InterruptedException异常外,还会调用interrupted()函数,调用时能获取到中断状态是true的状态,调用完之后会复位中断状态为false,所以异常抛出之后通过isInterrupted()是获取不到中断状态是true的状态,从而不能退出循环,因此在线程未进入阻塞的代码段时是可以通过isInterrupted()来判断中断是否发生来控制循环,在进入阻塞状态后要通过捕获异常来退出循环。因此使用interrupt()来退出线程的最好的方式应该是两种情况都要考虑:
public class ThreadSafe extends Thread {
public void run() {
while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
try{
Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
}catch(InterruptedException e){
e.printStackTrace();
break;//捕获到异常之后,执行break跳出循环。
}
}
}
}
6.3 使用stop方法终止线程
程序中可以直接使用thread.stop()来强行终止线程,但是stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出ThreadDeatherror的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用stop方法来终止线程。
7 并发处理方法
7.1 局部变量是否存在并发问题
(1)不存在。因为每个线程都有自己的调用栈,局部变量保存在线程各自的调用栈里面,不会共享,所以自然也就没有并发问题。
(2)没有共享,就没有伤害。方法里的局部变量,因为不会和其他线程共享,所以没有并发问题,这个思路很好,已经成为解决并发问题的一个重要技术,同时还有个名字叫:线程封闭,官方的解释是:仅在单线程内访问数据。由于不存在共享,所以即便不同步也不会有并发问题,性能杠杠的。