JAVA多线程之synchronized、lock、volatile

synchronized、lock的简介

假设一个Integer类型的全局变量i同时被A,B,C三个线程访问,A线程主要是给i做加1的操作,B线程主要是给i做减1的操作,C线程主要是读取i的值并打印出来。那么问题来了,C线程打印的i值是没有变了,还是已经减1,或者已经加1呢?

这里就涉及到线程同步的问题,线程同步是多个线程按照预定的先后次序来运行,Java中可以通过synchronized或者lock来实现线程的同步,下面将主要介绍synchronized、lock的用法以及两者的区别。

1.synchronized

synchronized是Java中的关键字,使用synchronized能够防止多个线程同时进入并访问程序的临界区(程序的某个方法或者代码块)。synchronized可以修饰方法或者代码块,当A线程访问被synchronized修饰的方法或者代码块时,A线程就获取该对象的锁,此时如果B线程想访问该临界区,就必须等待A线程执行完毕并释放该对象的锁。

1)synchronized method():被synchronized修饰后,该方法就变成了一个同步方法,其作用范围就是整个方法,而作用对象要分两种情况来考虑。

情况一:该方法是非静态方法,其作用对象就是调用该方法的对象;

情况二:该方法是静态方法,其作用对象就是调用该方法的所有类的实例对象。

2)synchronized ():括号里可以是类或者对象。

synchronized(className.class):作用对象是访问该代码块的该类所有对象,当某个线程已经在访问该代码块时,其它该类的所有对象都不能访问该代码块。

synchronized(object):是给object加锁,其他线程访问synchronized (object)同步代码块时将会被阻塞(同一个类的不同对象可以访问该代码块)。

synchronized(this):作用对象是当前对象,其他线程访问该对象的同步方法块时将会被阻塞(同一个类的不同对象可以访问该代码块)。

下面给出一个简单例子,通过synchronized关键字,两个线程交替地输出ABABABAB字符串,代码如下:

public class PrintAB {
    private   final Object object = new Object();
    private  boolean flag = false;
    
    public static void main(String[] args) {
        PrintAB printA = new PrintAB();
        MyRunnable1 myRunnable1 = printA.new MyRunnable1();
        MyRunnable2 myRunnable2 = printA.new MyRunnable2();
        Thread thread1 = new Thread(myRunnable1);
        Thread thread2 = new Thread(myRunnable2);
        thread1.start();
        thread2.start();
    }
    
    public class MyRunnable1 implements Runnable {
        @Override
        public void run() {
            while (true) {
                synchronized (object) {
                    if (flag) {
                        try {
                            object.wait();
                        } catch (InterruptedException e) {

                        }
                    }
                    System.out.print('A');
                    flag = true;
                    object.notify();
                }
            }
        }
        }

        public class MyRunnable2 implements Runnable {
            @Override
            public void run() {
                while (true) {
                    synchronized (object) {
                        if (!flag) {
                            try {
                                object.wait();
                            } catch (InterruptedException e) {

                            }
                        }
                        System.out.print('B');
                        flag = false;
                        object.notify();
                    }

                }
            }
        }

    }

输出结果是:

ABABABABABAB

synchronized的更为详细的介绍可以参考Java多线程干货系列synchronized

2.Lock

synchronized是Java语言的关键字,是内置特性,而ReentrantLock是一个类(实现Lock接口的类),通过该类可以实现线程的同步。Lock是一个接口,源码很简单,主要是声明了四个方法:

public interface Lock {

void lock();

void lockInterruptibly() throws InterruptedException;

boolean tryLock();

boolean tryLock(long var1, TimeUnit var3) throws InterruptedException;

void unlock();

Condition newCondition();

}

Lock一般的使用如下:

Lock lock= ...;//获取锁

lock.lock();

try{

//处理任务

}catch(Exception e){

}finally{

lock.unlock();//释放锁

}

lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的,unLock()方法是用来释放锁的,其放在finally块里执行,可以保证锁一定被释放,newCondition方法下面会做介绍(通过该方法可以生成一个Condition对象,而Condition是一个多线程间协调通信的工具类)。

Lock接口的主要方法介绍:

lock():获取不到锁就不罢休,否则线程一直处于block状态。

tryLock():尝试性地获取锁,不管有没有获取到都马上返回,拿到锁就返回true,不然就返回false 。

tryLock(long time, TimeUnit unit):如果获取不到锁,就等待一段时间,超时返回false。

lockInterruptibly():该方法稍微难理解一些,在说该方法之前,先说说线程的中断机制,每个线程都有一个中断标志,不过这里要分两种情况说明:

1) 线程在sleep、wait或者join, 这个时候如果有别的线程调用该线程的 interrupt()方法,此线程会被唤醒并被要求处理InterruptedException。

2)如果线程处在运行状态, 则在调用该线程的interrupt()方法时,不会响应该中断。

lockInterruptibly()和上面的第一种情况是一样的, 线程在获取锁被阻塞时,如果调用lockInterruptibly()方法,该线程会被唤醒并被要求处理InterruptedException。下面给出一个响应中断的简单例子:

public class Test{

public static void main(String[] args){

MyRunnable myRunnable = new Test().new MyRunnable();

Thread thread1 = new Thread(myRunnable,"thread1");

Thread thread2 = new Thread(myRunnable,"thread2");

thread1.start();

thread2.start();

thread2.interrupt();

}

public class MyRunnable implements Runnable{

private Lock lock=new ReentrantLock();

@Override

public synchronized void run() {

try{

lock.lockInterruptibly();

System.out.println(Thread.currentThread().getName() +"获取了锁");

Thread.sleep(5000);

}catch(InterruptedException e) {

e.printStackTrace();

System.out.println(Thread.currentThread().getName() +"响应中断");

}

finally{

lock.unlock();

System.out.println(Thread.currentThread().getName() +"释放了锁");

}

}

}

}

输出结果是:

thread1获取了锁

thread1释放了锁

thread2响应中断

thread2在响应中断后,在finally块里执行unlock方法时,会抛出java.lang.IllegalMonitorStateException异常(因为thread2并没有获取到锁,只是在等待获取锁的时候响应了中断,这时再释放锁就会抛出异常)。

上面简单介绍了ReentrantLock的使用,下面具体介绍使用ReentrantLock的中的newCondition方法实现一个生产者消费者的例子。

生产者、消费者
例子:两个线程A、B,A生产牙刷并将其放到一个缓冲队列中,B从缓冲队列中购买(消费)牙刷(说明:缓冲队列的大小是有限制的),这样就会出现如下两种情况。
1)当缓冲队列已满时,A并不能再生产牙刷,只能等B从缓冲队列购买牙刷;
2)当缓冲队列为空时,B不能再从缓冲队列中购买牙刷,只能等A生产牙刷放到缓冲队列后才能购买。

public class ToothBrushDemo  {
    public static  void main (String[] args){
        final ToothBrushBusiness toothBrushBusiness =
                new ToothBrushDemo().new ToothBrushBusiness();
        new Thread(new Runnable() {
            @Override
            public void run() {
                executeRunnable(toothBrushBusiness, true);
            }
        }, "牙刷生产者1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                executeRunnable(toothBrushBusiness, false);

            }
        }, "牙刷消费者1").start();



    }

    //循环执行50次
    public static void executeRunnable(ToothBrushBusiness toothBrushBusiness,
                                       boolean isProducer){
        for(int i = 0 ; i < 50 ; i++) {
            if (isProducer) {
                toothBrushBusiness.produceToothBrush();
            } else {
                toothBrushBusiness.consumeToothBrush();
            }
        }

    }


    public class ToothBrushBusiness {
        //定义一个大小为10的牙刷缓冲队列
        private GoodQueue<ToothBrush> toothBrushQueue = new GoodQueue<ToothBrush>(new ToothBrush[10]);
        private int number = 1;
        private final ReentrantLock lock = new ReentrantLock();
        private final Condition notEmpty =  lock.newCondition();
        private final Condition notFull =  lock.newCondition();
        public ToothBrushBusiness() {

        }

        //生产牙刷
        public void produceToothBrush(){
            lock.lock();
            try {
                //牙刷缓冲队列已满,则生产牙刷线程等待
                while (toothBrushQueue.isFull()){
                    notFull.await();
                }
                ToothBrush toothBrush = new ToothBrush(number);
                toothBrushQueue.enQueue(toothBrush);
                System.out.println("生产: " + toothBrush.toString());
                number++;
                //牙刷缓冲队列加入牙刷后,唤醒消费牙刷线程
                notEmpty.signal();
            }
            catch (InterruptedException e){
                e.printStackTrace();
            } catch (GoodQueueException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }


        }

        //消费牙刷
        public void consumeToothBrush(){
            lock.lock();
            try {
                //牙刷缓冲队列为空,则消费牙刷线程等待
                while (toothBrushQueue.isEmpty()){
                    notEmpty.await();
                }
                ToothBrush toothBrush = toothBrushQueue.deQueue();
                System.out.println("消费: " + toothBrush.toString());
                //从牙刷缓冲队列取出牙刷后,唤醒生产牙刷线程
                notFull.signal();
            }
            catch (InterruptedException e){
                e.printStackTrace();
            } catch (GoodQueueException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }

        }

    }

    public class ToothBrush {
        private int number;

        public ToothBrush(int number) {
            this.number = number;
        }

        @Override
        public String toString() {
            return "牙刷编号{" +
                    "number=" + number +
                    '}';
        }
    }
}

这里缓冲队列的大小设成了10,定义了一个可重入锁lock,两个状态标记对象notEmpty,notFull,分别用来标记缓冲队列是否为空,是否已满。
1)当缓冲队列已满时,调用notFull.await方法用来阻塞生产牙刷线程。
2)当缓冲队列为空时,调用notEmpty.await方法用来阻塞购买牙刷线程。
3)notEmpty.signal用来唤醒消费牙刷线程,notFull.signal用来唤醒生产牙刷线程。

Object和Conditon对应关系如下:

              Object      Condition 
休眠             wait        await 
唤醒特定线程      notify      signal 
唤醒所有线程     notifyAll   signalAll 

对于同一个锁,我们可以创建多个Condition,就是多个监视器的意思。在不同的情况下使用不同的Condition,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。
Lock锁的介绍
ReentrantLock(可重入锁)是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。

synchronized和ReentrantLock都是可重入锁,可重入性举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。

上面的响应中断的例子已经地使用到了ReentrantLock,下面来介绍另外一种锁,可重入读写锁ReentrantReadWriteLock,该类实现了ReadWriteLock接口,该接口的源码如下:

public interface ReadWriteLock {

Lock readLock();

Lock writeLock();

}

ReadWriteLock接口只有获取读锁和写锁的方法,而ReentrantReadWriteLock是实现了ReadWriteLock接口,接着对其应用场景做简单介绍。

应用场景:
假设一个共享的文件,其属性是可读,如果某个时间有100个线程在同时读取该文件,如果通过synchronized或者Lock来实现线程的同步访问,那么有个问题来了,当这100个线程的某个线程获取到了锁后,其它的线程都要等该线程释放了锁才能进行读操作,这样就会造成系统资源和时间极大的浪费,而ReentrantReadWriteLock正好解决了这个问题。下面给一个简单的例子,并根据代码以及输出结果做简要说明:

public classTest{

public static voidmain(String[] args){

MyRunnable myRunnable = newTest().new MyRunnable();

Thread thread1 = new Thread(myRunnable,"thread1");

Thread thread2 = new Thread(myRunnable,"thread2");

Thread thread3 = new Thread(myRunnable,"thread3");

thread1.start();

thread2.start();

thread3.start();

}

public class MyRunnable implementsRunnable{

private ReadLocklock =new ReentrantReadWriteLock().readLock();

@Override

public synchronized void run() {

try{

lock.lock();

inti=0;

while(i<5) {

System.out.println(Thread.currentThread().getName() +"正在进行读操作");

i++;

}

System.out.println(Thread.currentThread().getName()+"读操作完毕");

}

finally{

lock.unlock();

}

}

}

}

输出结果:

thread1正在进行读操作

thread1正在进行读操作

thread1正在进行读操作

thread1正在进行读操作

thread1正在进行读操作

thread1读操作完毕

thread3正在进行读操作

thread3正在进行读操作

thread3正在进行读操作

thread3正在进行读操作

thread3正在进行读操作

thread3读操作完毕

thread2正在进行读操作

thread2正在进行读操作

thread2正在进行读操作

thread2正在进行读操作

thread2正在进行读操作

thread2读操作完毕

从输出结果可以看出,三个线程并没有交替输出,这是因为这里只是读取了5次,但将读取次数i的值改成一个较大的数值如100000时,输出结果就会交替的出现。

Lock与synchronized的比较

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

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

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

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

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

Lock和synchronized比较主要是参考Java Lock和synchronized的区别

volatile

当一个变量被定义成volatile类型之后,该变量对所有的线程是可见的,这里的可见性指的是当一个线程修改了该变量的值,那么新值对其它线程来说是立即可知的
虽然被volatile修饰的变量具有可见性,但是基于volatile变量的运算在并发下并不是安全的,因为Java里面的有些运算并非原子操作,下面举例说明:

public static volatile int index = 0;
    public static  void main(String[] args){
        for(int i = 0; i < 3; i ++ ){
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                  for(int i = 0 ; i < 10 ; i++){
                      index ++;
                  }
                }
            });
            thread.start();
            increase();

        }
        System.out.println("index:" + index);
    }

输出结果:

index:20

上面的代码,我们直观的反应,输出的index应该是30,而不应该是20。
其实不然,++操作不是一个原子操作,index++指令编译成字节码包含两个操作:取值,加操作。这里index被修饰成volatile,保证了此时从内存中取得的index值是正确的,但有可能其它线程通过加操作将index值加1,之前从内存中取得的index值过期了,这时候我们执行加1操作后将一个较小的index值同步到内存中。
volatile除了让变量具有可见性外,还有一个更为重要的语义:禁止指令重排优化,保证变量的赋值操作跟程序代码中的执行顺序是一致的。JVM往往会对代码进行优化,这些优化操作可能会造成程序指令在执行时会出现乱序,而volatile能够屏蔽掉JVM中必要的代码优化。

总结

很多东西觉得不用笔系统地写下来,过段时间找回就会花一定的时间,于是就写在简书上。同时也欢迎大家指正(其中有些部分是引用了其它的文章,大家也可以做下参考)。

附:Markdown简明教程

推荐阅读更多精彩内容