Java内存模型
Java 内存模型规定和指引Java 程序在不同的内存架构、CPU 和操作系统间有确定性地行为。它在多线程的情况下尤其重要。Java内存模型对一个线程所做的变动能被其它线程可见提供了保证。
线程内的代码能够按先后顺序执行,这被称为程序次序规则。
对于同一个锁,一个解锁操作一定要发生在时间上后发生的另一个锁定操作之前,也叫做管程锁定规则。
前一个对volatile的写操作在后一个volatile的读操作之前,也叫volatile变量规则。
一个线程内的任何操作必需在这个线程的 start ()调用之后,也叫作线程启动规则。
一个线程的所有操作都会在线程终止之前,线程终止规则。
一个对象的终结操作必需在这个对象构造完成之后,也叫对象终结规则。
线程
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。
- 进程和线程的区别?
- 进程:一个程序对一个数据集的动态执行过程,是分配资源的基本单位。
线程:存在于进程内,是进程内的基本调度单位。共享进程的资源。 - 进程拥有独立的内存单元,但是线程则是共享内存
- 多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。
- 进程:一个程序对一个数据集的动态执行过程,是分配资源的基本单位。
简言之,一个程序至少有一个进程,一个进程至少有一个线程。进程是资源分配的基本单位,线程共享进程的资源。
线程安全
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
Vector 是用同步方法来实现线程安全的, 而和它相似的 ArrayList 不是线程安全的。
竞态条件
多线程对一些资源进行竞争的时候就会产生竞态条件,如果首先要执行的程序竞争失败排到后面执行了,那么整个程序就会出现一些不确定的 bugs。由于线程间的随机竞争,这种 bugs 很难发现而且会重复出现。
线程状态
- 新建:线程被创建时短暂地处于这种状态,此时他已经分配了必须的系统资源并进行了初始化。此时线程已经有资格获得CPU时间了,之后这个线程会转变为可运行状态或阻塞状态。
- 就绪:只要调度器把时间片分配给线程,线程就可以运行。
- 阻塞:线程能够运行,但有某个条件阻止它运行。当线程处于阻塞状态时,调度器将忽略线程,不会分配给线程任何CPU时间。直到线程重新进入就绪状态才有可能执行操作。可能有如下原因进入阻塞状态:
- sleep
- wait使线程挂起,直到得到了notifyAll或者notify消息,才会进入就绪状态。
- 任务等待某个输入、输出完成
- 死亡:不再是可调度的,再也不会得到CPU时间。
java中的线程可以通过继承Thread类或实现Runnable接口来实现
Runnable接口
线程可以驱动任务,因此需要一种描述任务的方式,这可以由Runnable接口来提供。
要想定义任务,只需实现Runnable接口并编写run方法,使得该任务可以执行你的命令。
public class Demo implements Runnable{
public void run(){
……
}
public static void main(String[] args){
Demo d = new Demo();
d.run();
}
}
当从Runnable导出一个类时,它必须具有run()方法。
Thread类
Thread构造器需要一个runnable对象,调用thread对象的start方法为该线程执行必须的初始化操作,然后start内部调用了Runnable的run方法,以便在这新线程中启动该任务。
Thread t = new Thread(new Demo());
t.start();
start方法和run方法的区别?
start ()方法被用来启动新创建的线程,而且 start ()内部调用了 run ()方法,这和直接调用 run ()方法的效果不一样。
当你调用 run ()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start ()方法才会启动新线程。
也就是用start方法来启动线程,才是真正实现了多线程。而run方法只是一个普通方法。使用Runnable还是Thread?
Java 不支持类的多重继承,但允许你调用多个接口。所以如果你要继承其他类,当然是实现Runnable接口好了。
Callable接口
Runnable是执行工作的独立任务,但是它不返回任何值。如果你希望在任务完成时能够返回一个值,那么可以实现Callable接口而不是Runnable接口。
Callable接口是在JDK1.5 增加的,使用的是call方法而不是run方法。
class Task implement Callable<String>{
public string call(){
return "result of the task";
}
}
Callable的类型参数(这里是String)表示从call方法中返回的值。这个值必须使用ExecutorService.submit()方法来调用:
public static void main(String[] args){
ExecutorSevice exec = ……
String result = exec.submit(new Task());
}
- Runnable 和 Callable 有什么不同?
Runnable和 Callable 都代表那些要在不同的线程中执行的任务。Runnable 从 JDK1.0 开始就有了,Callable 是在 JDK1.5 增加的。它们的主要区别是 Callable 的 call () 方法可以返回值和抛出异常,而 Runnable 的 run ()方法没有这些功能。
ThreadLocal
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,每个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本,是线程隔离的。
与synchronized同步机制的比较:
首先,它们都是为了解决多线程中相同变量访问冲突问题。不过,在synchronized同步机制中,要通过对象的锁机制保证同一时间只有一个线程访问该变量。该变量是线程共享的,使用同步机制要求程序缜密地分析什么时候对该变量读写,什么时候需要锁定某个对象,什么时候释放对象锁等复杂的问题,程序设计编写难度较大,是一种“以时间换空间”的方式。 而ThreadLocal采用了以“以空间换时间”的方式。
CyclicBarrier 与 CountDownLatch
它们都是JUC下的类,CyclicBarrier 和 CountDownLatch 都可以用来让一组线程等待其它线程。
CountDownLatch
用来同步一个或多个任务,强制它们等待由其他任务执行的一组操作完成。
可以像CountDownLatch对象设置一个初始计数器,任何在这个对象上调用wait()的方法都将阻塞,直到这个计数值到达0。其他任务可以通过countDown()方法来减小这个计数值。
CountDownLatch被设计为只触发一次,计数值不能被重置。
CylicBarrier
与CountDownLatch相似,但是可以多次重用,而CountDownLatch只能被触发一次。
wait、notify、notifyAll
wait使你可以等待某个条件发生变化,在此期间将任务挂起,存在的锁也会被释放。
只有在notify或者notifyAll发生时,这个任务才会被唤醒并去检查所产生的变化。
sleep和wait的区别:
- sleep是Thread类的静态方法,wait来自object类
- sleep不释放锁,wait释放锁
- wait,notify,notifyall必须在同步代码块中使用,sleep可以在任何地方使用
- 都可以抛出InterruptedException
notify与notifyAll的区别:
- notify在众多等待同一个锁的任务中唤醒一个,因此只有一个线程在等待的时候它才有用武之地。
- notifyAll则唤醒等待相应锁的所有任务,并允许他们争夺锁确保了至少有一个线程能继续运行。
wait、notify和notifyAll这三个方法是基类Object的一部分,而不是属于Thread的一部分。原因是这些方法操作的锁也是所有对象的一部分,所以可以把wait()放进任何同步控制方法里,而不用考虑这个类时继承了Thread还是实现了Runnable接口。
synchronized
当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
当两个并发线程访问同一个对象object中的synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
public class Synchronized Counter {
private int c = 0;
public synchronized void increment() {
c++;
}
public synchronized void decrement() {
c--;
}
public synchronized int value() {
return c;
}
}
如果 count 是这个类的实例化将有两个效果:
- 不可能同时调用同一个对象的同一个方法, 防止造成冲突.同一时间只有一个线程可以调用这对象的同步方法.
- 当一个同步方法退出时, 它会和随后一个同步方法的调用自动建立happens-before关系. 这保证了所有线程都知道对象的状态改变了.
volatile
在当前的 Java 内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
要解决这个问题,就需要把变量声明为 volatile,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。一般说来,多任务环境下,各任务间共享的变量都应该加 volatile 修饰符。
Volatile 修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
volatile 是一种稍弱的同步机制,在访问 volatile 变量时不会执行加锁操作,也就不会执行线程阻塞,因此 volatilei 变量是一种比 synchronized 关键字更轻量级的同步机制。
volatile保证原子性
原子操作是不能被线程调度机制中断的操作。要么全部执行完毕,要么不执行。
原子性可以应用于除long和double之外的所有基本类型之上。
但是对于long和double,它们是64位的,JVM可以将64位的读取和写入当做两个分离的32位的操作和执行。因此如果一个线程正在修改该 long 变量的值,另一个线程可能只能看到该值的一半(前 32 位),这个操作就是非原子性的。
但是对long和double变量使用了volatile关键字,就可以将上面的非原子性操作改为原子性操作。volatile保证可见性
如果你将一个域声明为volatile,那么只要对这个域产生了写操作,所有的读操作都可以看到这个修改。因为volatile域的读写都是发生在主存中的。volatile保证有序性
JVM 为了获得更好的性能会对语句重排序,但是volatile关键字能禁止指令重排序,因此可以在一定程度上保证有序性。
volatile关键字禁止指令重排序有两层意思:
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
java中可以创建volatile数组,但是volatile修饰的只是针对这个数组的引用,如果改变引用指向的数组,将会受到 volatile 的保护;若要改变数组内部元素的话,volatile是不起作用的。