1 线程概念
1.1 进程
- 在现代的操作系统中,进程是资源分配的最小单位,而线程是CPU调度的基本单位。
- 一个进程中最少有一个线程,名叫主线程。进程是程序执行的一个实例,比如说10个用户同时执行Chrome浏览器,那么就有10个独立的进程(尽管他们共享一个可执行代码)。
1.1.1 进程的特点:
- 每一个进程都有自己的独立的一块内存空间、一组资源系统。其内部数据和状态都是完全独立的。
- 进程的优点是提高CPU的运行效率,在同一个时间内执行多个程序,即并发执行。但是从严格上将,也不是绝对的同一时刻执行多个程序,只不过CPU在执行时通过时间片等调度算法不同进程告诉切换。
所以总结来说:进程由操作系统调度,简单而且稳定,进程之间的隔离性好,一个进程的奔溃不会影响其他进程。单进程编程简单,在多个情况下可以把进程和CPU进行绑定,从分利用CPU。
1.1.2 进程的缺点:
- 一般来说进程消耗的内存比较大,进程切换代价很高,进程切换也像线程一样需要保持上一个进程的上下文环境。
- 比如在Web编程中,如果一个进程处理一个请求的话,如果提高并发量就要提高进程数,而进程数量受内存和切换代价限制。
1.2 线程
- 线程是进程的一个实体,是CPU调度和分配的基本单位,它比进程更下能独立运行的基本单位,线程自己基本上不拥有系统资源,只拥有一点在运行中不可少的资源(如程序计数器、栈),但是它可与同属一个进程的其他线程共享进程所拥有的全部资源。
- 同类的多线程共享一块内存空间个一组系统资源,线程本身的数据通常只有CPU的寄存器数据,以及一个供程序执行的堆栈。
- 线程在切换时负荷小,因此,线程也称为轻负荷进程。一个进程中可以包含多个线程。
- 在JVM中,本地方法栈、虚拟机栈和程序计数器是线程隔离的,而堆区和方法区是线程共享的。
1.3 进程线程的区别
- 地址空间:线程是进程内的一个执行单元;进程至少有一个线程;一个进程内的多线程它们共享进程的地址空间;而进程自己独立的地址空间
- 资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源。
- 线程是处理器调度的基本单位,但进程不是
- 二者均可并发执行
- 并发:多个事件在同一个时间段内一起执行
- 并行:多个事件在同一时刻同时执行
1.4 多线程
虽然多线程进一步提高了应用的执行效率,但是由于线程之间会共享内存资源,这会导致一些资源同步的问题,另外,线程之间切换也会对资源有所消耗。
如果一台电脑只有一个CPU核心,那么多线也并没有真正的"同时"运行,它们之间需要通过相互切换来共享CPU核心,所以,只有一个CPU核心的情况下,多线程不会提高应用效率。
-
但是,现在计算机一般都会有多个CPU,并且每个CPU可能还有会多个核心,所以现在硬件资源条件下,多线程编程可以极大的提高应用的效率。
在Java程序中,JVM负责线程的调度。线程调度是按照特定的机制为多个线程分配CPU的使用权。
调度的模式有两种:分时调度和抢占式调度。分时调度是所有线程轮流获得CPU使用权,并平均分配每个线程占用CPU的时间;抢占式调度是根据线程的优先级别来获取CPU的使用权。JVM的线程调度模式采用了抢占式模式。
2 Thread类
public class Thread implements Runnable {
.....
}
通过上面代码,我们可以知道Thread实现了Runnable,侧面也说明线程是"可执行的代码"。
public interface Runnable {
public abstract void run();
}
Runnable是一个接口类,唯一提供的方法就是run()。
2.1 Thread的使用
- java的JDK自带了对多线程技术的支持
- 实现多线程变成主要有两种
一:继承Thread类--重写run方法。在run方法中写线程要执行的任务
二:实现Runnable接口
- Thread类实现了Runanble接口,他们之间具有多态关系。
- 使用继承Thread类来创建新线程,最大的局限就是不支持多继承。所以为了支持多继承,完全可以实现Runnable接口的方式。使用这两种方式创建线程在工作时的性质是一样的,没有本质区别。
- 如果多次调用start()方法,则会出现异常Exception in thread"mian"java.lang.IllegalThreadStateException
异步和同步
- Thread.java类中start()方法通知“线程规划器”此线程已经准备就绪,等待调用线程对象的run()方法。这个过程就是让系统安排一个时间来调用Thread中的run()方法,也就是使线程得到运行,启动线程,具有异步执行的效果。执行start()方法的顺序不代表线程启动的顺序
- 如果调用代码thread.run()就不是异步执行了,而是同步,那么此线程对象并不交给“线程规划器”来进行处理,而是由main主线程来调用run()方法,也就是必须等run方法中的代码执行完才可以执行后面的代码。
- Thread类实现了Runnable接口--构造方法不仅可以传入Runnable接口的对象还可以传入一个Thread类的对象,这样就可以将一个thread对象中的run()方法交由其他的线程进行调用。
2.2 Thread的常用方法:
2.3 Thread的常用字段:
volatile ThreadGroup group;
volatile boolean daemon;
volatile String name;
volatile int priority;
volatile long stackSize;
Runnable target;
private static int count = 0;
/**
* Holds the thread's ID. We simply count upwards, so
* each Thread has a unique ID.
*/
private long id;
/**
* Normal thread local values.
*/
ThreadLocal.Values localValues;
- group:每一个线程都属于一个group,当线程被创建是,就会加入一个特定的group,当前程运行结束,会从这个group移除。
- deamon:当前线程是不是守护线程,守护线程只会在没有非守护线程运行下才会运行
- name:线程名称
- priority:线程优先级,Thread的线程优先级取值范围为[1,10],默认优先级为5
- stackSize:线程栈大小,默认是0,即使用默认的线程栈大小(由dalvik中的全局变量gDvm.stackSize决定)
- target:一个Runnable对象,Thread的run()方法中会转调target的run()方法,这是线程真正处理事务的地方。
- id:线程id,通过递增的count得到该id,如果没有显式给线程设置名字,那么久会使用Thread+id当前线程的名字。注意这里不是真正的线程id,即在logcat中打印的tid并不是这个id,那tid是指dalvik线程id
- localValues:本地线程存储(TLS)数据 关于TLS后面会详细介绍
2.4 create()方法:
为什么要研究create()方法?因为Thread一种有9个构造函数,其中8个里面最终都是调用了create()方法
在Thread.java 402行
/**
* Initializes a new, existing Thread object with a runnable object,
* the given name and belonging to the ThreadGroup passed as parameter.
* This is the method that the several public constructors delegate their
* work to.
*
* @param group ThreadGroup to which the new Thread will belong
* @param runnable a java.lang.Runnable whose method <code>run</code> will
* be executed by the new Thread
* @param threadName Name for the Thread being created
* @param stackSize Platform dependent stack size
* @throws IllegalThreadStateException if <code>group.destroy()</code> has
* already been done
* @see java.lang.ThreadGroup
* @see java.lang.Runnable
*/
private void create(ThreadGroup group, Runnable runnable, String threadName, long stackSize) {
//步骤一
Thread currentThread = Thread.currentThread();
//步骤二
if (group == null) {
group = currentThread.getThreadGroup();
}
if (group.isDestroyed()) {
throw new IllegalThreadStateException("Group already destroyed");
}
this.group = group;
synchronized (Thread.class) {
id = ++Thread.count;
}
if (threadName == null) {
this.name = "Thread-" + id;
} else {
this.name = threadName;
}
this.target = runnable;
this.stackSize = stackSize;
this.priority = currentThread.getPriority();
this.contextClassLoader = currentThread.contextClassLoader;
// Transfer over InheritableThreadLocals.
if (currentThread.inheritableValues != null) {
inheritableValues = new ThreadLocal.Values(currentThread.inheritableValues);
}
// add ourselves to our ThreadGroup of choice
//步骤二
this.group.addThread(this);
}
我把create内部代码大体上分为3个部分
- 步骤一:通过静态函数currentThread获取创建线程所在的当前线程
- 步骤二:将创新线程所在的当前线程的一些属性赋值给即将创建的线程
- 步骤三:通过调用ThreadGroup的addThread方法将新线程添加到group中。
2.4 Thread的生命周期:
线程共有6种状态;在某一时刻只能是这6种状态之一。这些状态由Thread.State这个枚举类型表示,并且可以通过getState()方法获得当前具体的状态类型。
/**
* A representation of a thread's state. A given thread may only be in one
* state at a time.
*/
public enum State {
/**
* The thread has been created, but has never been started.
*/
NEW,
/**
* The thread may be run.
*/
RUNNABLE,
/**
* The thread is blocked and waiting for a lock.
*/
BLOCKED,
/**
* The thread is waiting.
*/
WAITING,
/**
* The thread is waiting for a specified amount of time.
*/
TIMED_WAITING,
/**
* The thread has been terminated.
*/
TERMINATED
}
- NEW : 起劲尚未启动的线程的状态。当使用new一个新线程时,如new Thread(runnable),但还没有执行start(),线程还有没有开始运行,这时线程的状态就是NEW。
- RUNNABLE:可运行线程的线程状态。此时的线程可能正在运行,也可能没有运行。
- BLOCKED:受阻塞并且正在等待监视锁的某一线程的线程状态。
进入阻塞状态的情况:
① 等待某个操作的返回,例如IO操作,该操作返回之前,线程不会继续后面的代码
② 等待某个"锁",在其他线程或程序释放这个"锁"之前,线程不会继续执行。
③ 等待一定的触发条件
④ 线程执行了sleep()方法
⑤ 线程被suspend()方法挂起
一个被阻塞的线程在下列情况下会被重新激活
① 执行了sleep(),随眠时间已到
② 等待的其他线程或者程序持有"锁"已经被释放
③ 正在等待触发条件的线程,条件已得到满足
④ 执行suspend()方法,被调用了resume()方法
⑤ 等待的操作返回的线程,操作正确返回。- WAITING:某一等待线程的线程状态。线程因为调用了Object.wait()或者Thread.join()而未运行,就会进入WAITING状态。
- TIMED_WAITING:具有指定等待时间的某一等待线程的线程状态。线程是应为调用了Thread.sleep(),或者加上超时值在调用Object.wait()或者Thread.join()而未运行,则会进入TIMED_WAITING状态。
- TERMINATED:已终止线程状态。线程已运行完毕,它的run()方法已经正常结束或者通过抛出异常而技术。线程的终止,run()方法结束,线程就结束了。
2.5 线程的启动:
代码在Thread.java 1058行
/**
* Starts the new Thread of execution. The <code>run()</code> method of
* the receiver will be called by the receiver Thread itself (and not the
* Thread calling <code>start()</code>).
*
* @throws IllegalThreadStateException - if this thread has already started.
* @see Thread#run
*/
public synchronized void start() {
//保证线程只启动一次
checkNotStarted();
hasBeenStarted = true;
nativeCreate(this, stackSize, daemon);
}
private void checkNotStarted() {
if (hasBeenStarted) {
throw new IllegalThreadStateException("Thread already started");
}
}
start()方法里面首先是判断是不是启动过,如果没启动过直接调用nativeCreate(Thread , long, boolean)方法,通过方法名,我们知道是一个nativce方法
代码在Thread.java 1066行
private native static void nativeCreate(Thread t, long stackSize, boolean daemon);
2.5.1 nativeCreate()函数
nativeCreate()这是一个native方法,那么其所对应的JNI方法在哪里?在java_lang_Thread.cc中国getMethods是一个JNINativeMethod数据
代码在java_lang_Thread.cc 179行
static JNINativeMethod gMethods[] = {
NATIVE_METHOD(Thread, currentThread, "!()Ljava/lang/Thread;"),
NATIVE_METHOD(Thread, interrupted, "!()Z"),
NATIVE_METHOD(Thread, isInterrupted, "!()Z"),
NATIVE_METHOD(Thread, nativeCreate, "(Ljava/lang/Thread;JZ)V"),
NATIVE_METHOD(Thread, nativeGetStatus, "(Z)I"),
NATIVE_METHOD(Thread, nativeHoldsLock, "(Ljava/lang/Object;)Z"),
NATIVE_METHOD(Thread, nativeInterrupt, "!()V"),
NATIVE_METHOD(Thread, nativeSetName, "(Ljava/lang/String;)V"),
NATIVE_METHOD(Thread, nativeSetPriority, "(I)V"),
NATIVE_METHOD(Thread, sleep, "!(Ljava/lang/Object;JI)V"),
NATIVE_METHOD(Thread, yield, "()V"),
};
其中一项为:
NATIVE_METHOD(Thread, nativeCreate, "(Ljava/lang/Thread;JZ)V"),
这里的NATIVE_METHOD定义在java_lang_Object.cc文件中,如下:
代码在java_lang_Object.cc 25行
#define NATIVE_METHOD(className, functionName, signature, identifier) \
{ #functionName, signature, reinterpret_cast<void*>(className ## _ ## identifier) }
将宏定义展开并带入,可得所对应的方法名为Thread_nativeCreate
2.5.2 Thread_nativeCreate()函数
代码在java_lang_Thread.cc 49行
static void Thread_nativeCreate(JNIEnv* env, jclass, jobject java_thread, jlong stack_size,
jboolean daemon) {
Thread::CreateNativeThread(env, java_thread, stack_size, daemon == JNI_TRUE);
}
看到 只是调用了Thread类的CreateNativeThread
2.5.3 Thread::CreateNativeThread()函数
代码在thread.cc 388行
void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
CHECK(java_peer != nullptr);
Thread* self = static_cast<JNIEnvExt*>(env)->self;
Runtime* runtime = Runtime::Current();
// Atomically start the birth of the thread ensuring the runtime isn't shutting down.
bool thread_start_during_shutdown = false;
{
MutexLock mu(self, *Locks::runtime_shutdown_lock_);
if (runtime->IsShuttingDownLocked()) {
thread_start_during_shutdown = true;
} else {
runtime->StartThreadBirth();
}
}
if (thread_start_during_shutdown) {
ScopedLocalRef<jclass> error_class(env, env->FindClass("java/lang/InternalError"));
env->ThrowNew(error_class.get(), "Thread starting during runtime shutdown");
return;
}
Thread* child_thread = new Thread(is_daemon);
// Use global JNI ref to hold peer live while child thread starts.
child_thread->tlsPtr_.jpeer = env->NewGlobalRef(java_peer);
stack_size = FixStackSize(stack_size);
// Thread.start is synchronized, so we know that nativePeer is 0, and know that we're not racing to
// assign it.
env->SetLongField(java_peer, WellKnownClasses::java_lang_Thread_nativePeer,
reinterpret_cast<jlong>(child_thread));
// Try to allocate a JNIEnvExt for the thread. We do this here as we might be out of memory and
// do not have a good way to report this on the child's side.
std::unique_ptr<JNIEnvExt> child_jni_env_ext(
JNIEnvExt::Create(child_thread, Runtime::Current()->GetJavaVM()));
int pthread_create_result = 0;
if (child_jni_env_ext.get() != nullptr) {
pthread_t new_pthread;
pthread_attr_t attr;
child_thread->tlsPtr_.tmp_jni_env = child_jni_env_ext.get();
CHECK_PTHREAD_CALL(pthread_attr_init, (&attr), "new thread");
CHECK_PTHREAD_CALL(pthread_attr_setdetachstate, (&attr, PTHREAD_CREATE_DETACHED),
"PTHREAD_CREATE_DETACHED");
CHECK_PTHREAD_CALL(pthread_attr_setstacksize, (&attr, stack_size), stack_size);
/***这里是重点,创建线程***/
pthread_create_result = pthread_create(&new_pthread,
&attr,
Thread::CreateCallback,
child_thread);
CHECK_PTHREAD_CALL(pthread_attr_destroy, (&attr), "new thread");
if (pthread_create_result == 0) {
// pthread_create started the new thread. The child is now responsible for managing the
// JNIEnvExt we created.
// Note: we can't check for tmp_jni_env == nullptr, as that would require synchronization
// between the threads.
child_jni_env_ext.release();
return;
}
}
// Either JNIEnvExt::Create or pthread_create(3) failed, so clean up.
{
MutexLock mu(self, *Locks::runtime_shutdown_lock_);
runtime->EndThreadBirth();
}
// Manually delete the global reference since Thread::Init will not have been run.
env->DeleteGlobalRef(child_thread->tlsPtr_.jpeer);
child_thread->tlsPtr_.jpeer = nullptr;
delete child_thread;
child_thread = nullptr;
// TODO: remove from thread group?
env->SetLongField(java_peer, WellKnownClasses::java_lang_Thread_nativePeer, 0);
{
std::string msg(child_jni_env_ext.get() == nullptr ?
"Could not allocate JNI Env" :
StringPrintf("pthread_create (%s stack) failed: %s",
PrettySize(stack_size).c_str(), strerror(pthread_create_result)));
ScopedObjectAccess soa(env);
soa.Self()->ThrowOutOfMemoryError(msg.c_str());
}
}
这里面重点是pthread_create()函数,pthread_create是pthread库中的函数,通过syscall再调用到clone来创建线程。
- 原型:int pthread_create((pthred_t thread,pthread_attr_t * attr, void * (start_routine) (void * ), void * arg))
- 头文件:#include
- 入参: thread(线程标识符)、attr(线程属性设置)、start_routine(线程函数的起始地址)、arg(传递给start_rountine参数):
- 返回值:成功则返回0;失败则返回-1
- 功能:创建线程,并调用线程其实地址指向函数start_rountine。
再往下就到内核层了,由于篇幅限制,就先不深入,有兴趣的同学可以自行研究
3 线程的阻塞
线程阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。Java提供了大量的方法来支持阻塞,下面让我们逐一分析。
3.1 sleep()方法
- sleep()允许指定以毫米为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU时间,指定的时间已过,线程重新进入可执行状态。
- 典型地,sleep()被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段后重新测试,直到条件满足为止。
3.2 suspend()和resume()方法
- 两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume()被调用,才能使得线程重新进入可执行状态。
- 典型地,suspend()和resume()被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用resume()使其恢复。
- 不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题
3.3 yield()方法
- yeield()使得线程放弃当前分得的CPU时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分的CPU时间。
- 调用yield()效果等价于调度程度认为该线程已执行了足够的时间从而转到另一个线程。
3.4 wait()和notify()方法
- 两个方法配套使用,wait()使得线程进入阻塞状态,它有两种形式,一种允许指定以毫秒为单位的一段时间作为参数,另一种没有参数
- 当前对应的notify()被调用或者超出指定时间线程重新进入可执行状态,后者则必须对应的notify()被调用。
- 初看起来它们与suspend()和resume()方法对没有什么分别,但是事实上它们是截然不同的。区别的核心在于,前面叙述的所有方法,阻塞时都不会释放占用的锁(如果占用的话),而这一对方法则相反。
- 首先,前面叙述的所有方法都隶属于Thread类,但是这一对却直接隶属于Object类。这一对方法阻塞时需要释放占用的锁,而锁是任何对象都具有的,调用对象wait()方法导致线程阻塞,并且该对象上的锁释放。而调用对象的notify()方法则导致因调用对象的wait()方法而阻塞线程中随机选择的一个解除阻塞(但要等待获得锁后才真正可执行)。
- 其次,前面叙述的所有方法都可在任何位置调用,但是这一对方法却必须在synchronized方法或块中调用,理由也很简单,只有synchronized方法或块中当前线程才占有所,才有锁可以释放。调用这一对方法的对象上的锁必须为当前线程锁拥有,这样才有锁可以释放。因此,这一对方法调用必须防止在这样的synchronized方法或者块中,该方法或者块的上锁对象就是调用这对方法的对象。若不满足这一条件,则程序虽然仍能编译,但是在运行时会出现IllegalMonitorStateException异常。
- 第三,调用notify()方法导致解除阻塞的线程是从因调用该对象的wait()方法而阻塞的线程中随机选取的,我们无法预料那个一个线程将会被选择,所以编程时需要特别小心,避免因这种不确定性而产生问题。
- 最后,除了notify(),还有一个方法notifyAll()也可能其到类似作用,唯一的区别是在于,调用notifyAll()方法将把 因 调用该对象的wait()方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。
4 关于线程上下文切换
在多线程编程中,多个线程公用资源,计算机会多各个线程进行调度。因此各个线程会经历一系列不同的状态,以及在不同的线程间进行切换。
既然线程需要被切换,在生命周期中处于各种状态,如等待、阻塞、运行。吧线程就需要能够保存线程,在线程被切换后/回复后需要继续在上一个状态运行。这就是所谓的上下文切换。为了实现上下文切换,势必会消耗资源,造成性能损失。因为我们在进行多线程编程过程中需要减少上下文切换,提高程序运行性能。
java多线程(一)---上下文切换
5 关于线程的安全问题
线程安全无非是要控制多个线程对某个资源的有序访问或修改。即多个线程,一个临界区,通过通知这些线程对临界区的访问,使得每个线程的每次执行结果都相同
5.1 实现线程安全的工具:
- 1 隐式锁:synchronized
- 2 显式锁:java.util.concurrent.lock
- 3 关键字:volatile
- 4 原子操作:
6 停止线程
7 线程中的优先级
- 线程可以划分优先级,优先级较高的线程得到的CPU资源较多,也就是CPU优先执行优先级较高的线程对象中的任务。
设置线程的优先级使用setPriority()方法,默认优先级是5。
线程的优先级分为1~10这10个等级,如果小于1或大于10,则JDK抛出异常throw new IllegalArgumentException()。
JDK中使用3个常量来预置定义优先级的值,代码如下:
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
-
优先级高的线程分配时间片的数量要多于优先级低的线程。
- 设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级
- 偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。
在不同的JVM以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定
继承性
在Java中,线程的优先级具有继承性,比如A线程启动B线程,则B线程的优先级与A是一样的。
优先级被更改再继续继承
规则性
高优先级的线程总是大部分先执行完,但不代表高优先级的线程全部先执行完。
当线程优先级的等级差距很大时,谁先执行完和代码的调用顺序无关。
随机性
线程的优先级还具有“随机性”,也就是优先级较高的线程不一定每一次都先执行完。
不要把线程的优先级与运行结果的顺序作为衡量的标准,优先级较高的线程并不一定每一次都先执行完run()方法中的任务,也就是说,线程优先级与打印顺序无关,不要将这两者的关系相关联,它们的关系具有不确定性和随机性
8 守护线程
8.1 概念
守护线程我觉得还是很有用的。首先看看守护进程是什么?守护线程就是后台运行的线程。普通线程结束后,守护线程自动结束。一般main线程视为守护线程,以及GC、数据库连接池等,都做成守护线程。
8.2 特点
守护线程就像备胎一样,JRE(女神)根本不会管守护进行有没有,在不在,只要前台线程结束,就算执行完毕了。
8.3 如何使用
直接调用setDeamon() 即可。
8.4 注意事项
setDaemon(true) 必须在start()方法之前调用;在守护线程中开启的线程也是守护线程;守护线程中不能进行I/O操作。