一、概述
线程交互通常是通过共享变量完成的,当线程之间没有交互时,开发多线程的应用程序就会变得简单很多。一旦发生了交互,很多诱发线程不安全的因素就会暴露出来。
在了解同步这个概念之前最好学一学JVM,我推荐《深入了解JVM》这本书,讲的很好,身边的人接触虚拟机都是从这本书开始的。
java的内存划分一般是栈和堆(其实划分的很复杂,但这里不介绍那么多),栈和堆不是数据结构里面的意思,你可以理解为栈和堆就是jvm内存的两块区域。
栈是线程私有的,像基本数据结构,int,char等等,对象引用、方法出口,程序计数等信息放在栈里,这里不存在同步的问题。
堆是线程共享的,存储的是类、静态变量、对象等等唯一性的信息。
同时对应的内存区域是栈是工作内存,堆是主内存,工作内存请求唯一变量是拿走了主内存的变量的拷贝,工作内存和主内存之间是通过四个原子操作进行的,分别对应着读写俩操作
读:read读取主内存变量,load 作用于工作内存变量,将read得来的值写入工作内存
读写之间是使用,也对应着俩原子操作,use使用这个变量,和assign赋值
写:store将工作内存的变量存储到主内存,write作用于主内存变量,它把store操作从工作内存得到的变量的值放入主内存的变量
因为有些变量是共享的,所以在多线程的环境下,就有可能出现单线程不可能出现的问题。
比如
private static int a=0;
public void setA(){
a=a+1;
}
这里对于setA的过程是先读取a的值,然后+1,再赋值给a
多线程的环境下就会发生,线程1读取a,在还没赋值给a前,线程2读取a。造成的结果本来是a=2,结果变成了a=1;
所以我们需要加限制去避免这种情况
二、同步,synchronized关键字
同步就是多个线程合作去顺利的完成某项任务,比如线程1让a+1,线程2让a+1,总的结果一定得是a+2,不能出现其他情况,否则就是线程不安全。线程不安全的意思就是线程多次执行的结果不确定。
同步是JVM的一个特性,旨在保证两个或者多个并发的线程不会同时执行同一块临界区,临界区就是必须以串行的方式访问的一段代码块
因为其他线程在临界区中的时候每条线程堆该临界区的访问都会互斥地执行,这种同步属性就称为互斥。由于这个原因,线程获取到的锁经常称为互斥锁。
同步也表现出可见性,该属性能够保证一条线程在临界区执行的时候总是能看到共享变量最近的修改。当进入临界区时,它从主存中读入这些变量,离开时把这些变量的值写入内存。
同步是通过监听器来实现的,监听器是针对临界区构建的并发访问控制,并发必须以不可分割的形式执行。每个java对象都和一个监听器相关联,这样线程就可以通过获取和释放监听器的锁来上锁和解锁。
只有一个线程可以持有监听器的锁,任意尝试锁住该监听器的线程都会一直阻塞,直到能够获取锁为止。当线程离开临界区,它会通过释放锁来解释监听器
java提供synchronized关键字来串行线程对方法和语句块的访问。
synchronzied可以锁class、实例对象、方法
一般给方法加synchronzied都是add,get,size()这几个方法(安全容器),是因为着三个操作最容易受到并发的影响。
synchronize(对象名){}可以锁住某个对象
结论:要么访问同一段代码序列的两条或两条以上线程必须获取同一把锁,要么不存在同步。
同步可能会产生死锁的问题,死锁很简单
就是A持有资源1,想要资源2,B持有资源2,想要资源1,谁的资源都不能被抢,也没有多余的资源。这就发生了死锁,死锁问题很难排查,因为它会消耗大量的性能资源却完成不了任务,但也不报错,所以在一开始的时候规范设计程序,并发编程时注意细节。
三、volatile和final变量
如果你认真看了上文的话就应该明白什么叫工作内存和主内存,以及六大原子操作(读写用)
volatile修饰的变量,能让它被所有线程“看见”,什么叫看见呢
就是被volatile修饰的变量,在使用之前都会从主内存刷新读取,赋值后都会同步刷新主内存中的变量的值。而线程与线程之间交互是通过主内存完成的,所以各个线程都能看到这个变量。
被final修饰的变量或对象,只要被正确的构造出来并赋值了,就不会再变。以前的版本会出来不同线程读取同一个final变量,然后都做初始化赋值操作,很显然是错的。后来Sum加强了final的语义。