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