并发编程中的volatile、synchronized和lock

451 阅读6分钟

Java并发编程中经常会用到synchronized、volatile和lock,三者都可以解决并发问题,这里做一个总结。

1、volatile

volatile保证了共享变量的可见性,也就是说,线程A修改了共享变量的值时,线程B能够读到该修改的值。但是,对任意单个volatile变量的读/写具有原子性,但类似于i++这种复合操作不具有原子性。因此,volatile最适用一个线程写,多个线程读的场合。

2、synchronized

synchronized是Java中的关键字,可以用来修饰变量、方法或代码块,保证同一时刻最多只有一个线程执行这段代码。

修饰普通方法(实例方法):

synchronized修饰普通方法时锁的是当前实例对象 ,进入同步代码前要获得当前实例对象的锁。线程A和线程B调用同一实例对象的synchronized方法时才能保证线程安全,若调用不同对象的synchronized方法不会出现互斥的问题。对比如下两段代码:

public class TestSync implements Runnable{ //共享资源 static int i=0;

/**
 * synchronized 修饰实例方法
 */
public synchronized void addI(){
    i++;
}

public void run() {
    for(int j=0;j<10000;j++){
        addI();
    }
}

public static void main(String[] args) throws InterruptedException {
    TestSync instance=new TestSync();
    Thread t1=new Thread(instance);
    Thread t2=new Thread(instance);
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(i);//输出20000
}

}

public class TestSync implements Runnable{ //共享资源 static int i=0;

/**
 * synchronized 修饰实例方法
 */
public synchronized void addI(){
    i++;
}

public void run() {
    for(int j=0;j<10000;j++){
        addI();
    }
}

public static void main(String[] args) throws InterruptedException {
    TestSync instance1=new TestSync();
    TestSync instance2=new TestSync();
    Thread t1=new Thread(instance1);
    Thread t2=new Thread(instance2);
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(i);//输出可能比20000小
}

}

第二段代码对不同的实例对象加锁,也就是t1和t2使用不同的锁,操作的又是共享变量,因此,线程安全无法保证。解决这种问题的方法是将synchronized作用于静态的addI方法,这样的话,对象锁就是当前类的Class对象,由于无论创建多少个实例对象,但类对象只有一个,在这样的情况下对象锁就是唯一的。

修饰静态方法:

当synchronized作用于静态方法时,锁的是当前类的Class对象锁,由于静态成员不属于任何一个实例对象,是类成员,因此通过Class对象锁可以控制静态成员的并发操作。线程A访问static synchronized方法,线程B访问非static synchronized方法,A和B不互斥,因为使用不同的锁。

public class TestSync implements Runnable{ //共享资源 static int i=0;

/**
 * synchronized 修饰实例方法
 */
public static synchronized void addI(){
    i++;
}

public void run() {
    for(int j=0;j<10000;j++){
        addI();
    }
}

public static void main(String[] args) throws InterruptedException {
    TestSync instance1=new TestSync();
    TestSync instance2=new TestSync();
    Thread t1=new Thread(instance1);
    Thread t2=new Thread(instance2);
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(i);//输出20000
}

}

修饰代码块:

synchronized除了修饰方法(普通方法、静态方法)外,还可以修饰代码块。如果一个方法的方法体较大,而需要同步的代码只是一小部分时就可以用该种使用方式。

public class TestSync implements Runnable{
    static String instanceStr=new String();
    static int i=0;
    @Override
    public void run() {
    
        //使用同步代码块对变量i进行同步操作,锁对象为instance
        synchronized(instanceStr){
            for(int j=0;j<10000;j++){
                i++;
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(new TestSync());
        Thread t2=new Thread(new TestSync());
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);//20000,如果instanceStr不是static则不能保证线程安全,同上
    }
}

除此之外,synchronized还可以对this或Class对象加锁,保证同步的条件同上。

    public void run() {
    
        //this加锁
        synchronized(this){
            for(int j=0;j<10000;j++){
                i++;
            }
        }
    }
    
    public void run() {
    
        //Class对象加锁
        synchronized(TestSync.class){
            for(int j=0;j<10000;j++){
                i++;
            }
        }
    }

3、lock

Lock是一个类,通过这个类可以实现同步访问,先来看一下Lock中的方法,如下:

public interface Lock {

   /**
    * 获取锁,锁被占用则等待
    */
   void lock();

   /**
    * 获取锁时,如果线程处于等待,则该线程能够响应中断而去处理其他事情
    */
   void lockInterruptibly() throws InterruptedException;

   /**
    * 尝试获取锁,如果锁被占用则返回false,否则返回true
    */
   boolean tryLock();

   /**
    * 较tryLock多一个等待时间,等待时间到了仍不能获得锁则返回false
    */
   boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

   /**
    * 释放锁
    */
   void unlock();

}

常见用法:

Lock lock = new ReentrantLock();//ReentrantLock是Lock的唯一实现类
lock.lock();
try{    
    
}catch(Exception ex){

}finally{
    lock.unlock();   
}

Lock lock = new ReentrantLock();
if(lock.tryLock()) {
    try{ 
    
    }catch(Exception ex){

    }finally{
        lock.unlock();   //释放锁
    } 
}else {
    //如果不能获取锁,则处理其他事情
}

Reetrantlock

Reetrantlock是Lock的实现类,它表示可重入锁。ReentrantLock虽然没能像synchronized关键字一样支持隐式的重进入,但是在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。

ReadWriteLock

Reetrantlock属于排他锁,这些锁在同一时刻只允许一个线程进行访问,ReadWriteLock是读写锁,读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

volatile与synchronized的区别

(1)volatile主要应用在多个线程对实例变量更改的场合,通过刷新主内存共享变量的值从而使得各个线程可以获得最新的值;synchronized则是锁定当前变量,通过加锁方式保证变量的可见性。

(2)volatile仅能修饰变量;synchronized则可以使用在变量、方法和类上。

(3)volatile不会造成线程的阻塞;多个线程争抢synchronized锁对象时,会出现阻塞。

(4)volatile仅能实现变量的修改可见性,不能保证原子性。

(5)volatile标记的变量不会被编译器优化,可以禁止进行指令重排;synchronized标记的变量可以被编译器优化。

synchronized与lock的区别

(1)synchronized在执行完同步代码或发生异常时,能自动释放锁;而Lock则需要在finally代码块中主动通过unLock()去释放锁;

(2)Lock可以让等待锁的线程响应中断,Lock提供了更灵活的获取锁的方式,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

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

(4)Lock可以提高多个线程进行读操作的效率。如果竞争资源不激烈,两者的性能是差不多的,而当有大量线程同时竞争时,此时Lock的性能要佳。所以说,在具体使用时要根据情况适当选择。