锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

Java面试题总结一

时间:2022-09-21 20:00:01 复合式通用序列总线连接器

java 操作字符串有哪些类型?它们有什么区别?

操作字符串有:String、StringBuffer、StringBuilder。

区别:

String 和 StringBuffer、StringBuilder 的区别在于 String 声明是不可变的对象,每次操作都会产生新的对象 String 对象,然后指向新指针 String 对象,而 StringBuffer、StringBuilder 可以在原对象的基础上操作,所以最好不要经常改变字符串的内容 String。

StringBuffer 和 StringBuilder 最大的区别在于,StringBuffer 是线程安全,而且 StringBuilder 但是 StringBuilder 性能高于 StringBuffer,因此,建议在单线程环境下使用 StringBuilder,多线程环境下推荐使用 StringBuffer。
原文链接:https://blog.csdn.net/xuewei_ing/article/details/123381796

Array和List转换

1.数组转换成List集合
方法一
笨方法就是通过add添加数组中的数据循环List集合中

    List<String> mlist = new ArrayList<>();     String[] array = new String[] { 
        "zhu", "wen", "tao"};     // String数组转List集合     for (int i = 0; i < array.length; i ) { 
                 mlist.add(array[i]);     }     // 输出List集合     for (int i = 0; i < mlist.size(); i ) { 
                 System.out.println("mlist-->"   mlist.get(i));     } 

方法二
采用java集合自带asList()方法可以完成转换

   String[] array = new String[] { 
        "zhu", "wen", "tao"};
    // String数组转List集合
    List<String> mlist = Arrays.asList(array);
    // 输出List集合
    for (int i = 0; i < mlist.size(); i++) { 
        
        System.out.println("mlist-->" + mlist.get(i));
    }

2.List集合转换成数组
方法一
笨方法是把List中的数据循环添加到数组中

    List<String> mlist = new ArrayList<>();
    mlist.add("zhu");
    mlist.add("wen");
    mlist.add("tao");
    String[] array = new String[mlist.size()];
    // List转换成数组
    for (int i = 0; i < mlist.size(); i++) { 
        
        array[i] = mlist.get(i);
    }
    // 输出数组
    for (int i = 0; i < array.length; i++) { 
        
        System.out.println("array--> " + array[i]);
    }

方法二
采用集合的toArray()方法直接把List集合转换成数组,这里需要注意,不能这样写:
String[] array = (String[]) mlist.toArray();
这样写的话,编译运行时会报类型无法转换java.lang.ClassCastException的错误,这是为何呢,这样写看起来没有问题啊
因为java中的强制类型转换是针对单个对象才有效果的,而List是多对象的集合,所以将整个List强制转换是不行的
正确的写法应该是这样的

String[] array = mlist.toArray(new String[0]);

List<String> mlist = new ArrayList<>();
    mlist.add("zhu");
    mlist.add("wen");
    mlist.add("tao");
    // List转成数组
    String[] array = mlist.toArray(new String[0]);
    // 输出数组
    for (int i = 0; i < array.length; i++) { 
        
        System.out.println("array--> " + array[i]);
    }

不管是数组转换成集合,还是集合转换成数组,都要注意转换类型的一致性,String[]数组转String类型的集合,当需要使用int,double等集合的时候,需要使用对应的对象
如:数组int[]用Integer[],double[]用Double[] ,因为List集合是对象的集合,而int、double等不是对象,所以需要用字段的对应对象类
原文链接:https://blog.csdn.net/simon_it/article/details/80184834
https://blog.csdn.net/zhuwentao2150/article/details/51713565
参考:https://www.cnblogs.com/goloving/p/7693388.html

List、Set、Map 之间的区别是什么?

在这里插入图片描述
原文链接:https://blog.csdn.net/fangchao2011/article/details/89184615

ArrayList和linkedList的区别

ArrayList
优点:ArrayList 是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)。
缺点:因为地址连续,ArrayList 要移动数据,所以插入和删除操作效率比较低。
LinkedList
优点:LinkedList 基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址。对于新增和删除操作,LinkedList 比较占优势。LinkedList 适用于要头尾操作或插入指定位置的场景。
缺点:因为 LinkedList 要移动指针,所以查询操作性能比较低。
适用场景分析
当需要对数据进行对随机访问的时候,选用 ArrayList。
当需要对数据进行多次增加删除修改时,采用 LinkedList。
如果容量固定,并且只会添加到尾部,不会引起扩容,优先采用 ArrayList。
当然,绝大数业务的场景下,使用 ArrayList 就够了,但需要注意避免 ArrayList 的扩容,以及非顺序的插入。

HashMap和HashTable的相同点与区别

相同点:
1、都实现了 Map、Cloneable、Serializable(当前 JDK 版本 1.8)。
2、Hashtable、HashMap 都使用了 Iterator。而由于历史原因,Hashtable 还使用了Enumeration 的方式。
区别:
1、两者父类不同
HashMap是继承自AbstractMap类,而Hashtable是继承自Dictionary类。不过它们都实现了同时实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口。
2、对外提供的接口不同
Hashtable比HashMap多提供了elments() 和contains() 两个方法。 elments() 方法继承自Hashtable的父类Dictionnary。elements() 方法用于返回此Hashtable中的value的枚举。contains()方法判断该Hashtable是否包含传入的value。它的作用与containsValue()一致。事实上,contansValue() 就只是调用了一下contains() 方法。
3、对null的支持不同
Hashtable:key和value都不能为null。这个可以从 Hashtable 源码中的 put 方法看到,判断如果 value 为 null 就直接抛出空指针异常,在 put 方法中计算 key 的 hash 值之前并没有判断 key 为 null 的情况,那说明,这时候如果 key 为空,照样会抛出空指针异常。
HashMap:key可以为null,但是这样的key只能有一个,因为必须保证key的唯一性;可以有多个key值对应的value为null。在计算 hash 值的时候,有判断,如果key==null ,则其 hash=0 ;至于 value 是否为 null,根本没有判断过。
4、安全性不同
HashMap是线程不安全的,在多线程并发的环境下,可能会产生死
等问题,因此需要开发人员自己处理多线程的安全问题。
Hashtable是线程安全的,它的每个方法上都有synchronized 关键字,因此可直接用于多线程中。
虽然HashMap是线程不安全的,但是它的效率远远高于Hashtable,这样设计是合理的,因为大部分的使用场景都是单线程。当需要多线程操作的时候可以使用线程安全的ConcurrentHashMap。ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。
5、计算hash值的方法不同
Hashtable 直接使用对象的 hash 值。hash 值是 JDK 根据对象的地址或者字符串或者数字算出来的 int 类型的数值。然后再使用除留余数法来获得最终的位置。然而除法运算是非常耗费时间的,效率很低。HashMap 为了提高计算效率,将哈希表的大小固定为了 2 的幂,这样在取模预算时,不需要做除法,只需要做位运算。位运算比除法的效率要高很多。
6、出生的版本不一样
Hashtable 出生于 Java 发布的第一版本 JDK 1.0,HashMap 出生于 JDK1.2。
7、初始容量大小和每次扩充容量大小不同
默认情况下,初始容量不同,Hashtable 的初始长度是 11,之后每次扩充容量变为之前的2n+1(n 为上一次的长度)而 HashMap 的初始长度为 16,之后每次扩充变为原来的两倍。另外在 Hashtable 源码注释中有这么一句话:
阿里内部资料
Hashtable is synchronized. If a thread-safe implementation is not needed, it is recommended to use HashMap in place of Hashtable . If a thread-safe highly-concurrent implementation is desired, then it is recommended to use ConcurrentHashMap in place of Hashtable.
大致意思:Hashtable 是线程安全,推荐使用 HashMap 代替 Hashtable;如果需要线程安全高并发的话,推荐使用 ConcurrentHashMap 代替 Hashtable。PS:这个回答完了,面试官可能会继续问:HashMap 是线程不安全的,那么在需要线程安全的情况下还要考虑性能,有什么解决方式?这里最好的选择就是 ConcurrentHashMap 了,但面试官肯定会叫你继续说一下ConcurrentHashMap 数据结构以及底层原理等。

HashMap 与 ConcurrentHashMap 的异同

1、 都是 key-value 形式的存储数据;
2、HashMap 是线程不安全的,ConcurrentHashMap 是 JUC 下的线程安全的;
3、 HashMap 底层数据结构是数组 + 链表(JDK 1.8 之前)。JDK 1.8 之后是数组 + 链表 + 红黑树。当链表中元素个数达到 8 的时候,链表的查询速度不如红黑树快,链表会转为红黑树,红黑树查询速度快;
4、 HashMap 初始数组大小为 16(默认),当出现扩容的时候,以 0.75 * 数组大小的方式进行扩容;
5、 ConcurrentHashMap 在 JDK 1.8 之前是采用分段锁来现实的 Segment + HashEntry,Segment 数组大小默认是 16,2 的 n 次方;JDK 1.8 之后,采用 Node + CAS + Synchronized来保证并发安全进行实现。

JDK1.7和1.8中HashMap的区别小结

1、 存储的数据结构不同
JDK1.7及之前以数组+链表的形式存储元素。
JDK1.8及之后以数组+链表+红黑树的形式存储元素。(默认长度为16)
为什么这样这样做?
因为当链表的长度过长的时候,查询的效率就直线下降,所以在JDK1.8之后将其设计为当链表的长度达到一定的阈值的时候,就会将链表结构转换为红黑树结构,红黑树是一种自平衡的二叉搜索树,提高查询效率。

2、插入数据的方式不同
JDK1.7及之前采用的是链表头部插入数据。
JDK1.8及之后采用的是链表尾部插入数据。

扩容后转换数据不需要遍历到链表的尾部再插入。
最近添加的元素可能马上就要被获取,头插的方式只需要遍历到链表的头部就能匹配到了。
扩容后链表可能会倒序,并发扩容可能会产生循环链

3、 hash运算不同
JDK1.7及之前计算hash运算多
JDK1.8及之后计算hash运算少

4、 扩容时调整的数据方式不同
JDK1.7及之前扩容后数据会根据hash值重新计算索引的位置,然后将数据存放到对应的位置上。
JDK1.8及之后扩容后数据要么等于原来的位置,要么等于原来的位置+旧容量。

5、 扩容方式不同
JDK1.7及之前首先检查是否需要进行扩容,再插入数据(扩容为原来的两倍)

存放新的元素时,已经存在的元素的个数必须大于等于阈值。
存放新的元素时,与已经存在的元素发生hash碰撞(新元素key计算hash值换算出来的数组下标的位置上已存在元素),满足这两个条件就会进行扩容。
JDK1.8及之后首先插入数据,再检查是否需要扩容

当数组的容量未达到64的时候,则以两倍进行扩容。
当数组的容量达到64之后,若链表的元素大于等于8个时,则将链表结构转换为红黑树结构。
删除元素后,当红黑树中的节点小于等于6个时,则将红黑树结构转换回链表结构。
当红黑树的节点不少于32个的时候,才会继续进行扩容,扩容机制更加优化。
原文链接:https://blog.csdn.net/qq_40980826/article/details/120397873

HashMap遍历的五种方法

使用 Iterator 遍历 HashMap EntrySet
使用 Iterator 遍历 HashMap KeySet
使用 For-each 循环迭代 HashMap
使用 Lambda 表达式遍历 HashMap
使用 Stream API 遍历 HashMap

public class IterateHashMapExample { 
        
    public static void main(String[] args) { 
        
        // 1. 使用 Iterator 遍历 HashMap EntrySet
        Map < Integer, String > coursesMap = new HashMap < Integer, String > ();
        coursesMap.put(1, "C");
        coursesMap.put(2, "C++");
        coursesMap.put(3, "Java");
        coursesMap.put(4, "Spring Framework");
        coursesMap.put(5, "Hibernate ORM framework");

        Iterator < Entry < Integer, String >> iterator = coursesMap.entrySet().iterator();
        while (iterator.hasNext()) { 
        
            Entry < Integer, String > entry = iterator.next();
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        }
    }
}


  1. 使用 Iterator 遍历 HashMap KeySet
public class IterateHashMapExample { 
        
    public static void main(String[] args) { 
        
        Map < Integer, String > coursesMap = new HashMap < Integer, String > ();
        coursesMap.put(1, "C");
        coursesMap.put(2, "C++");
        coursesMap.put(3, "Java");
        coursesMap.put(4, "Spring Framework");
        coursesMap.put(5, "Hibernate ORM framework");

        // 2. 使用 Iterator 遍历 HashMap KeySet
        Iterator < Integer > iterator = coursesMap.keySet().iterator();
        while (iterator.hasNext()) { 
        
            Integer key = iterator.next();
            System.out.println(key);
            System.out.println(coursesMap.get(key));
        }
    }
}


  1. 使用 For-each 循环遍历 HashMap
public class IterateHashMapExample { 
        
    public static void main(String[] args) { 
        
        Map < Integer, String > coursesMap = new HashMap < Integer, String > ();
        coursesMap.put(1, "C");
        coursesMap.put(2, "C++");
        coursesMap.put(3, "Java");
        coursesMap.put(4, "Spring Framework");
        coursesMap.put(5, "Hibernate ORM framework");

        // 3. 使用 For-each 循环遍历 HashMap
        for (Map.Entry < Integer, String > entry: coursesMap.entrySet()) { 
        
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        }
    }
}
  1. 使用 Lambda 表达式遍历 HashMap
public class IterateHashMapExample { 
        
    public static void main(String[] args) { 
        
        Map < Integer, String > coursesMap = new HashMap < Integer, String > ();
        coursesMap.put(1, "C");
        coursesMap.put(2, "C++");
        coursesMap.put(3, "Java");
        coursesMap.put(4, "Spring Framework");
        coursesMap.put(5, "Hibernate ORM framework");

        // 4. 使用 Lambda 表达式遍历 HashMap
        coursesMap.forEach((key, value) -> { 
        
            System.out.println(key);
            System.out.println(value);
        });
    }
}

  1. 使用 Stream API 遍历 HashMap
public class IterateHashMapExample { 
        
    public static void main(String[] args) { 
        
        Map < Integer, String > coursesMap = new HashMap < Integer, String > ();
        coursesMap.put(1, "C");
        coursesMap.put(2, "C++");
        coursesMap.put(3, "Java");
        coursesMap.put(4, "Spring Framework");
        coursesMap.put(5, "Hibernate ORM framework");

        // 5. 使用 Stream API 遍历 HashMap
        coursesMap.entrySet().stream().forEach((entry) - > { 
        
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        });
    }
}

原文链接:https://blog.csdn.net/weixin_60589106/article/details/122482640

java创建线程的四种方式

1.直接初始化Thead类,实现Runnable接口

2.继承Thread类

3.实现callable接口、通过Callable和Future创建线程

4.使用线程池创建线程
(线程池解决两个不同的问题:它们通常在执行大量异步任务时提高性能,因为减少了每个任务的调用开销,并且它们提供了一种方法来限制和管理执行任务集合时消耗的资源(包括线程)。每个ThreadPoolExecutor还维护一些
基本的统计信息,比如完成任务的数量。)
a.使用ThreadPoolExecutor创建线程.这种方式比较灵活
b.使用Executors创建线程池

什么是守护线程?

守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程。
守护线程是一种支持性线程,主要用于后台调度以及支持性的工作。守护线程具备自动结束生命周期的特性,而非守护线程则不具备。

为什么JAVA对象需要实现序列化?

序列化是一种用来处理对象流的机制

所谓对象流:

就是将对象的内容进行流化,可以对流化后的对象进行读写操作,也可以将流化后的对象传输于网络之间。

序列化是为了解决在对对象流进行读写操作时所引发的问题

序列化的实现:

将需要被序列化的类实现serializable接口(标机接口),该接口没有需要实现的方法,implements serializable只是为了标注该对象是可被序列化的,然后使用一个输出流(如:fileOutputStream)来构造一个objectOutoutStream(对象流)对象;接着使用objectOutputStream对象的writeObject(object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话则用输出流。

什么时候使用序列化:

1、对象序列化可以实现分布式对象

主要应用例如RMI(即远程调用Remote Method Invocation)要利用对象序列化运行远程主机上的服务,就像在本地机上运行对象时一样

2、java对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据

可以将整个对象层次写入字节流中,可以保存在文件中或在网络连接上传递,利用对象序列化可以进行对象的“深复制”,即复制对象本身及引用的对象本身,序列化一个对象可能得到整个对象序列

3、序列化可以将内存中的类写入文件或数据库中

比如:将某个类序列化后存为问价,下次读取时只需将文件中的数据反序列化就可以将原先的类还原到内存中,也可以将类序列化为流数据进行传输,总的来说就是将一个已经实例化的类转成文件存储,下次需要实例化的时候只要反序列化即可将类实例化到内存中,并保留序列化时类中的所有变量和状态

4、对象、文件、数据有许多不同的格式,很难统一传输和保存

序列化以后就都是字节流了,无论是什么东西,都能变成一样的东西,就可以进行通用的格式传输或保存,传输结束以后,要再次使用,就进行反序列化还原,这样对象还是对象,文件还是文件

因为java中要将对象序列化为流的形式进行传输

对象的序列化就是为了数据传输,在你的代码的里是对象格式,而在传输的时候不可能还保持这对象的样子。

当两个进程再进行远程通信时,彼此可以发送各种类型的数据,无论是何种类型的数据都会以二进制序列的形式在网络上传输,发送方需要将这个java对象转换为字节序列才能在网络上传送,接收方则需要把字节序列再恢复为java对象

1、概念

序列化:把java对象转换为字节序列的过程

反序列化:把字节序列恢复为java对象的过程

2、用途:

a、把对象的字节序列永久的保存到硬盘上,通常放在一个文件中

b、在网络上传输对象的字节序列

所谓的serializable,就是java提供的通用数据保存和读取的接口,至于从什么地方读出来和保存到那里去都被隐藏在函数参数的背后了,这样子,任何类型只要实现了serializable接口,就可以被保存到文件中,或者作为数据流通过网络发送到别的地方,也可以用管道来传输系统的其他程序中,这样子极大地简化了类的设计,只要设计一个保存一个读取功能就能解决上面说的所有问题
java的对象序列化能让你将一个实现了serializable接口的对象转换成一组byte,这样日后要用这个对象的时候,你就能把这些byte数据恢复出来,并据此从新构建那个对象了
工作流当中流程变量的几种数据类型:string、integer、short、long、double、boolean、date、binary、serializable,这就是为什么要将javabean实现序列化的原因,因为你将对象设置到流程变量中必须要实现序列化,否则会在设置流程变量的时候报错找不到该类型
java对象序列化机制就是把内存中的java对象转成二进制流,java对象序列化后很方便的存储或者在网络中传输
java的序列化机制是通过运行时判断类的序列化ID(serialVersionUID)来判断版本的一致性
在反序列时,java虚拟机会通过二进制流中的serialVarsionUID与本地的相对应的实体类进行比较,如果相同就认为是一致的,可以反序列化,正确获得信息,否则抛出序列化版本不一致的异常
所以涉及到数据传输或者存储的类,严格意义上来说都要加上序列化ID,这也是一种良好的编程习惯
原文链接:https://blog.csdn.net/qq_52052392/article/details/118388052

java锁的种类

Java锁的种类:自旋锁、可重入锁、悲观锁、乐观锁、同步锁、互斥锁

宏观上:乐观锁、悲观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。
自旋锁:自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
自适应自旋锁:根据上一次自旋情况动态调整自旋时间
偏向锁、轻量级锁、重量级锁

原文链接:https://blog.csdn.net/SwjtuPC/article/details/123234460

Java 中线程同步锁和互斥锁
一 概述
1.1 互斥
所谓互斥,就是不同线程,通过竞争进入临界区(共享的数据和硬件资源),为了防止访问冲突,在有限的时间内只允许其中之一独占性的使用共享资源。如不允许同时写。

1.2 同步
同步关系则是多个线程彼此合作,通过一定的逻辑关系来共同完成一个任务。一般来说,同步关系中往往包含互斥,同时,对临界区的资源会按照某种逻辑顺序进行访问。如先生产后使用。

1.3 两者区别
总的来说,两者的区别就是:互斥是通过竞争对资源的独占使用,彼此之间不需要知道对方的存在,执行顺序是一个乱序。同步是协调多个相互关联线程合作完成任务,彼此之间知道对方存在,执行顺序往往是有序的。

二 总结
纠结到底是同步锁还是互斥锁其实是没有什么意义的,你可以认为它们就属于一个东西,如果你只是抠这些概念的话,很容易陷入在一个"活锁"中,出也出不来。

在 Java 中,互斥锁就是两种,synchronized 和 Lock 接口的 xxxLock 实现类。但是道理都是一样的。无非就是哪种写起来更方便。

锁的目的就是避免多个线程对同一个共享的数据并发修改带来的数据混乱。如果存在线程安全性问题,一个非常有效的方式就是加锁,这里的同步锁和互斥锁其实就是一个意思。

锁在操作系统层面的意思就是 Mutex,互斥,意思就是说我(某个线程)获取锁(进入临界区)之后,其他线程不能再进入临界区,这样就达到了互斥的目的,如下图所示。

锁的实现要处理的大概就只有以下4类问题:

“谁拿到了锁“,这个信息存哪里(可以是当前 class,当前 instance 的 markword,也可以是某个具体的 Lock 的实例)
谁能抢到锁的规则(只能一个人抢到 - Mutex;能抢有限多个数量 - Semaphore;自己可以反复抢 - 重入锁;读可以反复抢到但是写独占 - 读写锁……)
抢不到时怎么办(抢不到玩命抢;抢不到暂时睡着,等一段时间再试/等通知再试;或者二者的结合,先玩命抢几次,还没抢到就睡着)
如果锁被释放了还有其他等待锁的怎么办(不管,让等的线程通过超时机制自己抢;按照一定规则通知某一个等待的线程;通知所有线程唤醒他们,让他们一起抢……)
有了这些选择,你就可以按照业务需求组装出你需要锁。

互斥就是线程 A 访问了一组数据,线程 BCD 就不能同时访问这些数据,直到 A 停止访问了
同步就是 ABCD 这些线程要约定一个执行的协调顺序。比如 D 要执行,B 和 C 必须都得做完,而 B 和 C 要开始,A 必须先得做完
这是两种典型的并发问题。恰当的使用锁,可以解决同步或者互斥的问题。

你可以说 Mutex 是专门被设计来解决互斥的;Barrier,Semaphore 是专门来解决同步的。但是这些都离不开上述对上述4个问题的处理。同时,如果遇到了其他的具体的并发问题,你也可以定制一个锁来满足需要。

原文链接:https://blog.csdn.net/liuwg1226/article/details/119900991

静态方法和非静态方法上加锁的区别

1.静态方法加锁:类锁
Synchronized修饰静态方法,实际上是对该类对象加锁,俗称“类锁”
2.非非静态方法加锁:对象锁
Synchronized修饰非静态方法,实际上是对调用该方法的对象加锁,俗称“对象锁”

1.对象锁钥匙只能有一把才能互斥,才能保证共享变量的唯一性
2.在静态方法上的锁,和 实例方法上的锁,默认不是同样的,如果同步需要制定两把锁一样。
3.关于同一个类的方法上的锁,来自于调用该方法的对象,如果

相关文章