多进程是指系统能同时运行多个任务(程序)。多线程是指在同一程序中有多个顺序流在执行。例如:在编辑或下载邮件的同时可以打印文件。
我的另一篇文章:Java基础之详解Thread与Runnable
进程和线程都是一个时间段的描述,是CPU工作时间段的描述,不过是颗粒大小不同。
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
或者说:一个类或者程序所提供的接口对于线程来说是原子操作,或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题,那就是线程安全的。
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没执行完,另一个线程参与进来执行,导致共享数据的错误。
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();
}
}
在当前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
要解决这个问题,只需要把该变量声明为volatile,这就指示JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。一般,多任务环境下各任务间共享的标志都应该加volatile修饰。
(1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
(2)禁止进行指令重排序。
下面这段话摘自《深入理解Java虚拟机》:“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”。lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
(1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
(2)它会强制将对缓存的修改操作立即写入主存;
(3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在主内存的变量的值,然后把堆内存变量的具体值读到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前)强制将线程变量副本的值立即写入主存。
Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
保证可见性:当一个共享变量被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。
在前面提到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是可见的。
(1)volatile变量是一种稍弱的同步机制在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。
(2)从内存可见性的角度看,写入volatile变量相当于退出同步代码块,而读取volatile变量相当于进入同步代码块。
(3)在代码中如果过度依赖volatile变量来控制状态的可见性,通常会比使用锁的代码更脆弱,也更难以理解。仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它。
(4)synchronized机制(即同步机制)既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。原因是声明为volatile的简单变量如果当前值与该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作:“count++”、“count = count+1”。
通常来说,使用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)实现单例(常用)
需要同步的时候,第一选择应该是synchronized关键字,这是最安全的方式,尝试其他任何方式都是有风险的。尤其在、jdK1.5之后,对synchronized同步机制做了很多优化,如:自适应的自旋锁、锁粗化、锁消除、轻量级锁等,使得它的性能明显有了很大的提升。
在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(); //如果在临界区的代码抛出异常,锁必须被释放。否则,其他线程将永远阻塞。
}
}
}
等待唤醒机制----wait及notify方法。注意:Adater的notifyDatasetChanged()方法通知ListView更新显示,基于此原理。
阻塞状态的线程的特点是:该线程放弃CPU的使用,暂停运行,只有等到导致阻塞的原因消除之后才恢复运行。或者是被其他的线程中断,该线程也会退出阻塞状态,同时抛出InterruptedException。以下为线程阻塞条件:
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();
}
}
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
线程对象属于一次性消耗品,一般线程执行完run方法之后,线程就正常结束了,线程结束之后就报废了,不能再次start,只能新建一个线程对象。但有时run方法是永远不会结束的。有三种方法可以结束线程:
使用一个变量来控制循环,例如最直接的方法就是设一个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的值。
使用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跳出循环。
}
}
}
}
程序中可以直接使用thread.stop()来强行终止线程,但是stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出ThreadDeatherror的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用stop方法来终止线程。
(1)不存在。因为每个线程都有自己的调用栈,局部变量保存在线程各自的调用栈里面,不会共享,所以自然也就没有并发问题。
(2)没有共享,就没有伤害。方法里的局部变量,因为不会和其他线程共享,所以没有并发问题,这个思路很好,已经成为解决并发问题的一个重要技术,同时还有个名字叫:线程封闭,官方的解释是:仅在单线程内访问数据。由于不存在共享,所以即便不同步也不会有并发问题,性能杠杠的。
深入理解volatile关键字
Java 并发:volatile 关键字解析
Java JVM(十二):指令重排序
Java多线程看这一篇就足够了