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的性能要佳。所以说,在具体使用时要根据情况适当选择。