一、Java内存模型
首先来看一下操作系统的内存模型
再看java的内存模型
在java的内存模型中,首先把线程工作内存中的值写入到主内存,另一个线程从主内存中读取这个值。,由于可见性原则,另一个线程拿到的值并不是实时的。
举个例子:
/**
* @Author jhYang
* @Date 2020/2/27 0027 9:08
* @Discription todo 线程共享变量实践
*/
public class ThreadSharedVariables {
private static int a = 0;
public static void main(String[] args) {
Thread threadA = new Thread(()->{
System.out.println(Thread.currentThread().getName()+" a="+a);
a = 1;
System.out.println(Thread.currentThread().getName()+" a="+a);
},"ThreadA");
Thread threadB = new Thread(()->{
System.out.println(Thread.currentThread().getName()+" a="+a);
a = 1;
System.out.println(Thread.currentThread().getName()+" a="+a);
},"ThreadB");
threadA.start();
threadB.start();
try {
threadA.join();
threadB.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
ThreadA a=0
ThreadB a=0
ThreadB a=1
ThreadA a=1
再次运行:
ThreadA a=0
ThreadA a=1
ThreadB a=0
ThreadB a=1
根据java的内存模型的图示可知,共享变量a是在java的内存模型中,属于内存共享的区域,各个线程从这个这里拿到共享变量到自己的工作内存中,完成计算之后再放回公共区域。对于第一种结果当线程B去公共内存拿到a时,a已经被线程A计算过了,所以结果是1.对于结果2,当线程B去拿结果的时候,线程A还在自己的给你工作内存中计算,计算后的结果还没有放到共享内存。
二、synchronized和volatile关键字
1、synchronized的实现和使用
synchronized是Java语言关键字,用来给方法或者代码块加锁,控制方法或代码块同一时间只有一个线程执行,用来解决多个线程同时访问出现的并发问题。
synchronized给方法加锁示例:
/**
* @Author jhYang
* @Date 2020/2/27 0027 10:24
* @Discription todo Synchronized使用
*/
public class SynchronizedTest {
public static void main(String[] args) {
sellTickets();
}
private static int totalTickets = 1000;
private static final Object LOCK = new Object();
private static void sellTickets(){
ExecutorService executorService = Executors.newFixedThreadPool(2);
Runnable runnableA = ()->{
int threadAtickits = 0;
while(sellTicktsWithSyncMethod(1)){
threadAtickits++;
}
System.out.println("A buys "+threadAtickits +" tickts");
};
Runnable runnableB = ()->{
int threadBtickits = 0;
while(sellTicktsWithSyncMethod(1)){
threadBtickits++;
}
System.out.println("B buys "+threadBtickits +" tickts");
};
executorService.execute(runnableA);
executorService.execute(runnableB);
}
/**
* 方法级别的使用synchronized关键字的方法
* @param count 卖票的数量
* @return 返回剩余票数是否足够
*/
private static synchronized boolean sellTicktsWithSyncMethod(int count){
if(totalTickets - count < 0){
return false;
}else {
totalTickets = totalTickets - count;
return true;
}
}
}
运行结果:
A buys 392 tickts
B buys 608 tickts
再运行一次:
A buys 1000 tickts
B buys 0 tickts
从两次运行结果来看,程序是正确的 ,总票数和卖出的总票数是一致的。
2、synchronized给对象加锁示例
/**
* @Author jhYang
* @Date 2020/2/27 0027 10:24
* @Discription todo Synchronized使用
*/
public class SynchronizedTest {
public static void main(String[] args) {
sellTickets();
}
private static int totalTickets = 1000;
private static final Object LOCK = new Object();
private static void sellTickets(){
ExecutorService executorService = Executors.newFixedThreadPool(2);
Runnable runnableA = ()->{
int threadAtickits = 0;
while(sellTicktsWithSyncObject(1)){
threadAtickits++;
}
System.out.println("A buys "+threadAtickits +" tickts");
};
Runnable runnableB = ()->{
int threadBtickits = 0;
while(sellTicktsWithSyncObject(1)){
threadBtickits++;
}
System.out.println("B buys "+threadBtickits +" tickts");
};
executorService.execute(runnableA);
executorService.execute(runnableB);
}
/**
* 对象级别的使用synchronized关键字
* @param count 卖票的数量
* @return 返回剩余票数是否足够
*/
private static boolean sellTicktsWithSyncObject(int count){
synchronized (LOCK){
if(totalTickets - count < 0){
return false;
}else {
totalTickets = totalTickets - count;
return true;
}
}
}
}
运行结果:
A buys 1000 tickts
B buys 0 tickts
再次运行:
B buys 494 tickts
A buys 506 tickts
可以看到这两种方式均可。
值得注意的是,当使用synchoronized修饰方法的时候,如果方法被static修饰,则说明这个锁是作用在类级别的,就是说整个类拥有一把锁。如果这个方法被static修饰则说明锁是作用在对象级别的,对象级别的锁,各线程之间互不影响,也就是每个对象拥有一把锁。
3、另外除了synchronized锁之外还有ReentrantLock
二者的比较如下:
/**
* @Author jhYang
* @Date 2020/2/27 0027 11:30
* @Discription todo Synchronized与ReentrantLock的比较
*/
public class SynchronizedAndReentrantLock {
private static Object LOCK = new Object();
public static void main(String[] args) {
}
private static void useSync(){
synchronized (LOCK){
System.out.println("hoding lock");
synchronized (LOCK){
System.out.println("hoding lock agin");
}
}
}
private static void useReentantLock(){
ReentrantLock lock = new ReentrantLock();
try {
lock.lock();
System.out.println("hoding lock");
} finally {
lock.unlock();
}
}
}
根据使用示例得出以下相同点不同点:
相同点:
- 都是用于多线程中对资源的加锁,控制代码同一时间只有一个线程在执行。
2)当一个线程获取了锁,其他线程均需要阻塞等待。
3)均为可重入的锁
不同点:
- synchronized是java关键字,由虚拟机字节码指令实现,ReentantLock是Java SDK 提供的API级别的锁是实现。
2)synchronized可以在方法级别加锁,ReentantLock不行。
3)ReentantLock可以通过方法tryLock()等待指定时间内的锁,synchronized则不行。
4)ReentantLock提供了公平锁和非公平锁,synchronized只有非公平锁。(非公平锁需要阻塞的线程去竞争,公平锁按照先来后到的原则拿锁)
二、volatile关键字的存取规则
1、volatile是Java语言关键字,也是一个指令的关键字。该关键字可以有以下作用:
a) 用来保证多线程之间对变量的内存可见性,讲最新的变量值及时通知给其他线程。
b) 禁止volatile前后的程序指令进行重排序。
c) 不保证线程安全,不可用于数字的线程安全递增。
2、volatile的使用场景
a) 用于修饰状态变量,线程间访问该变量保证各个线程看到最新的内存值。
b) 用于单实例对象的构造,避免多线程情况下由于内存不可见而重复多次构造对象。
三、synchronized和volatile关键字的区别
1、synchronized是用于同步锁控制,具有原子性。控制同一时间只有一个线程执行一个方法或者代码块。
2、volatile只保证线程间内存的可见性,不具备锁的特性,无法保证修饰对象的原子性。
可见性:一个线程对共享变量的修改,更够及时的被其他线程看到
原子性:即不可再分了,不能分为多步操作。
volatile只保证可见性,不保证原子性,导致非原子性的操作不安全,具体可以参考https://blog.csdn.net/qq_27409289/article/details/84981453
因此使用volatile修饰变量时一般用于标志,这样的操作具有原子性,于是可以达到线程安全。