常见线程同步的三种方式

一:什么是线程同步

        线程同步是指两个或多个线程协同步调,按预期的顺序执行代码。
        1:若两个或多个线程同时访问同一个共享资源时,需要让多个线程之间按照顺序访问。
        2:若线程A的执行依赖线程B的结果,需要依赖线程同步来保证两个线程的执行的顺序。

二:实现线程同步的几种方式

        (一):synchronized

              1:synchronized作用有三:

              (1):保证程序执行的原子性

                 在多线程环境下,线程是CPU调度的基本单位,CPU根据不同的调度算法进行线程换。当一个线程获得时间片后开始执行,在时间片耗尽之后,就会失去CPU使用权。因此,在多线程场景下,由于时间片切换的原因,原子性问题可能会出现。

                 例如,线程1获得时间片开始执行,但在执行过程中,CPU时间片耗尽,线程1需要让出CPU。这时线程2获得了时间片开始执行。然而,对于线程1而言,它的操作可能并没有完全执行完成,也没有完全不执行,这就是原子性问题的产生。因此,保证原子性是非常重要的。

                synchronized是如何保证程序执行的原子性呢?

public class SynchronizedDemo {
    public void methodA(){
        synchronized (this){
            System.out.println("synchronized  -----");
        }
    }
}

                   通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行javap -c -s -v -l SynchronizedDemo.class     

                从图中可以看出:

      synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。

                 在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。

                 在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

                在锁未释放之前,其他线程无法再次获取锁,因此通过monitorenter和monitorexit指令可以保证被synchronized修饰的代码在同一时间只能被一个线程访问,其他线程在锁未释放之前无法访问该代码块。这样,synchronized可以保证方法和代码块内的操作是原子性的。

                当线程执行monitorenter指令时,会对Monitor进行加锁,其他线程无法获取锁,除非线程主动解锁。即使在执行过程中,例如CPU时间片用完,线程放弃了CPU,但是并没有进行解锁。由于synchronized的锁是可重入的,线程在下一个时间片中仍然能够获取到锁,并继续执行代码,直到所有代码执行完毕。这样就保证了原子性。

             (2):保证可见性

                可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值。

                在Java内存模型中,所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中使用到的变量的主内存副本拷贝。线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。

                不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递需要通过主内存进行数据同步。因此,就可能出现一个线程修改了某个变量的值,但是其他线程无法立即看到修改的值的情况。

                为了保证可见性,使用synchronized关键字修饰的代码会在开始执行时加锁,在执行完成后解锁。根据可见性的规则,对一个变量解锁之前,必须先把此变量的值同步回主内存中。这样解锁后,后续的线程就可以访问到被修改后的值。因此,通过synchronized关键字锁住的对象,其值具有可见性。

             (3):保证有序性

                有性问题可以理解为在多线程环境下,一个线程中的操作可能会被重排或者乱序执行,而在另一个线程中观察这些操作的顺序可能是无法确定的。

               Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。

                2:synchronized作为java的关键字,可以作用在代码块、实例方法、静态方法、和类。

              (1):修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁            
    synchronized void  methodA(){
        System.out.println("synchronized  method-----");
    }

                

                synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法。JVM 通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

                (2):修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份)。所以,如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁
synchronized  static void methodB(){
        System.out.println("synchronized static   -----");
    }
                 (3):修饰代码块

                   指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁                  

public void methodC(){
        synchronized (this){
            System.out.println("synchronized  -----");
        }
    }

                        

    (二):ReentrantLock    

                        ReentranLock是一个支持重入的独占锁,在JUC(java.util.concurrent)包中,底层就是基于AQS实现的。

                        ReentranLock类本身并没有直接继承AQS(AbstractQueuedSynchronizer),而是创建了一个内部类Sync来继承了AQS,而ReentrantLock类本身的那些方法都是调用Sync里面的方法来实现,而Sync本身自己也是一个抽象类,它还有两个子类,分别是NonfairSyncFairSync,对锁各种实际的实现其实在这两个类中实现,顾名思义,这两个类分别实现了非公平锁和公平锁,在创建ReentrantLock时可以进行选择。 

    /**
     *默认创建一个公平锁
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * 根据参数创建一个公平锁或者非公平锁
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
                        1:lock()    
   //会调用Sync类中的lock()方法,所以需要看创建的是公平锁还是非公平锁
   public void lock() {
        sync.lock();
    }

                        当定义为非公平锁时,先试用CAS的方式更新AQS中的state的状态,默认是0代表没有被获取,当前线程就可以获取锁,然后把state改为1,接着把当前线程标记为持有锁的线程,如果if中的操作失败就表示锁已经被持有了,就会调用acquire()方法

 final void lock() {
            /**
            使用CAS更细state的值,默认是0代表没有被获取,当前线程就可以获取锁,然后把state改为        
            1,接着把当前线程标记为持有锁的线程
            */
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
            //如果if中的操作失败就表示锁已经被持有了,就会调用acquire()方法
                acquire(1);
 }

    /**
    acquire(1)方法会调用abs的方法,这里面调用子类实现的tryAcquire()方法,最终是调用到Sync类中的                
    nonfairTryAcquire()方法,可以看到先判断state是不是0,也就是能不能获取锁,如果不能则判断请求 
    锁的线程和持有锁的是不是同一个,如果是的话就把state的值加1,也就是实现了重入锁
    */
protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
}


                      当定义为公平锁,在子类FairSync中重写了tryAcquire方法,注意if(c==0)判断中的代码,也就是线程抢夺锁的时候会调用hasQueuedPredecessors()方法,这个方法会判断队列中有没有已经先等待的线程了,如果有则当前线程不会抢到锁,这就实现了公平性,上面nonfairTryAcquire()方法则没有这种判断,所以后来的线程可能会比先等待的线程先拿到锁。

 protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                //判断是否存在等待队列
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
                          2:tryLock()   

                          可以看到tryLock实际上是非公平锁的实现,不能保证正在排队的线程能拿到锁,因为可能被新来的线程抢走。

 public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }
                          3:unlock()   

                          这个方法是释放锁,最终会调用到Sync类中的tryRelease()方法。在这个方法里面会对state减1,如果减1之后为0就表示当前线程持有次数彻底清空了,需要释放锁。

 public void unlock() {
        sync.release(1);
 }
 public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
 }
 protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
 }

          (三):CountDownLatch      

                        CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉。

                        CountDownLatch的作用也是如此,在构造CountDownLatch(int count):的时候需要传入一个整数count,在这个整数“倒数”到0之前,主线程需要等待在门口,而这个“倒数”过程则是由各个执行线程驱动的,每个线程执行完一个任务“倒数”一次。

                        总结来说,CountDownLatch的作用就是等待其他的线程都执行完任务,必要时可以对各个任务的执行结果进行汇总,然后主线程才继续往下执行

                          1:CountDownLatch(int count):构造方法,创建一个新的 CountDownLatch 实例,用给定的计数初始化。参数 count 表示线程需要等待的任务数量

                          2:void await():使当前线程等待,直到计数器值变为0,除非线程被 interrupted。如果计数器的值已经为0,则此方法立即返回。在实际应用中,通常在主线程中调用此方法,等待其他子线程完成任务。

                         3:boolean await(long timeout, TimeUnit unit):使当前线程等待,直到计数器值变为0,或者指定的等待时间已到,或者线程被 interrupted。如果计数器的值已经为0,则此方法立即返回。
                        参数 timeout 是指定的等待时间,
                        参数 unit 是 timeout 的单位(如秒、毫秒等)。
                        此方法返回一个布尔值,表示在等待时间内计数器是否变为0。

                        4:void countDown():递减计数器的值。如果计数器的结果为0, 则释放所有等待的线程。在实际应用中,通常在线程完成任务后调用此方法。

                        5:long getCount():获取当前计数的值。返回当前 CountDownLatch 实例内部的计数值。

                        6:利用CountDownLatch实现ABC的的顺序打印

public class CountDownLatchB {

    public static void main(String[] args) throws InterruptedException {

        CountDownLatch countDownLatchA = new CountDownLatch(1);
        CountDownLatch countDownLatchB = new CountDownLatch(1);
        CountDownLatch countDownLatchC = new CountDownLatch(1);

        Thread threadA =  new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    countDownLatchA.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("A");
                countDownLatchB.countDown();
            }
        });

        Thread threadB =  new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    countDownLatchB.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("B");
                countDownLatchC.countDown();
            }
        });


        Thread threadC =  new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    countDownLatchC.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("C");
            }
        });

        threadA.start();
        threadB.start();
        threadC.start();
        countDownLatchA.countDown();

    }



}

           

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值