java内存模型(jmm)
java内存模型分为两大类型即,主内存和本地内存
1.主内存
也就是主进程所占用的内存
2.本地内存
线程中开辟的属于线程自己的内存,其中存放着全局变量的副本数据
也就是全局共享的数据在主内存中的,线程中复制一份放入自己的本地内存,线程执行结束后将其变动刷新至主内存。
这也就是为什么多线程会有线程安全的问题所在。
多个线程同时做了修改,都去刷新主内存,会造成结果和实际不一致。
volatile关键字
其功能为多线程可见。
怎么说呢?实际上就是当线程在本地内存中修改了被volatile修改的变量后,会立刻将其值刷新至主内存中,其他线程
也会马上得到这个修改的结果
实际看上去就像线程在直接修改主线程中的这个变量一样。
还有就是防止指令重新排序
代码自上而下执行,这是常识,但是,计算机执行时,可能会将,没有依赖关系的代码指令的执行顺序打乱,但一般不会
影响结果。
如:int a=1;int b=1;这时无论怎么变化这两条指令,都没有影响。
如果再来一条int c=a+b;就会相互之间有了依赖关系,计算机就不会将其重新排序
指令并不是代码行,指令是原子的,通过javap命令可以看到一行代码编译出来的指令,当然,像int i=1;这样的代码行也
是原子操作。
而对于那些比较隐晦的,指令重拍可能会引发问题得数据,volatile关键字可以指定其对应的代码不会参与重新排序
案例
package live.yanxiaohui;
/**
* @Description 仿真消息的生产和消费(多线程之间的通讯),即来一个消息,消费一条消息
* @CSDN https://blog.csdn.net/yxh13521338301
* @Author: yanxh<br>
* @Date 2020-05-15 11:08<br>
* @Version 1.0<br>
*/
public class Test2 {
public static void main(String[] args) {
Money money = new Money();
new Task1(money).start();
new Task2(money).start();
}
}
class Task1 extends Thread{
private Money money;
private int count;
public Task1(Money money) {
this.money = money;
}
@Override
public void run() {
while (true){
if(count % 2 ==0){
money.name = "马云";
money.componet = "阿里";
}else {
money.name = "马化腾";
money.componet = "腾讯";
}
count++;
}
}
}
class Task2 extends Thread{
private Money money;
public Task2(Money money) {
this.money = money;
}
@Override
public void run() {
while (true){
System.out.println(money.name + "," + money.componet);
}
}
}
class Money{
public String name;
public String componet;
}
首先我们运行程序,会发现数据错乱的现象
这是由于多个线程访问全局共享数据造成的线程安全问题。
说人话:
写入的线程做了修改,将本地内存改动的数据要同步至主内存,刚好同步一半,这时被挂起,
读线程从主内存读取数据就是一个错误的数据。
那么加锁如何?
对共享的money对象加锁。关键代码如下
写入操作:
synchronized (money){
if(count % 2 ==0){
money.name = "马云";
money.componet = "阿里";
}else {
money.name = "马化腾";
money.componet = "腾讯";
}
}
读操作:
synchronized (money){
System.out.println(money.name + "," + money.componet);
}
一定会有读者有疑问:读为什么还要加锁?
此处对数据的修改和读取是两个变量,这两个变量需要保证其原子性,否则依旧是可能出现线程安全的
比如:如果读不加锁,那么其读到第一个属性值后,被挂起,写入的线程做了修改,读线程继续执行后
就会出现第二个属性和第一个属性不同步的现象
运行程序
数据是保证一致性了,但依然不满足我们消费的机制,所以需要再优化下
我们可以使用wait和notify配合进行多个线程之间对同一共享的全局数据进行通讯
package live.yanxiaohui;
/**
* @Description 仿真消息的生产和消费(多线程之间的通讯),即来一个消息,消费一条消息
* @CSDN https://blog.csdn.net/yxh13521338301
* @Author: yanxh<br>
* @Date 2020-05-15 11:08<br>
* @Version 1.0<br>
*/
public class Test2 {
public static void main(String[] args) {
Money money = new Money();
new Task1(money).start();
new Task2(money).start();
}
}
class Task1 extends Thread{
private Money money;
private int count;
public Task1(Money money) {
this.money = money;
}
@Override
public void run() {
try{
while (true){
synchronized (money){
// 方便查看效果,设置阻塞时间
Thread.sleep(100);
if(!money.success){
// 交出对象的使用权,等待消息消费
money.wait();
}
if(count % 2 ==0){
money.name = "马云";
money.componet = "阿里";
}else {
money.name = "马化腾";
money.componet = "腾讯";
}
money.success = false;
// 唤醒此对象所有等待的线程
money.notify();
}
count++;
}
} catch (Exception e){
e.printStackTrace();
}
}
}
class Task2 extends Thread{
private Money money;
public Task2(Money money) {
this.money = money;
}
@Override
public void run() {
try {
while (true){
synchronized (money){
if(money.success){
// 交出对象的使用权,等待消息生产
money.wait();
}
System.out.println(money.name + "," + money.componet);
money.success = true;
// 唤醒此对象所有等待的线程
money.notify();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Money{
public String name;
public String componet;
// true表示可以写,false表示可以读
public boolean success;
}
由此可以看出,线程执行是抢夺CPU分配权的,和主线程代码的执行顺序没太大关系
我们可以使用lock锁,去替代sync
package live.yanxiaohui;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Description 仿真消息的生产和消费(多线程之间的通讯),即来一个消息,消费一条消息
* @CSDN https://blog.csdn.net/yxh13521338301
* @Author: yanxh<br>
* @Date 2020-05-15 11:08<br>
* @Version 1.0<br>
*/
public class Test3 {
public static void main(String[] args) {
Money money = new Money();
new Task1(money).start();
new Task2(money).start();
}
}
class Task1 extends Thread {
private Money money;
private int count;
public Task1(Money money) {
this.money = money;
}
@Override
public void run() {
while (true) {
try {
// 对象上锁
money.lock.lock();
// 方便查看效果,设置阻塞时间
Thread.sleep(100);
if (!money.success) {
// 交出对象的使用权,等待消息消费
money.condition.await();
}
if (count % 2 == 0) {
money.name = "马云";
money.componet = "阿里";
} else {
money.name = "马化腾";
money.componet = "腾讯";
}
money.success = false;
// 唤醒此对象所有等待的线程
money.condition.signal();
count++;
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放锁
money.lock.unlock();
}
}
}
}
class Task2 extends Thread {
private Money money;
public Task2(Money money) {
this.money = money;
}
@Override
public void run() {
try {
while (true) {
// 对象上锁
money.lock.lock();
if (money.success) {
// 交出对象的使用权,等待消息生产
money.condition.await();
}
System.out.println(money.name + "," + money.componet);
money.success = true;
// 唤醒此对象所有等待的线程
money.condition.signal();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
money.lock.unlock();
}
}
}
class Money {
public String name;
public String componet;
// true表示可以写,false表示可以读
public boolean success;
public Lock lock = new ReentrantLock();
public Condition condition = lock.newCondition();
}
运行结果也是相同
总结
1.多线程之间实现通讯基于对全局共享数据的锁定,即多个线程都必须去争夺锁,获取锁之后依照业务进行对应的等待或运行。
2.wait和notify必须要配合synchronized使用, 且必须要在同步代码块中执行
3.lock锁因为是手动添加的,所以它可控,实际开发中可用到的最多
4.wait底层是将对象的锁释放掉,自身线程进入阻塞等待状态,直到被对象的notify唤醒
5.sleep只是线程的定时阻塞,不会释放锁
6.volatile指定全局共享变量在内存模型中线程间数据的可见性
7.volatile还可以防止指令重新排序