问题点收集二

一、多线程不安全的原因和基本的解决方案
Java内存模型(JMM)描述了Java程序中变量(线程公用变量)的访问规则(可以看做是一种规范),以及在JVM中将变量存储到内存和内存中读取出变量这样的底层细节。

所有的变量都存储在主内存中
每个线程都有自己独立的工作内存,里面保存该线程使用到的变量副本(主内存中该变量的一份拷贝)

并且规定:线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写
不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量的传递主要通过主内存来完成
举个例子(修改线程A中的变量):

把工作内存A中更新过的共享变量更新到主内存中
将主内存中的最新共享变量的值更新到工作内存B中
如果满足上面两点,也就是说线程A中更新的共享变量线程B中能够及时得到更新,就称为变量是可见的,反正则是不可见。

共享变量、可见性和原子性
共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量。
可见性:一个线程对共享变量值的修改,能够及时地被其他线程看到。
原子性: 一段指令像原子一样不可分割,在执行结束之前,其他线程不可打断。

指令重排序和as-if-serial
代码书写顺序与代码实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做出的优化。

主要有三种方式:

编译器优化的重排序(编译器优化,主要是单线程下,在保证执行结果正确的前提下,重排序代码顺序让代码更符合机器执行)
指令级并行重排序(处理器优化,主要为多核计算机同时执行做了优化)
内存系统的重排序(处理器优化,主要是运行内存,如主内存、工作内存进行重排序)
举个例子:

int a = 2;
int b = 3;
int c = a + b;
在实际运行中可能是:

int b = 3;
int a = 2;
int c = a + b;
上述例子中演示了指令重排序,那么可能会有人问,第三行代码如果重排序到前两行代码之前,岂不是会报错吗?
有一个as-if-serial,其内容如下:
无论如何重排序,程序执行的结果应该与代码顺序执行结果一致。(Java编译器、运行时和处理器都要保证Java在单线程下遵循as-if-serial语义)
因此在单线程的情况下你不必担心指令重排序带来什么不良后果。
但是在多线程交错执行时,重排序就可能造成内存可见性问题,详情请继续阅读下文。

多线程中的指令重排序
package cn.com.dotleo;
public class SynchronizedDemo {

    // 共享变量
    private boolean ready = false;
    private int result = 0;
    private int number = 1;

    // 写操作
    public void write() {
        ready = true;  // 1.1
        number = 2;  // 1.2
    }

    // 读操作
    public void read() {
        if (ready) {  // 2.1
            result = number * 3;  //2.2
        }
        System.out.println("result的值为:" + result);
    }

    private class ReadWriteThread extends Thread {
        private boolean flag;
        public ReadWriteThread(boolean flag) {
            this.flag = flag;
        }

        // 根据传入执行不同的读写操作
        @Override
        public void run() {
            if (flag) {
                write();
            } else {
                read();
            }
        }
    }

    public static void main(String[] args) {
        SynchronizedDemo demo = new SynchronizedDemo();
        // 启动写线程
        demo.new ReadWriteThread(true).start();
        // 启动读线程
        demo.new ReadWriteThread(false).start();
    }

}

对于这个程序,如果执行main方法将可能有一下几种情况:

1.1 -> 2.1 -> 2.2 -> 1.2 result:3
1.2 -> 2.1 -> 2.2 -> 1.1 result:0
其实,2.1和2.2也是可以重排序的:

int temp = number * 3;
if (ready) {
    result = temp;
}
因此就有了:

2.2 -> 2.1 ->1.2 -> 1.1
...

导致共享变量线程不安全的原因
到这里,可以总结一下为什么会出现共享变量线程不安全的主要原因了:

线程的交叉执行(原子性)
重排序结合线程的交叉执行(可见性)
共享变量更新后的值没有在工作内存中与主内存之间及时更新(可见性)
可见性的实现方式
从Java语言层面讲,主要支持以下两种方式:

Synchronized
Volitile
不包括JDK 1.5提供的Java并发包

Synchronized
JMM关于Synchronized的两条规定:

线程解锁前,必须把共享变量的最新值刷新到主内存中
线程加锁时,将清空工作内存中共享变量的值,使得使用共享变量时需要从主内存中重新获取最新的值。(加锁和解锁需要是同一把锁)
我们先来修改一下原来的代码,保证其共享变量在多线程下的可见性。

    // 写操作
    public synchronized void write() {
        ready = true;  // 1.1
        number = 2;  // 1.2
    }

    // 读操作
    public synchronized void read() {
        if (ready) {  // 2.1
            result = number * 3;  //2.2
        }
        System.out.println("result的值为:" + result);
    }

为什么这个操作能保证其可见性呢?我们通过分析导致共享变量线程不可见的3个原因逐一分析:

Synchronized关键词加锁后,保证了线程不会交叉执行
如果线程不交叉执行,那么无论如何重排序,都相当于单线程中的重排序,遵循as-if-serial语义
上面提到的Synchronized的两条规定能保证它及时获取并且在操作结束后及时更新主内存中的共享变量的值。

volatile
关于volatile,它有以下特性:

能够保证变量的可见性
不能保证变量的原子性
它的这些特性是通过内存屏障和禁止指令重排序优化来实现的。

对volatile变量执行写操作时,会在写操作后加入一条store屏障指令,让主内存中的变量及时更新
对volatile变量执行读操作时,会在读操作前加入一条load屏障指令,更新主内存中的变量
举一个volatile的例子说明它不具备原子性。

package cn.com.dotleo;
public class VolatileDemo {

    private volatile int num = 0;

    public int getNum() {
        return this.num;
    }

    public void increase() {
        this.num++;
    }

    public static void main(String[] args) {
        final VolatileDemo volatileDemo = new VolatileDemo();
        for (int i = 0; i < 500; i++) {
            new Thread(new Runnable() {
                public void run() {
                    volatileDemo.increase();
                }
            }).start();
        }

        // 为了让所有线程执行完毕
        // 如果还有子线程执行
        // 主线程让出cpu资源
        while (Thread.activeCount() > 1) {
            Thread.yield();
        }
        System.out.println("num" + volatileDemo.getNum());
    }
}

该程序的运行结果不总是500。因为increase()方法中的num++并非原子操作,它包括了:

从主内存中获取num的值
num的值 + 1
将num + 1的值赋值给num
试分析一种情况:

线程A获取到num的值 = 0,由于它不是原子性,cpu资源被线程B抢走
线程B获取num的值 = 0
线程B对num + 1 = 1
线程B把num值更新到主内存中后结束
线程A重新获得cpu资源, 对它内存中num的副本 + 1 = 1
线程A将num = 1更新到主内存中
至此,两次循环却少加了,导致并非我们想要的num最终 = 500

volatile适用场景
要在多线程中安全的使用volatile,必须同时满足:

对变量的写入操作不依赖当前值
该变量没有包含在具有其他变量的不变式中

二、多线程对同一个对象操作
多线程实现的两种方式:
  继承Thread类
  实现Runnable接口
这两种方式都必须重写run()方法。
当多线程对同一个对象操作时,必须实现Runnable接口,而不能继承Thread类。因为继承Thread类实现多线程时,操作的是不同的对象。
1 Runnable r=new RunnableImpl();
2 Thread t1=new Thread(r);
3 Thread t2=new Thread(r);
4 t1.start();
5 t2.start();
两个线程传同一个对象,相当于这两个线程对同一个对象进行操作。
注意:多线程对同一个对象操作的时候一定要加锁synchronized,同时传一个锁旗标。
   加锁时,原则上要双重检查。
   锁旗标对同一个对象有用,如果是两个对象,则这两个对象分别有自己的锁旗标,此时锁旗标就会失效。
   synchronized不仅可以加在代码块上,还可以加在方法上。当加在方法上时,锁旗标是this,谁调用这个方法,this就是谁,只要是同一个对象就行。
   synchronized提高了安全性,降低了性能。

三、Java中的String,StringBuilder,StringBuffer三者的区别
这三个类之间的区别主要是在两个方面,即运行速度和线程安全这两方面。
1. 首先说运行速度,或者说是执行速度,在这方面运行速度快慢为:StringBuilder > StringBuffer > String
String最慢的原因:
String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可更改的,但后两者的对象是变量,是可以更改的。以下面一段代码为例:
String str="abc";
System.out.println(str);
str=str+"de";
System.out.println(str);
如果运行这段代码会发现先输出“abc”,然后又输出“abcde”,好像是str这个对象被更改了,其实,这只是一种假象罢了,JVM对于这几行代码是这样处理的,首先创建一个String对象str,并把“abc”赋值给str,然后在第三行中,其实JVM又创建了一个新的对象也名为str,然后再把原来的str的值和“de”加起来再赋值给新的str,而原来的str就会被JVM的垃圾回收机制(GC)给回收掉了,所以,str实际上并没有被更改,也就是前面说的String对象一旦创建之后就不可更改了。所以,Java中对String对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,所以执行速度很慢。
而StringBuilder和StringBuffer的对象是变量,对变量进行操作就是直接对该对象进行更改,而不进行创建和回收的操作,所以速度要比String快很多。
另外,有时候我们会这样对字符串进行赋值
String str="abc"+"de";
StringBuilder stringBuilder=new StringBuilder().append("abc").append("de");
System.out.println(str);
System.out.println(stringBuilder.toString());
这样输出结果也是“abcde”和“abcde”,但是String的速度却比StringBuilder的反应速度要快很多,这是因为第1行中的操作和
String str="abcde";
是完全一样的,所以会很快,而如果写成下面这种形式
String str1="abc";
String str2="de";
String str=str1+str2;
那么JVM就会像上面说的那样,不断的创建、回收对象来进行这个操作了。速度就会很慢。
2. 再来说线程安全
在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的
如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。所以如果要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的StringBuilder。
3. 总结一下
String:适用于少量的字符串操作的情况
StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值