开发者学堂课程【高校精品课-上海交通大学-企业级应用体系架构:Threading 1 】学习笔记,与课程紧密联系,让用户快速学习知识。
课程地址:https://developer.aliyun.com/learning/course/75/detail/15834
Threading 1
内容介绍:
一、Processes and Threads
二、Thread Objects
三、Defining and Starting a Thread
四、Synchronization
一、Processes and Threads
事务隔离级别是四个,数据库有很大的缓存,左边有a用户,右边有b用户进行访问,操作的时候是在缓存里,当能提交的时候才会写到硬盘上,无论是a还是B,在对个数据进行操作的时候,数据都在服务器的缓存里面,如果A能看到B正在处理的数据,并且数据是没提交的,会碰到脏读问题,如果在里面写数据,比如转账,从M账户里面减一加到N的账户上,转账的操作,如果a在做B也在做,M现在的值是0,按道理a做完一次,b做完一次,里面应该变成2,因为它俩同时在做,读走的M都等于0,做加1操作的时候,B会变成1,如果A能看到B现在正在做的把0变成一,没有做隔离,直接能看到1,做加一操作的变成2,但是B操作放弃掉,回滚,在账户上做一次操作,但是最终写回的值变成二,为了解决问题,加一次隔离,什么都不读,什么都不加,可以看到没有提交的东西,加控制,只能读到提交之后的数据,A没有办法读到别人正在缓存处理的数据,于是只能读到数据库里的数据,读走的只能是0,再写成1的时候往回写,B在内存里面把改成1,a是读不到的,所以看到仍然是0,如果B被放弃,仍然是基于a,仍然是基于0进行操作,只会写进1动作,不会有问题,第一个隔离级别是缓存里不允许读,只能读在硬盘上,提高数据,但是现在防止的是B在缓存里操作 about,操作本身不会反映到数据库上,只会去读已经在数据库里写进去的情况,如果B不设报警,确实提交,会变成a 读走 m 变成0,b读走M也是也是0,加一之后先写回,在数据库里m+1,A再把数据写回数据库中,比如做表,比较防范性的编程,写回去的时候读数据库里的数据是不是跟读走的数据一样,读完发现M等于一,而不是等于0,基于一做加一变成二的动作,再到数据库读一读,看是不是一,是一才能写,否则有问题,在过程当中可能又有C用户读过,问题在当读走数据时没有做任何的处理,数据在数据库里没有做任何的处理,没有加锁,所以导致不可重复读的问题,所以数据读走以后在上面加锁,如果a读走加个锁,B和C能读,但是不能写,可以保证数据在写回之前是没有人可以进行改写,可以重复读,只要在事务执行期间不断的读数据,读到的都是同一个值,不会发现值被改写过,真正的加锁,别人只能读,力度也可以更粗一点,更强一点,不能读,事务提交也没关系,不会产生影响,但是还有另外一种情况,a 进行 queny,比如给员工涨工资,所有人的工资只要小于1000块钱,给每个人涨两百块钱,读到数据库里面的五条记录,对着五条记录锁死,别人不可能修改五条,加钱的动作没问题,但是 C 用户往里面插入新员工的工资也小于1000,这时会发现插入的条件满足 queny 条件,非常的麻烦,确定一批人正在做操作时又有新的数据加入,而且有可能新的数据满足 queny,不但要把读走的所有小于1000的人的数据锁死,还要把整个表锁死,只要处理员工的薪资,整个表没有任何人能往里写,可以防止有人会插入一条满足查询条件的记录,整个是 server library 完全串进化,等于a做完,把表释放给 B 或者给 C,尽管 B 和 C 不会操作 A 读走的数据,但是操作的数据在同一张表里面,也不让 B 和 C 做,把整个表锁死,第一种隔离级别是什么都没做,在缓存里直接可以读到别人操作数据,第二种隔离级别把给锁死,不能读,隔离开,只能读真正写入数据库里的,第三种隔离级别在读走数据的时候,把读走数据锁死,第四个级别在读数据时候,把整个表锁死,别人也不能操作,每提升一次级别,解决一个问题,第一个级别是所有问题不存在,锁在多线程编程里面非常常用。
1、In concurrent programming, there are two basic units of
execution: processes and threads.
In the Java programming language, concurrent programming is mostly concerned with threads.
2、Processes
A process has a self-contained execution environment. A process generally has a complete, private set of basic run-time resources; in particular, each process has its own memory space.
3、Threads
Threads are sometimes called lightweight processes. Both processes and threads provide an execution environment, but creating a new thread
requires fewer resources than creating a new process.
Threads exist within a process - every process has at least one. Threads share the process's resources, including memory and open files. This makes for efficient, but potentially problematic, communication.
进程有自己独立的内存空间,而线程在共享整个进程,因为所有的线程在共享进程里的所有的资源包括内存,包括打开的文件等等,资源共享,是它们的差异。在 java 中如何实现多线程?
二、Thread Objects
Each thread is associated with an instance of the class Thread.
There are two basic strategies for using Thread objects to create a concurrent application.
To directly control thread creation and management, simply
instantiate Thread each time the application needs to initiate an asynchronous task.
To abstract thread management from the rest of your application, pass the application's tasks to an executor.
在 Java 中做多线程编程,每个线程都是做thread对象进行抽象,thread 对象进行抽象时有两种方式,每一次应用想要创建线程实例化,thread 对象出来,实例化之后线程在做异步的任务,创建 thread 对象,run 法自己跑,主线程还继续往下执行,进行线程等待用户输入,程序没有卡死,鼠标都不能动,还可以做其它操作,在等待用户输入的时候还可以做其它事情,所以每一个线程都是异步执行的任务。
三、Defining and Starting a Thread
An application that creates an instance of Thread must provide the code that will run in that thread.
There are two ways to do this:
Provide a Runnable object.
The Runnable interface defines a single method, run, meant to contain the code executed in the thread. The Runnable object is passed to the Thread constructor, as in the HelloRunnable example:
public class HelloRunnable implements Runnable {
public void run() {
System. out . println("Hello from a thread!");
}
public static void
main(String args[]) {
(new Thread( new HelloRunnable())) .start();
}
}
定义线程在 java 中,做实例化的时候有两种方式,一种方式是通过实现接口的方式,runnabe 接口里面有run方法,定义线程在执行时要干什么,可以定义成定时器任务,也可以定义成死循环或者语句,实现接口后,有一个run方法,可以被线程化的对象,所有的对象都是 thread 对象,创建的时候给构造器传递进去,实现 runnable 的实例,启动新的线程,在创建线程之后要调 start 启动,启动的时候调用 runnable 里面的 run 方法,这种方法是是以实现接口方式做。
An application that creates an instance of Thread must provide
the code that will run in that thread.
There are two ways to do this:
Subclass Thread.
The Thread class itself implements Runnable, though its run method
does nothing. An application can subclass Thread, providing its own implementation of run, as in the HelloThread example:
public class HelloThread extends Thread {
public void run() {
System . out. println("Hello from a thread!" ) ;
}
public static void main(String args[]) {
(new HelloThread()) . start();
}
}
直接扩展 thread 类,run 方法,创建方式基本上类似,但是因为已经是扩展的 thread,所以直接创建 hellothread即可,不需要创建 runnable 对象作为参数去调构造器器做初始化动作,run 方法里面只是一条语句,跑完之后语句就结束,都是类似的,语句虽然很简单,从输出到输出一条信息,但实际上通过线程跑,当前程序里有两个线程,是一个主程序在执行,一个是创建 hellothread 实例的线程,主线程开启新的线程,线程跑结果,跑完之后主线程结束。在java 中有约定,单根集成,在扩展语句,只能扩展一个类,c++可以扩展bc,扩展两个类,但是在 java中只能扩展一个类,所以 thread还需要实现其它接口,比如实现 listener 接口,用 thread 方式不合适,因为只能是扩展一个类,可以让它实现 runnable 或者实现 extends listener,如果 listener 不是类是接口,直接在后面加接口即可,因为实现java 的类可以实现多个接口,所以用实线或者扩展都可以,只会有一点小差异。
Thread.sleep causes the current thread to suspend execution for a specified
period.
public class SleepMessages {
public static void main(String args[] ) throws
InterruptedException
{
String importantInfo[] = {
"Mares
eat oats", "Does
eat oats" ,
"Little lambs eat ivy", "A kid will eat ivy too"
};
for (int i = 0; i < importantInfo. length; i++) {
/ /Pause for 4 seconds
Thread. sleep(4000);
//Print a message
System. out . println( importantInfo[i]);
}
}
}
主线程在跑,直接调用 thread 类的静态方法,如果不加特别声明,直接在 thread 类调用静态方法,会作用于当前的线程,执行 main 的线程,每四秒输出一条消息,把数组里的文件消息输出例子,thread 调用当前线程表,执行main的线程,每四秒输出一条语句,4000是不是四秒,是不是精确的四秒不一定,如果有其它的语言经验,比如c++,时间不能精确的保证,有时候差异会比较大,隔一段时间,如果要精确定时,不能依靠依靠 sleep,靠其它更精确的东西,比如 timer 类,可以做到精确,时间给改成一秒,运行快,所以在执行的时候会发现每一秒钟输出一个东西,输出很慢,一条一条输出,在 sleep 的时候会碰成各种各样问题,当前的线程被挂起,可能会休眠,
在四秒之后恢复不出来,两个县城在进行竞争,有线程可能饿死,抛异常,如果 slepp 调用,在 catch里面,捕获异常做处理,如果不做捕获,会看到会异常往外跑,继续跑,所以 main 函数有可能会抛出异常,输出把所有的四条语句输出两遍,一遍没有进行 catch,一遍进行 catch,如果进行 catch,main 函数不会抛出异常,如果没做自然往外抛,抛给当前 main 函数,main 函数抛出异常。
An interrupt is an indication to a thread that it should stop what it is doing and do something else.
It's up to the programmer to decide exactly how a thread responds to an interrupt, but it is very common for the thread to terminate.
A thread sends an interrupt by invoking interrupt on the Thread object for the thread to be interrupted.
for (int i = 0; i < importantInfo. length; i++) {
// Pause for 4 seconds
try {
Ihread. sleep(4000);
}catch (InterruptedException e) {
// We've been interrupted: no more messages.
return;
//Print a message
System. out . println( importantInfo[i]);
}
没有人告诉睡眠四秒,程序一定能正常执行,四秒之后一定能够恢复回来,恢复不出来时,线程会抛异常,捕获掉,一旦休眠,歇在这,其它阶段还会做执行,在四秒之后由外部的时间触发把它唤醒,不一定能唤醒,所以良好的习惯是 interrupted 捕获,中断异常,休眠暂时被中断掉,如果线程在执行时,需要外界唤醒,唤醒之后进行操作。
What if a thread goes a long time without invoking a method
that throws InterruptedException?
Then it must periodically invoke Thread.interrupted, which
returns true if an interrupt has been received. For example:
for (int i = 0; i < inputs .length; i++) {
heavyCrunch(inputs [i]);
if (Thread . interrupted()) {
// We've been interrupted: no more crunching .
return;
}
}
In more complex applications, it might make more sense to throw
an InterruptedException:
if (Thread . interrupted()) {
throw new InterruptedException( ) ;
}
主动发现存在问题,线程有属性 interrupted,东西会标识当前的线程是不是处于一种中断状态,在循环里会进行操作,操作是虚拟的,没有写代码,假设进行非常重的计算任务,耗时会非常长,线程执行完之后,再判断当前的线程会不会被中断,是不是已经处于中断状态,在多线程线系统里面,线程是 cpu 赋的时间,再执行非常长的动作,在执行动作的中途可能被剥夺 cpu 的执行时间,于是被中断掉,方法执行不下去回过头,执行到底下,判断当前的线程是不是处于中断状态,如果是做事情,如果不是不做,主动发现线程是不是已经没有在正常中断。
Joins
The join method allows one thread to wait for the completion
of another.
If t is a Thread object whose thread is currently executing,
t.join();
causes the current thread to pause execution until t's thread terminates.
Overloads of join allow the programmer to specify a waiting period.
However, as with sleep, join is dependent on the OS for timing, so you should not assume that join will wait exactly as long as you specify.
Like sleep, join responds to an interrupt by exiting with an InterruptedException.
被外部中断,人为的也可以主动的要求中断,join 方法让其中一个线程等到另外一个线程执行完成,t是thread对象,t上面调 join,把当前正在执行的代码,线程终止掉,一直到t完成位置,再恢复过来,上面有节代码,下面有节代码,执行代码在某一个线程上执行,一旦在线程里调用t.join 把当前的线程挂起,t执行完之后才会再恢复出来,继续执行下面的,在过程中要注意和不是线程里面跑,把t的 join 插在这里面跑,看起来一样,如果正常执行下来功能一样,因为已经执行完前面的,整个线程挂起,执行T,终止完之后回来执行,但是现在是两个线程在跑,如果在执行,很有可能T线程在执行时又被别人中断,或者因为某种原因T线程崩掉,但不管如何,线程还是处于挂起状态,只要能被唤醒,可以继续执行,所以只是 T,所以为什么用 join,看起来t任务加入到当前的线程里面一样,但是又不影响线程本身会不会被放弃掉或者会不会崩溃,相对独立的两个,像 sleep,join 也被挂起中断,等到满足某个条件,比如时间到或者t执行完恢复,但是恢复可能会存在问题,所以只要掉 join 或者 sleep 都要让函数抛出中断方法,中断异常,抛出进行捕获,进行处理,join 什么都没做,参数什么都没写,承载的版本可以指定时间,在给定时间内直接回,让线程恢复掉,只给t一秒钟时间执行,时间本身是依赖于操作系统,不一定精确,有可能会有一点差异。
SimpleThreads consists of 'two threads.
The first is the main thread that every Java application has.
The main thread creates a new thread from the Runnable object, MessageLoop, and waits for it to finish.
If the MessageLoop thread takes too long to finish, the main thread interrupts it.
The MessageLoop thread prints out a series of messages.
If interrupted before it has printed all its messages,
the MessageLoop thread prints a message and exits.
执行 main 函数是一个线程,主线程创建一个新线程,main 是主线程,在执行 main 的时候,如果在执行时传参数,可以等待多长时间,再传参数,参数表示可以等待多长时间,时间是秒数,成立1000将传递给 join 或者 sleep。创建线程,MessageLoop 执行,MessageLoop 是用实现 runnable 的方法实现另一个线程的类,run 就是分装的逻辑,四个消息每隔四秒钟打印出一个消息,现在打印只用 threadMessage,助手函数,助手函数在前面编译,给一个字符串,输出当前的线程,currentthread 的静态方法,返回当前正在执行的线程,如果在某一个 Message 入口里面,输出 Message 入口的实例的线程,输入线程的名字,用 system.out 输出线程名字和 message,知道是哪个线程产生输出,启动 Runnable loop 开始,进入循环等待,不断的判断,当前的进程是不是活的,等待,主线程让掉,挂起等t,确定时间一秒,一秒之后即使不执行完,也要继续往下执行,把时间抢回来,当前时间减去线程已经起始的时间,获取系统时间,如果让系统执行的时间已经超出了容忍范围,并且t还是活着的,把t打断,整个线程没有办法往下执行,所以整个线程要控制,主线程结束,t到后面自己再做。定的时间比较短才能看到效果,
即使 join 也会抛异常,只要 interrupted 人为设置,把线程终止掉,又把自己挂起,输出,继续执行,抛异常。如果把时间设长,默认值是一秒,等四秒,休眠两秒,可以在输出两条信息之后才会终止,message 入口什么都没做,直接挂掉,输出一条信息,又输出第二条信息,四秒钟到,不想等,还没做完,结束。例子是有主线程在跑,
主线程创建新的线程 messageloop,如果花很长时间做完,比如每两秒输出一条要,输出八秒,时间非常长,主线程打断,因为主线程最多只等待四秒,四秒之后把 message 入口中断掉,相当于把 messageloop t,interrupted属性设置为 true,中断之后 messageloop 继续做,一旦被中断只能到 cach 中,打印消息,还没完整个线程就结束了,main 就结束了,输出 simple 的代码,在t结束之后,join 之后还没做完,输出一个 finally 的结束。例子中可以看到两个线程互相之间的交互,一个是如何 join 一个是如何中断。
The SimpleThreads Example
messageloop 正常情况下会执行循环,四条语句全部执行完,一旦把 interrupted 设置好,就会抛出异常,即使再让线程恢复也只能在 catch 捕获异常,输出信息。
开启一个线程,让线程跑,自身执行线程,不断着盯着t,有没有活着,只要活着就给一秒钟做,每一秒钟判断一次,给的时间已经大于能容忍的时间,每隔一秒做一次,能容忍四秒,所以前四次什么都没做,再判断是不是活着,一旦时间超过并且线程还活着,还没死,还没执行完,中断,再继续执行,把自己收尾的工作给收掉,比较暴力把线程终止,会带来很多的弊端,比如线程循环的做,现在中断没什么问题,但是如果在线程里面获取数据库的连接,还有其它的 Timer 对象,如果比较暴力,直接中断不管,操作并不好,应该在 catch 里面想办法,把连接释放,否则还占用连接,timer 清零,clear,比较好的终止 message 方法,而不是仅仅中断,因为中断只是被挂,还在占用系统资源,在等待时机有人去唤醒,代码虽然很简单,实现主线程和定义的线程之间的交互并且安全的关闭 Message loop的线程,比较合适比较优雅的方式。
四、Synchronization
Threads communicate primarily by sharing access to fields and the objects reference fields refer to.
This form of communication is extremely efficient, but makes two kinds of errors possible: thread interference and memory consistency errors.
The tool needed to prevent these errors is synchronization.
However, synchronization can introduce thread contention, which occurs when two or more threads try to access the same resource
simultaneously and cause the Java runtime to execute one or more threads more slowly, or even suspend their execution. Starvation and livelock are forms of thread contention.
跟a和b转账的例子类似,如果在一个 java 的程序里面,比如有一个变量a加一,b减一,再共同访问内存里一个变量的时候,跟转账操作类似,线程之间也会产生类似的干扰,导致内存的不一致性错误,比如值现在是0,做加一操作应该变成一,做减一操作应该变成负一,比如a先执行完变成一,在做简易操作的时候恢复到里面,如果同时在读,改写为一,输出都是零,b把零改写为负一,谁后提交就把前面覆盖,跟事务的操作一样,线程跟资源产生竞争,这就是临近资源竞争,都想写它,都想有多个线程,都想访问相同的资源,并且同时访问,同时访问就会产生很多的问题,这就是解决它的思路,考虑的问题在哪里,某一个线程饿死,死锁,活锁三种情况。
Thread Interference
Consider a simple class called Counter
class Counter {
private int c= 0; 只有一个变量,c标识基础值
public void increment() { c++; }方法,加一
public void decrement() {c--; }减一
public int value(){ return c; }返回值
}
if a Counter object is referenced from multiple threads, interference between threads may prevent this from happening as expected.
Counter 对象可能会被多个线程进行处理。
Thread Interference
Suppose Thread A invokes increment at about the same time
Thread B invokes decrement.
If the initial value of c is 0, their interleaved actions might
follow this sequence:
- Thread A: Retrieve c.
- Thread B: Retrieve c.
- Thread A: Increment retrieved value; result is 1. .
- Thread B: Decrement retrieved value; resultis -1.
- Thread A: Store resultin c; c is now 1.
- Thread B: Store resultin c; c is now-1.
Thread A's result is lost, overwritten by Thread B.
一个要做加一操作,一个要做减一操作,c的值是零,同时取出c的值,取出来都是零,a操作变成一,b操作变成负一,A写回c是一,B写回是c负一,负一把一覆盖,看不到曾经做过一次加一操作,希望最后c是零,因为做一次加一操作,做一次减一,两个线程如果同时访问,B会把a的值覆盖。
Memory consistency errors
Memory consistency errors occur when different threads have
inconsistent views of what should be the same data.
The key to avoiding memory consistency errors is
understanding the happens-before relationship.
This relationship is simply a guarantee that memory writes by one specific statement are visible to another specific statement.
问题是内存的不一致性,无论是哪种语言在并发编程里面考虑的不是只是 java,在任何编程语言里面都强调,当有多个线程在进行临近资源的访问,线程是在共享进城里面所有资源,变量是进程的资源,所以会被所有的线程访问到,所以竞争是不可避免的,但是操作必须要按照 happens-before 的顺序进行执行,A的操作和B的操作满足happens-before 的关系,确保对内存进行写操作的时候,必须是某一条具体的语句,写内存的语句,对于另一条语句是可视的,A在加一,B在减一,A加一的动作必须要让B减一的动作看见,反过来也是一样,想满足条件必须a先发生,B再发生,B要跟着a发生之后已经变成基础之上才操作,才能看到是负一,计算机可以并行,微观上还是串行,宏观上是并行,说的同时是不可能两个同时做,在微观上一定是a先B后或者是B先a后,有差异,但是在多核系统里面不太保险,确实有可能两条指令在两个河里加载同时执行。必须保证俩个在时序上有先后,必须是执行完才能执行,能看到前面执行的效果,确保 happens-before 的关系,关系一旦确保不会存在问题,A一旦执行完,B必须能看到,a执行完的结果,一直执行,不可能是 B 和 a 真的是完全同步执行。
Memory Consistency Errors
Suppose a simple int field is defined and initialized:
int counter= 0;
The counter field is shared between two threads, A and B. Suppose thread A increments counter:
counter++;
Then, shortly afterwards, thread B prints out counter:
System.out.println(counter);
If the two statements had been executed in the same thread, it would be safe to assume that the value printed out would be "1".
But if the two statements are executed in separate threads, the value printed out might well be "O", because there's no guarantee that thread
A's change to counter will be visible to thread B - unless the
programmer has established a happens-before relationship between these two statements.
B 线程要在 a 线程之后再执行,必须要建立 A 和 B 两条语句,加一,减一之间的 happens before 的关系,加一和减一的动作不能直接同时执行。
在 java 中提供关键字,不安全的多线程,创建 counter 对象,定义 counterloop 的 runnable 线程类,每隔一秒钟对 C 做加一的操作,获取 C 的值,对 C 的值做加一操作。
在主线程里面,创建两个新的线程对象,两个线程都是 counterloop,
启动,两个同时做操作,都在做加一操作,输出里面的值,做加一操作,没有做任何同步的控制,0和0,突然间变成二二四四,两个线程做加的操作,中间看不到一三的状态,因为读走都是0,在往回写的时候先顺序,并发的执行,看不到一的状态,看不到三的状态,有问题,执行两遍。
Synchronized Methods
To make a method synchronized, simply add the synchronized keyword to
its declaration:
public class SynchronizedCounter {
private intc= 0;
public synchronized void increment() { c++; }
public synchrorized void decrement(){C--; }
public synchronized int value(){ return c; }
If count is an instance of SynchronizedCounter, then making these methods
synchronized has two effects:
First, it is not possible for two invocations of synchronized methods on the same object to interleave. When one thread is executing a synchronized method for an object, all other threads that invoke synchronized methods for the same object block (suspend execution) until the first thread is done with the object.
Second, when a synchronized method exits, it automatically establishes a happens- before relationship with any subsequent invocation of a synchronized method for the same object. This guarantees that changes to the state of the object are visible to all threads.
Synchronized 关键字,在一个类里面,Synchronized 关键字在任何一个时刻都不可能有两个对方法调用同时执行,如果 a 在调 Synchronized 修饰过的 increment,b 在调 Synchronized 修饰过的 decrement,俩个方法一定不可能在时间上有交叠,有重复的部分,一个执行完,执行另外一个,不管先执行 a 还是先执行 b,只能先执行一个,再执行另外一个,这是 Synchronized关 键词的作用。当有人想要获取基础值的时候,不能有人对它做加一,减一的操作,方法返回之后才能做加一减一的操作,不可能在读取基础值的同时执行,人为建立,三个方法不可能同时执行,只要有方法执行,另外两个方法不可能被执行,一定要等方法结束,另外两个方法才有可能得到执行。