synchronized原理&Callable接口

synchronized原理

特点 

1.synchronized既是一个悲观锁,又是个乐观锁,自适应的!
   synchronized默认是乐观锁,但是如果发现锁竞争比较激烈,就会变成悲观锁!!
2.synchronized既是轻量级锁,又是一个重量级锁,自适应!
   synchronized默认是轻量级锁,当锁冲突剧烈后,就变成重量级锁!
3.synchronized这里的轻量级锁是基于自旋锁的方式实现的
  synchronized这里的重量级锁是基于挂起等待锁的方式实现的


4.synchronized不是读写锁
5.synchronized是可重入锁
6.synchronized是非公平锁

两个线程针对同一个变量加锁,就会阻塞等待.除了上述基本原理,synchronized还有一些内部的优化机制,存在的目的就是为了让锁更高效,好用.

加锁工作过程——锁升级/锁膨胀

当执行到加锁的代码块儿时,加锁过程就可能经历下面几个升级阶段为了在线程安全和性能做权衡  

无锁
无锁状态,还没开始加锁

偏向锁进行加锁的时候,首先会进入偏向锁状态

偏向锁,并不是真正的加锁,而只是先占个位置,如果有需要就加锁,没需要就不加锁了

相当于"懒汉模式"提到的懒加载一样,非必要,不加锁

synchronized加锁的时候,并不是真正的加锁,而是先进入偏向锁状态,就相当于做一个标记,如果一直没有别的线程来获取这个锁,那么就不会升级,仅仅只做个标记,因为这个变量本来就只有这个线程要使用,过程也没有出现锁竞争,执行完synchronized{}代码块后,再取消掉标记(偏向锁)即可。
但是如果出现了锁竞争,再另一个线程加锁之前,偏向锁会迅速升级为真正的加锁状态!!另一个线程阻塞等待...

偏向锁不是真的加锁, 而只是在锁的对象头中记录一个标记(记录该锁所属的线程). 如果没有其他线
程参与竞争锁, 那么就不会真正执行加锁操作, 从而降低程序开销. 一旦真的涉及到其他的线程竞
争, 再取消偏向锁状态, 进入轻量级锁状态.


轻量级锁
当synchronized发生锁竞争的时候,就会从偏向锁升级为轻量级锁(自旋锁)

此时,synchronized是通过自旋的方式来进行加锁的(就和刚刚伪代码一样的逻辑)

但是,如果很快就释放锁了,自旋是值得的,可以立即获取被释放的锁,反之,迟迟不被释放,那么久迟迟拿不到锁,自旋就不划算了..这时候就需要再次升级了!

重量级锁
一直自旋但是又拿不到锁,synchronized也不会无止境的自旋,此时升级为重量级锁(挂起等待锁)

重量级锁(挂起等待锁)则是基于操作系统原生的API来进行加锁了

linux原生提供了mutex一组API,操作系统北河提供的加锁功能,这个锁是会影响到线程的调度的

此时,如果线程进行了重量级锁的加锁,并且发生了锁竞争,此时线程就会被放入阻塞队列中,暂时不参加CPU的调度了,直到锁被释放了,这个线程才有机会被调度到并有机会获取到锁
锁升级了就不能降级了。

优化过程——锁消除

这是编译器的优化判定,看当前代码是否真的需要加锁,如果这个场景不用加锁,就会自动把加的锁销毁

就像StringBuffer中的关键的方法都是带有synchronized修饰的,就不需要程序员再加锁,加了编译器也会自动销毁!

编译器只会在自己有把握的时候进行锁消除,这个过程在编译过程中触发,而偏向锁是在运行时过程中出现多线程调度的情况,这个线程有可能有人竞争也可能没人竞争,而在编译阶段无法判断锁是否必要,就只能留着。

优化过程——锁粗化

先了解锁的粒度:

synchronized包含的代码越多,粒度就越粗,包含的代码越少,粒度就越细.

通常情况下,粒度细一点比较好,加锁的代码是不能并发执行的,锁的粒度越细,能并发执行的代码就越多,更有利于利用多核CPU资源。粒度越粗能并发的越少,但是如果粒度细的锁被反复进行加锁(涉及反复的锁竞争),可能实际效果不如粒度粗的锁。

eg:领导布置三项简单任务,每次做完你就给领导打电话,打电话相当加锁,每次加锁都涉及到锁竞争,领导那边可能还会有别的事情(别的线程需要加锁,比如开会),领导都烦你了。还不如三项任务都完了再给领导打电话,避免了不必要的所竞争。

有些情况,粒度粗反而更好

这种情况下,两次加锁解锁之间的间隙非常小,反反复复加锁解锁效率低开销大,可以直接加一个大锁,将间隙也包括,效率反而高些,毕竟间隙很小,这块儿代码能不能并发执行影响不大 !

一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化.

实际开发过程中 , 使用细粒度锁 , 是期望释放锁的时候其他线程能使用锁 .
但是实际上可能并没有其他线程来抢占这个锁 . 这种情况 JVM 就会自动把锁粗化 , 避免频繁申请释放锁.

Callable接口

是什么 

Callable 是 Java 中的一个接口,位于 java.util.concurrent 包下,用于表示一个可以返回结果并且可能抛出异常的任务,把线程封装了一个 "返回值". 。与 Runnable 接口相比,Callable 接口的 call() 方法支持泛型的返回值,并且允许抛出异常。

类似于Runnable,是用来描述一个任务,区别就是Runnable描述的任务没有返回值,而Callable描述的任务有返回值,Callable通常搭配FutureTask使用,FutureTask用来保存Callable的返回结果,因为Callable往往是在另一个线程中执行的,不知道具体执行完的时间,FutureTask就是负责等待结果出来并保存的。

使用

futureTask就像买餐取餐的小票。而后台厨师就是Thread t,凭小票取餐。

public static void main(String[] args) throws ExecutionException, InterruptedException {
定义任务       
 Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1; i <= 1000 ; i++) {
                    sum+=i;
                }
                return sum;
            }
        };
创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的 call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();
在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结
果. 
 
      
        System.out.println(futureTask.get());
    }
package thread;

public class Test {
    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);
        }
//        t.join();
//        System.out.println(result.sum);
    }
 
}

小结: 线程的创建方式
1.继承 Thread, 重写 run(创建单独的类,也可以匿名内部类)2.实现 Runnable,重写 run(创建单独的类, 也可以匿名内部类)3.实现 Callable, 重写 call (创建单独的类,也可以匿名内部类)
4.使用 lambda 表达式
5.ThreadFactory 线程工厂
6.线程池 

  • 17
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sqyaa.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值