java锁和并发类

ConcurrentHashMap线程安全原理

https://www.jianshu.com/p/e10bde0f3cff

volatile关键字:
https://www.huaweicloud.com/articles/9cc350bccdc33dd12a5f277197804ba4.html

如果一个变量被声明volatile的话,那么这个变量不会被进行重排序,也就是说,虚拟机会保证这个变量之前的代码一定会比它先执行,而之后的代码一定会比它慢执行。
例如把上面中的number声明为volatile,那么number = 42一定会比ready = true先执行。

不过这里需要注意的是,虚拟机只是保证这个变量之前的代码一定比它先执行,但并没有保证这个变量之前的代码不可以重排序。之后的也一样。

volatile关键字能够保证代码的有序性,这个也是volatile关键字的作用。
总结一下,一个被volatile声明的变量主要有以下两种特性保证保证线程安全。

可见性。
有序性。

精选文章 volatile不能保证线程安全
volatile不能保证线程安全
作者:麋鹿迷路迷了路 时间: 2021-02-05 10:05:31
标签:volatile不能保证线程安全
【摘要】对于volatile这个关键字,相信很多朋友都听说过,甚至使用过,这个关键字虽然字面上理解起来比较简单,但是要用好起来却不是一件容易的事。 这篇文章将从多个方面来讲解volatile,让你对它更加理解。 计算机中为什么会出现线程不安全的问题 volatile既然是与线程安全有关的问题,那我们先来了解一下计算机在处理数据的过程中为什么会出现线程不安全的问题。 大家都知道,计算机在执行程序时…
对于volatile这个关键字,相信很多朋友都听说过,甚至使用过,这个关键字虽然字面上理解起来比较简单,但是要用好起来却不是一件容易的事。
这篇文章将从多个方面来讲解volatile,让你对它更加理解。
计算机中为什么会出现线程不安全的问题
volatile既然是与线程安全有关的问题,那我们先来了解一下计算机在处理数据的过程中为什么会出现线程不安全的问题。
大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中会涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。
为了处理这个问题,在CPU里面就有了高速缓存(Cache)的概念。当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
我举个简单的例子,比如cpu在执行下面这段代码的时候,

t = t + 1;
会先从高速缓存中查看是否有t的值,如果有,则直接拿来使用,如果没有,则会从主存中读取,读取之后会复制一份存放在高速缓存中方便下次使用。之后cup进行对t加1操作,然后把数据写入高速缓存,最后会把高速缓存中的数据刷新到主存中。

这一过程在单线程运行是没有问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的,本次讲解以多核cup为主)。这时就会出现同一个变量在两个高速缓存中的值不一致问题了。
例如:
两个线程分别读取了t的值,假设此时t的值为0,并且把t的值存到了各自的高速缓存中,然后线程1对t进行了加1操作,此时t的值为1,并且把t的值写回到主存中。但是线程2中高速缓存的值还是0,进行加1操作之后,t的值还是为1,然后再把t的值写回主存。
此时,就出现了线程不安全问题了。

Java中的线程安全问题
上面那种线程安全问题,可能对于不同的操作系统会有不同的处理机制,例如Windows操作系统和Linux的操作系统的处理方法可能会不同。
我们都知道,Java是一种夸平台的语言,因此Java这种语言在处理线程安全问题的时候,会有自己的处理机制,例如volatile关键字,synchronized关键字,并且这种机制适用于各种平台。
Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。
由于java中的每个线程有自己的工作空间,这种工作空间相当于上面所说的高速缓存,因此多个线程在处理一个共享变量的时候,就会出现线程安全问题。

这里简单解释下共享变量,上面我们所说的t就是一个共享变量,也就是说,能够被多个线程访问到的变量,我们称之为共享变量。在java中共享变量包括实例变量,静态变量,数组元素。他们都被存放在堆内存中。

volatile关键字
上面扯了一大堆,都没提到volatile关键字的作用,下面开始讲解volatile关键字是如何保证线程安全问题的。

可见性

什么是可见性?

意思就是说,在多线程环境下,某个共享变量如果被其中一个线程给修改了,其他线程能够立即知道这个共享变量已经被修改了,当其他线程要读取这个变量的时候,最终会去内存中读取,而不是从自己的工作空间中读取。
例如我们上面说的,当线程1对t进行了加1操作并把数据写回到主存之后,线程2就会知道它自己工作空间内的t已经被修改了,当它要执行加1操作之后,就会去主存中读取。这样,两边的数据就能一致了。
假如一个变量被声明为volatile,那么这个变量就具有了可见性的性质了。这就是volatile关键的作用之一了。

volatile保证变量可见性的原理

当一个变量被声明为volatile时,在编译成会变指令的时候,会多出下面一行:

0x00bbacde: lock add1 $0x0,(%esp);
这句指令的意思就是在寄存器执行一个加0的空操作。不过这条指令的前面有一个lock(锁)前缀。
当处理器在处理拥有lock前缀的指令时:
在之前的处理中,lock会导致传输数据的总线被锁定,其他处理器都不能访问总线,从而保证处理lock指令的处理器能够独享操作数据所在的内存区域,而不会被其他处理所干扰。
但由于总线被锁住,其他处理器都会被堵住,从而影响了多处理器的执行效率。为了解决这个问题,在后来的处理器中,处理器遇到lock指令时不会再锁住总线,而是会检查数据所在的内存区域,如果该数据是在处理器的内部缓存中,则会锁定此缓存区域,处理完后把缓存写回到主存中,并且会利用缓存一致性协议来保证其他处理器中的缓存数据的一致性。

缓存一致性协议

刚才我在说可见性的时候,说“如果一个共享变量被一个线程修改了之后,当其他线程要读取这个变量的时候,最终会去内存中读取,而不是从自己的工作空间中读取”,实际上是这样的:
线程中的处理器会一直在总线上嗅探其内部缓存中的内存地址在其他处理器的操作情况,一旦嗅探到某处处理器打算修改其内存地址中的值,而该内存地址刚好也在自己的内部缓存中,那么处理器就会强制让自己对该缓存地址的无效。所以当该处理器要访问该数据的时候,由于发现自己缓存的数据无效了,就会去主存中访问。

有序性

实际上,当我们把代码写好之后,虚拟机不一定会按照我们写的代码的顺序来执行。例如对于下面的两句代码:

int a = 1;
int b = 2;
对于这两句代码,你会发现无论是先执行a = 1还是执行b = 2,都不会对a,b最终的值造成影响。所以虚拟机在编译的时候,是有可能把他们进行重排序的。
为什么要进行重排序呢?
你想啊,假如执行 int a = 1这句代码需要100ms的时间,但执行int b = 2这句代码需要1ms的时间,并且先执行哪句代码并不会对a,b最终的值造成影响。那当然是先执行int b = 2这句代码了。
所以,虚拟机在进行代码编译优化的时候,对于那些改变顺序之后不会对最终变量的值造成影响的代码,是有可能将他们进行重排序的。
更多代码编译优化可以看我写的另一篇文章:
虚拟机在运行期对代码的优化策略

那么重排序之后真的不会对代码造成影响吗?
实际上,对于有些代码进行重排序之后,虽然对变量的值没有造成影响,但有可能会出现线程安全问题的。具体请看下面的代码

public class NoVisibility{ private static boolean ready; private static int number; private static class Reader extends Thread{ public void run(){ while(!ready){ Thread.yield(); } System.out.println(number); }
} public static void main(String[] args){ new Reader().start(); number = 42; ready = true; }
}
这段代码最终打印的一定是42吗?如果没有重排序的话,打印的确实会是42,但如果number = 42和ready = true被进行了重排序,颠倒了顺序,那么就有可能打印出0了,而不是42。(因为number的初始值会是0).
因此,重排序是有可能导致线程安全问题的。

如果一个变量被声明volatile的话,那么这个变量不会被进行重排序,也就是说,虚拟机会保证这个变量之前的代码一定会比它先执行,而之后的代码一定会比它慢执行。
例如把上面中的number声明为volatile,那么number = 42一定会比ready = true先执行。

不过这里需要注意的是,虚拟机只是保证这个变量之前的代码一定比它先执行,但并没有保证这个变量之前的代码不可以重排序。之后的也一样。

volatile关键字能够保证代码的有序性,这个也是volatile关键字的作用。
总结一下,一个被volatile声明的变量主要有以下两种特性保证保证线程安全。

可见性。
有序性。
volatile真的能完全保证一个变量的线程安全吗?

我们通过上面的讲解,发现volatile关键字还是挺有用的,不但能够保证变量的可见性,还能保证代码的有序性。
那么,它真的能够保证一个变量在多线程环境下都能被正确的使用吗?
答案是否定的。原因是因为Java里面的运算并非是原子操作。

原子操作

原子操作:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
也就是说,处理器要嘛把这组操作全部执行完,中间不允许被其他操作所打断,要嘛这组操作不要执行。
刚才说Java里面的运行并非是原子操作。我举个例子,例如这句代码

int a = b + 1;
处理器在处理代码的时候,需要处理以下三个操作:

从内存中读取b的值。
进行a = b + 1这个运算
把a的值写回到内存中
而这三个操作处理器是不一定就会连续执行的,有可能执行了第一个操作之后,处理器就跑去执行别的操作的。

证明volatile无法保证线程安全的例子

由于Java中的运算并非是原子操作,所以导致volatile声明的变量无法保证线程安全。
对于这句话,我给大家举个例子。代码如下:

public class Test{ 
public static volatile int t = 0; 
public static void main(String[] args){ 
Thread[] threads = new Thread[10]; 
for(int i = 0; i < 10; i++){ 
//每个线程对t进行1000次加1的操作 
threads[i] new Thread(new Runnable(){ @Override 
public void run(){ 
for(int j = 0; j < 1000; j++){ 
t = t + 1; } } }); 
threads[i].start(); } 
//等待所有累加线程都结束 
while(Thread.activeCount() > 1){ 
Thread.yield(); } 
//打印t的值 
System.out.println(t); }
}

最终的打印结果会是1000 * 10 = 10000吗?答案是否定的。
问题就出现在t = t + 1这句代码中。我们来分析一下
例如:
线程1读取了t的值,假如t = 0。之后线程2读取了t的值,此时t = 0。
然后线程1执行了加1的操作,此时t = 1。但是这个时候,处理器还没有把t = 1的值写回主存中。这个时候处理器跑去执行线程2,注意,刚才线程2已经读取了t的值,所以这个时候并不会再去读取t的值了,所以此时t的值还是0,然后线程2执行了对t的加1操作,此时t =1 。
这个时候,就出现了线程安全问题了,两个线程都对t执行了加1操作,但t的值却是1。所以说,volatile关键字并不一定能够保证变量的安全性。

什么情况下volatile能够保证线程安全

volatile解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。如果是count++操作,使用如下类实现:AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是JDK8,推荐使用LongAdder对象,比AtomicLong性能更好(减少乐观锁的重试次数)。

刚才虽然说,volatile关键字不一定能够保证线程安全的问题,其实,在大多数情况下volatile还是可以保证变量的线程安全问题的。所以,在满足以下两个条件的情况下,volatile就能保证变量的线程安全问题:

运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
变量不需要与其他状态变量共同参与不变约束。

ConcurrentHashMap

https://crossoverjie.top/2018/07/23/java-senior/ConcurrentHashMap/

1.8中的结构图:
在这里插入图片描述

jvm垃圾回收

https://cloud.tencent.com/developer/article/1623210
https://segmentfault.com/a/1190000023637649
https://www.jianshu.com/p/5261a62e4d29

多线程

https://www.huaweicloud.com/articles/af3779552b006af34add13092728bdfd.html
https://www.huaweicloud.com/articles/c0553b1cde014350e91620af1ce89f68.html

  • 继承Thread类
    通过继承 Thread 类来创建线程的一般步骤如下:
    定义一个 Thread 类的子类,重写 run() 方法,将相关逻辑实现,run() 方法就是线程要执行的业务逻辑方法;
    创建自定义的线程子类对象;
    调用子类实例的 start() 方法来启动线程。
public class MyThread extends Thread{ @Override public void run() { System.out.println(Thread.currentThread().getName()); }
}
public class MyThreadTest { public static void main(String[] args) { // 创建线程 MyThread thread = new MyThread(); // 启动线程 thread.start(); }
}
  • 实现 Runnable 接口
    通过实现 Runnable 接口创建线程一般步骤如下:
    定义 Runnable 接口实现类 MyRunnable,并重写 run() 方法;
    创建 MyRunnable 实例 runnable,以 runnable 作为 target 创建 Thead 对象,该 Thread 对象才是真正的线程对象;
    调用线程对象的 start() 方法。
public class MyRunnable implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName()); }
}
public class MyRunnableTest { public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); // 创建线程 Thread thread = new Thread(myRunnable); // 启动线程 thread.start(); }
}
  • 使用 Callable 和 Future 创建线程
    与 Runnable 接口不一样,Callable 接口提供了一个 call() 方法作为线程执行体,call() 方法比 run() 方法功能要强大,比如:call() 方法可以有返回值、call() 方法可以声明抛出异常。
    Java5 提供了 Future 接口来代表 Callable 接口里 call() 方法的返回值,并且为 Future 接口提供了一个实现类 FutureTask,这个实现类既实现了 Future 接口,还实现了 Runnable 接口,因此可以作为 Thread 类的 target。在 Future 接口里定义了几个公共方法来控制它关联的 Callable 任务。
    使用 Callable 和 Future 创建线程的一般步骤如下:
    创建实现 Callable 接口的类 myCallable;
    以 myCallable 为参数创建 FutureTask 对象;
    将 FutureTask 作为参数创建 Thread 对象;
    调用线程对象的 start() 方法。
import java.util.concurrent.Callable;
public class MyCallable implements Callable { @Override public Integer call() throws Exception { System.out.println(Thread.currentThread().getName()); return 99; }
}
public class MyCallableTest { public static void main(String[] args) { FutureTask futureTask = new FutureTask<>(new MyCallable()); // 创建线程 Thread thread = new Thread(futureTask); // 启动线程 thread.start(); // 结果返回 try { Thread.sleep(1000); System.out.println("返回的结果是:" + futureTask.get()); } catch (Exception e) { e.printStackTrace(); } }
}
  • 使用线程池创建线程
    Executors 提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService 接口。
    主要有四种:
    newFixedThreadPool
    newCachedThreadPool
    newSingleThreadExecutor
    newScheduledThreadPool
public class MyRunnable implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName()); }
}
public class SingleThreadExecutorTest { public static void main(String[] args) { ExecutorService executorService = Executors.newSingleThreadExecutor(); MyRunnable myRunnable = new MyRunnable(); for(int i = 0; i < 10; i++){ executorService.execute(myRunnable); } System.out.println("=======任务开始======="); executorService.shutdown(); }
}
synchronized关键字底层实现

https://juejin.cn/post/6844903726545633287

单例模式

https://blog.csdn.net/cselmu9/article/details/51366946

redis

Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)

java集合

list是怎么做排序的,排序的list是什么

  • Collections.sort实现排序
  • 重写Comparable接口的compareTo方法
  • 使用匿名内部类实现排序
  • list.sort(Comparator.comparing(User::getAge));
    https://blog.csdn.net/zhengqiqiqinqin/article/details/8434132
    List,Set,Map将持有对象一律视为Object型别。
  • Collection、List、Set、Map都是接口,不能实例化。
    继承自它们的 ArrayList, Vector, HashTable, HashMap是具象class,这些才可被实例化。

  • vector容器确切知道它所持有的对象隶属什么型别。vector不进行边界检查。

  • 在各种Lists中,最好的做法是以ArrayList作为缺省选择。当插入、删除频繁时,使用LinkedList();
    Vector总是比ArrayList慢,所以要尽量避免使用。

  • 在各种Sets中,HashSet通常优于HashTree(插入、查找)。只有当需要产生一个经过排序的序列,才用TreeSet。
    HashTree存在的唯一理由:能够维护其内元素的排序状态。

  • 在各种Maps中
    HashMap用于快速查找。

  • 当元素个数固定,用Array,因为Array效率是最高的。

结论:最常用的是ArrayList,HashSet,HashMap,Array。

java虚拟机

Java堆(Heap),是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。方法区(Method Area),方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。程序计数器(Program Counter Register),程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。JVM栈(JVM Stacks),与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。本地方法栈(Native Method Stacks),本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
链接:https://juejin.cn/post/6844903885115490311
在这里插入图片描述
https://xie.infoq.cn/article/b783efac59c5817b886329321

java语法

反射:https://juejin.cn/post/6844903526477332494
反射怎么优化:
java反射框架主要提供以下功能:
1.在运行时判断任意对象所属的类;
2.在运行时构造任意一个类的对象;
3.在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);
4.在运行时调用任意一个对象的方法;

1.我们在使用ide,输入一个对象,并想调用它的属性和方法的时候,一按点号,编译器就会自动列出它的属性和方法,这里就会用到反射。
2.通用框架,很多框架都是配置化的(比如Spring通过xml配置Bean或者Action), 为了保证框架的通用性,可能需要根据不同的配置文件加载不同的对象或者类,调用不同的方法,这个时候就需要反射,运行时动态加载需要加载的对象。

反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。

public class Apple {

    private int price;

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public static void main(String[] args) throws Exception{
        //正常的调用
        Apple apple = new Apple();
        apple.setPrice(5);
        System.out.println("Apple Price:" + apple.getPrice());
        //使用反射调用
        Class clz = Class.forName("com.chenshuyi.api.Apple");
        Method setPriceMethod = clz.getMethod("setPrice", int.class);
        Constructor appleConstructor = clz.getConstructor();
        Object appleObj = appleConstructor.newInstance();
        setPriceMethod.invoke(appleObj, 14);
        Method getPriceMethod = clz.getMethod("getPrice");
        System.out.println("Apple Price:" + getPriceMethod.invoke(appleObj));
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值