并发编程之Java内存模型

一、线程安全问题

当多个线程同时共享,同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题。但是做读操作是不会发生数据冲突问题

1.1、案例:模拟两个售票窗口(两条线程)同时抢100张票会出现的线程安全问题

/**
 * 案例:模拟两个售票窗口(两条线程)同时抢100张票
 * @author jsonString
 *
 */
class ThreadDemo01 implements Runnable{
	//共享资源100张票
	public int count=100;
	@Override
	public void run() {
		while (count>0) {
			try {
				//为了增加代码出现线程安全问题的概率这里让线程睡30毫秒
				Thread.sleep(30);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			sale();
		}
		
	}
	public void sale() {
		System.out.println(Thread.currentThread().getName()+"售出第"+(100-count+1)+"张票");
		count--;
	}
	
}
public class Test0001 {
	public static void main(String[] args) {
		ThreadDemo01 threadDemo01 = new ThreadDemo01();
		Thread t1 = new Thread(threadDemo01,"1号窗口");
		Thread t2 = new Thread(threadDemo01,"2号窗口");
		t1.start();
		t2.start();
	}

}

以上代码运行结果
在这里插入图片描述

二、如何解决线程安全问题(使用同步方式)

什么是多线程之间同步:当多个线程共享同一个资源,不会受到其他线程的干扰

2.1内置锁

Java提供了一种内置的锁机制来支持原子性,每一个Java对象都可以用作要给实现同步的锁,称为内置锁,线程进入同步代码块之前自动获取到锁,代码块执行完成正常退出或代码块中抛出异常退出时会释放掉锁,内置锁为互斥锁,即线程A获取到锁后,线程B堵塞直到线程A释放锁,线程B才能获取到同一个锁,内置锁使用synchronized关键字实现,synchronized关键字有两种用法:

  1. 修饰需要进行同步的方法(所有访问状态变量的方法都必须进行同步),此时充当锁的对象为调用同步方法的对象
  2. 同步代码块和直接使用synchronized修饰需要同步的方法是一样的,但是锁的粒度可以更细,并且充当锁的对象不一定是this,也可以是其他对象,所以使用起来更加灵活

2.2、同步代码块

synchronized(任意全局对象){}

class ThreadDemo02 implements Runnable{
	//共享资源100张票
	private int count=100;
	//锁对象
	private Object obj=new Object();
	@Override
	public void run() {
		while (count>0) {
			try {
				//和Test0001一样让线程睡30毫秒
				Thread.sleep(30);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			synchronized (obj) {
				if(count>0) {
					System.out.println(Thread.currentThread().getName()+"卖出第"+(100-count+1)+"张");
					count--;
				}
			}
			
		}
		
	}
	
}
public class Test002 {
	public static void main(String[] args) {
		//注意:这里因为count和obj都需要共享所以都用同一个ThreadDemo02对象
		ThreadDemo02 threadDemo02 = new ThreadDemo02();
		Thread t1 = new Thread(threadDemo02,"1号窗口");
		Thread t2 = new Thread(threadDemo02,"2号窗口");
		t1.start();
		t2.start();
	}
}

2.3 非静态同步方法(修饰在方法上)

非静态同步方法是使用this做为锁

//非静态同步方法
class ThreadDemo03 implements Runnable{
	private int count=100;

	@Override
	public void run() {
		while (count>0) {
			try {
				//和Test0001一样让线程睡30毫秒
				Thread.sleep(30);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			sale();
			
		}
	}
	public synchronized void sale() {
		if(count>0) {
			 System.out.println(Thread.currentThread().getName()+"卖出第"+(100-count+1)+"张");
		 count--;
		}
		
	}
}
public class Test003 {
	public static void main(String[] args) {
		ThreadDemo03 threadDemo03 = new ThreadDemo03();
		Thread t1 = new Thread(threadDemo03,"1号窗");
		Thread t2 = new Thread(threadDemo03,"2号窗");
		t1.start();
		t2.start();
	}

}

证明非同步方法使用this锁的案例:
两个线程,分别为A和B,共享同一个全局变量,A线程使用非静态同步方法,B线程使用this作为锁的同步代码块,如果能解决线程安全问题就能证明非同步方法使用的就是this作为锁

class ThreadDemo04 implements Runnable {
	public int count = 100;
	public boolean flag = true;

	@Override
	public void run() {
		if (flag) {
			while (count > 0) {
				try {
					// 和Test0001一样让线程睡30毫秒
					Thread.sleep(30);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				//这里用this作为锁,如果输出结果是线程安全就证明非静态同步方法使用的就是this作为锁
				synchronized (this) {
					if (count > 0) {
						System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - count + 1));
						count--;
					}

				}
			}

		} else {
			while (count > 0) {
				try {
					// 和Test0001一样让线程睡30毫秒
					Thread.sleep(30);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				sale();
			}

		}

	}

	private synchronized void sale() {
		if (count > 0) {
			System.out.println(Thread.currentThread().getName() + "卖出第" + (100 - count + 1));
			count--;
		}

	}

}

public class Test004 {
	public static void main(String[] args) throws InterruptedException {
		ThreadDemo04 threadDemo04 = new ThreadDemo04();
		Thread t1 = new Thread(threadDemo04,"1号窗口");
		Thread t2 = new Thread(threadDemo04,"2号窗口");
		t1.start();
		Thread.sleep(30);
		threadDemo04.flag=false;
		t2.start();
		
	}
}

2.4 静态同步方法(修饰在方法上)

静态的同步方法使用的锁是,该方法所属字节码文件对象

/**
 * 
 * 静态同步方法
 *
 */
class ThreadDemo05 implements Runnable{
	private static int count=100;
	@Override
	public void run() {
		while (count>0) {
			try {
				Thread.sleep(30);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			sale();
		}
	}
	public static synchronized void sale() {
		if(count>0) {
			System.out.println(Thread.currentThread().getName()+"卖出了"+(100-count+1));
			count--;
		}
	}
	
}

public class Test005 {
	public static void main(String[] args) {
		ThreadDemo05 threadDemo05 = new ThreadDemo05();
		Thread t1 = new Thread(threadDemo05,"1号");
		Thread t2 = new Thread(threadDemo05,"2号");
		t1.start();
		t2.start();
	}

}

同样使用刚才的方法验证静态同步方法使用的锁就是,该方法所属字节码文件对象

/**
 * 
 * 证明静态同步方法使用的就是,该方法所属的字节码文件对象
 *
 */
class ThreadDemo06 implements Runnable {
	private static int count = 100;
	public boolean flag = true;

	@Override
	public void run() {
		if (flag) {
			while (count > 0) {
				try {
					Thread.sleep(30);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				synchronized (ThreadDemo06.class) {
					if (count > 0) {
						System.out.println(Thread.currentThread().getName() + "卖出了" + (100 - count + 1));
						count--;
					}
				}
			}

		} else {
			while (count > 0) {
				try {
					Thread.sleep(30);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				sale();
			}
		}
	}

	public static synchronized void sale() {
		if (count > 0) {
			System.out.println(Thread.currentThread().getName() + "卖出" + (100 - count + 1));
			count--;
		}
	}
}

public class Test006 {
	public static void main(String[] args) throws InterruptedException {
		ThreadDemo06 threadDemo06 = new ThreadDemo06();
		Thread t1 = new Thread(threadDemo06,"1号");
		Thread t2 = new Thread(threadDemo06,"2号");
		t1.start();
		Thread.sleep(30);
		threadDemo06.flag=false;
		t2.start();
	}

}

三、多线程死锁

什么是多线程死锁:就是同步中嵌套同步,导致锁无法释放

/**
 * 
 * 多线程死锁
 *
 */
class ThreadDemo07 implements Runnable {
	private int count = 100;
	private Object obj = new Object();
	public boolean flag = true;

	@Override
	public void run() {
		if (flag) {
			//1.t1线程进来获取obj锁
			synchronized (obj) {
				while (count > 0) {
					try {
						Thread.sleep(30);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					sale();
				}

			}
		} else {
			while(count>0) {
				sale();
			}
			
		}
	}
	//2.t1线程再获取this锁
	//4.t2线程进来获取this锁
	public synchronized void sale() {
		//3.因为synchronized是重入锁(重入锁的意思就是相同一把锁,同一个线程,当次进入可重用,
		//就是说t1线程在这里已经有了obj锁不需要再获取)
		//5.t2线程进来获取obj锁
		/**
		 * 这时如果t2进来了获取了obj锁,但obj锁被t1线程获取了,t1线程在等待this锁,而t2线程已经获取了this锁,t2线程在等待obj锁就会造成死锁
		 */
		synchronized (obj) {
			System.out.println(Thread.currentThread().getName() + "卖出了" + (100 - count + 1));
			count--;
		}
	}

}

public class Test007 {
	public static void main(String[] args) throws InterruptedException {
		ThreadDemo07 threadDemo07 = new ThreadDemo07();
		Thread t1 = new Thread(threadDemo07, "1号");
		Thread t2 = new Thread(threadDemo07, "2号");
		t1.start();
		Thread.sleep(30);
		threadDemo07.flag = false;
		t2.start();

	}

}

四、Threadlocal

4.1、什么是Threadlocal

ThreadLocal提高一个线程的局部变量,访问某个线程拥有自己局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本

4.2、ThreadLocal的接口方法

ThreadLocal类接口很简单,只有4个方法,我们先来了解一下

  • void set(Object value)设置当前线程的线程局部变量的值
  • public Object get()该方法返回当前线程所对应的线程局部变量
  • public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显示调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度
  • protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显示是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第一次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null

4.3、Threadlocal案例

案例:创建两个线程,每个线程生成自己独立序列号

class Res{
	public static ThreadLocal<Integer> threadLocal=new ThreadLocal<Integer>(){
		protected Integer initialValue() {
			return 0;
		}
	};
	public int getNumber() {
		int count=threadLocal.get()+1;
		threadLocal.set(count);
		return count;
				
	}
}
public class Test008 extends Thread{
	private Res res;
	public Test008(Res res) {
		this.res=res;
	}
	@Override
	public void run() {
		for(int i=0;i<3;i++) {
			System.out.println(Thread.currentThread().getName()+","+res.getNumber());
		}
	}
	public static void main(String[] args) {
		Res res2 = new Res();
		Test008 t1 = new Test008(res2);
		Test008 t2 = new Test008(res2);
		t1.start();
		t2.start();
	}
}

4.4、Threadlocal底层

threadlocal底层就是一个map集合
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
如果为null,就调用setInitialValue()方法
在这里插入图片描述

五、java内存模型(JMM)

共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本

  • 主内存:共享变量
  • 本地内存:共享变量的副本

5.1、多线程的特性

  • 原子性
  • 可见性
  • 有序性

5.1.1、原子性

即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
比如i=i+1; 其中就包括了,读取i的值,计算i,写入i。这行代码在Java中是不具备原子性的,则多线程运行肯定会出现问题,所以也需要我们使用同步和lock这些东西来确保这个特性了。
原子性其实就是保证数据一致、线程安全一部分

5.1.2、可见性

当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值。
若两个线程在不同的cpu,那么线程1改变了i的值还没有刷新到主内存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题

5.2Java内存模型的误区

有一个经常被混淆的地方就是内存模型和内存结构

  • 内存模型:jmm 关于多线程的
  • 内存结构:jvm内存结构关于堆、栈概念的

1.3图解Java内存模型

在这里插入图片描述
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面两个步骤

  • 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去
  • 然后,线程B到主内存中读取线程A之前已经更新过的共享变量
    下图说明了这两个步骤
    在这里插入图片描述
    **如上图所示:**本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1
    从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要通过主内存。JMM通过控制主内存与每个线程的本地内存之间的互换

5.3、总结什么是Java内存模型

java内存模型简称jmm,定义了一个线程对另一个线程可见。共享变量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题

六、Volatile

6.1、什么是Volatile

可见性也就是说一旦某个线程修改了该被volatile修饰的变量,它会保证修改的值会立即被更新到主内存,当有其他线程需要读取时,可以立即获取修改之后的值。
在Java中为了加快程序的运行效率,对一些变量的操作通常是在该线程的寄存器或是CPU缓存上进行的,之后才会同步到主内存中,而加了volatile修饰符的变量则是直接读写主内存。
Volatile保证了线程间共享变量的可见性,也禁止指令重排序优化,但不能保证原子性

6.2、案例

class ThreadDemo09 extends Thread{
	public boolean flag=true;

	@Override
	public void run() {
		System.out.println("线程开始");
		while(flag) {
			
		}
		System.out.println("线程结束");
	}


	public void setFlag(boolean flag) {
		this.flag = flag;
	}
}
public class Test009 {
	public static void main(String[] args) throws InterruptedException {
		ThreadDemo09 threadDemo09 = new ThreadDemo09();
		threadDemo09.start();
		Thread.sleep(3000);
		threadDemo09.setFlag(false);
		Thread.sleep(1000);
		System.out.println("flag="+threadDemo09.flag);
	}
}

运行结果
在这里插入图片描述

主线程在等待三秒后已经把flag变量修改为了false,运行结果也把false打印出来了,但因为线程的可见性问题,主线程没有把修改后的flag刷新到主内存中,所以在子线程flag变量还是为true,这时只需要把public boolean flag=true;修改为public volatile boolean flag=true;就可以了
修改后的运行结果
在这里插入图片描述
加上volatile 关键字之后它会保证修改的值会立即被更新到主内存

6.3Volatile与Synchronized区别

  • 从而我们可以看出volatile虽然具有可见性但是并不能保证原子性
  • 性能方面,synchronized关键字是防止多个线程同时执行一段代码,就会影响程序执行效率,而volatile关键字在某些情况下性能要有优于synchronized但是要注意volatile关键字是无法代替synchronized关键字的,因为volatile关键字无法保证操作的原子性

6.4Volatile应用场景

项目中,什么时候会使用到volatile,只要是全局共享变量,全部都需要加上vlolatile

6.5volatile与synchronized

  • volatile作用:可以保证可见性,但是不能保证原子性(线程安全问题)、禁止重排序
  • synchronized作用:既可以保证原子性还可以保证可见性

七、重排序概念

cpu会对代码执行实现优化,不会对有依赖关系做重排序,代码执行的顺序可能会发生改变,但是执行的结果不会发生任何改变,重排序概念只会出现在多线程中

7.1什么是数据依赖性关系

int a=1;
int b=2;
int c=a*b;
//a和b没有依赖关系在重排序之后有可能先执行int b=2;再执行int a=1;
//int c=a*b;和a跟b有依赖关系不会重排序

7.2 as-if-serial语义

不管怎么去做重排序,目的是提高并行度,但是不能影响到正常的结果,重排序的问题只会出现再多线程的情况下

7.3重排序对多线程的影响

现在让我们来看看,重排序是否会改变多线程程序的执行结果,请看下面的示例代码

public class Test001 {
	int a=0;
	boolean flag=false;
	//写入线程
	public void writer() {
		a=1;//1
		flag=true;//2
	}
	//读取线程
	public void reader() {
		if(flag) {//3
			int i=a*a;//4
		}
	}

}

flag变量是个标记,用来标识变量a是否被写入。这里假设有两个线程写入线程和读取线程,写入线程首先执行writer()方法,随后读取线程接着执行reader()方法。读取线程在执行操作4时,不一定能看到写入线程在操作1(对共享变量a的写入)
由于操作1和操作2没有数据依赖关系,编译器和处理器可能对这两个操作重排序,同样操作3和操作4没有数据依赖关系,编译器和处理器也可能对这两个操作进行重排序。我们来看看,当操作1和操作2重排序时,可能会产生什么效果
在这里插入图片描述
如上图所示,操作1和操作2做了重排序。程序执行时,写入线程首先写标记变量flag,随后读取线程读这个变量。由于条件判断为true,读取线程将读取变量a。此时,变量a还根本没有被写入线程写入,在这里多线程程序的语义被重排序破坏了
下面我们再看看,当操作3和操作4重排序会产生什么效果(借助这个重排序,可以顺便说明控制依赖性)
在这里插入图片描述
在程序中,操作3和操作4存在控制依赖关系,当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜测(Speculation)执行来克服控制相关性对并行度的影响。以处理器的猜测执行为例,执行读取线程的处理器可以提前读取并计算a*a,然后把计算结果临时保存到一个名为重排序缓冲(reorder buffer ROB)的硬件缓存中。当接下来操作3的条件判断为true时,就把该计算结果写入变量i中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值