赞
踩
目录
二、我们一条一条分析,看是否能改变,即加上一些操作使多线程变得安全
三、JMM(java memory model)java内存模型【重点】
四、synchronized和volatile的区别【经典面试题】
1.线程之间是抢占式执行的
2.多个线程修改同一个变量
3.原子性
4.内存可见性
5.指令重排序
1.对于第一条,我们没有办法,因为这种执行方式是由操作系统的内核实现的,我们改变不了
2.对于第二条,
一个线程修改一个变量,没有线程安全问题,结果确定;
多个线程读取同一变量,也没有线程安全问题,读只是单纯的把内存中的数据放到CPU中;
多个线程修改不同的变量,举例来说就是十个线程修改十个变量,一对一的关系,也没有线程安全问题,这个类似于第一种情况
所以为了规避线程安全问题,可以变换代码的组织形式,让一个线程只修改一个变量,但是有些场景下可以这么变换,有些却不可以。
3.对于第三条,像++这样的操作,本质上是三个步骤,是一个“非原子”的操作
像=这样的操作,本质上是一个步骤,是一个“原子”操作,++操作本身不是原子操作,可以通过加锁(synchronized)的方式,把这个操作变成原子操作,因此它是可以改变的。
- public class ThreadDemo16 {
- static class Counter{
- public int count=0;
- synchronized public void increase(){
- count++;
- }
- }
- public static void main(String[] args) throws InterruptedException {
- Counter counter=new Counter();
- Thread t1=new Thread(){
- @Override
- public void run() {
- for (int i = 0; i < 50000; i++) {
- counter.increase();
- }
- }
- };
- t1.start();
-
- Thread t2=new Thread(){
- @Override
- public void run() {
- for (int i = 0; i < 50000; i++) {
- counter.increase();
- }
- }
- };
- t2.start();
- t1.join();
- t2.join();
- System.out.println(counter.count);
- }
- }
执行结果为
上述代码之前也写过,只不过之前的代码有线程安全问题,即得到的结果不是我们想要的,而现在的代码只不过在increase()方法的前面加了一个synchronized
如果两个线程同时并发地尝试调用这个synchronized修饰的方法,此时一个线程会先执行这个方法,另外一个线程会等待,等到第一个线程方法执行完了之后,第二个线程才会继续执行。
其实调用带synchronized的方法,就相当于是加锁和解锁,进入synchronized修饰的方法,就相当于加锁,出来synchronized修饰的方法,就相当于是解锁,如果当前已经是加锁的状态,其他的线程无是法执行这里的逻辑的,只能阻塞等待。
synchronized的功能本质上就是把“并发”变成“串行”,适当地牺牲一些速度,换一个更加准确的值
synchronized除了修饰方法之外,还可以修饰代码块
- public void increase(){
- synchronized(this){
- count++;
- }
- }
synchronized如果是修饰代码块的时候,需要显示地在()中指定一个要加锁的对象
如果是synchronized直接修饰的非静态方法,相当于加锁的对象就是this
Java中任意的对象都可以作为“加锁的对象”
synchronized不光可以起到的加锁的作用,还可以刷新内存(解决内存可见性的问题)
- public void run() {
- for (int i = 0; i < 50000; i++) {
- counter.increase();
- }
- }
上述代码中每次自增的过程都是load、add、save,编译器为了提升效率就进行了优化,即把中间的一些load和save给省略了
加上synchronized之后,就把上面的优化给禁止了,保证了每次进行自增的时候都能够从内存中取出数据,进行add操作后,把数据写回内存。它也是为了结果的正确使得速度变慢。
加入这个功能是为了防止程序员误加两次锁(忘了自己加了两次,且很难发现)出现死锁的情况
- synchronized public void increase(){
- count++;
- }
- synchronized public void increase2(){
- increase();
- }
这个仔细看,还可以发现,但实际开发中几百行代码,看出来的难度就可想而知了。
可重入意思是synchronized可以针对一把锁,连续加锁两次
- synchronized public void increase(){
- synchronized(this){
- count++;
- }
- }
进入increase方法,加了一次锁,进入代码块,又加了一次锁,这种操作对synchronized来说没问题,因为synchronized在这里进行了特殊处理,但是其他语言的话,这里可能会发生死锁。
第一次加锁,加锁成功
第二次尝试对这个线程加锁的时候,此时对象头的锁标记已经确认了,比如说为true,按照之前说的,此时这个线程就要阻塞等待,等待这个锁标记被改为false,然后竞争这把锁,可是,这把锁已经被自己用了,是永远等不到的,此时,就被认为是产生死锁了。
对象头的标记位:对象分为两部分,一部分是对象头,存贮这种对象的公共属性,其中一个属性为“锁标记”,对象的另一部分存储的是字段
Java在这里之所以没有产生死锁是因为synchronized内部记录了当前这个锁是哪个线程持有的。
synchronized修饰普通方法的话,相当于是对this进行加锁,这时如果两个线程并发的调用这个方法,此时是否会触发锁竞争就要看锁对象是否是同一个了。上面讲第三条时举的例子就是同一个锁对象,因此会触发锁竞争。
synchronized修饰静态方法的话,相当于是对类对象进行加锁,由于类对象是单例的,两个线程并发调用该方法一定会触发锁竞争。
synchronized(synchronizedDemo.class)
提到类对象,我们首先要了解反射
反射是面向对象中的一个基本特性,和继承、封装、多态是并列关系
反射也叫“自省”,在程序运行时,通过反射,我们可以知道这个对象包含哪些属性,每个属性的名字,是什么类型,如public 、private......,包含哪些方法,方法名各是什么,参数列表......,而这些信息是来自.class文件(.java被编译生成的二进制字节码)
.class文件会在JVM运行的时候加载到内存中,通过“类对象”来描述这个具体的.class文件的内容
类名.class就得到了这个类对象,特点是每个类的类对象都是单例的
这里的集合类,大部分是线程不安全的,即不能在多线程环境下去并发修改同一个变量。
线程安全的有
它是一个顺序表(动态数组),可以自动扩容,使用synchronized来保证线程安全,即给它的很多方法都加上了synchronized,因为大多数情况下,并不需要在多线程中使用Vector,加了太多的synchronized会降低单线程环境下的执行效率。所以不建议使用。
它是哈希表结构。做法和Vector类似,也是把很多方法都加上了synchronized,因此也是不建议使用。
继承自Vector
是一个线程安全的哈希表,和HashMap相比较设计的就非常好了,这个后面介绍。
它也是线程安全的,也是很多方法都加上了synchronized,因此也是不建议使用。
它被认为是线程安全的,但并没有加锁,原因是String是不可变对象,就是说你看似是改了,但实际上是新建了一个String对象,因此不可能存在两个线程并发修改同一个String
在计算机中一般理解为“可变的,容易改变的”
volatile的功能是保证内存可见性,不保证原子性
举例代码
- public class ThreadDemo17 {
- static class Counter{
- //volatile public int count=0;
- public int count=0;
-
- }
- public static void main(String[] args) {
- Counter counter=new Counter();
- Thread t1=new Thread(){
- @Override
- public void run() {
- while(counter.count==0){
-
- }
- System.out.println("线程运行结束");
- }
- };
- t1.start();
- Thread t2=new Thread(){
- @Override
- public void run() {
- System.out.println("请输入数据:");
- Scanner scanner=new Scanner(System.in);
- counter.count=scanner.nextInt();
- }
- };
- t2.start();
- }
- }
执行结果为
可以看出线程1并没有结束,按我们的预期是输入一个不为0的数,线程1就会终止,但是实际情况却没有挺停,没有听的原因就是内存可见性,即编译器优化了执行过程,不再每次都从内存中取数据。解决这个问题的办法就是在
public int count = 0;这条语句前加上关键字volatile,原理是禁止编译器进行刚才的优化。
加上之后,执行结果为
volatile的用法比较单一,只能修饰一个具体的属性,此时代码中针对这个属性的读写操作就一定会涉及内存操作了。
synchronized不仅保证原子性,也保证了内存可见性
- public class ThreadDemo17 {
- static class Counter{
- //volatile public int count=0;
- public int count=0;
-
- }
- public static void main(String[] args) {
- Counter counter=new Counter();
- Thread t1=new Thread(){
- @Override
- public void run() {
- while(true){
- synchronized (this){
- if ( counter.count != 0) {
- break;
- }
- }
- }
-
- System.out.println("线程运行结束");
- }
- };
- t1.start();
- Thread t2=new Thread(){
- @Override
- public void run() {
- System.out.println("请输入数据:");
- Scanner scanner=new Scanner(System.in);
- counter.count=scanner.nextInt();
- }
- };
- t2.start();
- }
- }
执行结果为
一般来说,如果某个变量,在同一个线程中读和写,大概率要用到。
我们需要知道的是,代码中需要读一个变量的时候,不一定是真的在读内存,可能这个数据已经在内存或者cache中缓存着了,这个时候就可能绕过内存,直接从CPU或者cache中来取这个数据
JMM针对计算机的硬件结构又进行了一次抽象(考虑到java的跨平台性)
把CPU的寄存器L1、L2、L3cache统称为“工作内存”
把真正的内存称为“主内存”
上图中,三个线程都有自己的工作内存,每个线程都有自己独立的上下文,独立的上下文就是各自的一组寄存器/cache上的内容
CPU在和内存交互的时候,经常会把主内存的内容拷贝到工作内存,然后进行操作写回到主内存,这个过程就容易出现数据不一致的情况,这一点尤其在编译器开启优化后更严重。
volatile和synchronized能够强制接下来的操作是操作内存,原理是在生成的Java字节码中强制插入一些“内存屏障”的指令,这些指令的效果是强制同步主内存和工作内存的内容 ,牺牲了效率,换来了正确结果。
首先synchronized既保证原子性,又保证内存可见性,而volatile只保证内存可见性
像++这样的操作,本质上是三个步骤,是一个“非原子”的操作
像=这样的操作,本质上是一个步骤,是一个“原子”操作
我们需要知道的是操作系统调度线程的时候是“抢占式执行”的方式,也就是竞争的关系,某个线程什么时候上CPU执行,什么时候切换出CPU,是完全不确定的。而且另一方面,两个线程在两个不同的CPU可以并发执行。因此,两个线程的执行顺序是完全不可预测的。
我们先假设两个线程针对一个变量进行++操作,我们需要知道count++的操作并不是完全三步都要执行完,也可能是线程1只执行完load,线程2把三步都执行完,线程1再执行后面的两步,此时,算出的结果就会出问题,假设内存中初始值为0,线程2执行完count++后内存中的值变为了1,线程1把后面两步都执行完之后,内存中的值还是1,这就出现了问题,因为我们的两个线程执行了两次count++操作,内存中的结果按理来说应该是2。而且这只是出现问题的其中一种情况。
++操作本身不是原子操作,但是我们可以通过加锁的方式,也就是在方法前加synchronized,把这个操作变成原子操作。
内存可见性具体就是同一个线程修改和读取,由于编译器的优化,可能把++操作一些中间环节的LOAD和SAVE省略掉了,此时读的线程可能读到的是未修改过的结果。
这里我们需要知道:++操作每次执行都有LOAD和SAVE,由于ADD比LOAD和SAVE要快一万倍,所以在执行很多次++操作的时候,很多LOAD和SAVE操作就被省略掉了,这样做是为了提高程序的整体效率。这个省略操作是编译器和JVM综合配合达成的效果。
这种优化在单线程下具有很高的效率,但是在多线程的时候,另一个线程也尝试读取/修改这个数据,这时候就会出问题。
提到内存可见性,我们需要提到冯诺伊曼体系结构中的CPU(可能还有缓存cache)和内存,线程在CPU上运行,代码中需要读一个变量的时候,不一定是真的在读内存,可能这个数据已经在CPU或者cache中缓存着了,这个时候就可能绕过内存,直接从CPU或者cache中来取这个数据
JMM针对计算机的硬件结构又进行了一次抽象(考虑到java的跨平台性)
我们把CPU的寄存器L1、L2、L3cache统称为“工作内存”,把真正的内存称为“主内存
CPU在和内存交互的时候,经常会把主内存的内容拷贝到工作内存,然后进行操作写回到主内存,这个过程就容易出现数据不一致的情况,这一点尤其在编译器开启优化后更严重。
而volatile和synchronized能够强制接下来的操作是操作内存,原理是在生成的Java字节码中强制插入一些“内存屏障”的指令,这些指令的效果是强制同步主内存和工作内存的内容 ,牺牲了效率,换来了正确结果。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。