目录
Volatile关键字
volatile关键字只能修饰变量。
内存可见性
volatile修饰的变量,能保证“内存可见性”
以下是一个没有加volatile关键字的时候执行的代码:
//首先创造一个类命名为NCount
class NCount{
//创建一个变量
public int flag = 0;
}
public class ThreadD15 {
public static void main(String[] args) {
NCount nCount = new NCount();
//创建两个线程
Thread t1 = new Thread(() ->{
//第一个线程之进行条件判断,设置一个循环,如果满足循环条件的时候一直执行循环体
while (nCount.flag == 0){
//这里不添加任何语句
}
//如果不满足上面的while循环条件(flag的值被修改,则跳出循环)
System.out.println("t1循环结束");
});
//在第二个线程里,我们来修改一下flag的值,这样就可以使t1线程不满足循环的条件。
Thread t2 = new Thread(() -> {
//选择从控制台进行输入
Scanner scan = new Scanner(System.in);
System.out.println("请手动输入一个非0的数");
//将NCounter中的flag值修改为控制台输入进去的数字
nCount.flag = scan.nextInt();
});
//运行t1和t2线程
t1.start();
t2.start();
}
}
当我们进行代码运行的时候可以在控制台上看到以下的结果:
可以看到的是,关于我们刚才进行的代码编写,我们想得到的效果是线程t2将flag的值修改为不是0的数字后,被线程t1在下一次循环时,对while中的条件进行判定。发现不满足flag=0,跳出循环后打印出
可是并没有和我们所想的一样,程序就出现了bug。我们运行程序,打开jconsole:
可以看到的是t2线程已经没了,只剩下了t1线程继续执行循环。
以上这种情况就是内存可见性问题。
从汇编来理解内存可见性问题:
大概就是两步操作:
1.load,把内存中的flag值,读取到寄存器中。
2.cmp,把寄存器中的值和0进行比较,根据比较的结果,决定下一步需要向哪个方向执行。
这个循环的执行速度是非常快的,一秒执行百万次以上。
循环执行了很多很多次,在t2进行真正修改之前,load得到的结果都是相同的,另一方面,load操作与cmp操作相比是要慢很多很多的。由于load的执行速度“太慢“,再加上反复得到的结果都是相同的,JVM就自主的进行决定:判定load不会进行更改,不再重复的load了,干脆只读一次。
内存可见性问题:
一个线程针对一个变量进行读取操作,同时另一个线程对这个变量进行修改,此时独到的值,不一定是修改后的值。(归根到底是编译器/JVM在多线程环境下优化时产生了误判)
此时就需要我们手动的进行修改,可以在flag变量前加上关键字volatile意思就是告诉编译器这个变量是”易变的“,要确保每一次都会重新读取这个变量。
此时我们再次运行程序:
需要注意的小问题:
volatile只能修饰变量,但是不能修饰方法里的变量。
方法里的变量都是局部变量。
概念扩展:
局部变量:
局部变量(Local Variable)定义在函数体内部的变量,作用域仅限于函数体内部。离开函数体就会无效。再调用就是出错。
全局变量:
全局变量(Global Variable)定义在所有的函数外部定义的变量,它的作用域是整个程序,也就是所有的源文件,包括.c和.h文件。
全局变量既可以是某对象函数创建,也可以是在本程序任何地方创建。全局变量是可以被本程序所有对象或函数引用。
方法里的局部变量只能在当前的线程里面引用,不能多线程之间同时读取/修改,天然规避了线程安全的问题。局部变量:只在当前的方法里使用,出了方法变量就没了,方法内部的变量在”栈“这样的内存空间上,每个线程都有自己的栈空间,即使是同一种方法,在多个线程中被调用,这里的局部变量也会出现在不同的栈空间中,本质上也是不同的变量。
上面说的内存可见性,编译器优化的问题也不是始终会出现的,编译器可能会误判,但也不是会百分之百进行误判。例如我们将代码进行调整,在循环体里面加上100ms的休眠。
输出结果:
可以看出,此时我们即使不加volatile 代码也正确了,刚才的错误优化也没有了。
从JVM的角度表述内存可见性问题
Java程序里,主内存,每个线程还有自己的工作内存(t1与t2的工作内存不是一回事)
t1线程进行读取的时候,只读取了工作内存的值。
t2线程进行修改的时候,先修改的工作内存的值,然后再把工作内存的内容同步到主内存中。
但是由于编译器的优化,导致t1没有重新的从主内存同步数据到工作内存,读到的结果就是”修改之前“的结果。
这里可以按照以下的对应关系进行记忆
为什么在Java这里不直接叫”CPU寄存器“,而是专门搞了一个”工作内存“的说法?
这里的工作内存不一定只是CPU的寄存器,也有可能是CPU的缓存cache。
CPU读取寄存器,速度比读取内存快太多,因此就会在CPU内部引入cache。
寄存器 | 存储空间小 | 读写速度快 | 贵 |
cache | 存储空间居中 | 读写速度居中 | 成本居中 |
内存 | 存储空间大 | 读写速度慢 | 相对寄存器来说便宜 |
当CPU进行读取一个内存数据的时候可能是直接读内存的,可能是读cache的,也可能是读寄存器中的。
当引入cache之后,硬件结构就变的复杂了。工作内存(工作存储区):CPU寄存器+CPU的cache,为了表述简单且避免涉及到一些硬件细节的差异,所以在JAVA中就使用”工作内存“了。
volatile不保证原子性
volatile不能保证,原子性是靠synchronized来保证的。
二者都可以保证线程的安全,但是不能使用volatile来处理两个线程并发++这样的问题。
运行修改之后的代码:
对比synchronized的程序与结果:
wait和notify
线程最大的问题:抢占式执行,随即调度。因此线程之间的执行顺序是不确定的。
但是在实际的开发中,有些时候我们希望能够合理的协调多个线程之间的关系。所以程序员就发明了一些办法,来控制线程之间的执行顺序,虽然在内核中的调度是随机的,但是可以通过API让线程进行主动的阻塞,主动放弃CPU,给别的线程让路。
【例子】在篮球比赛中,队员们通过传球操作,将球投入到篮框中,这几个相互配合的队员就可以看做是不同的线程。
这里我们需要思考一个问题,在前面的博客中也记录了一些可以让线程等待的方式,例如join()和sleep()操作。那么使用这些可以么?
join如果被使用,则必须要t1彻底的执行完,t2才可以运行,那么如果希望t1执行百分之五十的进度,然后让t2开始执行这种情况下则是不可实现的。
至于sleep,sleep是指定了一个休眠时间,但是t1完成这些任务究竟花费多久的时间是没有办法估计的。
使用wait与notify可以解决以上提出的问题。
完成这个协调操作,主要涉及到三个方法:
wait( )/wait(long timeout)让当前线程进入等待线程
notify( )/notifyAll( ) :唤醒当前对象上的等待线程
注意!:wait,notify,notifyAll 都是Object的类方法,换句话说也就是JAVA中的随机一个类都可以有这三种方法
wait( )方法
wait与synchronized
某个线程调用wait方法,就会进入阻塞(无论是通过哪个对象wait)。此时就处于WAITING状态。
下面我们来看一个简单的代码案例;
public class ThreadD16 {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
System.out.println("wait之前");
object.wait();//这里wait不加任何参数,就是死等
System.out.println("wait之后");
}
}
我们来执行一下这个代码:
错误提示:非法锁状态异常
为什么会出现这个异常?
首先我们需要研究一下wait操作具体都做了什么?
(1)释放锁
(2)进行阻塞等待
(3)收到通知后,重新尝试获取锁,并在获取锁之后,继续向下执行。
这里的第一步释放锁,而在代码中,还没有加锁,所以就会出现异常。
因此,wait操作与synchronized操作需要搭配使用!!
对代码进行更改:
执行结果:
可以看出这里的wait进行了阻塞等待,阻塞在了synchronized代码块中,实际上这里的阻塞是已经释放了锁,此时的其他线程是可以获取到object这个对象的。
这里的阻塞就处于是WAITING状态。
notify()方法
notify方法是唤醒等待线程
方法notify()也要在同步方法或同步块中使用,该方法是用来通知那些可能等待该对象的对象锁的其他线程,对其通知,并使他们重新获取该对象的对象锁。
如果多个线程等待,则会随机调度一个呈现wait状态的线程(无顺序)
在notify()方法之后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程执行完,也就是退出同步代码块后才会释放对象锁。
代码示例:
public class ThreadD17 {
//创建两个线程来阐述wait和notify的操作
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
Thread t1 = new Thread(() -> {
System.out.println("wait之前");
try {
synchronized (obj) {
obj.wait();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("wait之后");
});
Thread t2 = new Thread(() -> {
System.out.println("notify之前");
synchronized (obj) {
obj.notify();
}
System.out.println("notify之后");
});
t1.start();
Thread.sleep(1000);
t2.start();
}
}
运行结果:
此处的notify得和wait配对,如果二者使用的对象不同,则notify不会有·任何效果。notify只能唤醒在同一个对象上等待的线程。
notifyAll()方法
notifyAll与notify非常相似,多个线程wait的时候,notify随机唤醒一个,notifyAll将所有的线程都唤醒,这些线程继续进行锁竞争。