多线程基础学习
一、线程概述
1、线程的相关概念
进程:进程(process)是计算机中的程序关于某数据集合上的一次运行活动,是操作系统进行资源分配与调度的基本单位。
可以把进程简单的理解为正在操作系统中运行的一个程序
线程:线程(thread)是进程的一个执行单元
一个线程就是进程中一个单一顺序的控制流,进程的一个执行分支
进程是线程的容器,一个进程至少有一个线程,一个进程中也可以有多个线程
在操作系统中是以进程为单位分配资源,如虚拟存储空间,文件描述符等,每个线程都有各自的线程栈,自己的寄存器环境,自己的线程本地存储。
主线程与子线程:
JVM启动时会创建一个主线程,该主线程负责执行main方法,主线程就是运行main方法的线程
java中的线程是不孤立的,线程之间存在着一些联系,如果在A线程中创建了B线程,称B线程为A线程的子线程,相应的A线程就是B线程的父线程。
串行并发和并行:
串行(sequential):需要等待上一个任务完成后才能进行下一个任务
并发(concurrent):任务可以交替进行
并行(Parallel):任务同时开始进行,耗时取决于最长的一个任务
并发可以提高事物的处理效率,即一段时间内可以处理或者完成更多的事情。
并行是一种更加严格的并发
从硬件的角度上来说,如果是单核cpu,一个处理器只能执行一个线程的情况下,处理器可以使用时间片轮转技术,可以让cpu快速的在不同的线程之间切换,对于用户来说,就感觉线程是在同步运行。如果是多核cpu,就可以为不同的线程分配不同的cpu内核。
2、线程的创建与启动:
在java中,创建一个线程就是创建一个Thread类(子类)的对象(实例)
Thread类有两种常用的构造方法:Thread()和Thread(Runnable)。对应的创建线程的两种方式:
1)定义Thread的子类:继承Thread类然后重写run方法,通过start方法开启线程。
2)定义一个Runnable接口的实现类(通过匿名的内部类调用函数的,也可以通过lambda表达式通过函数式接口来执行)
Thread thread = new Thread(()->{ for(int i = 0;i<100;i++) { System.out.println("新线程" + i); } }); thread.start();
这两种创建线程的方式没有本质的区别
通过调用start()方法来启动线程,启动线程的实质就是请求JVM运行相应的线程,这个线程具体在什么时候运行由线程调度器(scheduler)决定
注意:
start()方法调用结束并不意味着子线程开始运行
新开启的线程会执行run()方法
如果开启了多个线程,Start()调用的顺序并不一定就是线程启动的顺序
多线程运行结果与代码执行顺序或者调用顺序无关
多线程的一个运行的结果是随机的。
Thread的一些常用方法:
(1)currentThread():获得当前线程
java中的任何一段代码都是执行在某个线程当中的,执行当前代码的线程就是当前线程
同一段代码可能被不同的线程执行,因此当前线程是相对的,Thread.currentThread()方法的返回值是在代码实际运行时的线程对象。
(2)setName()/getName()
thread.setName(线程名称),设置线程名称
thread.getName() 返回线程名称
通过设置线程名称,有助于程序调试,提高程序的可读性,建议为每个线程都设置一个能够体现线程功能的名称
(3)isAlive()
thread.isAlive()判断当前线程是否属于活动状态
活动状态就是线程已经启动并且尚未终止。
(4)sleep()
Thread.sleep(millis),让当前线程休眠指定的毫秒数
当前线程指的是Thread.currentThread()返回的线程
在run()方法中如果出现异常只能够捕获处理异常,不能够将异常抛出,这是因为run()是一个重写的方法,在原类的run()方法中并没有抛出异常,因此继承的类的方法也不能抛出异常。
实例:通过sleep()设计一个倒计时:通过while循环设置一个变量,每循环一次便sleep一秒钟,当变量小于0时就退出循环,最后达到倒计时的目的。
(5)getId()
thread.getId()可以获得线程的唯一标识
注意:某个编号的线程运行结束后,该编号可能被后续创建的线程使用
重启后的JVM后,同一个线程的编号可能不一样。
(6)yield()
Thread.yield()方法的作用是放弃当前的cpu资源。
向调度程序提示当前线程愿意放弃其当前对处理器的使用。 调度程序可以随意忽略此提示。
Yield 是一种启发式尝试,旨在改善线程之间的相对进展,否则会过度使用 CPU。 它的使用应与详细的分析和基准测试相结合,以确保它确实具有预期的效果。
很少适合使用这种方法。 它对于调试或测试目的可能很有用,它可能有助于重现由于竞争条件引起的错误。 在设计并发控制结构(例如java.util.concurrent.locks包中的结构)时,它也可能很有用。
(7)setPriority() 默认值为5
thread.setPriority(num);设置线程的优先级
java线程的优先级取值范围是1~10,如果超出这个范围会抛出异常illegalArgumentException。
在操作系统中,优先级高的线程获得的cpu的资源的机会越多
线程的优先级本质上是只是给线程调度器一个提示信息,以便于调度器决定先调度哪些线程,注意不能保证优先级高的线程先运行
java优先级设置不当或者滥用可能会导致某些线程永远无法得到运行,即产生了线程饥饿。
线程的优先级并不是设置的越高越好,一般情况下使用普通的优先级即可,即在开发时不必要设置线程的优先级
线程的优先级具有继承性,在A线程中创建了B线程,则B线程的优先级与A线程的是一样的。
(8)interrupt()
中断线程 注意调用interrupt()方法仅仅是在当前线程打一个停止标志,并不是真正的停止线程。如果需要真的中断线程可以通过interruputed判断然后通过return退出run方法即可。
(9)setDaemon
java中的线程分为用户线程和是守护线程
守护线程是为其他线程提供服务的线程,如垃圾回收器(GC)就是一个典型的守护线程
守护线程不能够单独运行,当JVM中没有其他用户线程,只有守护线程时,守护线程会自动销毁JVM会退出
设置线程守护的代码应该在线程启动之前
线程的声明周期:
线程的生命周期是线程对象的生老病死,即线程的状态
线程生命周期可以通过getState()方法获得,线程的状态是Thread.State枚举类型定义的,有一下几种:
NEW 新建状态:创建了线程对象,在调用start之前的状态
RUNNABLE 可运行状态:它是一个复合状态,包含:READY和RUNNING两个状态。READY状态 该线程可以被调度器进行调度使它处于RUNNING状态,RUNNING状态表示该线程正在执行。
Thread.yield() 方法可以把线程由RUNNING状态转换为READY状态
BLOCKED阻塞状态:线程发起阻塞的I/O操作,或者申请由其他线程占用的独占资源,线程会转换为BLOCKED阻塞状态。处于阻塞状态的线程不会占用CPU资源。当阻塞I/O执行完后,或者线程获得了其申请的资源,线程可以转换称RUNNABLE
WAITING 等待状态 线程执行了object.wait(),thread.join()方法会把线程转换为WAITING等待状态,执行object.notify()方法,或者加入的线程执行完毕,当前线程会转换为RUNNABLE状态。
TIMED_WAITING状态 与WAITING状态类似,都是等待状态。区别在于处于该状态下的线程不会无限等待,如果没有在指定的时间内完成期望的操作,该线程自动转换成RUNNABLE状态。
TERMINATED状态:终止状态,线程结束处于终止状态。
状态图:

多线程编程具有以下优势:
1)提高系统的吞吐率(Throughout)。多线程编程可以使一个进程有多个并发(concurrent,即同时进行的)的操作
2)提高响应性(Responsiveness).Web服务器会采用一些专门的线程负责用户的请求处理,缩短了用户的等待时间
3)充分利用多核(Multicore)处理器资源.通过多线程可以充分利用CPU资源。
多线程编程存在的问题与风险
1)线程安全问题(Thread safe)问题。多线程共享数据时,如果没有采取正确的并发访问控制措施,就可能会产生数据一致性问题,如读取脏数据(过期的数据),如丢失数据更新。
2)线程活性(thread liveness)问题。由于程序自身的缺陷或者由资源稀缺性导致线程一直处于非RUNNABLE状态,这就是线程活性问题
常见的活性故障有以下几种:
(1)死锁(Deadlock)类似于鹬蚌相争
(2)锁死(Lockout),类似于睡美人的故事中王子死了
(3)活锁(Livelock)。类似于小猫咬自己的尾巴
(4)饥饿(Starvation):类似于健壮的雏鸟总是从母鸟嘴中抢到食物
3)上下文切换(Context Switch)处理器从执行一个线程切换到执行另外一个线程 资源消耗
4)可靠性 可能会由一个线程导致JVM意外终止,其他的线程也无法执行
------------恢复内容开始------------
一、线程概述
1、线程的相关概念
进程:进程(process)是计算机中的程序关于某数据集合上的一次运行活动,是操作系统进行资源分配与调度的基本单位。
可以把进程简单的理解为正在操作系统中运行的一个程序
线程:线程(thread)是进程的一个执行单元
一个线程就是进程中一个单一顺序的控制流,进程的一个执行分支
进程是线程的容器,一个进程至少有一个线程,一个进程中也可以有多个线程
在操作系统中是以进程为单位分配资源,如虚拟存储空间,文件描述符等,每个线程都有各自的线程栈,自己的寄存器环境,自己的线程本地存储。
主线程与子线程:
JVM启动时会创建一个主线程,该主线程负责执行main方法,主线程就是运行main方法的线程
java中的线程是不孤立的,线程之间存在着一些联系,如果在A线程中创建了B线程,称B线程为A线程的子线程,相应的A线程就是B线程的父线程。
串行并发和并行:
串行(sequential):需要等待上一个任务完成后才能进行下一个任务
并发(concurrent):任务可以交替进行
并行(Parallel):任务同时开始进行,耗时取决于最长的一个任务
并发可以提高事物的处理效率,即一段时间内可以处理或者完成更多的事情。
并行是一种更加严格的并发
从硬件的角度上来说,如果是单核cpu,一个处理器只能执行一个线程的情况下,处理器可以使用时间片轮转技术,可以让cpu快速的在不同的线程之间切换,对于用户来说,就感觉线程是在同步运行。如果是多核cpu,就可以为不同的线程分配不同的cpu内核。
2、线程的创建与启动:
在java中,创建一个线程就是创建一个Thread类(子类)的对象(实例)
Thread类有两种常用的构造方法:Thread()和Thread(Runnable)。对应的创建线程的两种方式:
1)定义Thread的子类:继承Thread类然后重写run方法,通过start方法开启线程。
2)定义一个Runnable接口的实现类(通过匿名的内部类调用函数的,也可以通过lambda表达式通过函数式接口来执行)
Thread thread = new Thread(()->{ for(int i = 0;i<100;i++) { System.out.println("新线程" + i); } }); thread.start();
这两种创建线程的方式没有本质的区别
通过调用start()方法来启动线程,启动线程的实质就是请求JVM运行相应的线程,这个线程具体在什么时候运行由线程调度器(scheduler)决定
注意:
start()方法调用结束并不意味着子线程开始运行
新开启的线程会执行run()方法
如果开启了多个线程,Start()调用的顺序并不一定就是线程启动的顺序
多线程运行结果与代码执行顺序或者调用顺序无关
多线程的一个运行的结果是随机的。
Thread的一些常用方法:
(1)currentThread():获得当前线程
java中的任何一段代码都是执行在某个线程当中的,执行当前代码的线程就是当前线程
同一段代码可能被不同的线程执行,因此当前线程是相对的,Thread.currentThread()方法的返回值是在代码实际运行时的线程对象。
(2)setName()/getName()
thread.setName(线程名称),设置线程名称
thread.getName() 返回线程名称
通过设置线程名称,有助于程序调试,提高程序的可读性,建议为每个线程都设置一个能够体现线程功能的名称
(3)isAlive()
thread.isAlive()判断当前线程是否属于活动状态
活动状态就是线程已经启动并且尚未终止。
(4)sleep()
Thread.sleep(millis),让当前线程休眠指定的毫秒数
当前线程指的是Thread.currentThread()返回的线程
在run()方法中如果出现异常只能够捕获处理异常,不能够将异常抛出,这是因为run()是一个重写的方法,在原类的run()方法中并没有抛出异常,因此继承的类的方法也不能抛出异常。
实例:通过sleep()设计一个倒计时:通过while循环设置一个变量,每循环一次便sleep一秒钟,当变量小于0时就退出循环,最后达到倒计时的目的。
(5)getId()
thread.getId()可以获得线程的唯一标识
注意:某个编号的线程运行结束后,该编号可能被后续创建的线程使用
重启后的JVM后,同一个线程的编号可能不一样。
(6)yield()
Thread.yield()方法的作用是放弃当前的cpu资源。
向调度程序提示当前线程愿意放弃其当前对处理器的使用。 调度程序可以随意忽略此提示。
Yield 是一种启发式尝试,旨在改善线程之间的相对进展,否则会过度使用 CPU。 它的使用应与详细的分析和基准测试相结合,以确保它确实具有预期的效果。
很少适合使用这种方法。 它对于调试或测试目的可能很有用,它可能有助于重现由于竞争条件引起的错误。 在设计并发控制结构(例如java.util.concurrent.locks包中的结构)时,它也可能很有用。
(7)setPriority() 默认值为5
thread.setPriority(num);设置线程的优先级
java线程的优先级取值范围是1~10,如果超出这个范围会抛出异常illegalArgumentException。
在操作系统中,优先级高的线程获得的cpu的资源的机会越多
线程的优先级本质上是只是给线程调度器一个提示信息,以便于调度器决定先调度哪些线程,注意不能保证优先级高的线程先运行
java优先级设置不当或者滥用可能会导致某些线程永远无法得到运行,即产生了线程饥饿。
线程的优先级并不是设置的越高越好,一般情况下使用普通的优先级即可,即在开发时不必要设置线程的优先级
线程的优先级具有继承性,在A线程中创建了B线程,则B线程的优先级与A线程的是一样的。
(8)interrupt()
中断线程 注意调用interrupt()方法仅仅是在当前线程打一个停止标志,并不是真正的停止线程。如果需要真的中断线程可以通过interruputed判断然后通过return退出run方法即可。
(9)setDaemon
java中的线程分为用户线程和是守护线程
守护线程是为其他线程提供服务的线程,如垃圾回收器(GC)就是一个典型的守护线程
守护线程不能够单独运行,当JVM中没有其他用户线程,只有守护线程时,守护线程会自动销毁JVM会退出
设置线程守护的代码应该在线程启动之前
线程的声明周期:
线程的生命周期是线程对象的生老病死,即线程的状态
线程生命周期可以通过getState()方法获得,线程的状态是Thread.State枚举类型定义的,有一下几种:
NEW 新建状态:创建了线程对象,在调用start之前的状态
RUNNABLE 可运行状态:它是一个复合状态,包含:READY和RUNNING两个状态。READY状态 该线程可以被调度器进行调度使它处于RUNNING状态,RUNNING状态表示该线程正在执行。
Thread.yield() 方法可以把线程由RUNNING状态转换为READY状态
BLOCKED阻塞状态:线程发起阻塞的I/O操作,或者申请由其他线程占用的独占资源,线程会转换为BLOCKED阻塞状态。处于阻塞状态的线程不会占用CPU资源。当阻塞I/O执行完后,或者线程获得了其申请的资源,线程可以转换称RUNNABLE
WAITING 等待状态 线程执行了object.wait(),thread.join()方法会把线程转换为WAITING等待状态,执行object.notify()方法,或者加入的线程执行完毕,当前线程会转换为RUNNABLE状态。
TIMED_WAITING状态 与WAITING状态类似,都是等待状态。区别在于处于该状态下的线程不会无限等待,如果没有在指定的时间内完成期望的操作,该线程自动转换成RUNNABLE状态。
TERMINATED状态:终止状态,线程结束处于终止状态。
状态图:

多线程编程具有以下优势:
1)提高系统的吞吐率(Throughout)。多线程编程可以使一个进程有多个并发(concurrent,即同时进行的)的操作
2)提高响应性(Responsiveness).Web服务器会采用一些专门的线程负责用户的请求处理,缩短了用户的等待时间
3)充分利用多核(Multicore)处理器资源.通过多线程可以充分利用CPU资源。
多线程编程存在的问题与风险
1)线程安全问题(Thread safe)问题。多线程共享数据时,如果没有采取正确的并发访问控制措施,就可能会产生数据一致性问题,如读取脏数据(过期的数据),如丢失数据更新。
2)线程活性(thread liveness)问题。由于程序自身的缺陷或者由资源稀缺性导致线程一直处于非RUNNABLE状态,这就是线程活性问题
常见的活性故障有以下几种:
(1)死锁(Deadlock)类似于鹬蚌相争
(2)锁死(Lockout),类似于睡美人的故事中王子死了
(3)活锁(Livelock)。类似于小猫咬自己的尾巴
(4)饥饿(Starvation):类似于健壮的雏鸟总是从母鸟嘴中抢到食物
3)上下文切换(Context Switch)处理器从执行一个线程切换到执行另外一个线程 资源消耗
4)可靠性 可能会由一个线程导致JVM意外终止,其他的线程也无法执行
线程安全问题
非线程安全主要是指多个线程对同一个对象进行的实例变量进行操作时,会出现值被更改,值不同步的情况。
线程安全问题表现为三个方面:原子性,可见性和有序性。
原子性
原子(Atomic) 就是不可分割的意思。原子操作的不可分割有两层含义:
1)访问(读,写)某个共享变量的操作从其他的线程来看,该操作要么已经执行完毕,要么尚未发生,即其他线程看不到当前线程操作的中间结果
2)访问同一组共享变量的原子操作是不能够交错的
如现实生活中从ATM机取款,对于用户来说,要么操作成功,用户拿到钱,余额减少了,增加了一条交易记录;要么没拿到钱,相当于取款操作没有发生
java有两种方式实现原子性:一种是使用锁;另一种利用处理器的CAS(Compare and Swap)指令
锁具有排它性,保证共享变量在某一时刻只能被一个线程访问
CAS指令直接在硬件(处理器和内存)层次上实现,看作是硬件锁。
可见性
在多线程环境中,一个线程对某个共享变量进行更新之后,后续其他的线程可能无法立即读到这个更新的结果,这就是线程安全问题的另外一种形式:可见性(visibility)
如果一个线程对共享变量更新后,后续访问该变量的其他线程可以读到更新的结果,称这个线程对共享变量的更新对其他线程可见,否则称这个线程对共享变量的更新对其他线程不可见。
多线程程序因为可见性问题可能会导致其他线程读取到了旧数据(脏数据)。
导致一个线程看不到另一个线程更新变量的原因可能是:
1)JIT即时编译器可能会对run方法中while循环进行优化为(可能优化,不是一定优化):
while(!toCancel){ if(doSomething()){ } } 修改为: if(!toCancel){ while(true){ if(doSomething()){ } } }
2)可能与计算机的存储系统有关。假设分别有两个cpu内核运行main线程与子线程,运行子线程的cpu无法立即读取到运行main线程cpu中的数据。
有序性
有序性(ordering)是指在什么情况下一个处理器上运行的一个线程所执行的 内存访问操作在另外一个处理器运行的其他线程看来是乱序的(out of Order)乱序是指内存访问操作的顺序看起来发生了变化
在多核处理器的环境下,编写的顺序结构,这种操作执行的顺序可能是没有保障的:
编译器可能会改变两个操作的先后顺序;
处理器也可能不会按照目标代码的顺序执行;
这种一个处理器上执行的多个操作,在其他处理器来看它的顺序与目标代码指定的顺序可能不一样,这种现象称为重排序。
重排序是对内存访问有序操作的一种优化,可以在不影响单线程程序正确的情况下提升程序的性能。但是可能对多线程程序的正确性产生影响,即可能导致线程安全问题。重排序与可见性问题类似,不是必然出现的。
与内存操作顺序有关的几个概念:
源代码顺序 就是源码中指定的内存访问顺序
程序顺序 处理器上运行的目标代码所指定的内存访问顺序
执行顺序 内存访问操作在处理器上的实际执行顺序
感知顺序 给定处理器所感知到的该处理器及其他处理器的内存访问操作的顺序
可以把重排序分为指令重排序与存储子系统重排序两种。
指令重排序主要是由JIT编译器,处理器引起的,指程序顺序与执行顺序不一样
存储子系统重排序是由高速缓存,写缓存器引起的,感知顺序与执行顺序不一致
指令重排序
在源码顺序和程序顺序不一致,或者程序顺序和执行顺序不一致的情况下,我们就说发生了指令重排序(Instruction Reorder)
指令重排是一种动作,确实对指令的顺序做了调整,重排序的对象指令
javac编译器一般不会执行指令重排序,而JIT编译器可能执行指令重排序
处理器也可能执行指令重排序,使得执行顺序与程序顺序不一致。
指令重排不会对单线程程序的结果正确性产生影响,可能导致多线程程序出现非预期的结果。
存储子系统重排序
存储子系统是指写缓冲器与高速缓存
高速缓存(Cache)是CPU中为了匹配与主存处理速度不匹配而设计的一个高速缓存
写缓冲器(Store buffer,Write buffer)用来提高写高速缓存操作的效率
即使处理器严格按照程序顺序执行两个内存访问操作,在存储子系统的作用下,其他处理器对这两个操作的感知顺序不一致,
即这两个操作的顺序看起来像是发生了变化,这种现象称为存储子系统重排序。
存储子系统重排序并没有真正的对指令执行顺序进行调整,而是造成一种指令执行顺序被调整的现象。
存储子系统重排序对象是内存操作的结果。
从处理器角度来看,读内存就是从指定的RAM地址中加载数据到寄存器,称为Load操作;写内存就是把数据存储到指定的地址表示的RAM存储单元中,称为Store操作。内存重排序有以下四种可能:
LoadLoad重排序,一个处理器先后执行两个读操作L1和L2,其他处理器对两个内存操作的感知顺序可能是L2->L1
StoreStore重排序,一个处理器先后执行两个写操作W1和W2,其他处理器对两个内存操作的感知顺序可能是W2->W1
LoadStore重排序,一个处理器先执行读操作L1再执行写内存操作W1,其他处理器对两个内存操作的感知顺序可能是W1->L1
StoreLoad重排序,一个处理器先执行写内存操作W1再执行读内存操作L1,其他处理器对两个内存操作的感知顺序可能是L1->W1
内存重排序与具体的处理器微架构有关,不同架构的处理器所允许的内存重排序不同
内存重排序可能会导致线程安全问题。假设有两个共享变量。
int data = 0; bollean ready = false;
|
处理器1 |
处理器2 |
|
data = 1 //S1 W1 ready = true //S2 W2 |
|
|
while(!ready){} //L3 sout(data); //L4 |
写进缓存但是另外一个处理器读取不到
貌似串行语义
JIT编译器,处理器,存储子系统是按照一定的规则对指令,内存操作的结果进行重排序,给单线程程序造成一种假象--指令是按照源码的顺序执行的。这种假象被称为貌似串行语义。并不能保证多线程环境程序的正确性
为了保证貌似串行语义,有数据依赖关系的语句不会被重排序,只有不存在数据依赖关系的语句才会被重排序。
如果两个操作(指令)访问同一个变量,且其中一个操作(指令)为写操作,那么这两个操作之间就存在着数据依赖关系。(Data Dependency)
如:
x = 1 ;y = x +1;后一条语句的操作数包含前一条语句的执行结果;
y=x;x=1,先读取x变量,再更新x变量的值
x=1;x=2;两条语句同时对一个变量进行写操作
如果不存在数据依赖关系则可能进行重排序,如:
double price = 45.8;
int quantity = 10;
double sum = price *quantity
存在控制依赖关系的语句允许重排。一条语句(指令)的执行结果会决定另一条语句(指令)能否被执行,这两条语句(指令)存在控制依赖关系(control Dependency)。如在if语句中允许重排,可能存在处理器先执行if代码块,再判断if条件是否成立。
保证内存访问的顺序性
可以使用volatile关键字,synchronized关键字实现有序性。
java内存模型

1)每一个线程都有独立的栈空间
2)每个线程都可以访问堆内存
3)计算机的CPU并不直接从主内存中读取数据,CPU读取数据时,先把主内存中的数据读到Cache缓存中,把Cache中的数据读到Register寄存器中。
4)JVM中的共享数据可能会被分配到Register寄存器中,每个CPU都有自己的Register寄存器,一个CPU不能读取其他CPU上寄存器中的内容,如果两个线程分别运行在不同的处理器(CPU)上,
而这个共享的数据被分配到寄存器上,会产生可见性问题
5)即使JVM中的共享数据分配到主内存中,也不能保证数据的可见性。CPU不直接对主内存访问,而是通过Cache高速缓存Cache进行的,一个处理器上运行的线程对数据的更新可能只是更新到处理器的写缓冲器(Store Buffer),
还没有到达Cache缓存,更不用说主内存了,
另外一个处理器不能读取到该处理器写缓冲器上的内容,会产生运行在另外一个处理器上的线程无法看到该处理器对共享数据的更新。
6)一个处理器的Cache不能直接读取另一个处理器的Cache。但是一个处理器可以通过缓存一致性协议(Cache Coherence Protocol)来读取其他处理器缓存中的数据,并将读取的数据更新到该处理器的Cache中,这个过程称为缓
存同步。缓存同步使得一个处理器上运行的线程可以读取到另外一个处理器上运行的线程对共享数据所做的更新,即保障了可见性。为了保障可见性,必须使一个处理器对共享数据的更新最终被写入该处理器的Cache,这个过程称为
冲刷处理器缓存。
可以把Java内存模型抽象为:

规定:每个线程之间的共享数据都存储在主存中。
每个线程都有一个私有的本地内存(工作内存),线程的工作内存是抽象的概念,不是真实存在的,它涵盖写缓冲器,寄存器,其他硬件的优化。
每个线程从主内存中把数据读取到本地工作内存中,在工作内存中保存共享数据的副本
线程在自己的工作内存中处理数据,仅对当前线程可见,对其他线程是不可见的。
线程同步
线程同步机制简介
线程同步机制是一套用于协调线程之间的数据访问的机制。该机制可以保障线程安全
java平台提供的线程同步机制包括:锁,volatile关键字,final关键字,static关键字,以及以及相关的API,如Object.wait()/Object.notify()等。
锁概述
线程安全问题的产生前提是多个线程并发访问共享数据。
将多个线程对共享数据的并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问。锁就是利用这种思路来保障线程安全的。
锁(Lock)可以理解为对共享数据进行保护的一个许可证。对于同一个许可证保护的共享数来说,任何线程想要访问这些共享数据必须先持有该许可证。一个线程只有在持有许可证的情况下才能对这些共享数据进行访问;
并且一个许可证一次只能被一个线程持有;许可证在结束对共享数据的访问后必须释放其持有的许可证。
一线程在访问共享数据前必须先获得锁;获得锁的线程称为锁的持有线程;一个锁一次只能被一个线程持有。锁的持有线程在获得锁之后 和释放锁之前这段时间所执行的代码称为临界区(Critical Section)
锁具有排他性(Exclusive),即一个锁一次只能被一个线程持有。这种锁称为排它锁或互斥锁(Mutex)
JVM把锁分为内部锁和显示锁两种。内部锁通过synchronized关键字实现;显示锁通过java.concurrent.locks.lock实现类实现的。
锁的作用
锁可以实现对共享数据的安全访问。保障线程的原子性,可见性与有序性。
锁是通过互斥保障原子性。一个锁只能被一个线程持有,这就保证临界区的代码一次只能被一个线程执行。使得临界区代码所执行的操作自然而然的具有不可分割的特性,即具备了原子性。
可见性的保障是通过写线程冲刷处理器的缓存和读线程刷新处理器缓存这两个动作实现的。在java平台中,锁的获得隐含着刷新处理器缓存的动作,锁的释放隐含着冲刷处理器缓存的动作。
锁能够保障有序性。写线程在临界区所执行的在读线程所执行的临界区看来像是完全按照源码顺序执行的。
注意:
使用锁保障线程的安全性,必须满足以下条件:
这些线程在访问共享数据时必须使用同一个锁
即使是读取共享变量的线程也需要使用同步锁
锁相关的概念
1)可重入性
可重入性(Reentrancy)描述这样一个问题:一个线程持有该锁的时候能够再次(多次)申请该锁
如果一个线程持有一个锁的时候还能够继续成功申请该锁,称锁是可重入的,否则就称该锁为不可重入的
2)锁的争用与调度
java平台中内部锁属于非公平锁,显示Lock锁既支持公平锁又支持非公平锁。
3)锁的粒度
一个锁可以保护的共享数据的数量大小称为锁的粒度。
锁保护共享数据量大,称该锁的粒度粗,否则就称该锁的粒度细。
锁的粒度过粗会导致线程在申请锁时会进行不必要的等待。锁的粒度过细会增加锁调度的开销。
内部锁:synchronized关键字实现的。synchronized关键字修饰代码块,修饰该方法。
修饰代码块的语法:
synchronized(对象锁){
同步代码块,可以在同步代码块中访问共享数据
}
修饰实例方法就称为同步实例方法
修饰静态方法就称为同步静态方法
synchronized同步代码块
不同的锁对象不能够实现同步
也可以通过常量对象进行同步。没有获取到锁就进入到等待队列中等待。(关键点就是必须要相同的锁对象才能够进行同步)
同步方法
同步实例方法 :默认this作为锁对象 因此和代码块的不同
同步静态方法 :默认当前运行时类对象作为锁对象,可以简单理解为将当前类的字节码作为锁对象。因此又被称为类锁。
同步方法和同步代码块如何选择
同步方法锁的粒度粗,执行效率低,同步代码块执行效率高。主要时因为同步代码块在获得锁的同时可以进行准备。
脏读:
出现读取属性值出现了一些意外,读取的是中间值,而不是修改之后的值,出现脏读的原因是对共享数据的修改与对共享数据的读取不同步
解决方法:
不仅要对修改数据的代码块进行同步,还要对读取数据的代码块进行同步。
线程如果出现异常会自动释放锁。

浙公网安备 33010602011771号