1、volatile
概念:轻量级锁。
使用场景:多个线程同时对未加volatile 的变量读写,会导致数据不准确。
使用:
volatile boolean status = false;
volatile特性:
-
保证共享变量对所有线程的可见性(只对原子操作有效,针对复合操作无效(比如:num++))。
1)当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去;
2)这个写会操作会导致其他线程中的缓存无效。
-
禁止指令重排序优化
其他概念:
Java内存模型(JMM):JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存(副本)中进行,而不能直接读写主内存中的变量。这三者之间的交互关系如下:
可见性:是指当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的。
重排序:是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。
原子操作:
注意:
(1) 对复合类操作无效:
num++不是个原子性的操作,而是个复合操作。(读取、加一、赋值)
public class Counter {
public static volatile int num = 0;
//使用CountDownLatch来等待计算线程执行完
static CountDownLatch countDownLatch = new CountDownLatch(30);
public static void main(String []args) throws InterruptedException {
//开启30个线程进行累加操作
for(int i=0;i<30;i++){
new Thread(){
public void run(){
for(int j=0;j<10000;j++){
num++;//自加操作 复和操作
}
countDownLatch.countDown();
}
}.start();
}
//等待计算线程执行完
countDownLatch.await();
System.out.println(num); //执行结果小于300000
}
}
在多线程环境下,有可能线程A将num读取到本地内存中,此时其他线程可能已经将num增大了很多,线程A依然对过期的num进行自加,重新写到主存中,最终导致了num的结果不合预期,而是小于30000。
解决num++操作的原子性问题:
针对num++这类复合类的操作,可以使用java并发包中的原子操作类解决。
原子操作类:是通过循环CAS的方式来保证其原子性的。
public class Counter {
//使用原子操作类
public static AtomicInteger num = new AtomicInteger(0);
//使用CountDownLatch来等待计算线程执行完
static CountDownLatch countDownLatch = new CountDownLatch(30);
public static void main(String []args) throws InterruptedException {
//开启30个线程进行累加操作
for(int i=0;i<30;i++){
new Thread(){
public void run(){
for(int j=0;j<10000;j++){
num.incrementAndGet();//原子性的num++,通过循环CAS方式
}
countDownLatch.countDown();
}
}.start();
}
//等待计算线程执行完
countDownLatch.await();
System.out.println(num); //执行结果300000
}
}
(2)禁止指令重排序优化
处理器为了优化程序性能而对指令序列进行排序的一种手段
重排序也需要遵守一定规则:
1)重排序操作不会对存在数据依赖关系的操作进行重排序。
比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
2)重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。
比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系,所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。
注意:重排序在单线程模式下是一定会保证最终结果的正确性,但是在多线程环境下,问题就出来了,来开个例子,我们对第一个TestVolatile的例子稍稍改进,再增加个共享变量a。
比如:假设线程A执行changeStatus后,线程B执行run,我们能保证在4处,b一定等于3么?
答案依然是无法保证! 也有可能b仍然为2。(上面我们提到过,为了提供程序并行度,编译器和处理器可能会对指令进行重排序,而上例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。)
public class TestVolatile {
int a = 1;
boolean status = false;
/**
* 状态切换为true
*/
public void changeStatus(){
a = 2;//1
status = true;//2
}
/**
* 若状态为true,则running。
*/
public void run(){
if(status){//3
int b = a+1;//4
System.out.println(b);
}
}
}
volatile禁止指令重排序也有一些规则,简单列举一下:
1.当第二个操作是voaltile写时,无论第一个操作是什么,都不能进行重排序
2.当地一个操作是volatile读时,不管第二个操作是什么,都不能进行重排序
3.当第一个操作是volatile写时,第二个操作是volatile读时,不能进行重排序**
总结:
volatile是一种轻量级的同步机制,主要有两个特性:
- 一是保证共享变量对所有线程的可见性;
- 二是禁止指令重排序优化。(同时需要注意的是,volatile对于单个的共享变量的读/写具有原子性,但是像num++这种复合操作,volatile无法保证其原子性,当然文中也提出了解决方案,就是使用并发包中的原子操作类,通过循环CAS地方式来保证num++操作的原子性。)
2、synchronized
概念:线程同步锁。
分类:
-
对象锁:用于对象实例方法(普通方法)或对象实例。
范围:synchronized修饰的实例方法被一个线程占用后,所有被synchronized修改的对象方法(包括代码块修饰),其他线程都不能访问。未被synchronized修饰的方法,其他线程可以访问。
-
类锁:用于类静态方法或类class对象上。
范围::synchronized修饰的静态方法被一个线程占用后,所有被synchronized修饰的静态方法(包括代码块修饰),其他线程都不能访问。未被synchronized修饰的静态方法,其他线程可以访问。
用法:
-
非静态方法的同步
-
静态方法同步
-
代码块
注意:类锁和对象锁是不一样的锁,是互相独立的。
类锁和对象锁其实是一样的,由于静态方法是类所有对象共用的,所以进行同步后,该静态方法的锁也是所有对象唯一的。每次只能有一个线程来访问对象的该非静态同步方法。
-
不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
-
类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。
其他概念:
-
内置锁
一、对象锁
(1) 非静态方法使用 synchronized 修饰,锁定的是当前对象:
(2) 代码块使用 synchronized 修饰,如果传入的参数是 this,那么锁定的也是当前的对象:
public synchronized void test(){
...
}
public void test(){
synchronized (this) { //注意传入this
...
}
}
实例:
public class MySynchronized {
public static void main(String[] args) {
final MyTest mt = new MyTest();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
mt.Min1();
}
}, "t1");
//同一个实例+同一个synchronized
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
mt.Min1();
}
}, "t2");
//同一个实例+不同synchronized
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
mt.Min2();
}
}, "t3");
//同一实例+不同synchronized(代码块)
Thread t4 = new Thread(new Runnable() {
@Override
public void run() {
mt.Min3();
}
}, "t4");
//同一实例+非synchronized
Thread t5 = new Thread(new Runnable() {
@Override
public void run() {
mt.Min4();
}
}, "t5");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
class MyTest{
//synchronized修改实例方法
public synchronized void Min1(){
int c = 5;
for (int i = 0; i < Num; i++) {
c--;
System.out.println(Thread.currentThread().getName() + " - " + c);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}
public synchronized void Min2(){
int c2 = 5;
for (int i = 0; i < Num; i++) {
c2--;
System.out.println(Thread.currentThread().getName() + " - " + c2);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}
public void Min3(){
synchronized (this){
int c3 = 5;
for (int i = 0; i < Num; i++) {
c3--;
System.out.println(Thread.currentThread().getName() + " - " + c3);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}
}
public void Min4(){
int c4 = 5;
for (int i = 0; i < Num; i++) {
c4--;
System.out.println(Thread.currentThread().getName() + " - " + c4);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}
}
输出:
结论:对象锁是对象唯一的。(两个线程 t1 和 t2,同时访问对象 synchronized 修饰的普通方法,t1 对象锁后,t2访问对象所有synchronized 修饰的方法都必须等待。但t2可以可以访问对象的未被synchronized修饰的方法。)
二、类锁
(1) 修饰静态 static 方法或代码块
(2)使用代码块,需引用当前的类
public static synchronized void test(){
...
}
public static void test(){
synchronized (TestSynchronized.class) { //注意传入的是TestSynchronized.class
...
}
}
实例:
public class MySynchronized {
public static void main(String[] args) {
Thread t6 = new Thread(new Runnable() {
@Override
public void run() {
MyTest.Min5();
}
}, "t6");
Thread t7 = new Thread(new Runnable() {
@Override
public void run() {
MyTest.Min5();
}
}, "t7");
Thread t8 = new Thread(new Runnable() {
@Override
public void run() {
MyTest.Min6();
}
}, "t8");
t6.start();
t7.start();
t8.start();
}
}
class MyTest{
static final int Num = 3;
public static synchronized void Min5() {
int c5 = 5;
for (int i = 0; i < Num; i++) {
c5--;
System.out.println(Thread.currentThread().getName() + " - " + c5);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
}
}
public static synchronized void Min6() {
int c6 = 5;
for (int i = 0; i < Num; i++) {
c6--;
System.out.println(Thread.currentThread().getName() + " - " + c6);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
}
}
}
输出:
结论:静态方法的锁是所有对象唯一的。每次只能有一个线程来访问。(t1线程拥有锁之后,t2不能访问所有被synchronized修饰的静态方法,但是可以访问**未**被synchronized修改的静态方法。)
三、对象锁和类锁不会相互影响
类锁和对象锁是不一样的锁,是互相独立的。
实例:
/*
静态方法synchronized和实例synchronized互相独立
*/
public class MySynchronized {
public static void main(String[] args) {
final MyTest mt = new MyTest();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
mt.Min1();
}
}, "t1");
Thread t7 = new Thread(new Runnable() {
@Override
public void run() {
MyTest.Min5();
}
}, "t7");
t1.start();
t7.start();
}
}
class MyTest{
static final int Num = 3;
//synchronized静态方法
public static synchronized void Min5() {
int c5 = 5;
for (int i = 0; i < Num; i++) {
c5--;
System.out.println(Thread.currentThread().getName() + " - " + c5);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
}
}
//synchronized实例方法
public synchronized void Min1(){
int c = 5;
for (int i = 0; i < Num; i++) {
c--;
System.out.println(Thread.currentThread().getName() + " - " + c);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}
}
输出: