Java多线程编程核心技术笔记(二)-对象及变量的并发访问

  • A+
所属分类:Java学习

多线程中的同步问题是学习多线程的重中之重。

synchronized同步方法

线程安全与非线程安全:

  • 线程安全:是以获得的实例变量的值经过同步处理的,不会出现脏读的现象。
  • 非线程安全:在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是“脏读”,也就是取到的数据其实是被更改过的。

方法内的变量为线程安全的

实例变量非线程安全

只有共享资源的读写访问才需要同步化,如果不是共享资源,就不需要同步。

实例方法添加synchronized关键字创建的是该实例对象锁。

synchronized方法与锁对象的关系:

  • A线程先持有object对象的Lock锁,B线程可以以异步的方式调用object对象中的非synchronized类型的方法。
  • A线程先持有object对象的Lock锁,B线程如果在这时调用object对象中的synchronized类型的方法则需等待,也就是同步。

synchronized锁重入:
关键字synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象后,再次请求此对象锁时是可以再次得到该对象的锁的。这也证明在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。

可重入锁的概念:自己可以再次获取自己的内部锁。比如有一条线程获得了某个对象的锁,此时这个对象还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果锁不可重入的话,就会造成死锁。

可重入锁也支持在父子类继承的环境中。当存在父子类继承关系时,子类是完全可以通过“可重入锁”调用父类的同步方法的。

出现异常,锁自动释放:当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

同步不具有继承性:同步不可被继承,所以再重写同步方法时需要在子类方法中添加synchronized关键字。

synchronized同步语句块

使用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个长时间的任务,那么B线程则必须等待比较长的时间。在这种情况下可以使用synchronized同步语句块来解决。

synchronized代码间的同步性:在使用同步synchronized(this)代码块时需要注意的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对同一个object中所有其他synchronized(this)同步块的访问将被阻塞,这说明synchronized使用的是“对象监视器”。

和synchronized方法一样,synchronized(this)代码块也是锁定当前对象的。

任意对象都可以作为对象监视器。

synchronized同步方法的作用:

  1. 对其他synchronized同步方法或synchronized(this)同步代码块调用呈阻塞状态。
  2. 同一时间只有一个线程可以执行synchronized同步方法中的代码。

synchronized(this)同步代码块的作用:

  1. 对其他synchronized同步方法或synchronized(this)同步代码块调用呈阻塞状态。
  2. 同一时间只有一个线程可以执行synchronized(this)同步代码块中的代码。

synchronized(非this对象)同步代码块的作用:

  1. 在多个线程持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this对象)同步代码块。
  2. 当持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this对象)同步代码块。

锁非this对象具有一定的优点:如果在一个类中有很多个synchronized方法,这时虽然能够实现同步,但会受到阻塞,所以影响运行效率;但如果使用同步代码块锁非this对象,则synchronized(非this对象)代码块中的程序与同步方法是异步的,不与其他锁this同步方法争抢this对象,则可大大提高运行效率。

synchronized(非this对象x)格式结论如下:

  1. 当多个线程同时执行synchronized(x){}同步代码块时呈同步效果。
  2. 当其他线程执行x对象中的同步方法时呈同步效果。
  3. 当其他线程执行x对象方法里的synchronized(this){}代码块时也呈现同步效果。

注意: 如果其他线程调用不加synchronized关键字的方法时,还是异步调用。

静态同步synchronized方法与synchronized(class)代码块

  • 关键字还可以应用到静态方法上,如果这样写,那是对当前的*.java文件对应的Class类进行加锁。
  • Class锁可以对类的所有对象实例起作用。
  • 同步synchronized(class)代码块的作用其实和synchronized static方法的作用一样。

数据类型String的常量池特性:在JVM中具有String常量池缓存功能,将synchronized(String)同步块与String联合使用,会带来意想不到的后果;因此synchronized代码块中尽量不要使用String作为锁对象,而改用其他的对象,比如new Object()实例化一个Object对象。

同步synchronized方法无限等待与解决:同步方法中如果存在死循环容易造成死锁,可以使用同步块来解决这个问题,使用不同的对象锁。

多线程的死锁

  • Java线程死锁是一个经典的多线程问题,因为不同的线程都在等待根本不可能被释放的锁,从而导致所有的任务都无法继续完成。在多线程技术中,“死锁”是必须避免的,因为这会造成线程的“假死”。
  • 死锁是程序的BUG,在设计程序时就要避免双方互相持有对方的锁的情况。

锁对象改变:尽量不要在运行过程中改变锁对象,否则会造成不同步现象。

volatile关键字:

主要作用是使变量在多个线程间可见。强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值。

多线程的三个重要特性:原子性、可见性、有序性。只有在满足了这三个特性,才能保证并发程序正确执行,否则就会出现各种各样的问题。

volatile缺点:不支持原子性。

关键字synchronized和volatile比较:

  • 关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized好,并且volatile只能修饰于变量,而synchronized可以修饰方法,以及代码块。随着JDK新版本的发布,synchronized关键字在执行效率上得到很大提升,在开发中使用synchronized关键字的比率还是比较大的。
  • 多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。
  • volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以简介保证可见性,因为它会将私有内存和公共内存中的数据做同步。
  • 关键字volatile解决的是变量在多线程之间的可见性;而synchronized关键字解决的是多线程之间访问资源的同步性。

volatile变量在自增、自减等计算操作时不是原子性操作,因此时非线程安全的。

如表达式i++的操作步骤可以分解为:

  1. 从内存中取出i的值;
  2. 计算i的值;
  3. 将i的值写到内存中。

变量在内存中的工作过程如下:

  • read和load阶段:从主存复制到当前线程工作内存;
  • use和assign阶段:执行代码,改变共享变量值;
  • store和write阶段:用工作内存数据刷新主存对应变量的值。

多线程环境中,use和assign是多次出现的,这一操作不是原子性的,也就是在read和load之后,如果主内存count变量发生变化,线程工作内存中的值由于已经加载,不会产生对应的变化,也就是私有内存和公共内存中的变量不同步,所以计算出来的结果会和预期不一样,也就出现了非线程安全问题。

对于用volatile修饰的变量,JVM只是保证从主内存加载到线程工作内存的值是最新的,例如线程1和线程2在进行read和load的操作中,发现主内存中的count的值都是5,那么都会加载这个最新值。也就是说,volatile关键字解决的是变量读时的可见性问题,但无法保证原子性,对于多个线程访问同一个实例变量还是需要加锁同步。

主内存与工作内存的交互操作(8种操作):

  • lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  • load(加载):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

使用原子类进行i++操作:
- 除了在i++操作时使用synchronized关键字实现同步外,还可以使用AtomicInteger原子类进行实现。
- 原子操作是不能分割的整体,没有其他线程能够中断或检查正在原子操作中的变量。原子类中的方法是原子的,但方法和方法之间的调用却不是原子的,所以同时调用多个原子方法也是需要加锁同步的。

weinxin
微信公众号
微信搜索Java技术宅或者扫描左边二维码进行关注,这样可以及时查看潘超博客每日内容推送

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: