多线程编程必备技术—— volatile,synchronized,lock

volatile:

​ volatile是一个类型修饰符(type specifier)。它是被设计用来修饰被不同线程访问和修改的变量。确保本条指令不会因编译器的优化而省略,且要求每次直接读值。

​ 简单来说:就是将变量共享到多线程环境下,让所有线程都能读取到变量再主内存的值。

 

volatile具备两种特性

一:保证共享变量对所有线程的可见性。将一个共享变量声明为volatile后,会有以下效应:

1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去;

2.这个写会操作会导致其他线程中的缓存无效。

二:禁止指令重排序优化。

 

内存模型:

​ (1)每个线程都有自己的本地内存空间(java栈中的帧)。线程执行时,先把变量从内存读到线程自己的本地内存空间,然后对变量进行操作。 ​ (2)对该变量操作完成后,在某个时间再把变量刷新回主内存。

那么我们再了解下锁提供的两种特性:互斥(mutual exclusion) 和可见性(visibility)

​ (1)互斥(mutual exclusion):互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据;

​ (2)可见性(visibility):简单来说就是一个线程修改了变量,其他线程可以立即知道。保证可见性的方法:volatile,synchronized,final(一旦初始化完成其他线程就可见)。

 

例子:

public class Thread_volatile extends Thread {

    private boolean isRunning = true;
    // volatile:变量修饰符,标识该变量为即使共享,当发生修改时,即使的同步到其它线程。
//    volatile private boolean isRunning = true;
 
    public void setRunning(boolean isRunning) {
        this.isRunning = isRunning;
    }

    @Override
    public void run() {
        System.out.println("进入到run方法中了");
        while (isRunning == true) {
            //如果休眠了,修改值就会成功,会重新从主内存读取。
//                Thread.sleep(1000);
        }
        System.out.println("线程执行完成了");
    }

    public static void main(String[] args) {
        try {
            Thread_volatile thread = new Thread_volatile();
            thread.start();
            Thread.sleep(1000);
            thread.setRunning(false);
            //这里并没能成功修改isRunning的值,where循环进入了死循环。那是因为:
            //TODO 在JVM设置成 -server 模式运行程序时,线程是先把变量从内存读到线程自己的本地内存空间,然后对变量进行操作。
            //TODO 所以主main线程,和RunThread分别拥有了自己的本地内存,当main线程修改了isRuning的值后,并未能使RunThread的本地内存同步改变。
            //TODO 解决办法: 使用 volatile 修饰变量即可在改变内存值时,清除本地内存,达到即使共享的目的。
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

原理 :

​        当对volatile标记的变量进行修改时,会将其他缓存中存储的修改前的变量清除,然后重新读取。一般来说应该是先在进行修改的缓存A中修改为新值,然后通知其他缓存清除掉此变量,当其他缓存B中的线程读取此变量时,会向总线发送消息,这时存储新值的缓存A获取到消息,将新值穿给B。最后将新值写入内存。当变量需要更新时都是此步骤,volatile的作用是被其修饰的变量,每次更新时,都会刷新上述步骤。

JVM本质:

volatile本质是修改了jvm的配置,告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取。

 

失效的情况(留意符合操作类):

​       上面已经表示了volatile的作用,它主要是更改变量后能再多线程下共享变量值,但是再某些并发情况下volatile也是不能够胜任的,原因是volatile没有锁机制,它同一时间可以被多个线程访问并操作。所以我们不能拿volatile和synchronized相比较,volatile并不能完全替代synchronized,它依然是个轻量级锁,在很多场景下,volatile并不能胜任。

举例:

public class AtomicIntegerTest {

    private static final int THREADS_CONUT = 20;
    //volatile public static int count = 0; //加volatile没用,count还是无法到20000
    public static volatile int num = 0;

    public static void increase() {
        num++; //Java里的自增并不是原子性的。
    }

    public static AtomicInteger num2 = new AtomicInteger();
    public static void increase2() {
        num2.incrementAndGet(); //这个自增是原子性的。
    }

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_CONUT];
        for (int i = 0; i < THREADS_CONUT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                        //increase();
                        increase2();
                    }
                }
            });
            threads[i].start();
        }

        //Thread.activeCount() 还有个守护线程!!!所以就会一直陷入无限循环。
            // 加Thread.currentThread().getThreadGroup().list()后可结束守护线程。
        while (Thread.activeCount() > 1) { //activeCount():此方法返回活动线程的当前线程的线程组中的数量
            System.out.println(Thread.activeCount());
            //Thread.currentThread().getThreadGroup().list();
            Thread.yield(); //该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)

            //获取所有线程
            ThreadGroup currentGroup = Thread.currentThread().getThreadGroup();
            int noThreads = currentGroup.activeCount();
            Thread[] lstThreads = new Thread[noThreads];
            currentGroup.enumerate(lstThreads);
            for (int i = 0; i < noThreads; i++)
                System.out.println("线程号:" + i + " = " + lstThreads[i].getName());

        }
        System.out.println(Thread.activeCount());

        System.out.println(num);
        //说明:debug模式下可以 Thread.activeCount() > 1;最后一个线程是main;
            // 但是run方式下必须Thread.activeCount() > 2,跟idea有关。会有一个Monitor Ctrl-Break线程产生
    }
}

​      如上当使用 num++时最终的总量总数少于理想的预期值。这是因为num++它不具备原子性。num++ 翻译过来就是num = num + 1,虽然volatile能共享变量,但是没有上锁,同一时间可以有多个线程读取并操作。当运行 num = num + 1时,可能其它线程已经执行了这个运算并且刷新了主内存,这个时候的num值是过期的,再参与运算会将过期的值替换掉较大或相等的新值。所以最后的总量总是少于预期值。

​       比如 num = 1时,有两个线程读取了这个值,并且都进行num++的操作,那么就都执行了num = 1+1,当第一个线程运算出 num = 2后将值刷入了主内存,这时候num = 2,但是另一个线程的num还是等于1,于是又运算出了num = 2,又重新刷新到主内存,于是总量增加变少了1了。

 

volatile的第二个特性:禁止指令重排序优化。

重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。但是重排序也需要遵守一定规则:

  1. 重排序操作不会对存在数据依赖关系的操作进行重排序;

    • 比如:a=1;b=2;c=a+b; 这个指令序列,由于a =1,b=2这两个操作没有数据依赖关系,所以可能被重 排序;但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。

  2. 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。

    • 在多线程环境下,未必能保证接口不被而改变。

例子: 我们将上面的列子增加一个变量 a

public class Thread_volatile extends Thread {

  	int a = 1;
    private boolean isRunning = true;
  
    public void setRunning(boolean isRunning) {
      	a = 2; // 1
        this.isRunning = isRunning;// 2 (若被重排序,可能先执行2这行代码)
    }

    @Override
    public void run() {
        System.out.println("进入到run方法中了");
        while (isRunning == true) { //3
			int b = a+1;// 4
            System.out.println(b);
        }
    }

    public static void main(String[] args) {
        try {
            Thread_volatile thread = new Thread_volatile();
            thread.start();
            Thread.sleep(1000);
            thread.setRunning(false);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

假设主线程main执行setRunnig后,线程thread执行run,我们能保证在4处,b一定等于3么?

答案是无法保证的,这是为何?

​ 编译器和处理器可能会对指令进行重排序,而上例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行isRunning=true再执行a=2。而此时线程thread会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。

​ 使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序

volatile禁止指令重排序也有一些规则:

  1. 当第二个操作是voaltile写时,无论第一个操作是什么,都不能进行重排序

  2. 当地一个操作是volatile读时,不管第二个操作是什么,都不能进行重排序

  3. 当第一个操作是volatile写时,第二个操作是volatile读时,不能进行重排序

总结:

​       volatile是一种轻量级的同步机制,它主要有两个特性:一是保证共享变量对所有线程的可见性;二是禁止指令重排序优化。同时需要注意的是,volatile对于单个的共享变量的读/写具有原子性,但是像num++这种复合操作,volatile无法保证其原子性,当然文中也提出了解决方案,就是使用并发包中的原子操作类,通过循环CAS地方式来保证num++操作的原子性。

 

synchronized


        Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问object的一个加锁代码块时,另一个线程仍然可以访问该object中的非加锁代码块。


 

(1)synchronized 方法

​ 方法声明时使用,放在范围操作符(public等)之后,返回类型声明(void等)之前.这时线程获得的是成员锁,即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候。当前线程(就是在synchronized方法内部的线程)执行完该方法后,别的线程才能进入。

示例:

public synchronized void synMethod(){
      //方法体
}

​ 如在线程t1中有语句obj.synMethod(),那么由于synMethod被synchronized修饰,在执行该语句前,需要先获得调用者obj的对象锁, 如果其他线程(如t2)已经锁定了obj (可能是通过obj.synMethod,也可能是通过其他被synchronized修饰的方法obj.otherSynMethod锁定的obj),t1需要等待直到其他线程(t2)释放obj, 然后t1锁定obj,执行synMethod方法,返回之前之前释放obj锁。

(2)synchronized 块

​ 对某一代码块使用,synchronized后跟括号,括号里是变量,这样一次只有一个线程进入该代码块。此时线程获得的是成员锁。

(3)synchronized (this)

  1. 当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

  2. 当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。  

  3. 然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的除synchronized(this)同步代码块以外的部分。 

  4. 当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。会导致其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。  

  5. 以上规则对其它对象锁同样适用。

举例说明:

public class Thread2 {  
     public void m4t1() {  
          synchronized(this) {  
               int i = 5;  
               while( i-- > 0) {  
                    System.out.println(Thread.currentThread().getName() + " : " + i);  
                    try {  
                         Thread.sleep(500);  
                    } catch (InterruptedException ie) {  
                    }  
               }  
          }  
     }  
     public void m4t2() {  
          int i = 5;  
          while( i-- > 0) {  
               System.out.println(Thread.currentThread().getName() + " : " + i);  
               try {  
                    Thread.sleep(500);  
               } catch (InterruptedException ie) {  
               }  
          }  
     }  
  
     public static void main(String[] args) {  
          final Thread2 myt2 = new Thread2();  
          Thread t1 = new Thread(  new Runnable() {  public void run() {  myt2.m4t1();  }  }, "t1"  );  
          Thread t2 = new Thread(  new Runnable() {  public void run() { 
            //TODO 该方法依然能立即访问,不需要等到t1释放对象锁。假如m4t2方法也上了锁(或者方法内部也用了synchronized)就不行了。
            //原因是:synchronized只能占有对应的代码块,其它线程依然可以访问非上锁的代码块个方法。
            myt2.m4t2();  
          } }, "t2"  );  
          t1.start();  
          t2.start();  
     } 
}

含有synchronized同步块的方法m4t1被访问时,线程中m4t2()依然可以被访问。

应用场景:商品出售后,扣减库存

​       每个商品出售后,库存都要减1;在单线程情况下,只需要写个if( 库存>0 ),但是当多线程并发时可能存在多个线程进入了这个判断,从而使得库存出现了负数的情况。列如:库存 num = 1,此时 if( num >1) num-- 成立,当同时有两个线程进行这个判断的时候,判断都成立,于是num-- 了两次,最后num = -1。

解决办法:使用synchronized同步锁

package com.wenxue.thread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * synchronized 同步测试
 */
public class Runnable_synchronized implements Runnable{

    // 定义票
    private int tickets = 100;

    @Override
    public  void run() {

         while (tickets > 0) {
             //System.out.println("tickets == " + tickets);
          	//synchronized (this) //当不加这行代码的时候就会重复卖票,卖出负数票的情况
             {
                try {
                    //System.out.println(Thread.currentThread().getName());
                    if (tickets > 0) {
                        try {
                            //这里需要等一下才能测出更好的结果,因为线程运行太快了,可能不会出现并发问题,票数就不会卖重复,出现负数的情况。
                            Thread.sleep(100);
                            //1秒的时候足够让释放掉锁的线程,重新进入while循环。所以在初次进入wherek后可以打印三次("tickets == " + tickets);
                          
                          //this.wait(1000); //释放锁,进入休眠。跟sleep不同的是 sleep不会释放锁。
                          //this.notify(); //10秒在唤醒等待线程。notityAll() 唤醒所有等待的线程。
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()
                                + "正在出售第" + (tickets--) + "张票");
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            //这里释放锁以后,需要等待一下。如果不等马上回再抢占锁,于是就始终只有一个线程在卖票了。
             try {
                 Thread.sleep(100);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
      
    }

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

        Runnable_synchronized runnable = new Runnable_synchronized();

        Thread tr1 = new Thread(runnable, "窗口1");
        Thread tr2 = new Thread(runnable, "窗口2");
        Thread tr3 = new Thread(runnable, "窗口3");

        tr1.start();
        tr2.start();
        tr3.start();
    }

}

运行以上代码将会出现卖重复票,卖负数票的情况。加synchronized后就不会。

测试的时候需要注意的是需要休眠:

​ 1.测试未使用synchronized时if判断中要休眠,原因是列子中的程序太简单,运行很快,可能达不到高并发的条件,于是不会出现卖重复票,负数票的情况。

​ 2.使用synchronized时,释放锁要休眠,因为等待的线程不会无时无刻的判断对象锁是否释放了,它存在一定的“休眠期”,如果一个线程释放锁后不休眠一会,这个线程又会立马占用对象锁,就会存在一个线程一直站在一个锁。

lock

(1)synchronized的缺陷

​ synchronized是java中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢?

​ 其实Lock的出现是为了弥补synchronized的缺陷,当一个代码块被synchronized修饰了,一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

  1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;

  2)线程执行发生异常,此时JVM会让线程自动释放锁。

  那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能等待,试想一下,这多么影响程序执行效率。

  因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。

再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。

  但是采用synchronized关键字来实现同步的话,就会导致一个问题:

  如果多个线程都只是进行读之类的操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。

  因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。

  另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。

  总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:

1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;

2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

 

(2)java.util.concurrent.locks包下常用的类

public interface Lock {
    //获取锁,如果锁被其他线程获取,则进行等待
    void lock(); 
 
    //当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
    void lockInterruptibly() throws InterruptedException;
 
    /**tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成
    *功,则返回true,如果获取失败(即锁已被其他线程获取),则返回
    *false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。*/
    boolean tryLock();
 
    //tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock(); //释放锁
    Condition newCondition();
}

 

例子:

package com.wenxue.thread;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Lock锁 同步测试
 */
public class Runnable_lock implements Runnable{

    // 定义票
    private int tickets = 100;

    // 定义锁对象
    private Lock lock = new ReentrantLock();


    @Override
    public  void run() {

         while (tickets > 0) {

            try {
                // 获取锁
                // lock()和lockInterruptibly() 的区别在于后者 能用 Thread.currentThread().interrupt()中断线程的等待过程。
                lock.lock();
                //lock.lockInterruptibly(); // 没有获取锁时,线程调用interrupt()会结束线程,并抛出InterruptedException

                //lock.tryLock(); //判断是否获取锁
                //boolean b = lock.tryLock(1000, TimeUnit.MILLISECONDS);
//                if (!b){
//                    System.out.println("没有获取锁");
//                    Thread.currentThread().interrupt();//结束等待
//                }else {
//                      //如果不能获取锁,则直接做其他事情
//                }

                if (tickets > 0) {
                    try {
                        //这里需要等一下才能测出更好的结果,因为线程运行太快了,可能不会出现并发问题。
                        //1秒的时间足够让释放掉锁的线程,重新进入while循环。
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()
                            + "正在出售第" + (tickets--) + "张票");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            //必须手动释放锁,否则会实现死锁。
            finally {
                lock.unlock();// 释放锁
            }

        }
    }


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

        Runnable_lock runnable = new Runnable_lock();

        Thread tr1 = new Thread(runnable, "窗口1");
        Thread tr2 = new Thread(runnable, "窗口2");
        Thread tr3 = new Thread(runnable, "窗口3");
      
        tr1.start();
        tr2.start();
        tr3.start();

        //Thread.sleep(1000);
        //tr1.interrupt(); //当占用锁时是无法停止的,只有没有占锁时才会停止。
    }

}

注意:

​ 当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为本身在前面的文章中讲过单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。

​ 而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

 

可重入锁:

如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁。ReentrantLock,意思是“可重入锁”,是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。

例如:当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。

class MyClass {
    public synchronized void method1() {
        method2();
    }
 
    public synchronized void method2() {
 		//由method1()中调用时,不必重新获取锁。
    }
}
假如synchronized不具备可重入性,此时线程A调用method1()获取锁后,再调用method2()需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁(且并没有释放锁),而这个时候又在申请获取该对象的锁,这样线程A就会一直自己等待自己释放锁,而自己又不会释放锁,所以等到天荒地老永远也不会获取到的锁,也不会释放锁,就会陷入死锁。

ReentrantLock也同样具有这样的特性

package com.wenxue.thread;

import java.util.ArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLock_test {

    private Lock lock = new ReentrantLock();    //注意这个地方

    public static void main(String[] args)  {

            final ReentrantLock_test test = new ReentrantLock_test();
      
            new Thread(){
                public void run() {
                    System.out.println("线程1");
                    test.insert(Thread.currentThread());
                };
            }.start();

            new Thread(){
                public void run() {
                    System.out.println("线程2");
                    test.insert(Thread.currentThread());
                };
            }.start();
        }

        public void insert(Thread thread) {
            lock.lock();
            try {
                System.out.println(thread.getName()+"得到了锁");
                reentrant();
            } catch (Exception e) {
                // TODO: handle exception
            }finally {
                lock.unlock();
                System.out.println(thread.getName()+"释放了锁");
            }
        }

        public void reentrant() {
            lock.lock();
            try {
                System.out.println("可重入锁");
            } catch (Exception e) {
                // TODO: handle exception
            }
            finally {
                System.out.println("释放了锁");
                lock.unlock();
            }
        }
}

打印结果:


线程1

Thread-0得到了锁

可重入锁

释放了锁

Thread-0释放了锁

线程2

Thread-1得到了锁

可重入锁

释放了锁

Thread-1释放了锁


上面的列子中,insert()与reentrant()方法都上了Lock锁,加入Lock没有可重入性,是会陷入死锁的。

 

总结:volatile,synchronized,lock的区别

synchronized和lock区别:

  1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

  2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

  3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

  4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

  5)Lock可以提高多个线程进行读操作的效率。

  在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

volatile和synchronized区别:

1、volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

2、volatile仅能使用在变量级别,synchronized则可以使用在变量,方法和代码块。

3、volatile仅能实现变量的修改可见性,而synchronized则可以保证变量的修改可见性和原子性。

4、volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞。

5、当一个域的值依赖于它之前的值时,volatile就无法工作了,如n=n+1,n++等。如果某个域的值受到其他域的值的限制,那么volatile也无法工作,如Range类的lower和upper边界,必须遵循lower<=upper的限制。

6、使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域。

 

扩展: 对象锁的释放与唤醒

wait() 与notify()/notifyAll()

​       wait():释放占有的对象锁,线程进入等待池,释放cpu,而其他正在等待的线程即可抢占此锁,获得锁的线程即可运行程序。而sleep()不同的是,线程调用此方法后,会休眠一段时间,休眠期间,会暂时释放cpu,但并不释放对象锁。也就是说,在休眠期间,其他线程依然无法进入此代码内部。休眠结束,线程重新获得cpu,执行代码。wait()和sleep()最大的不同在于wait()会释放对象锁,而sleep()不会!

​       notify(): 该方法会唤醒因为调用对象的wait()而等待的线程,其实就是对对象锁的唤醒,从而使得wait()的线程可以有机会获取对象锁。调用notify()后,需要重新抢占锁,而后继续执行当前代码,直到synchronized中的代码全部执行完毕,才会释放对象锁。JVM则会在等待的线程中调度一个线程去获得对象锁,执行代码。需要注意的是,wait()和notify()必须在synchronized代码块中调用。

       notifyAll()则是唤醒所有等待的线程。

package com.wenxue.thread;

public class Synchronized_Test {

    public void test(){
        String threadName = Thread.currentThread().getName();
        System.out.println(name);
        synchronized(Synchronized_Test.class){
            System.out.println(threadName + "进来了 代码块");
            if (Thread.currentThread().getName().equals("Thread-0")){
                try {
                    Synchronized_Test.class.wait(1000);
                    System.out.println("唤醒后继续执行未完成的syn代码块");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if (Thread.currentThread().getName().equals("Thread-1")){
                try {
                    Synchronized_Test.class.notify(); //唤醒等待的线程
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            System.out.println(threadName + "结束了");
        }

    }

    public static void main(String[] args) {
        Synchronized_Test test = new Synchronized_Test();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.test();
            }
        }).start();
      
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.test();
            }
        }).start();    
    }
}
                                                                             -- 龚文学 2019年4月12日。

更多原创技术文章,敬请关注微信公众号:《Java深圳编程》。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值