美文网首页
锁与多线程同步的实现

锁与多线程同步的实现

作者: 不知名的蛋挞 | 来源:发表于2020-02-17 09:37 被阅读0次

    Java当中的锁都是为了保证多线程同步执行。如果没有锁的话,多线程是异步执行的。

    什么是多线程同步?

    请看下面的代码:

    public class SynTest {
    
        public static void main(String[] args){
            Thread t1 = new Thread(){
                @Override
                public void run(){
                  testsync();
                }
            };
            t1.setName("t1");
    
            Thread t2 = new Thread(){
                @Override
                public void run(){
                    testsync();
                }
            };
            t2.setName("t2");
    
            t1.start();
            t2.start();
        }
    
        public static void testsync(){
            System.out.print(Thread.currentThread().getName());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    输出:

    t1
    t2
    或者
    t2
    t1
    

    可以看到线程t1和t2基本上是同时打印出来的。为了让线程按顺序一前一后执行,我们就可以给方法加锁。

    public class SynTest {
    
        static ReentrantLock reentrantLock = new ReentrantLock();
    
        public static void main(String[] args){
            Thread t1 = new Thread(){
                @Override
                public void run(){
                  testsync();
                }
            };
            t1.setName("t1");
    
            Thread t2 = new Thread(){
                @Override
                public void run(){
                    testsync();
                }
            };
            t2.setName("t2");
    
            t1.start();
            t2.start();
        }
    
        public static void testsync(){
            reentrantLock.lock();
            System.out.print(Thread.currentThread().getName());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                reentrantLock.unlock();
            }
        }
    }
    

    此次输出就是有间隔地输出,也就是第一个线程执行完释放锁之后第二个线程才开始执行。

    Java当中有了synchronized关键字为啥还要用ReentrantLock呢?JDK1.6之前,synchronized关键字实现同步是一个重量级的锁,因为会去调用OS的函数(native方法),所以synchronized关键字没有源码,它都是用c++实现的。

    比如当t1拿到锁执行synchronized中的同步代码时,线程t2此时尝试去执行synchronized中的同步代码,此时synchronized会去调用OS函数,CPU就需要切换成内核态,此时这个OS函数会进入阻塞状态。t1执行完释放锁之后t2拿到锁需要继续往下执行同步代码的话,CPU又要切换成用户态。

    有个人看不惯这种同步方法,所以就开发了一个包解决同步技术。ReentrantLock就是这个包里面的一个同步技术。这个锁比synchronized关键字还快。

    sun公司这下也坐不住了,所以在JDK1.7做了优化,synchronized虽然也会触发到内核,但是会让同步在JVM层面解决而不是在OS层面解决,优化成偏向锁、轻量锁和重量锁。

    多线程同步内部是如何实现同步的

    wait/notify,synchronized,ReentrantLock

    模拟一些同步的思路

    (1)自旋

    所谓自旋锁就是循环循环自己不断循环。

       // 标识---是否有线程在同步块---是否有线程上锁成功
        volatile int status = 0;
    
        void lock(){
            // 不断循环while 直到拿到锁才跳出while循环
            while (!compareAndSet(0,1)){
            }
            //lock
        }
    
        void unlock(){
            status = 0;
        }
    
        boolean compareAndSet(int except,int newValue){
            // cas操作,修改status成功则返回True
        }
    

    缺点:耗费cpu资源。没有竞争到锁的线程会一直占用cpu资源进行cas操作,假如一个线程获得锁后要花费Ns处理业务逻辑,那另一个线程就会白白地花费Ns的cpu资源。

    改进思路:让得不到锁的线程让出cpu

    (2)yield+自旋

        volatile int status = 0;
    
        void lock(){
            while (!compareAndSet(0,1)){
                yield();// 自己实现
            }
            //lock
        }
    
        void unlock(){
            status = 0;
        }
    

    要解决自旋锁的性能问题必须让竞争锁失败的线程不空转,而是在获取不到锁定的时候把cpu给让出来,yield()方法就能让出cpu资源,当线程竞争锁失败时,会调用yield方法让出cpu。自旋锁+yield的方法并没有完全解决问题,当系统只有两个线程竞争锁时,yield是有效的。需要注意的是该方法只是当前让出cpu,有可能操作系统下次还是选择运行该线程。

    (3)sleep+自旋

       volatile int status = 0;
    
        void lock(){
            while (!compareAndSet(0,1)){
                sleep(10);
            }
            //lock-------------5minute
        }
    
        void unlock(){
            status = 0;
        }
    

    sleep的时间为什么是10?怎么控制呢?就是你是调用者其实很多时候你也不知道这个时间怎么确定。

    比如线程t1拿到锁执行5minute,但是没拿到锁的线程t2只是睡眠10s,也就是每10s就去看一下可不可以拿到锁。而假设t1只执行了1s,但是t2睡了10s,那就足足浪费了9s。

    (4)park+自旋

    public class ParkTest {
    
        public static void main(String[] args){
            Thread t1 = new Thread(){
                @Override
                public void run(){
                    testsync();
                }
            };
            t1.setName("t1");
            t1.start();
    
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.print("-----------main");
    
            // 叫醒t1
            LockSupport.unpark(t1);
        }
    
        public static void testsync(){
            System.out.print("t1 -1");
            // 线程t1执行到park()时就会一直睡眠,需要被叫醒
            LockSupport.park();
            System.out.print("t1 -2");
        }
    }
    

    输出:

    t1 -1
    -----------main
    t1 -2
    

    t1执行testsync()方法时,首先"t1 -1",然后就沉睡但是此时main()方法是继续往下执行的,所以打印了"-----------main",然后执行unpark()方法叫醒t1,t1醒过来之后就打印"t1 -2"。

    park()方法并不是由LockSupport类提供的,LockSupport仅仅对park()方法做了封装,park()方法是由unsafe类提供的。

    park()的实现机制如下:

       volatile int status = 0;
       Queue parkQueue; //集合 数组 list
    
        void lock(){
            while (!compareAndSet(0,1)){
                park();
            }
            //lock
            ....
            unlock();
        }
    
        void unlock(){
            status = 0;
            lock_notify();
        }
    
        void park(){
            // 将当前线程加入到等待队列
            parkQueue.add(currentThread);
            // 将当前线程释放cpu,被释放cpu的线程执行到这一步就不再往下执行了,不会继续执行while
            releaseCpu()
        }
    
        void lock_notify(){
            // 得到要唤醒的线程头部线程
            Thread t = parkQueue.header();
            // 唤醒等待线程
            unpark(t)
        }
    

    ReentrantLock其实就是基于这个机制实现的。

    相关文章

      网友评论

          本文标题:锁与多线程同步的实现

          本文链接:https://www.haomeiwen.com/subject/shmjfhtx.html