目录
Synchronized原理
Synchronized的优化手段
锁膨胀/锁升级
体现了Synchronized能够“自适应”这样的能力。
锁粗化
粗化的反面是细化
此处的粗细指的是“锁的粒度”。指的是加锁代码涉及到的范围。加锁代码的范围越大,认为锁的粒度越粗,范围越小,则认为粒度越细。
到底锁的粒度是粗好还是细好?各有各的好
如果锁的粒度比较细,多个线程之间的并发性就更高
如果锁的粒度比较粗,加锁解锁的开销就更小
编译器就会有一个优化,就会自动判定说,如果某个地方的代码锁的粒度太细了,就会进行粗化。
锁消除
有些代码,不用加锁,结果给加锁了。编译器就会发现这个加锁没必要,就直接把锁给去掉了。
像StringBuffer 、 Vector…在标准库中进行了加锁操作,在单个线程中用到了上述的类,就是单线程进行了加锁解锁。
Java中的JUC
iava.util.concurrent
Callable 接口
Callable 是一个interface,也是一种创建线程的方式。
Runnable创建线程,但是不太适合让线程计算出一个结果,这样的代码。
例如:像创建一个线程,让这个线程计算1+2+3+…+1000,如果基于Runnable来实现,就会比较麻烦。
Callable就是要解决Runnable不方便返回结果这个问题的
public class Demo28 {
public static void main(String[] args) {
//通过Callable来描述一个这样的任务
Callable<Integer> callable=new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum=0;
for(int i=0;i<=1000;i++){
sum += i;
}
return sum;
}
};
//为了让线程执行callable中的任务,光有构造方法还不够,还需要一个辅助的类
FutureTask<Integer> task=new FutureTask<>(callable);
//创建线程。来完成这样的计算工作
Thread t=new Thread(task);
t.start();
//如果线程的任务没有执行完,get就会阻塞
//一致阻塞到,任务完成了,结果算出来了
try {
System.out.println(task.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
执行结果
ReentrantLock
可重入锁
基础用法
提供了两种方法
lock()
unlock()
把加锁和解锁两个操作分开了(这种做法不太好,很容易遗漏unlock)
public class Demo29 {
public static void main(String[] args) {
ReentrantLock locker=new ReentrantLock();
//加锁
locker.lock();
//如果在这里抛出异常了,就容易导致unlock执行不到
//解锁
locker.unlock();
}
}
和Synchronized的区别
(1)Synchronized是一个关键字(背后的逻辑是JVM内部实现的,c++代码写的),ReentrantLock是一个标准库中的类(背后的逻辑是Java代码写的)。
(2)Synchronized不需要手动释放锁,出了代码块,锁自然释放。ReentrantLock必须要手动释放锁,要谨防忘记释放。
🎈(3)Synchronized如果竞争锁失败,就会阻塞等待,但是ReentrantLock除了阻塞等待,还会trylock,失败了直接返回。
trylock给了更多回旋余地
🎈(4)Synchronized是非公平锁,ReentrantLock提供了公平锁和非公平锁两个版本,在构造方法中,通过参数来指定当前十公平还是非公平。
(5)基于Synchronized衍生出来的等待机制是wait notify,功能相对有限; 基于ReentrantLock衍生出来的等待机制是Condition类,功能要更丰富一系。
信号量 Semaphore
是一个更广义的锁
锁是信号量里一种特殊情况,叫做“二次元信号量”。
举例:
开车的时候,经常会遇到一种情况—停车
停车场入口一般会有一个牌子,上面写着“当前空闲**个车位”
每次有车开进去,车位数-1
每次有车开出来,车位数+1
这个牌子就是信号量,描述了可用资源(车位)的个数
每次申请一个可用资源,计时器就-1(称为P操作)
每次释放一个可用资源,计数器就+1(称为V操作)
当信号量的计数器已经是0了,再次进行P操作,就会阻塞等待
public class Demo30 {
public static void main(String[] args) throws InterruptedException {
//初始化的值表示可用资源有4个
Semaphore semaphore=new Semaphore(4);
//申请资源 P操作
semaphore.acquire();
System.out.println("申请成功");
semaphore.acquire();
System.out.println("申请成功");
semaphore.acquire();
System.out.println("申请成功");
semaphore.acquire();
System.out.println("申请成功");
semaphore.acquire();
System.out.println("申请成功");
//释放资源,V操作
//semaphore.release();
}
}
CountDownLatch
类似于终点线
countDown给每个线程里面去调用,就表示到达终点了
await是给等待线程去调用,当所有的任务都到达终点了,await就从阻塞中返回,就表示任务结束。
public class Demo31 {
public static void main(String[] args) throws InterruptedException {
//构造方法的参数表示有几个选手参赛
CountDownLatch latch=new CountDownLatch(10);
for(int i=0;i<10;i++){
Thread t=new Thread(() ->{
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"跑完了");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}) ;
t.start();
}
//裁判就要等待所有的线程到达
//当这些线程没有执行完的时候,await就阻塞,所有的线程都执行完了,await才返回
latch.await();
System.out.println("比赛结束");
}
}
执行效果
copyOnWriteArrayList
写时拷贝,在修改的时候,会创建一个副本出来。
适合于读多写少的情况,也适合于数据小的情况
例如:有一个ArrayList
如果是多线程读这个ArrayList,此时没有线程安全问题,完全不需要加锁,也不需要其他方面的控制,如果是多线程写,就是把这个ArrayList复制了一份,先修改副本。
要想将 [1,2,3,4]中的1修改为100,先修改,在让副本转正。
这样做的好处,就是修改的同时对于读操作,是没有任何影响的。读的时候优先读旧的版本,不会说出现读到了一个“修改了一半”的中间状态。
多线程下使用哈希表
HashMap本身线程不安全
1.HashTable 【不推荐】
HashTable 是如何保证线程安全的?就是给关键方法加锁
2.ConcurrentHashMap
ConcurrentHashMap改进之处
(1)ConcurrentHashMap减少了锁冲突,就是让锁加到每个链表的头节点上
(2)ConcurrentHashMap只是针对写操作加锁了,读操作没加锁,只是使用Volatile
(3)ConcurrentHashMap中更广泛的使用CAS,进一步提高效率
(4)ConcurrentHashMap针对扩容,进行了巧妙的化整为零
对于HashTable来说,只要这次put触发了扩容就一口气搬运完,会导致这次put非常卡顿。对于ConcurrentHashMap,每次操作只搬运一点点,通过多次操作完成整个搬运的过程。
同时维护一个新的HashMap和一个旧的,查找的时候既需要查找旧的,也需要查找新的,插入的时候只插入新的,直到搬运完在销毁旧的。