Java线程同步、锁机制精解(5中同步方式)

为什么需要同步

java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。

同步方式

  1. synchronized同步方法
  2. synchronized同步代码块
  3. volatile特殊域变量
  4. ReenreantLock重入锁
  5. ThreadLocal局部变量

1 使用synchronized同步方法

即有synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

public synchronized void save(){}

注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类

2 使用synchronized同步代码块

即有synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步

代码如:

synchronized(object){}

注:同步是一种高开销的操作,因此应该尽量减少同步的内容。
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

3 volatile 关键字

  • volatile关键字为域变量的访问提供了一种免锁机制,
  • 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,
  • 因此每次使用该域就要重新计算,而不是使用寄存器中的值
  • volatile不会提供任何原子操作,它也不能用来修饰final类型的变量

代码实例:

//需要同步的变量加上volatile
private volatile int account = 100;

注:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。
用final域,有锁保护的域和volatile域可以避免非同步的问题。

详细解释请看这篇文章

4 使用重入锁(ReentrantLock)实现线程同步

在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。

ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力

ReenreantLock类的常用方法有:

ReentrantLock() \\创建一个ReentrantLock实例 
lock()          \\获得锁 
unlock()        \\释放锁 

注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用

代码实例:

//需要声明这个锁
private Lock lock = new ReentrantLock();
//这里不再需要synchronized 
public void save(int money) {
    lock.lock();
    account += money;
    lock.unlock();
}

注:关于Lock对象和synchronized关键字的选择:

  • 最好两个都不用,使用一种java.util.concurrent包提供的机制,能够帮助用户处理所有与锁相关的代码。
  • 如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码
  • 如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁

5 使用局部变量(ThreadLocal)实现线程同步

如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,
副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

ThreadLocal 类的常用方法:

ThreadLocal()   // 创建一个线程本地变量 
get()           // 返回此线程局部变量的当前线程副本中的值 
initialValue()  // 返回此线程局部变量的当前线程的"初始值" 
set(T value)    // 将此线程局部变量的当前线程副本中的值设置为value

代码实例:

//使用ThreadLocal类管理共享变量account
private static ThreadLocal<Integer> account = new                                         ThreadLocal<Integer>(){
    @Override
    protected Integer initialValue(){
        return 100;
    }
};
public void save(int money){
    account.set(account.get()+money);
}
public int getAccount(){
    return account.get();
}

注:ThreadLocal与同步机制:

  • ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。
  • 前者采用以”空间换时间”的方法,后者采用以”时间换空间”的方式

同步方法和同步代码块区别和优劣势

1. 同步方法

  • 它们所拥有的锁就是该方法所属的类的对象锁

    换句话说,也就是this对象
    而且锁的作用域也是整个方法
    这可能导致其锁的作用域可能太大,也有可能引起死锁

  • 可能包含不需要进行同步的代码块在内,会降低程序的运行效率

2. 同步块

  • 同步块可以更加精确的控制对象锁,也就是控制锁的作用域

    锁的作用域就是从锁被获取到其被释放的时间。

  • 可以选择要获取哪个对象的对象锁。

    在使用同步块机制时,如果使用过多的锁也会容易引起死锁问题,同时获取和释放所也有代价

不管是同步方法还是同步块,我们都不应该在他们的代码块内包含无限循环

关于锁

1. 同步方法的锁

1.1 非静态同步方法的锁

所有的非静态同步方法用的都是同一把锁——实例对象本身

也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。

1.2 静态同步方法的锁

所有的静态同步方法用的也是同一把锁——类对象本身

这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象!

2 同步块的锁

对于同步块,由于其锁是可以选择的,所以只有使用同一把锁的同步块之间才有着竞态条件,这就得具体情况具体分析了,但这里有个需要注意的地方,同步块的锁是可以选择的,但是不是可以任意选择的!!!!
这里必须要注意一个物理对象和一个引用对象的实例变量之间的区别!使用一个引用对象的实例变量作为锁并不是一个好的选择,因为同步块在执行过程中可能会改变它的值,其中就包括将其设置为null,而对一个null对象加锁会产生异常,并且对不同的对象加锁也违背了同步的初衷!这看起来是很清楚的,但是一个经常发生的错误就是选用了错误的锁对象,因此必须注意:同步是基于实际对象而不是对象引用的!多个变量可以引用同一个对象,变量也可以改变其值从而指向其他的对象,因此,当选择一个对象锁时,我们要根据实际对象而不是其引用来考虑!作为一个原则,不要选择一个可能会在锁的作用域中改变值的实例变量作为锁对象!!!!

锁的原理

Java中每个对象都有一个内置锁
当程序运行到非静态的synchronized同步方法上时,自动获得与正在执行代码类的当前实例(this实例)有关的锁。获得一个对象的锁也称为获取锁、锁定对象、在对象上锁定或在对象上同步。
当程序运行到synchronized同步方法或代码块时才该对象锁才起作用。
一个对象只有一个锁。所以,如果一个线程获得该锁,就没有其他线程可以获得锁,直到第一个线程释放(或返回)锁。这也意味着任何其他线程都不能进入该对象上的synchronized方法或代码块,直到该锁被释放。
释放锁是指持锁线程退出了synchronized同步方法或代码块。

关于锁和同步,有一下几个要点:
1. 只能同步方法,而不能同步变量和类;
2. 每个对象只有一个锁;当提到同步时,应该清楚在什么上同步?也就是说,在哪个对象上同步?
3. 不必同步类中所有的方法,类可以同时拥有同步和非同步方法。
4. 如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。
5. 如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制。
6. 线程睡眠时,它所持的任何锁都不会释放。
7. 线程可以获得多个重进入(synchronized )锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。
8. 同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块。
9. 在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁。
例如:

public int fix(int y) {
    synchronized (this) {
        x = x - y;
    }
    return x;
}

当然,同步方法也可以改写为非同步方法(同步块),但功能完全一样的
例如:

public synchronized int getX() {
    return x++;
}

public int getX() {
    synchronized (this) {
        return x;
    }
}

效果是完全一样的。

总结

  1. 线程同步的目的是为了保护多个线程反问一个资源时对资源的破坏。

  2. 线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他同步方法。

  3. 对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。

  4. 对于同步,要时刻清醒在哪个对象上同步,这是关键。

  5. 编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。

  6. 当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。

  7. 死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真让你写个死锁程序,不一定好使,呵呵。但是,一旦程序发生死锁,程序将死掉。

    原文作者:java锁
    原文地址: https://blog.csdn.net/lxchild/article/details/53206832
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞