开发者学堂课程【高校精品课-上海交通大学-企业级应用体系架构:Threading 2 】学习笔记,与课程紧密联系,让用户快速学习知识。
课程地址:https://developer.aliyun.com/learning/course/75/detail/15835
Threading 2
内容介绍:
一、Intrinsic Locks and Synchronization
二、Atomic Access
三、Liveness
四、Starvation and Livelock
五、Guarded Blocks
六、Immutable Objects
七、A Synchronized Class Example
一、Intrinsic Locks and Synchronization
Synchronization is built around an internal entity known as
the intrinsic lock or monitor lock.
Intrinsic locks play a role in both aspects of synchronization: enforcing exclusive access to an object's state and establishing happens-before relationships that are essential to visibility.
synchronized对 于任何对象都是内部锁,监视器锁,可以强制要求执行 synchronized 方法,线程必须要先获得对象上的锁,而且一旦获得,别人不能修改,所以强制要求对象状态访问必须是独占式,谁有锁,谁才能访问,其它他人不能访问,在执行时都需要获得锁,锁只有一把,哪个线程一旦获取再执行操作,其它人获取不了,尽管一个在调用increment,一个在调用 decrement,锁只有一把,a 获得,B 获得不了,所以任何一个对象本身是锁,因为里面有内部锁或者监视器锁,通过 synchronized关键字实现,所有的操作满足 happens-before 关系,有前后顺序关系,前面做执行操作,后面是格式。
Every object has an intrinsic lock associated with it.
By convention, a thread that needs exclusive and consistent access to an object's fields has to acquire the object' S intrinsic lock before accessing
them, and then release the intrinsic lock when it's done with them.
A thread is said to own the intrinsic lock between the time it has acquired the lock and released the lock. As long as a thread owns an intrinsic lock, no other thread can acquire the same lock. The other thread will block when it attempts to acquire the lock.
When a thread releases an intrinsic lock, a happens-before relationship is established between that action and any subsequent acquisition of the same lock.
每个对象都有锁关联,synchronized 实现每一个线程,只要想对象的状态,成员进行操作,必须用锁,一旦获取线程拥有锁,而其它任何线程的锁没有被释放期间,只要有线程拥有锁,没有任何其它线程可以再次获得锁,所以只能等到线程释放锁之后,其它的线程才能获取锁,谁能获得需要靠竞争,不管后面是谁获取锁,都建立 happens-before关系,不存在内存不一致问题。
Locks In Synchronized Methods
When a thread invokes a synchronized method,
it automatically acquires the intrinsic lock for that method's object and releases it when the method returns.
The lock release occurs even if the return was caused by an uncaught exception.
You might wonder what happens when a static synchronized
method is invoked,
since a static method is associated with a class, not an object.
In this case, the thread acquires the intrinsic lock for the Class object associated with the class. Thus access to class's static fields is controlled by a lock that's distinct from the lock for any instance of the class.
在对象上执行,方法不是对象方法,是类方法,不需要创建任何对象就可以在上面调用,没有任何对象只在类上,只要是 synchronized,要访问也要获得锁,但是锁不是对象,每一个对象都有锁,类上也有锁,叫类对象,只要定义synchronized,静态方法,也不会出现问题,不是没有对象没法控制锁,整个类上还有一把锁,如果是静态方法,要求获得类上面一把锁,定义 synchronized 关键字,increment 方法在调用的时候要获取锁,在执行过程中调用其它方法,也包括C也要调用 increment,它们都调用不了,因为是三个线程,其中一个获取锁,其它线程获取不到锁,只定义一个方法是 synchronized,在类里面只有一个方法是 synchronized 有没有意义,比如调用 increment就没办法调用decrement,执行方法线程必须要获取锁,只有 increment 方法,但是有ac两个线程同时想调用,也会出内存不一致的情况,现在是零,各自调用,执行两次是二,不做保护执行完就是一,有一个就会把另外一个覆盖掉,所以即使只有一个方法 increment,只要有方法被调用,synchronized 关键字修饰它也有具体意义,防止 ac 同时访问它的时候把它解锁,所以即使是类方法也有相应锁必须获取才能调用,所以类方法用 synchronized 关键字修饰也有意义。
Synchronized Statements
Another way to create synchronized code is with synchronized
statements.
Unlike synchronized methods, synchronized statements must specify the object that provides the intrinsic lock:
public void addName(String name) {
synchronized(this) {
lastName = name;
nameCount++;
}
nameList.add( name);
}
In this example, the addName method needs to synchronize changes to lastName and nameCount, but also needs to avoid synchronizing invocations of other objects' methods.
synchronized 关键字直接修饰在方法上,力度比较粗,如果一个方法有好几条指令,把某一个人加到列表里,但是还得记住现在有多少人,计数器不希望在多线程操作时候出问题,所以要保证只有一个线程能放,但是集合类list在java中大量集合类全是线程安全,有多个线程再往里插入之后不会有任何问题,两个名字都会插入,原来list中只有两个元素,a要插一个,B要插一个,不会有问题不会最后只插进一个,ac都插到末尾,不存在问题,没有必要把list也同步,允许 ac 多线程往里插,性能可以提高,但是 namecount 只能线程操作,否则用错,把整个 addname同步一下,namelist 在任何时候只有线程访问,显得力度太粗,可以用 synchronized 语句定义语句块同步,synchronized语句里面有参数,参数是 this 值,当前对象,一个对象就是一个锁,namelist 线程执行到这里,必须获取当前对象,lastname 等于 name,两个变量进行操作的时候必须是 synchronized,如果是ac两个线程调用 addname,执行到这里的时候,只能有 a 先执行,b 后执行,但任何时刻只有一个能执行,执行结束之后,执行到底下的时候,ac可以同时执行,所以 synchronized this,后面是语句块,只能被同步住语句是指lastName = name;
nameCount++;
避免多个线程同时做改写,导致内存变量发生变化,Synchronized Statements 可以让整个控制力度变得很细,完全按照要求控制,所以 Synchronized Statements 方法显得比较粗,Synchronized 语句显得比较细腻,完全可以自己控制可以充分提高系统性能。
Synchronized statements are also useful for improving concurrency with fine-
grained synchronization.
public class MsLunch {
private longc1 = 0;
private longc2= 0;
private Object lock1 = new Object();
private Object lock2 = new Object();
public void inc1() {
synchronized(lock1)
{c1++; }
}
public void inc2(){
synchronized(lock2)
{ c2++; }
}
}
Use this idiom with extreme care. You must be absolutely sure that it really is safe to interleave access of the affected fields.
传递是 this 明显在使用对象,每一个对象都在使用锁关联,但是系统中也会允许做自定义,根据自定义进行操作,既然每一个对象都有内部锁可以控制,两个对象是两个 object,对象本身可以像锁一样,在 Synchronized把lock1传进去,当线程执行c++语句时候,必须要获取 lock1上面的锁,lock1本身是对象,对象上面 本身有内部锁,获取上面锁执行即可,但是 inc1和 inc2因为操作变量不一样,所以在操作 C1变量和操作 C2变量可以同时执行,但是如果两个地方都要求对 this 进行操作,对 C1操作,对 C 2也不能操作,所以设置两个不同的锁,一个是 lock1,一个是lock2,把对 C1的多线程操作互相隔离开,对 C2进行操作隔离开,但是对 C1和对 C 2操作可以进行做,任何一个对象都是锁,如果想定义锁,定义一个对象,未来在对象上做 Synchronized,相当于在获取锁,只有锁住才能执行底下操作,可以进行细力度同步。
Reentrant Synchronization
Recall that a thread cannot acquire a lock owned by another
thread.
But a thread can acquire a lock that it already owns.
Allowing a thread to acquire the same lock more than once
enables reentrant synchronization.
This describes a situation where synchronized code, directly or
indirectly, invokes a method that also contains synchronized code, and both sets of code use the same lock.
Without reentrant synchronization, synchronized code would have to take many additional precautions to avoid having a thread cause itself to block.
线程确实获取锁,但是线程执行逻辑比较复杂,在执行过程中有可能会要求再次获取刚才的锁,比如在this上加锁,前面对 this 进行加锁之后进来,执行到底下调用某一个方法,方法里面又有 Synchronized 语句,锁已经被获取,在底下调用 Synchronized,在 A 方法里面 Synchronized this,在语句里面调用 m,在同一个类里定义方法,M进去以后也是要求 Synchronized,执行到 Synchronized this 获取当前锁,锁已经被当前执行的线程获取到,对于执行a方法线程,要想执行m必须获取类似锁,但是锁已经有,M 不认,要想执行 M 当前线程必须把获取的锁释放,在里面再获取一次,但是要想释放必须要把 M 执行完,语句结束才可以,如果允许一个线程不止一次获取锁,就是可重复锁,可重复锁可能是直接或者间接原因导致必须要做动作,当执行到a的时候已经获取到锁,到M的时候又要求获取锁,如果是普通锁获取不到就死锁在这里,如果能获取,能获取到必须是可重复锁。
二、Atomic Access
In programming, an atomic action is one that effectively
happens all at once.
There are actions you can specify that are atomic:
Reads and writes are atomic for reference variables and for most primitive variables (all types except long and double).
Reads and writes are atomic for all variables
declared volatile (including long and double variables) ,
加锁同步,跟前面的事务非常像,强调原子型工程,所有动作必须要一次性完成,再执行别的动作,不能在执行动作同时有其它动作插入进来做,在计算机里面存储数据的时候,所有机器元最后操作都是按字节操作,比如int类型,在内存里面如果占四个字节,机器做加一操作,对最后一个字节加一,看看有没有进位,如果有进位要和前面的字节再做加一,再把进位跟前面加一,再把进位给前面加一,四个字节全处理完加一动作才结束,基础值 C 加一的动作,其实在机器码的层面上应该至少有四条语句,比如它来自于某一个内存,把内存移到寄存器里,寄存器做完操作再补回来,加到一起,机器码不止一条,是不是原子型,要么都执行要么都不执行或者必须同时执行,在中间不会插入插句改写,比如当最后一个字节加一之后,用进位跟倒数第二字节做加一,如果这时插入操作把第二字节值改掉,加一操作的最后结果肯定是错的,c++或 java 都为了考虑问题,做约定,比如 java 对引用类型变量,对象变量和对绝大多数基本类型变量读和写都是原子型,但是 long 和 d ouble 除外,对 long 做操作可能有问题,不保证,所有声明称volatile 易变易失的变量全部都是原子型,long 和 double 也在里,所以在 java 关键字里面,volatile 无论是long还是 double 类型,当对这种类型变量做操作,做读和写操作,相当于把这串字节直接全部锁死,只有锁死必须在写完之后才能释放,保证这一条,但是 volatile 一旦做这样的动作,当有多线程要求写的时候,它会全部排开顺序执行,可以保证所有线程写进东西,要么全是线程写进去,在等另外线程执行完之后再写,不可能是一部分是线程写,另外一部分是另外的线程写,volatile 关键字的由来。
Atomic actions cannot be interleaved, so they can be used
without fear of thread interference.
However, this does not eliminate all need to synchronize atomic actions, because memory consistency errors are still possible.
Using volatile variables reduces the risk of memory consistency errors, because any write to a volatile variable establishes a happens-before relationship with subsequent reads of that same variable
线程之间互相干扰,所以用 volatile 降低线程一致性错误问题,因为在强制要求所有线程都必须满足 happens-before 关系。
三、Liveness
A concurrent application's ability to execute in a timely
Manner is known as its liveness.
Deadlock
Deadlock describes a situation where two or more threads
are blocked forever, waiting for each other.
Here's an example.
Alphonse and Gaston are friends, and great believers in courtesy.
A strict rule of courtesy is that when you bow to a friend, you must remain
bowed until your friend has a chance to return the bow.
Unfortunately, this rule does not account for the possibility that two friends might bow to each other at the same time.
多线程在同时访问很多临界资源的时候会出现三种情况,第一种情况,假如有两人遇到,要互相鞠躬,鞠躬要求,向朋友鞠躬,你的朋友回鞠的时候才能把身体直起来,如果两人碰到以后互相鞠躬,比如 a 给 B 鞠躬,在B给a回鞠的时候,a 可以直升,但是在 B 给 a 回鞠的时候,相当于 B 给 a 鞠躬,B 要直身必须 a 给 b 鞠躬,如果同时鞠躬,卡死,互相之间都在等对方指示才能直起来。
定义 friend 代码,friend 里面有叫做鞠躬的函数,还有回鞠的礼节性动作,两个方法是 synchronized,它俩之间只能有一个被调用,如果另外一个被调用,需要等,等其中一个完成,另外一个才能调用。bower 是向谁鞠躬,当前对象是 a 要向 B 鞠躬,会掉用 a 传递 b ,在屏幕上输出信息,A 在里面给 B 鞠躬时,b 会在上面调回鞠的动作,把自身传过去,B 被调用 Frirnd bower 方法,参数传进来是 A,B 做动作在屏幕上看输出想回鞠,A向B鞠躬的时候,B会向a进行回鞠,输出相应的信息。
public static void main(String[] args) {
final Friend alphonse =
new Friend("Alphonse");
final Friend gaston =
new Friend("Gaston");
new Thread(new Runnable() {
public void run() { alphonse.bow(gaston); }
}).start();
new Thread(new Runnable() {
public void run() { gaston.bow(alphonse); }
}).start();
}
}
定义两个,一个是 Friend alphonse,一个是 Friend gaston,一个在调用 a 上面的 bow,一个在调用b上面的bow,一个向 a 鞠躬,a 向 B 鞠躬,线程调用鞠躬,假设 alphonse 是 A,gaston 是b,调用 a.bow b,传递下来是个参数,调用 b 回鞠的动作,b 同时调用 a 的动作,调用 b.bow a,b 调用 a 时,调用 a 上面 bowback,a 调用b,b调用a,b 向 a 鞠躬,a 向 b 回鞠,a 上面的锁被 bow 方法拿到,b被调用时b上面的锁用 bow 方法拿到,b 的锁处于b的 bow 方法,a 的锁处于 a 的 bow 方法,a 要调 b 的 bowback,发现 bowback 是 synchronized 要获取锁,b 的锁被 bow 锁住,所以只能等,b 对象调 a的 bowback 时要求获取a上面的锁,a 上面的锁被 bow 获 取,所以 bowback 拿不到锁,要等着结束之后才能拿到锁,a 要想释放锁前提是完成调用,调用在等 b 上的锁,b释放锁必须要执行完 bow 操作,要等a上的锁,所以形成互等的局面,一直锁在这,没有办法解开。
四、Starvation and Livelock
Starvation
Starvation describes a situation where a thread is unable to gain regular access to shared resources and is unable to make progress. This happens when shared resources are made unavailable for long periods by " greedy" threads.
不是两个在死锁,有一个线程获取某一个内存变量进行访问,变量被其它资源占用,而且资源执行时间非常长,线程上面比如 join 传值一秒,线程在上面长期等待,因为资源被另外贪婪的线程或者若干线程占有,始终得不到线程,线程因为设置超时或者 join 动作给设时间不够等等原因,最后拿不到资源,完不成任务,没有任何资源,导致饿死。
Livelock
A thread often acts in response to the action of another thread. If the other thread's action is also a response to the action of another thread, then livelock may result. As with deadlock, livelocked threads are unable to make further progress. However, the threads are not blocked they are simply too busy responding to each other to resume work.
活锁跟死锁相对应,双方没有死,但是前面是互相等对方释放锁,但是对方都不释放锁,锁死,活锁是大家都能动,动完之后还是死锁状态,还是锁住的状态,比如有一个过道,走两人,但是怎么走都会碰到,活锁问题没有死,但是也没办法往前懂,看起来忙都在响应对方,但是走来走去,都动不了,线程出现竞争产生三个问题,死锁,饿死,活锁。
五、Guarded Blocks
Threads often have to coordinate their actions.
The most common coordination idiom is the guarded block.
Such a block begins by polling a condition that must be true before the block can proceed.
Suppose,for example guardedJoy is a method that must not proceed until a shared
variable joy has been set by another thread.
public void guardedJoy() {
// Simple loop guard. Wastes
// processor time. Don't do this!
while(!joy){}
System.out.println("Joy has been achieved!");
}
协调之间的关系,使用防卫的 block,用代码进行控制,guardedJoy 方法,等条件为 false,条件为 true时才等到,只要没达到,就一直在做,但是这种方式虽然能满足条件,只有条件达到才能相应的事情,在上面可以加超时,超市一旦到达,把持有的锁释放掉,可以让对方可以拿到锁,把锁解开,但是 cpu 什么也没做,空转,所以方式并不好,适当改进。
A more efficient guard invokes Object.wait to suspend the
current thread.
The invocation of wait does not return until another thread has issued a notification that some special event may have occurred - though not necessarily the event this thread is waiting for:
public synchronized void guardedJoy() {
//This guard only loops once for each special event, which may not
// be the event we' re waiting for.
while(!joy) {
try { wait(); }
catch (InterruptedExceptione) {}
}
System.out.println("Joy and efficiency have been achieved!");
}
用 wait 方法,在条件不满足的时候,wait 表示要释放当前持有资源,释放锁,线程挂起,等待有线程唤醒,cpu不空转,为了挂起什么都不做,挂起不占用 cpu 时间,当有人唤醒的时候,再继续往下执行,wait 会释放掉持有的资源,在其它某个代码里面,一旦 joy 变成 true,notifyall,通知所有出于规定等待锁的所有线程,只要不是就等,假如被唤醒是因为其它通知被唤醒,但是唤醒之后还是要在里面看,因为是循环,如果不是 true 继续等,一旦到把joy设置成ture 唤醒,notify 方法是组合使用实现,guardedjoy 只要在一个对象上就调用,避免 cpu 被空转运行。
Let's use guarded blocks to create a Producer-Consumer
application.
This kind of application shares data between two threads:
the producer, that creates the data, and the consumer, that does something with it.
The two threads communicate using a shared object.
Coordination is essential:
the consumer thread must not attempt to retrieve the data before the producer thread has delivered it,
and the producer thread must not attempt to deliver new data if the
consumer hasn't retrieved the old data.
生产者和消费者之间模拟程序,两个线程在共享对象,比如改写状态,改写之后消费者才能够消费,临界资源,只有一个状态,生产者改写状态,消费者消费,在生产者没有改写状态时消费者消费不到,如果生产者改写状态,消费者没有消费,生产者不允许再次生产,直接抹掉,消费者不能在生产者发送新状态之前获取里面数据,而生产者也是一样,新数据如果没有被 consumer,拿走之前 consumer 不能又把更新数据覆盖。
drop 工作,给消费者发送消息,里面有字符串类型的变量,是消息,里面有标示它的状态是不是当前 drop里面message 是空,表示有没有被消费过,设置成 true,看看里面是不是为空,如果为空等,如果不为空把它设置为空,消息拿走,一旦设置成空,notifyall,等着变成空的线程可以得到通知,可以检查是不是空。
检查 empty 是不是为空,如果不为空等,如果一直为空,把消息放进去,同时把状态改写,现在不为空,再发通知出去让所有等待的线程知道状态发生变化检查是不是达到想要状态,这是自身的写法 drop 的写法。不能有人在放的同时另外的线程再取。
传递进来 drop,开始动作,随机的睡眠时间零到五秒之间,输入一条信息,先放进一条消息,睡眠时间零到五秒,再放。
通过 consumer 传递 drop,从里面取消息,随机睡眠多长时间,只要取出的消息不断的 done,等到取到done结束,第一次 producer 在 drop,put,一进去,检查 empty,true,条件不满足,一直往下,发消息获取,通知所有线程,empty 不是空,有一条消息在里面,假设在这个过程中没有人收到消息,producer 放 第二条消息,第二条消息进来之后会发现 empty,false,循环满足条件,等是被挂起,等别人唤醒,所以 true 等于挂在这,consumer进去取,empty 不为空,notifyall,producer 挂起,被恢复出来,继续循环,已经为空,所以不满足条件再往下,制成不为空,被取走以后为空,不为空把新消息塞进去,如果 take 一条指令之后没有新消息进来,又想 take 第二条,没有被改写,所以两边可以保证一发一收,不会出现连续两次都能发消息或者连续两次都能取消息的情况,假设在改写完notify之后通知,线程被恢复,恢复以后,可以继续支持用方法组合实现 pv 操作。
public class ProducerConsumerExample {
public static void main(String[] args) {
Drop drop = néw Drop{};
(new Thread(new Producer(drop))).start(); :
(new Thread(new Consumer(drop))).start();
}}
真正例子是创建 drop,把 drop 分别传递给producer 和 consumer 线程启动会看到执行的效果。
六、Immutable Objects
An object is considered immutable if its state cannot change
after it is constructed.
Maximum reliance on immutable objects is widely accepted as a sound strategy for creating simple, reliable code.
Immutable objects are particularly useful in concurrent
applications.
Since they cannot change state, they cannot be corrupted by thread interference or observed in an inconsistent state.
在代码里面有,producer,consumer,代码一样,producer,consumer的例子在启动线程,在收发线程的时候会非常整齐,发和收,收发时间比较长,一次要等好几秒,输到 done 就终止了,没有再输出,一发一收,没有错乱开。
An object is considered immutable if its state cannot change
after it is constructed.
Maximum reliance on immutable objects is widely accepted as a sound strategy for creating simple, reliable code.
Immutable objects are particularly useful in concurrent
applications.
Since they cannot change state, they cannot be corrupted by thread interference or observed in an inconsistent state.
所有问题都是因为在改写对象值导致多线程共享对象状态时候改写搞乱变成一致性出问题,如果所有对象都不允许变化,就没有问题,真正设计出类是不能改写的,要创建新的对象,对象里面属性是希望改写成数据对象,一旦创建不能变,只能靠通过创建新方式达到想要的,带有想要的特征,状态,对象值出来,但是对象本身不能改,不可变对象来创建。
Programmers are often reluctant to employ immutable objects, because they worry about the cost of creating a new object as opposed to updating an object in place.
The impact of object creation is often overestimated, and can be offset by some of the efficiencies associated with immutable objects.
These include decreased overhead due to garbage collection, and the elimination of code needed to protect mutable objects from corruption.
创建对象要花掉很大代价,java 内存是分代的,第一代上面互相之间对象有引用,垃圾回收机制在考察它们之间的引用关系,如果发现没有外部引用的时候,可以直接移除掉,如果有引用关系,内存满之后之后搬到老一代里面,所以搬进去的动作上,内存满并且有新创建的请求过来,可能会执行动作,创建对象代价比较大,有人比较反感这种方式,但实际上创建对象的影响在现代机器上面不会那么大,首先对象都比较小,其次现在占用内存会比较多,真正碰上垃圾回收情况会比较少,所以可以忽略,保证绝对安全的对象。
七、A Synchronized Class Example
定义对象 SynchronizedRGB,有红绿蓝三个通道,每个通道值都在零到二五五之间,初始化的时候传递三个值以及颜色名字,比如品红或者天蓝等等。
set 方法先检查值是否在合理范围内,Synchronized,设颜色分别是什么,get 也是 Synchronized,获取名字把颜色反转,全部都是用 Synchronized 关键词处理。
SynchronizedRGB must be used carefully to avoid being seen in an inconsistent state.
Suppose, for example, a thread executes the following code:
SynchronizedRGB color = new SynchronizedRGB(0, 0, 0, "Pitch Black");
int myColorInt = color.getRGB0; //Statement 1
String myColorName = color.getName0); //Statement 2
If another thread invokes color.set after Statement 1 but before Statement 2, the value of myColorInt won't match the value of myColorName. To avoid this
outcome, the two statements must be bound together:
synchronized (color) {
int myColorInt = color.getRGB();
String myColorName = color.getName();
}
This kind of inconsistency is only possible for mutable objects it will not be an issue for the immutable version of SynchronizedRGB.
demo 创建对象,调 demo 方法,开线程,先获取颜色,睡一秒之后打印内容,线程睡一秒直接往下进行,匿名的线程类,定义一下,启动的时候先输出颜色是什么,睡一秒的同时定义线程2,把颜色内容改写,线程2启动,线程1和线程2都启动,线程1输出值,睡一秒,线程2把颜色给改写成 scarlet,纯红,绿和蓝都是零,动作是先执行,所以取出来开始默认值,睡一秒之后打印颜色的名字,因为t2已经设置过,可以看到结果,颜色是零,但是颜色名字是雪红色,问题出在颜色虽然是scarlet,但是 get 方法执行完锁就被释放了,线程睡,过程中有另外的线程进来调set,确实没有违反 Synchronized 关键字要求获取锁的动作,但是因为线程睡了一秒,另外再次获取 getname 的时候,中间已经被别人给改写,即使全部用 Synchronized 关键字做控制,仍然有问题,仍然潜在会爆出问题,所以最好方法是让颜色是不可变版本。






