线程独占:每个线程都会有它独立的空间,随线程生命周期而创建和销毁
线程共享:所有线程能访问这块内存数据,随虚拟机或者GC而创建和销毁
public class DemolVisibility {
int i=0;
boolean isRunning=true;
public static void main(String args[]) throws InterruptedException{
DemolVisibility demo=new DemolVisibility();
new Thread(new Runnable() {
@Override
public void run() {
while (demo.isRunning){
demo.i++;
}
System.out.println("i="+demo.i);
}
}).start();
Thread.sleep(3000L);
demo.isRunning=false;
System.out.println("shutdown....");
}
}
Java编程语言的语义允许Java编译器和微处理器进行执行优化,当多线程的时候这些优化导致了与其交互的代码不再同步,从而导致看似矛盾的行为。
脚本语言与编译语言的区别?
解释执行:即咱们说的脚本,在执行时,由语言的解释器将其一条条翻译成机器可识别的指令。编译执行:将我们编写的程序,直接编译成机器可以识别的指令码。
Java是脚本语言还是编译语言? Java介于脚本语言与编译语言之间
可见性问题:让一个线程对共享变量的修改,能够及时的被其他线程看到。
Java内存模型规定:
对volatile变量v的写入,与所有其他线程后续对v的读同步
要满足这些条件,所以volatile关键字就有这些功能:
可以在线程之间共享的内存称为共享内存或堆内存。
所有实例字段、静态字段和数组元素都存储在堆内存中,这些字段和数组都是标题中提到的共享变量。
′冲突:如果至少有一个访问是写操作,那么对同一个变量的两次访问是冲突的。
这些能被多个线程访问的共享变量是内存模型规范的对象。
read操作(一般读,即非volatile读)
write操作(一般写,即非volatile写)
volatile read
volatile write
Lock.(锁monitor)、Unlock>
线程的第一个和最后一个操作
外部操作
所有线程间操作,都存在可见性问题,JMM需要对其进行规范
对volatile变量v的写入,与所有其他线程后续对v的读同步
对于监视器m的解锁与所有后续操作对于m的加锁同步
对于每个属性写入默认值(0,false,null)与每个线程对其进行的操作同步
启动线程的操作与线程中的第一个操作同步
线程T2的最后操作与线程T1发现线程T2已经结束同步。( isAlive ,join可以判断线程是否终结)
如果线程T1中断了T2,那么线程T1的中断操作与其他所有线程发现T2被中断了同步
通过抛出InterruptedException异常,或者调用Thread.interrupted或 Thread.isInterrupted
happens-before关系用于描述两个有冲突的动作之间的顺序,如果一个action happends before另
一个action,则第一个操作被第二个操作可见,JVM需要实现如下happens-before规则:
某个线程中的每个动作都 happens-before该线程中该动作后面的动作。
某个管程上的unlock动作happens-before同一个管程上后续的lock动作
对某个volatile 字段的写操作 happens-before每个后续对该volatile字段的读操作在某个线程对象上调用start()方法 happens-before被启动线程中的任意动作如果在线程t1中成功执行了t2.join(),则t2中的所有操作对t1可见
如果某个动作 a happens-before动作 b,且 b happens-before动作c,则有 a happens-before c.\
final在该对象的构造函数中设置对象的字段,当线程看到该对象时,将始终看到该对象的final字段的正确构造版本。伪代码示例: f= new finalDemo();读取到的f.x一定最新,x为final字段。
如果在构造函数中设置字段后发生读取,则会看到该final字段分配的值,否则它将看到默认值;伪代码示例:public finalDemo(){x= l; y=x;}; y会等于l;
读取该共享对象的final成员变量之前,先要读取共享对象。
伪代码示例:r= new ReferenceObj(); k= r.f ;这两个操作不能重排序
通常被static final修饰的字段,不能被修改。然而System.in、System.out、System.err被static final修饰,却可以修改,遗留问题,必须允许通过set方法改变,我们将这些字段称为写保护,以区别于普通final字段;
由于《Java语言规范》的原因,对非 volatile的double、long的单次写操作是分两次来进行的,每次操作其中32位,这可能导致第一次写入后,读取的值是脏数据,第二次写完成后,才能读到正确值。
32位
32位
64位long/double
原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分(不可中断性)。
将整个操作视作一个整体,资源在该次操作中保持一致,这是原子性的核心特征。
Compare and swap比较和交换。属于硬件同步原语,处理器提供了基本内存操作的原子性保证。
CAS操作需要输入两个数值,一个旧值A(期望操作前的值)和一个新值B,在操作期间先对旧值进行比较,若没有发生变化,才交换成新值,发生了变化则不交换。
JAVA中的sun.misc.Unsafe类,提供了compareAndSwapInt()和compareAndSwapLong()等几个方法实现CAS
举个例子:
主内存有个数据值:A,两个线程A和B分别copy主内存数据到自己的工作区,A执行比较慢,需要10秒, B执行比较快,需要2秒, 此时B线程将主内存中的数据更改为B,过了一会又更改为A,然后A线程执行比较,发现结果是A,以为别人没有动过,然后执行更改操作。其实中间已经被更改过了,这就是ABA问题。
也就是ABA问题只要开始时的数据和结束时的数据一致,我就认为没改过,不管过程。
尽管A线程的CAS操作是成功的,但是不代表这个过程就是没问题的。
ABA问题说简单点就是,预判值还是和当初抓取的一样,但是这个“ 值 ”的版本可能不一样了,在某些不仅要考虑数据值是否一致,还要考虑版本是否一致的场景下需要注意.
Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。
thread1、thread2同时读取到 i=0后,
thread1、thread2都要执行CAS(0,1)操作,
假设thread2操作稍之后与thread1,则thread1执行成功> threadl紧接着执行了CAS(1,0),将i的值改回0
自旋锁: 是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
乐观锁∶ 假定没有冲突,在修改数据时如果发现数据和之前获取的不一致,则读最新数据,修改后重试修改悲观锁:假定会发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁。
独享锁(写): 给资源加上写锁,线程可以修改资源,其他线程不能再加锁;(单写)
共享锁(读)∶给资源加上读锁后只能读不能改,其他线程也只能加读锁,不能加写锁;(多读)
可重入锁、不可重入锁: 线程拿到一把锁之后,可以自由进入同一把锁所同步的其他代码。
公平锁、非公平锁: 争抢锁的顺序,如果是按先来后到,则为公平。
1、用于实例方法、静态方法时,隐式指定锁对象
2、用于代码块时,显示指定锁对象
3、锁的作用域:对象锁、类锁、分布式锁
4、引申:如果是多个进程,怎么办?
特性:可重入、独享、悲观锁
锁优化︰锁消除(开启锁消除的参数:-XX:+DoEscapeAnalysis-XX:+EliminateLocks)
synchronized ( this){
i++;
}
如上面的代码 状态会被记录到this对象中吗?
若锁占用,线程挂起,
释放锁时,唤醒挂起的线程,是如何做到的?
1…对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode;
2.Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例;
3.数组长度也是占用64位(8字节)的空间,这是可选的,只有当本对象是一个数组对象时才会有这个部分;
4.对象体是用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型;
5.对齐字是为了减少堆内存的碎片空间(不一定准确)。
其中类对象存储如下图:
public class Demo5_main {
public static void main (string args[]){
int a - 1;
Teacher james = new Teacher () ;
james.stu = new Student();
}}
class Teacher(
String name = "james" ;
int age = 40;
boolean gender = true;
student stu;
static....
}}
class student(
String name -""Emily";int age = 18;
boolean gender = false;
}
堆中存储的是实例对象的值
String str=System.getEnv(“JAVA_HOME”)
String类型是引用类型存在堆的形式是以字符串数组形式
一种形式是String str=“”a“”;
局部变量存的是堆对象的引用,堆中存的是常量池中的引用。
方法是行为需要的时候执行。方法是方法区是类元数据的一部分
static修饰的一部分作为不是对象,做为元数据的一部分存在方法区中。
方法是行为需要的时候执行。方法是方法区是类元数据的一部分
static修饰的一部分作为不是对象,做为元数据的一部分存在方法区中。
以上是Java对象处于5种不同状态时,Mark Word中64个位的表现形式,上面每一行代表对象处于某种状态时的样子。其中各部分的含义如下:
lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个Mark Word表示的含义不同。biased_lock和lock一起,表达的锁状态含义如下:
|在JDK6以后,默认已经开启了偏向锁这个优化,通过JVM参数-XX:-UseBiasedLocking来禁用偏向锁
若偏向锁开启,只有一个线程抢锁,可获取到偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入偏向锁。
当一个线程访问同步代码块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,
以后该线程再进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
如果测试成功,表示线程已经获得了锁。
如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置为1(表示指向当前进程):
如果没有,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前进程。
——《Java并发编程的艺术》
轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁
轻量级锁的加锁过程:
轻量级锁膨胀之后,就升级为重量级锁了。
重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被成为互斥锁。
synchronized就是一个典型的重量级锁 synchronized关键字
static Lock lock = new ReentrantLock(); //可重入锁
static Object baozidian=null;
static int i=0;
public static void main(String args[]) throws InterruptedException {
lock.lock(); //当前线程已获取锁
System.out.println("get lock 1...");
lock.lock(); //再次获取,是否能成功
System.out.println("get lock 2...");
lock.unlock();
// lock.unlock();
new Thread(()->{
System.out.println("child");
lock.lock();
System.out.println("get lock... child");
}).start();
}
首先t2获取锁,count加1 owner指向t2,t2线程在加锁就加1,t3这时去获取锁,先判断count是否为大于1,大于在判断owner是否属于t3这时不使用cas操作,如果不是属于t3进行放入等待队列,直到t2线程释放锁为0的时候唤醒t3线程,进行cas(0,1)操作这时如果t4也加入锁会进行抢锁,因为ReentrantLock是不公平锁,如果t3抢到就t3出列进行加锁,t4进入等待队列。reentrantLock(true)可变为公平锁
wait/notify使用了synchroinzed中用
Synchronized
优点:
概念
维护一对关联锁,一个只用于读操作,一个只用于写操作。
读锁可以由多个读线程同时持有,写锁是排他的。同一时间,两把锁不能被不同线程持有。
适用场景
适合读取操作多于写入操作的场景,改进互斥锁的性能
,比如:集合的并发线程安全性改造、缓存组件。
锁降级
指的是写锁降级成为读锁。持有写锁的同时,再获取读锁,随后释放写锁的过程。
写锁是线程独占,读锁是共享,所以写->读是降级。(读->写,是不能实现的)
AQS抽象队列同步器
aqs 提供了对资源占用、释放,线程的挂起、唤醒的逻辑。
预留了各种try方法给用户实现
可以用在各种需要控制资源争用的场景中。(ReentrantLock/CountDownLatch/Semphore)
,还有以下几个同步类是通过AQS的同步器进行同步管理的,不同的地方在于tryAcquire-tryRelease的实现方式不一样
ReentrantLock:可重入锁,与mutex一样实现了Lock接口,使用独占模式的同步器。
CountDownLatch:计数器,使用了共享模式的同步器进行多线程执行控制。
Semphore:一个计数信号量,维持一个许可证池(只计数)每次执行前获取许可证,执行完释放许可证。类似限流算法中的令牌桶算法。