多线程的三大特性
**在了解三大特性前,我们先复习下之前学过的多线程的生命周期
多线程的生命周期(线程状态)
1,新建状态(New)
新创建了一个线程对象。
2,就绪状态(Runnable)
线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3,运行状态(Running)
就绪状态的线程获取了CPU,执行程序代码。
4,阻塞状态(Blocked)
阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
阻塞的情况分三种:
(一),等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
(二),同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5,死亡状态(Dead)
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
(画图说明)
Java的内存模型(JMM)
全称:Java Memory Model
java内存模型
1,java中所有的变量都存储在主内存中(main memory),每条线程还有自己的工作内存(Working Memory),
2,线程的工作内存中保存了主内存的副本拷贝,线程对所有变量的操作都必须在工作内存中进行而不能操作主内存的变量。
3,不同的线程不能访问别的线程工作内存中的变量,线程之间的变量值传递必须要通过主内存来完成,
4,线程、主内存、工作内存三者的交互关系画图演示
JMM规范定义了工作内存和主内存之间的访问细节,通过保障原子性,可见性,有序性实现线程安全。
1),原子性
1,什么时原子性?
即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
2,举个栗子
一,银行账户转账问题
二,操作数据 :i=i+1;
这些不具备原子性的问题,则多线程运行肯定会出问题,所以也需要我们使用锁和lock这些东西来确保这个特性了。
2),可见性
1,什么是可见性?
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
2,Java内存模型理解可见性
java内存模型是通过,A线程对变量在修改后的新值同步回主内存中,这时B线程读取的变量,会是从主内存中读取A线程修改后的变量值。
这种依赖 主内存作为传递媒介的方式来实现可见性的。
通过volatile保证了线程之间的可见性。同样还有synchronized 和final 来实现线程可见性。
3,举个栗子
//线程1执行的代码
int i = 0;
i = 10;
//线程2执行的代码
j = i;
4,volatiles保障可见性
1,volatiles
它是依赖CPU提供的特殊指令内存屏障指令来控制可见性,被Volatile修饰的成员变量在被线程访问时在读操作前会强行插入一条内存屏障读指令强行从主存中读取(让高速缓存中的数据失效,重新从主内存加载数据),变量在被线程修改时会在写指令之后插入写屏障,让写入缓存的最新数据写回到主内存。
Volatile只是保证变量可见性,并不能确保原子性,不能确保原子性,是因为如果A线程和B线程同时读取到变量a值,A线程修改a后将值刷到主存、同时B线程也修改了a的值并刷到主存,这时候B线程就覆盖了A线程修改操作。
(代码举栗说明)
package com.thread.test;
public class VolatileTest {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final VolatileTest test = new VolatileTest();
for (int i = 0; i < 1000; i++) {
new Thread() {
public void run() {
for (int j = 0; j < 1000; j++)
test.increase();
};
}.start();
}
while (Thread.activeCount() > 1)
// 保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
2,Synchronized
Synchronized是通过对线程加锁(独占锁)控制线程同步,被Synchronized修饰的内存只允许一个线程访问 .
(代码举栗说明)
package com.thread.test;
public class SynchronizedTest {
public int inc = 0;
public synchronized void increase() {
inc++;
}
public static void main(String[] args) {
final SynchronizedTest test = new SynchronizedTest();
for(int i=0;i<1000;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
3,小结
volatile关键字和synchronized 都可以来保证可见性
但是 volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。 也就是volatile在某些情况下并不能保障线程安全。
3),有序性
1,什么是有序性?
即程序执行的顺序按照代码的先后顺序执行
2,举个简单的栗子,看下面这段代码:
int i = 0;
boolean flag = false;
i = 1; //语句1
flag = true; //语句2
1,重排序:
指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
2,再看一个栗子:
int a = 10; //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a*a; //语句4
所以从上来看重排序是不会影响单线程的,但是对于多线程呢?
3,再来一个栗子:
x = 2; //语句1
y = 0; //语句2
x = 4; //语句3
y = -1; //语句4
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
3,volatile关键字禁止指令重排序
1)
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)
在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
4),总结
要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。