大部分场景下,几个线程之间是需要协调配合工作(线程之间需要进行数据交换),一起完成一个总目标的。由于我们的线程都属于同一个进程,所以共享有OS分配过来的同样的资源(其中优先关注内存资源)
1.JVM下的内存区域划分PC保存区(PC);栈区(本地方法栈;虚拟机栈);堆;方法区;运行时常量池
这些内存资源是属于进程的。理论上来讲,确实也是这个进程下所有线程的,但在进程内部也有分配机制,这些内存在线程中还会分配一次。(类比:家庭财务,属于家庭,但细分每个人都有对应花销)并且不是 *** 作系统做分配了,是进程内部分配。
堆、方法区、运行时常量池是整个进程(JVM)只有一份,PC(保存PC值)、栈(虚拟机栈、本地方法栈)是每个线程独一份(各自有各自的)【对象是共有的,加载的类是共有的】
1)为什么每个线程要有自己的PC每个线程都是独立的执行流,要下一条要执行的指令和其他线程无关,所以有自己的PC。
2)为什么每个线程都得有自己的栈每个线程都是独立的执行流,各自有各自调用的方法链,有各自要处理的临时数据,所以栈也是独一份的。
3)体现到代码中的共有和私有局部变量,保存在栈桢中,也就是保存在栈中,是线程私有
类对象(Class对象-关于类的对象)、静态属性,保存在方法区中,所以是线程之间共享的【前提:有访问权限】
对象(对象内部属性),保存在堆中,所以是线程之间是可以共享的【前提:线程持有该对象的引用】(见4)5)这个两个例子)
1)区分不同内存块的r
/**
* 变量本质是对一块内存的抽象
* 观察下面r
* 两个r只是名字都叫r,但不是同一块内存
**/
public class Main {
static class MyThread extends Thread{
@Override
public void run() {
int r=0;//r存在于子线程run方法的栈帧中
for (int i=0;i<1000;i++){
r++;
}
System.out.println(Thread.currentThread().getName()+":"+r);
}
}
public static void main(String[] args) throws InterruptedException {
int r=0;//r存在于主线程main的栈帧中
MyThread t=new MyThread();
t.start();//开启线程
t.join();//等待线程结束
System.out.println(r);//此处r仍为0,因为++的是子线程的r,不是主线程的
}
}
2)一共在内存中开辟了几个名叫r的内存块
public class Main {
static class MyThread extends Thread{
private final int total;
public MyThread(int total){
this.total=total;
}
@Override
public void run() {
int r=0;//r存在于子线程run方法的栈帧中
for (int i=0;i<1000;i++){
r++;
}
System.out.println(Thread.currentThread().getName()+":"+r);
}
}
/**
* 一共在内存中开辟了几个名叫r的内存块
* 4个
* t1线程下的run方法下的r
* t2.run.r
* t3.run.r
* 主线程.main.r
*/
public static void main(String[] args) throws InterruptedException {
int r=0;//r存在于主线程main的栈帧中
MyThread t1=new MyThread(1000);
t1.start();//开启线程
MyThread t2=new MyThread(1000);
t2.start();//开启线程
MyThread t3=new MyThread(1000);
t3.start();//开启线程
t1.join();//
t2.join();//等待线程结束
t3.join();//等待线程结束
System.out.println(r);//此处r仍为0,因为++的是子线程调用到的r,不是主线程的
}
}
结果:
Thread-0:1000
Thread-2:1000
Thread-1:1000
0
3)r为静态属性时,整个进程中就这一个r
public class Main {
static int r=0;//定义为静态属性
//进程中就这一个r
static class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
r++;
}
System.out.println(getName()+":"+r);
}
}
public static void main(String[] args) throws InterruptedException {
MyThread t=new MyThread();
t.start();
t.join();
System.out.println(Thread.currentThread().getName()+":"+r);
}
}
结果:
Thread-0:1000
main:1000
分析:主线程虽然不对r做更改,但是子线程改变了r的值,主线程也就同步更改了,这也就是数据共享。
4)对象没有共享的情况
/**
*有几个r
* 两个
* 1.t线程run方法的someObject引用指向的对象中的r
* 2.主线程的main方法的someObject指向的对象中的r
* 对象不是同一个对象,所以r有两个
* 注意:虽然说对象是共享的,但前提是 *** 作的得是同一个数据
* 如果数据不是同一个,即使是对象也不能说一定共享
* 只能说,对象是可以被共享,但不是说对象一定被共享
**/
public class Main {
static class SomeObject{
int r=0;
}
static class MyThread extends Thread{
@Override
public void run() {
SomeObject someObject=new SomeObject();
for (int i = 0; i < 1000; i++) {
someObject.r++;
}
System.out.println(getName()+":"+someObject.r);
}
}
public static void main(String[] args) throws InterruptedException {
SomeObject someObject=new SomeObject();
MyThread t=new MyThread();
t.start();
t.join();
System.out.println(Thread.currentThread().getName()+":"+someObject.r);
}
}
//结果:
//Thread-0:1000
//main:0
5)对象之间共享的情况- *** 作的是同一批数据
package com.wy.about_data_share.demo5;
//想办法让子线程和主线程的引用指向同一个对象-利用构造方法
public class Main {
static class SomeObject{
int r=0;
}
static class MyThread extends Thread{
private final SomeObject someObject;
public MyThread(SomeObject someObject){
this.someObject=someObject;//这个someObject指向主线程main方法栈的someObject指向的对象
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
someObject.r++;
}
System.out.println(getName()+":"+someObject.r);
}
}
public static void main(String[] args) throws InterruptedException {
SomeObject someObject=new SomeObject();//只有这个地方创建对象
MyThread t=new MyThread(someObject);//把SomeObject对象的引用传入
t.start();
t.join();
System.out.println(Thread.currentThread().getName()+":"+someObject.r);
}
}
结果:
Thread-0:1000
main:1000
小结:
二、线程安全 1.演示线程不安全现象package com.wy.thread_safe.phenomenon;
/**
* 演示线程不安全现象
**/
public class Main {
//定义一个共享的数据--静态属性的方式来体现
static int r=0;
//定义加减的次数
static final int COUNT=100_0000;//COUNT越大,出错概率越大,COUNT越小,出错概率越小
//定义两个线程,分别对r进行加法和减法 *** 作
static class Add extends Thread{
@Override
public void run() {
for (int i = 0; i < COUNT; i++) {
r++;
}
}
}
static class Sub extends Thread{
@Override
public void run() {
for (int i = 0; i < COUNT; i++) {
r--;
}
}
}
public static void main(String[] args) throws InterruptedException {
Add add=new Add();
add.start();
Sub sub=new Sub();
sub.start();
//等待线程结束
add.join();
sub.join();
//理论上,线程结束后,r被加了COUNT次,也被减了COUNT次
//所以r结果应该是0,但是实际结果不一定,这就是线程不安全的现象
System.out.println(r);
}
}
结果:(一个每次执行结果都不同的随机值)。单看代码“没有问题”(暂时)的情况下,结果是错误的(无法100%预期效果),这就是线程不安全的现象。
COUNT越大,出错概率越大,COUNT越小,出错概率越小。
2.解释线程不安全现象出现的原因⭐⭐⭐ 1)开发者角度1)多个线程之间 *** 作同一块数据了(共享数据)——不仅仅是内存数据
2)至少有一个线程在修改这块共享数据
【总结12即多个线程中至少有一个对共享数据做修改(写) *** 作】
2)系统角度解释 a.原子性被破坏1)前置知识:1.java代码(高级语言)中的一条语句可能对应多条指令(eg:r++ *** 作,指令1:从r在的内存区域把数据加载到寄存器,指令2:完成+1 *** 作,指令3:寄存器中值写回内存);2.线程调度是可能发生在任意时刻的,但是不会切割指令。
2)原子性(atomic)问题:
程序员的预期是r++是一个原子性 *** 作(++指令全部完成或全部没完成)【r--同理】,但实际执行起来保证不了原子性,所以会出错。COUNT越大,出错概率越大,是因为COUNT越大,线程执行需要跨时间片的概率越大(碰到线程调度的概率越大),出错概率也就越大。原子性是破坏线程安全的最常见的原因。
b.内存可见性问题(读取到“脏数据”)1)前置知识:CPU中为了提升数据获取速度,一般在CPU中设置缓存(Cache)【指令执行速度>>内存的读写速度】
CPU缓存和内存的关系:(缓存就是放数据的地方)
现在的CPU,L1、L2缓存每个CPU都有,L3缓存是所有CPU共用一份。
JVM规定了JVM内存模型(JVM Memory Model/JMM),把一个线程想象成一个CPU,主内存/主存储:真实内存;工作内存/工作存储:CPU中缓存的模拟(不需要区分L123)。
线程的所有数据 *** 作(读/写),必须1.先从主内存加载到工作内存2.再在工作内存中处理3.处理结束后,再把数据存储回主内存。
2)基于上面理解解释内存可见性问题
一个线程对数据的 *** 作,很可能其他线程是无法感知的,甚至某些情况下,会被优化成完全看不到的结果。
c.代码重排序,导致数据配合出问题1)前置知识:程序从某些角度就是一个状态机(a=0,b=1,c=2....这是一个状态,经历有限步骤,a=100,b=100,c=100...新状态【中间通过语句/指令来进行状态迁移这个过程】),留给编译器很大的空间做优化(通过把中间变换的无用步骤取掉达到优化)。
eg:
我们写的程序,往往是经过中间很多环节优化的结果,并不保证最终执行的语言和我们写的语句是一模一样的。【可以做优化的环节:编译器(javac)、类加载器(少见)、运行期间->JVM:JIT(Just In Time 即时编译),硬件(CPU)层面——最终结论:作为应用开发,是无从得知做了哪些优化的】【所做的优化可以有哪些:删除某些步骤,调整某些步骤的执行数据】
2)基于上面理解,引出重排序
所谓重排序,就是指执行的指令和书写的指令并不一致。JVM规定了重排序的基本原则:happend-before规则:简要解释->JVM要求,无论怎么优化,对于单线程视角,结果不应该有改变,但并没有规定多线程的情况(因为不能规定),导致多线程环境可能出问题。
eg:理解:
A本身不优化时B介入在苹果前时间点,A最后没有事情,但A优化后B就来不及防解药,A中毒,B的介入使A的优化出现问题,这也就是重排序情况下导致多线程出现的问题。
2.什么是线程安全1)程序的线程安全:代码的运行结果100%符合预期,才能被称为线程安全。(理想情况,无法实 *** )
2)Java语境下,经常说某个类、某个对象是线程安全的:这个类、对象的代码中已经考虑了处理多线程的问题了,如果只是“简单”使用,可以不考虑线程安全问题。eg:ArrayList就不是线程安全的:ArrayList实现中,完全没考虑过线程安全的任何问题,所以无法直接使用在多线程环境(多个线程同时 *** 作一个ArrayList)下。
1)多线程中,哪些情况下不需要考虑线程安全问题?(哪些情况是安全的)1)几个线程之间互相没有任何数据共享的情况下,天生是线程安全的。
2)几个线程之间即使有数据共享,但是都是做读 *** 作,没有写 *** 作时,也是天生线程安全的。
2)作为程序员如何考虑线程安全问题1)尽可能让几个线程之间不做数据共享,各干各的,就不需要考虑线程安全了(如我们在线程(一)join()处的归并排序的例子:4个线程虽然处理的是同一个数组,但是我们提前画好了范围,每个排自己对应的区间对应内存的数据,所以其实也是没有数据共享的)
2)如果一定要共享,尽可能不去修改,只进行读 *** 作。(如static final int COUNT=...,即使多个线程同时使用COUNT也无所谓,因为COUNT只进行读 *** 作,不能被修改)
所以,我们接下来学习一些机制,目标是和JVM沟通,避免上述线程不安全问题的发生
三、保护线程安全的机制 1.之前学习过的一些常见类的线程安全的问题 1)线程不安全的:ArrayList、LinkedList、PriorityQueue、TreeMap、TreeSet、HashMap、HashSet都不是线程安全的。分析:ArrayList为什么不是线程安全的(多个线程同时对一个ArrayList对象有修改 *** 作时,结果会出错)
2)线程安全的:Vector、Stack、Dictionary、StringBuffer,这几个类都是Java设计失败的产品(StringBuilder vs StringBuffer,一个线程不安全,一个线程安全)
3)最常见的违反原子性的场景1.read-write场景
i++;;arr[size]=e;size++;
2.check-update场景
if(a==10){a=...;}
2.锁(lock)synchronized:锁/同步锁/monitor锁(监视器锁)
1)语法(synchronized修饰符) a.synchronized修饰方法(普通、静态)->同步方法synchronized int add(...){...}
//synchronized修饰普通方法
synchronized int add(){
return 0;
}
//synchronized修饰静态方法
private synchronized static int sub(){
return 0;
}
b.synchronized作同步代码块
synchronized(引用){...}
前言:
//类名.class:是一个引用,指向关于这个类对象(不是这个类实例化出来的对象,而是这个类数据表现出的对象(每个被加载进来的类都可以通过类名.class访问到))
//每个被加载的类有且仅有一个Class对象
synchronized void method(){
...
}
等价于
void method(){
synchronized (this){
...
}
}
static synchronized void method(){
...
}
等价于
static void method(){
synchronized (类.class){
...
}
}
所以,只要解释清楚同步代码块如何工作,就能理解同步方法怎么工作,这两个本质上等价 。
2)锁:理论上就是一段被多个线程之间共享的数据两种状态
a.锁上状态(locked) b.打开(unlocked)执行语句时可以将语句用锁包起来:
c.尝试加锁的内部 *** 作整个尝试加锁的 *** 作已经被JVM保证了原子性
当锁没有锁上时:
if(lock==false){//说明锁没有锁上
lock=true;//当前线程把锁lock
return;//正常向下执行
}
d.释放锁的 *** 作(纯理论)当锁已经锁上时:
//已经有线程锁上了,让多线程的其他线程进入阻塞队列等待加锁,同时让出CPU
Queue<线程(该锁的阻塞队列)> queue=...;
queue.add(Thread.currentThread());//把自己加到锁上,等待这把锁被释放
//既然我们无法获取到锁,所以就应该让出CPU
Thread.currentThread().state=阻塞;//锁的状态因为等锁就变为阻塞状态
Thread.yield();//这个理解为让出CPU
这个过程由系统保证了原子性
loke=false;
从等待锁的阻塞队列中,选一个线程出来,恢复CPU
Thread t=queue.poll();
t.state=就绪;//状态变为就绪,等待分配CPU
当多个线程都有加锁 *** 作,并且申请的是同一把锁时,会造成加锁过程中的代码(叫做临界区)s会互斥(mutex/exclusive)着进行【临界区代码必须持有锁才能进行,而每次只有一个线程能持有锁,所以临界区代码一定是互斥进行的,只有某个持有锁的进行完了,才能从一堆其他持有锁的线程中选另一个进行...】
package com.wy.thread_safe.exclusive;
public class Main {
//这个对象用来当锁对象
static Object lock=new Object();
static class MyThread1 extends Thread{
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 100000; i++) {
System.out.println("我是张三");
}
}
}
}
static class MyThread2 extends Thread{
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 100000; i++) {
System.out.println("我是李四");
}
}
}
}
public static void main(String[] args) {
Thread t1=new MyThread1();
t1.start();
Thread t2=new MyThread2();
t2.start();
//不加锁时,一会张三一会李四交替出现
//加锁后(两个都是加锁的,看哪个锁先抢上),谁先抢到谁打印完才开始另一个的打印——印证互斥,即不能同时在执行,一定互斥在进行
//锁锁住哪里哪里的代码就有互斥性
}
}
e.判断以下哪些会互斥,哪些不会?
互斥的必要条件:线程都有加锁 *** 作并且锁的都是同一个对象
t1线程 | t2线程 | t1和t2执行的代码块是否互斥 |
s1.m1()【有加锁 *** 作,对this加锁,this就是s1指向的对象,this==s1】 | s1.m1()【有加锁 *** 作,对this加锁,this就是s1指向的对象,this==s1】 | 有互斥 |
s1.m1();有加锁,this==s1 | s3.m1();有加锁,this==s3 | s1==s3,有互斥 |
s1.m1();有加锁加的s1 | s2.m1();有加锁加的s2 | s1,s2是两个不同对象,不等,没有互斥 |
s1.m1();有加锁,对this加锁,即对s1加锁 | s1.m2()有加锁,m2静态方法是对SomeObject.class加锁 | s1指向的对象与SomeObject.class指向的对象不一样(类型都不一样:SomeObject s1;Class SomeObject.class),不是同一把锁,不会互斥。 |
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)