JUC入门(二)
5、8锁现象:就是关于锁的八个问题
谁来充当锁?要锁的是什么东西?这个锁有什么用?
其实锁的作用就是:哪个线程先拿到锁,谁就有先执行同步方法的权力
那么谁能充当锁?任何对象都可以充当锁
要锁的是什么东西?其实把被synchronized修饰的代码,当成一个厕所(共享资源)会更好理解,线程执行这段代码,也就相当于要进去厕所,你要使用厕所(执行代码的权力),就应该先获得一把锁!
深刻理解我们的锁的例子
package com.yw.lock8;import java.util.concurrent.TimeUnit;
//8锁,就是关于锁的八个问题
//问题1:标准情况下,哪个线程先打印? 打电话
//答:两个方法均是用synchronized修饰,锁的对象是方法的调用者,也就是phone这个对象,这个对象的锁只有一把
// ,而A对象先拿到这把锁,所以先执行打电话的操作//问题2:此时对打电话操作延迟四秒,哪个线程先打印? 依旧是打电话
//答:因为依旧是A先拿到了锁住了new出来的phone对象这把锁,所以Hello这个方法只能等待call执行完释放锁//问题4:此时多new一个Phone对象,分别执行打call和hello,谁先执行?hello
//答:因为执行的对象不同,所以两个锁并不是一把锁,自然不会出现争抢锁的问题,hello之前只会休眠一秒,而
//call会休眠四秒,所以hello先输出
public class Demo1 {public static void main(String[] args) {Phone phone1 = new Phone();Phone phone2 = new Phone();new Thread(phone1::call,"A").start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}new Thread(phone2::hello,"B").start();
// new Thread(phone::hi,"C").start();}}class Phone{//synchronized锁的对象是方法的调用者//两个方法用的同一把锁,所以谁先拿到谁执行public synchronized void call(){try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("打电话");}public synchronized void hello(){System.out.println("Hello");}//问题3:增加了一个普通方法hi,我们添加一个线程来执行该方法,谁先执行? hi//答:hi方法没有被synchronized修饰,所以不需要锁,所以先执行,然后因为call方法先拿到锁,所以顺序为//hi->call->hello
// public void hi(){
// System.out.println("Hi~");
// }
}
package com.yw.lock8;import java.util.concurrent.TimeUnit;
//问题5:在两个方法前均加入static,只有一个对象,现在会先输出什么? 打电话
//答:因为只有一个Phone对象,谁先拿到锁就执行谁//问题6:两个对象,分别执行两个静态方法,谁先输出? 依旧是打电话
//答:因为static锁的是Phone这个类,无论有几个对象,锁只有一个,所以因为call先拿到这把锁,依旧先输出打电话
public class Demo2{public static void main(String[] args) {Phone2 phone1 = new Phone2();Phone2 phone2 = new Phone2();new Thread(()->{phone1.call();},"A").start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}new Thread(()->{phone2.hello();},"B").start();}}class Phone2{
//static 表示这是个静态方法,表示类一加载就有了,所以它锁的是类,它锁的是class,这里的class(类)只有一个,那就是Phonepublic static synchronized void call(){try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("打电话");}public static synchronized void hello(){System.out.println("Hello");}}
package com.yw.lock8;import java.util.concurrent.TimeUnit;
//问题7:一个对象,将hello的static取消,此时先输出什么? hello
//答:因为一个锁的是类Phone,一个锁的是对象,并不是一把锁,所以不会发生争抢,call延迟4s,hello延迟
//1s,所以先输出hello//问题8:两个个对象,将hello的static取消,此时先输出什么? hello
//答:依旧不是一个锁
public class Demo3{public static void main(String[] args) {Phone3 phone1 = new Phone3();Phone3 phone2 = new Phone3();new Thread(()->{phone1.call();},"A").start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}new Thread(()->{phone2.hello();},"B").start();}}class Phone3{//static 表示这是个静态方法,表示类一加载就有了,所以它锁的是类,它锁的是class,这里的class(类)只有一个,那就是Phonepublic static synchronized void call(){try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("打电话");}public synchronized void hello(){System.out.println("Hello");}}
6、集合类不安全
List
list不安全
比如当我们执行下面这段代码
package com.yw.unsale;import java.util.ArrayList;
import java.util.List;
import java.util.UUID;public class ListTest {public static void main(String[] args) {List<String> list = new ArrayList<>();for (int i = 1 ;i<=10000; i++){new Thread(()->{list.add(UUID.randomUUID().toString().substring(0,5));System.out.println(list);},String.valueOf(i)).start();}}
}
会抛出我们以后会熟悉的异常
java.util.ConcurrentModificationException 并发修改异常
这个异常在所有集合处理并发是都有可能出现,在list集合中应该如何解决?
-
List<String> list = new Vector<>();
-
List<String> list = Collections.synchronizedList(new ArrayList<>())
-
List<String> list = new CopyOnWriteArrayList<>();
其中Vector是以前的老技术,其底层的add方法就是通过synchronized来修饰的
而现在的新技术CopyOnWriteArrayList是通过lock锁来实现的
这时我们要介绍
CopyOnWrite:写入时复制 简称cow
当多个线程或进程共享同一份数据时,只有在需要修改数据时,才真正创建数据的副本,并对副本进行修改
CopyOnWrite和Vector两者有什么区别?
1. 线程安全机制
-
Vector
-
使用内置锁(
synchronized
)来保证线程安全。 -
所有修改操作(如
add
、remove
、set
等)和迭代操作都会加锁。 -
优点是实现简单,缺点是锁的粒度较粗,容易导致性能瓶颈,尤其是在高并发场景下。
-
-
CopyOnWriteArrayList
-
使用写入时复制(Copy-On-Write)机制来保证线程安全。
-
修改操作(如
add
、set
等)会创建一个新的数组副本,并在副本上进行操作,而读操作直接在原始数组上进行。 -
读操作不需要加锁,因此在读多写少的场景下性能更高。
-
2. 性能对比
-
读操作
-
Vector
:每次读操作都需要加锁,即使只是读取数据,也会因为锁的开销而降低性能。 -
CopyOnWriteArrayList
:读操作完全不需要加锁,直接在原始数组上进行,性能非常高。
-
-
写操作
-
Vector
:写操作需要加锁,并且每次修改都会触发数组扩容(如果达到容量上限),这可能导致较大的性能开销。 -
CopyOnWriteArrayList
:写操作需要创建数组副本,虽然第一次写入时会有较大的开销,但后续的读操作不会受到影响。如果写操作较少,这种开销是可以接受的。
-
3. 迭代安全性
-
Vector
-
在迭代过程中,如果对集合进行修改(如添加或删除元素),会抛出
ConcurrentModificationException
。 -
虽然
Vector
是线程安全的,但在并发环境下,迭代操作仍然需要额外的同步机制来避免异常。
-
-
CopyOnWriteArrayList
-
在迭代过程中,即使其他线程对集合进行了修改,也不会影响当前迭代操作。
-
因为迭代操作是在原始数组上进行的,而修改操作是在新的数组副本上进行的,所以不会抛出
ConcurrentModificationException
。
-
4. 内存占用
-
Vector
-
内存占用相对较小,因为它只维护一个数组。
-
但每次数组扩容时,会创建一个新的数组,并将旧数组的内容复制到新数组中,这可能会导致短暂的内存占用增加。
-
-
CopyOnWriteArrayList
-
内存占用可能会更大,因为每次写操作都会创建一个新的数组副本。
-
但在写操作较少的场景下,这种内存开销是可以接受的,并且可以避免频繁的锁竞争。
-
5. 使用场景
-
Vector
-
适用于读写操作较为均衡,且对性能要求不高的场景。
-
由于其锁机制较为简单,适合在单线程或多线程环境下使用,但并发性能较差。
-
-
CopyOnWriteArrayList
-
适用于读多写少的场景,例如:
-
日志记录:多个线程写入日志,但读取日志的操作较少。
-
配置管理:配置信息被频繁读取,但修改操作较少。
-
缓存数据:缓存数据被频繁读取,但更新操作较少。
-
-
总结
CopyOnWriteArrayList
在读多写少的场景下比 Vector
更具优势,主要体现在以下几点:
-
读操作性能高:
CopyOnWriteArrayList
的读操作不需要加锁,性能更高。 -
迭代安全性:
CopyOnWriteArrayList
在迭代过程中不会抛出ConcurrentModificationException
,更适合并发环境。 -
锁的粒度更细:
CopyOnWriteArrayList
的写操作不会影响读操作,减少了锁的竞争。
然而,CopyOnWriteArrayList
也有缺点,例如写操作时会创建数组副本,可能会导致较大的内存开销和首次写入的性能开销。因此,在选择时需要根据具体的使用场景来决定。
Set
set不安全
解决方法:
-
Set<String> set= Collections.synchronizedSet(new HashSet<>())
-
Set<String> set= new CopyOnWriteHashSet<>();
补充:HastSet的底层是什么? HashMap
那么HashMap的底层又是什么?
add set 的本质就是map key是无法重复的!
Map
map 不安全
解决方法:
-
Map<String> map= Collections.synchronizedMap(new HashMap<>())
-
Map<String> map= new CopyOnWriteHashMap<>();