synchronized原理
我们总结上面的锁策略,就可以总结出synchronized的一些特性(JDK1.8版本)
自适应锁,根据锁竞争激烈程度,开始是乐观锁竞争加剧就变成悲观锁
开始是轻量级锁,如果锁冲突加剧,那就变成重量级锁
实现轻量级锁是采用自旋锁策略,重量级锁采用挂起等待锁策略
是普通的互斥锁
可重入锁
加锁过程
synchronized是如何做到自适应过程的呢?
JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级。
我们用生活中的例子便于理解锁的变化!
好比一个男生喜欢上了一个女生(漂亮又对男生超好)
但是这个男生比较渣,不想和她纠缠,如果确认了关系,他就要要放弃一片森林了,但是他又想谈恋爱! 所以他选择和那个女生搞暧昧不确立关系(偏向锁)就是说避免了确立关系和分手的纠缠(避免了加锁解锁的开销),过了一段时间有一个其他的男生追这个女生,此时如果这个男生再不确立关系,就有可能失去女生,所以他马上和女生确立关系(自旋锁)…
synchronized锁的优化操作
锁膨胀/锁升级
体现了synchronized锁的自适应能力,根据锁的竞争激励程度自动升级锁
锁粗化/锁细化
这里的粗细指的是加锁的粒度,换句话说就是加锁代码的范围,范围越大,加锁的粒度越大,锁粗化!
编译器会根据你写的代码进行优化策略,在不改变代码逻辑的情况下,使代码效率更高!
Thread t1 = new Thread(()->{ Object locker = new Object(); int num = 0; synchronized (locker){ //针对一整个循环加锁,粒度大 for (int i = 0; i < 10; i++) { num++; } } }); Thread t2 = new Thread(()->{ Object locker = new Object(); int num = 0; for (int i = 0; i < 10; i++) { synchronized (locker) {//每次循环加锁,粒度小 num++; } } });
锁消除
顾名思义,锁消除就是将锁给去掉!
有时候加锁操作并没有起到作用,编译器就会将该锁去掉,提供代码效率!
比如我们知道Vector和Stringbuffer类的关键方法都进行了加锁操作,如果在单线程代码使用这两个类,编译器就会对代码进行优化,进行锁消除!
java中的JUC
啥是JUC?
java.util.concurrent这个包简化为JUC
这个包下有很多java多线程并发编程的接口和类!
我们来了解一下其他的一些重要的类和接口!
Callable
Callable是一个接口,创建线程的一中方法
我们就疑惑了,不是已经有Runnable了嘛,Callable实现的对象可以返回结果,而Runnable取不方便!
例如我们要实现1到100的相加,Runnable就会比较麻烦,而我们通过Callable就比较方便!
//Runnable方式实现 public class Demo2 { static class Result {//辅助类保存结果 public int sum = 0; public Object lock = new Object(); } public static void main(String[] args) throws InterruptedException { Result result = new Result(); Thread t = new Thread() { @Override public void run() { int sum = 0; for (int i = 1; i <= 1000; i++) { sum += i; } synchronized (result.lock) { result.sum = sum; result.lock.notify(); } } }; t.start(); synchronized (result.lock) { while (result.sum == 0) { result.lock.wait(); } System.out.println(result.sum); } } }
显然这个代码有点麻烦,还需要借助一个辅助类才能实现!
//Callable方式实现 import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class Demo3 { public static void main(String[] args) { Callable<Integer> callable = new Callable<Integer>() { @Override public Integer call() throws Exception { int result = 0; for (int i = 1; i <= 100; i++) { result+=i; } return result; } }; //callable描述了这个任务!(你去点餐) //辅助的类将callable任务标记,便于执行线程!(给了小票,区分谁的食物) FutureTask<Integer> task = new FutureTask<>(callable); Thread t1 = new Thread(task);//执行线程任务!(给你做好了) try { int result = task.get(); //获取到结果(凭小票取餐) System.out.println("计算结果:"+result); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
我们通过Callable接口实现时需要注意一些细节,我们要通过FutureTask对象将Callable传入标记,便于后面拿值(task.get())
ReentrantLock可重入锁
我们知道synchronized也是可重入锁!这个有什么过人之处呢?
public class Demo4 { public static void main(String[] args) { //一个参数的构造方法 true 为公平锁 false 非公平锁 默认非公平锁 ReentrantLock lock = new ReentrantLock(true); //加锁 lock.lock(); //解锁 lock.unlock(); } }
我们可以看到ReentrantLock将加锁和解锁分开操作!
其实分开的做法并不好,有时候可能会忘记解锁(lock,unlock())就会使线程造成阻塞!
和synchronized锁的区别
synchronized是一个关键字(背后逻辑是JVM,由C++实现),Callable是一个接口(背后逻辑由java代码编写)
synchronized不需要手动释放锁操作,出了代码块,锁自动释放,而ReentrantLock必须手动释放锁!
synchronized是一个非公平锁,ReentrantLock提供了公平锁和非公平两个版本,供选择!
synchronized锁竞争失败就会进行阻塞等待,而ReentrantLock除了阻塞等待外还提供了trylock失败直接返回
基于synchronized的等待机制是wait和notify功能相对有限,而ReentrantLock等待机制提供了Condition类功能强大
其实在日常开发synchronized功能就够用了!
semaphore 信号量
一个更广义的锁!
锁是信号量的一种为"二元信号量"
举个生活中的例子:
你去停车场停车:
当你到门口你可以看到有个牌子写了当前还剩多少车位!
进去一辆车 车位就减一
出来一辆车 车位就加一
如果当前车位为0 就阻塞等待!
这个标识多少车位牌子(描述可用资源的个数)就是信号量
每次申请一个资源 计数器就-1(称为p操作)
每次释放一个资源 计数器就+1(称为v操作)
资源为0阻塞等待!
锁是特殊的"二元信号量" 只有0或1标识资源个数!
信号量就是把锁推广到一般情况,可用资源更多的时候,如何处理
一般很少用到!
import java.util.concurrent.Semaphore; public class Demo5 { public static void main(String[] args) throws InterruptedException { //创建一个可用资源个数为3的信号量 Semaphore semaphore = new Semaphore(3); //p操作 申请信号量 semaphore.acquire(); System.out.println("申请成功"); semaphore.acquire(); System.out.println("申请成功"); semaphore.acquire(); System.out.println("申请成功"); semaphore.acquire(); System.out.println("申请成功"); //v操作 释放信号量 semaphore.release(); System.out.println("释放成功"); } }
我们只有3个资源,而却申请了4次,那么第4次就会进行阻塞等待!
CountDownLatch
重点线,这里怎么解释呢!
就好比一场跑步比赛,裁判要等到所有人越过终点线,比赛才算结束!
这样的场景在开放中也很常见!
例如多线程下载!
比如迅雷等 都是将一个文件分给多个线程同时下载,当所有线程下载完毕,才算下载完成!
import java.util.concurrent.CountDownLatch; public class Demo6 { public static void main(String[] args) throws InterruptedException { //5个线程! CountDownLatch latch = new CountDownLatch(5); for (int i = 0; i <5; i++) { Thread t1 = new Thread(()->{ try { Thread.sleep(300); //获取该线程名 System.out.println(Thread.currentThread().getName()); latch.countDown();//该任务执行 } catch (InterruptedException e) { e.printStackTrace(); } }); t1.start(); } latch.await(); //等待所有线程执行结束 System.out.println("比赛结束"); } }
CopyOnWriteArrayList写时拷贝
当我们在多线程环境下使用ArrayList时
读操作并不会导致线程不安全!
但是写操作就可能出现线程不安全问题!
当多个线程进行多写操作时,显然就线程不安全,会有脏读问题!
我们可以自己加锁操作!
我们java提供了多线程环境下使用的ArrayList
CopyOnWriteArrayList
当多线程去写操作时,我们的ArrayList会创建一个副本进行写操作!当写操作完成后再更新数据! 就不会出现数据修改一般的情况!
当ArrayList扩容时,也会慢慢搬运,不会一致性将ArrayList直接拷贝,导致操作卡顿!
多线程下使用hash表(常考)
HashMap线程不安全!
HashTable[不推荐]
因为HashTable就是将关键方法进行synchronized加锁!
也就相当于直接给HashTable加锁,效率很低!
无论进行什么操作都会导致锁竞争!
ConcurrentHashMap[推荐]
HashTable对象加锁
就好比一个公司里的员工需要请假,都要向老板请假才有用!
而老板就相当于锁,如果有很多员工请假就会导致锁竞争激烈,线程阻塞!
解决方案:
老板权利下放!
让每个部门的人向部门管理人员请假!
我们知道哈希表的结构是由数组,数组元素是链表!
并且链表长度相对短! 所以锁冲突就很小!!!
ConcurrentHashMap优点
针对读操作不进行加锁,只对写操作加锁
减少锁冲突,在每个表头加锁
广泛使用CAS操作,进一步提高效率(比如维护size操作)
进行扩容巧妙的化整为零,进行了优化