美文网首页
Java并发编程(二) - 线程(中) - 常用方法,关键字和锁

Java并发编程(二) - 线程(中) - 常用方法,关键字和锁

作者: ElliotG | 来源:发表于2020-05-22 21:40 被阅读0次

1. 线程常用方法

  • start
    这个方法让线程处于Runnable(可运行)状态。

  • yield
    使当前正在执行的线程向另一个线程交出运行权。
    yield方法属于一种启发式的方法,它会提醒调度器我愿意放弃当前的CPU资源。
    如果CPU资源不紧张,则会忽略这种提醒。

调用yield方法会使当前线程从Running状态切换到Runnable状态。

  • join
    join某个线程A,会使当前线程B进入等待,知道线程A结束生命周期,或者到达给定的时间。
    那么在此期间线程B是出于BLOCKED状态的。

 

2. 锁

在说锁之前,我们先要聊一下竞态条件(race condition)
什么是竞态条件?
竞态条件是指两个或以上线程需要共享对同意数据的存取。
该共享数据的正确性取决于线程访问数据的次序,线程之间会相互覆盖的情况。

重入锁(ReentrantLock)

有两种机制可以防止并发访问代码块。
一种是synchronized关键字,我们稍后会提到。
另一种是使用锁对象。
Java 5开始引入了ReentrantLock类
使用ReentrantLock高呼代码块的关键代码如下:

myLock.lock();
try
{
   ...
}
finally
{
   myLock.unlock();
}

eg:
Bank.java

public class Bank
{
   ...
   private var bankLock = new ReentrantLock();

   ...

   public void transfer(int from, int to, double amount) throws InterruptedException
   {
      bankLock.lock();
      try
      {
         System.out.print(Thread.currentThread());
         accounts[from] -= amount;
         System.out.printf(" %10.2f from %d to %d", amount, from, to);
         accounts[to] += amount;
         System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
      }
      finally
      {
         bankLock.unlock();
      }
   }
   
   ...
}

 

3. synchronized关键字

Java中的原子性操作
在说synchronized关键字之前,我们先来了解一下什么是原子性操作?
所谓原子性操作,是指执行一系列操作时,这些操作要么全部执行,要么全部不执行。
比如:
计数器,先读取当前值,然后+1,再更新。
这就是一个典型的读-改-写的原子性操作。

原子性操作如果不能保证,就会出现线程安全的问题。
例如下面的代码:
ThreadNotSafeCount

public class ThreadNotSafeCount {
  private Long value;

  public Long getCount() {
    return value;
  }

  public void addCount() {
    ++value;
  }
}

这个类明显是线程不安全的,因为如果有多个线程同时操作的时候,会发生竞态条件。

我们把它改成如下:
ThreadSafeCount

public class ThreadSafeCount {
  private Long value;

  public synchronized Long getCount() {
    return value;
  }

  public synchronized void addCount() {
    ++value;
  }
}

synchronized
如果一个方法声明为synchronized,那么对象的锁将保护整个方法,要调用这个方法,线程必须获得内部对象锁。
synchronized是**独占锁,没有获取锁的线程会被阻塞。

也就是说,同一时间只有一个线程可以调用这个方法。
这显然大大降低了并发性。

那么既然这样做性能会不佳,有没有更好的做法呢?
答案当然是肯定的,使用非阻塞的CAS算法实现的原子性操作类AtomicLong就是一个不错的选择!

 

4. volatile字段

Java提供了一种弱形式的同步,那就是使用volatile关键字。

如果一个字段声明为volatile,那么编译器和虚拟机就知道该字段可能被另一个线程并发更新。

确保一个变量的更新对其它线程马上可见.

当一个变量声明为volatile时,线程在写入变量时不会把值缓存在它自己的工作内存(寄存器与高速缓存)中,而是会把值刷新回主内存。因此,当其它线程读取该共享变量时,会从主内存重新获取最新值。

例子:

private volatile boolean done;
public boolean isDone() { return done; }
public void setDone() { done = true; }

上面的代码等同于下面的代码:

private boolean done;
public synchronized boolean isDone() { return done; }
public synchronized void setDone() { done = true; }

注: volatile不能完全取代synchronized同步方法,因为它缺乏原子性

 

5. CAS操作

锁在并发处理中占据了一席之地,但是锁有一个最大的弱点,那就是:
当一个线程没有获取锁的时候会被阻塞挂起。
这样就会导致线程上下文的切换和重新调度的开销。

volatile关键字的出现稍稍弥补了一部分这个弱点,但是volatile关键字虽然保证了共享变量的可见性
却不能解决类似读-改-写的这种原子性问题。

什么是CAS?
CAS即Compare and Swap。
CAS的思想很简单:三个参数,一个当前内存值V,一个旧的预期值A,一个即将更新的值B。
当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,
否则什么都不做,并返回false。

JVM中的CAS操作正是利用了提到的处理器提供的CMPXCHG指令实现的;循环CAS实现的基本思路就是循环进行CAS操作直到成功为止。

我们来看看CAS在atomic类中的应用

  public final native boolean compareAndSwapObject
     (Object obj, long valueOffset, Object expect, Object update);

  public final native boolean compareAndSwapInt
     (Object obj, long valueOffset, int expect, int update);

  public final native boolean compareAndSwapLong
    (Object obj, long valueOffset, long expect, long update);

atomic类,它们实现了对确认,更改再赋值操作的原子性。

AtomicInteger源码:

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;

代码解释:

  • Unsafe是CAS的核心类,Java没有方法能访问底层系统,因此需要本地方法来做,Unsafe就是一个后门,被提供来直接操作内存中的数据。

  • valueOffset:变量在内存中的偏移地址,Unsafe根据偏移地址找到获取数据。

  • value被volatile修饰,保证了内存可见性。

CAS的局限性

CAS存在ABA问题:比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。如果链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。

AtomicStampedReference来解决ABA问题:这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

相关文章

网友评论

      本文标题:Java并发编程(二) - 线程(中) - 常用方法,关键字和锁

      本文链接:https://www.haomeiwen.com/subject/uuacahtx.html