您的位置 首页 java

Java线程与锁

1 什么是 线程

Java线程与锁

线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。程序指令在单线程工作的情况下,表现出来的情况是顺序的。但是在多线程情况下呢?为了在底层最大限度的发挥cpu的工作效率,【有时候】我们看到的情况是程序指令并不是向我们当时书写的那样顺序。

2 什么是锁

锁是逻辑一个概念,它的出现是为了解决多线程的访问问题。在 java 语言中,每个对象头都有mark word字段来标示:

存储内容标志位状态 对象哈希码、对象分代年龄01未锁定指向锁记录的指针00轻量级锁定指向重量级锁的指针10膨胀(重量级锁定)空,不需要记录信息11GC标记偏向线程ID、偏向时间戳、对象分代年龄01可偏向

当进入临界区的时候,每个对象头的指向锁记录的指针会指向占有当前锁的线程。(描述的有可能不清楚)。比如说线程A带有synchronized修饰的方法时,当前对象的对象头的锁记录的指针就会指向A。

2.1 锁的作用以及分类

锁的作用主要就是提供互斥性。防止多个线程一个同一块数据进行读写,这会造成数据读写的不一致。锁的分类方法很多。个人就主要举例几种比较重要的锁。

2.1.1 偏向锁

偏向锁会偏向第一个访问锁的线程,接下来在运行过程中如果此线程再次访问锁的时候,则再也不要进行同步操作,提高了效率。(注意偏向锁并不代表这个线程拥有了这个线程,个人觉得它只是起到标示作用)。如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,jvm会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。

2.1.2 读写锁

当多个线程读写一个数据块时,读的概率远远大于写的概率的时候。这个时候我们应该怎么做呢?一种常规的做法是对读与写操作都进行加锁。但是在大多数是读操作中,这样会显得效率很低下。在此我们提出有没有一种办法在多线程访问读的操作的时候不需要加锁,只有读写同时进行的时候,才触发互斥 ?嗯,java的工程师已经想到了,读写锁横空出世。

class CachedData { Object data; volatile boolean cacheValid; ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); void processCachedData() { rwl.readLock().lock();//@1 if (!cacheValid) { // Must release read lock before acquiring write lock rwl.readLock().unlock();//@4 rwl.writeLock().lock();//@2 // Recheck state because another thread might have acquired // write lock and changed state before we did. if (!cacheValid) {//@3 data = ... cacheValid = true; } // Downgrade by acquiring read lock before releasing write lock rwl.readLock().lock(); rwl.writeLock().unlock(); // Unlock write, still hold read } use(data); rwl.readLock().unlock(); } }
 

3 java 内存模型

java内存模型的主要作用就是 控制变量 对线程的可见性,这是个人的理解。也就是一个变量何时被线程读取到最新的值这是有JMM控制的。

3.1 同步先后顺序

  • An unlock action on monitor m synchronizes-with all subsequent lock actions on m (where subsequent is defined according to the synchronization order).
  • A write to a volatile variable (§8.3.1.4) v synchronizes-with all subsequent reads of v by any thread (where subsequent is defined according to the synchronization order).
  • An action that starts a thread synchronizes-with the first action in the thread it starts.
  • The write of the default value (zero, false or null) to each variable synchronizes-with the first action in every thread. Although it may seem a little strange to write a default value to a variable before the object containing the variable is allocated, conceptually every object is created at the start of the program with its default initialized values.
  • The final action in a thread T1 synchronizes-with any action in another thread T2 that detects that T1 has terminated. T2 may accomplish this by calling T1.isAlive() or T1.join().
  • If thread T1 interrupts thread T2 , the interrupt by T1 synchronizes-with any point where any other thread (including T2 ) determines that T2 has been interrupted (by having an InterruptedException thrown or by invoking Thread.interrupted or Thread.isInterrupted).

3.2 happen-before 规则总结

  • An unlock on a monitor happens-before every subsequent lock on that monitor.
  • A write to a volatile field happens-before every subsequent read of that field.
  • A call to start() on a thread happens-before any actions in the started thread.
  • All actions in a thread happen-before any other thread successfully returns from a join() on that thread.
  • The default initialization of any object happens-before any other actions (other than default-writes) of a program.

1 释放锁的操作happen -before 拥有锁。

2 对 volatile变量 的写happen-before 读。

3 线程的start happen-before 线程的执行的所有方法。

4 线程中所有的操作happen-before 从其他线程返回过来的join方法。

5 默认值的初始化happen-before 其他操作对于变量的读取。比如说0,false,null。

自己再加一下:

6 happen-before 具有传递性。

结论: 在多线程的编程中,要严格按照happen-before规则和加锁顺序,这样才能够避免读取到不正确的值。

3.3 volatile

虽然说对于线程对于用volatile修饰的变量的write对其他线程具有可见性。(这并不代表这块内存的值在线程中没有)但是要注意到一点,如果说你对它更新后的值要依赖它之前的值,就有可能被其他线程读取到所谓的【脏值】。因为这很有可能并不是一个 原子操作 。比如说:i++;

3.4 final 域值的初始化

我们先来看一段demo:

class FinalFieldExample {
 final int x;
 int y;
 static FinalFieldExample f;
 public FinalFieldExample() {
 x = 3;
 y = 4;
 }
 static void writer() {
 f = new FinalFieldExample();
 }
 static void reader() {
 if (f != null) {
 int i = f.x;
 int j = f.y;
 }
 }
}
 

如果有两个线程,一个进行write,一个read。对于上述情况i,j 值会是多少呢?这里给出一下答案: x 的值肯定是3,但是对于y的值有可能为0,虽然这个几率非常小,但是会出现。为什么呢?

因为在构造函数执行之前,final 域的值就已经被初始化了。可以参考之前的happen-before中的变量初始化规则。

3.5 不是原子操作的 double long

对于非volatile的double和long变量的读写并不是一个原子操作,因为double和long都是64位的,但是在java虚拟机中一次性只能写入32位,所以对于他们的操作要分为两步。但是在java虚拟机鼓励对于double和long的读写要具有原子操作,目前主流的虚拟机也确实是这样的。

3.6 wait()方法

wait方法是使当前的线程释放所占用的锁,并使当前的锁进入等待状态。如何从这个状态跳出来有以下几个方法:

  • A notify action being performed on m in which t is selected for removal from the wait set.
  • A notifyAll action being performed on m .
  • An interrupt action being performed on t .
  • If this is a timed wait, an internal action removing t from m ‘s wait set that occurs after at least millisecs milliseconds plus nanosecs nanoseconds elapse since the beginning of this wait action.
  • An internal action by the implementation. Implementations are permitted, although not encouraged, to perform “spurious wake-ups” — to remove threads from wait sets and thus enable resumption without explicit instructions to do so. Notice that this provision necessitates the Java coding practice of using wait only within loops that terminate only when some logical condition that the thread is waiting for holds.

3.7 notification

Notification actions occur upon invocation of methods notify and notifyAll. Let thread t be the thread executing either of these methods on object m, and letn be the number of lock actions by t on m that have not been matched by unlock actions. One of the following actions occurs.

  • If n is zero an IllegalMonitorStateException is thrown. This is the case where thread t does not already possess the lock for target m .
  • If n is greater than zero and this is a notify action, then, if m ‘s wait set is not empty, a thread u that is a member of m ‘s current wait set is selected and removed from the wait set. (There is no guarantee about which thread in the wait set is selected.) This removal from the wait set enables u ‘s resumption in a wait action. Notice however, that u ‘s lock actions upon resumption cannot succeed until some time after t fully unlocks the monitor for m .
  • If n is greater than zero and this is a notifyAll action, then all threads are removed from m ‘s wait set, and thus resume. Notice however, that only one of them at a time will lock the monitor required during the resumption of wait.

文章来源:智云一二三科技

文章标题:Java线程与锁

文章地址:https://www.zhihuclub.com/194162.shtml

关于作者: 智云科技

热门文章

网站地图