一、线程安全问题
当多个线程同时共享,同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题。但是做读操作是不会发生数据冲突问题
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关键字有两种用法:
- 修饰需要进行同步的方法(所有访问状态变量的方法都必须进行同步),此时充当锁的对象为调用同步方法的对象
- 同步代码块和直接使用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中。