多线程
线程和进程
进程(Process):每一个程序进入内存运行,即变成一个进程。进程是处于运行过程中的程序。
-
进程三个特征:
-
独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程允许的情况下,一个用户就进程不能访问其他进程的地址空间。
动态性:进程与程序的区别在于,程序只是一个静态指令的集合,而进程是一个正在系统中运行的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
并发性:多个进程可以在单个处理器上并发执行,多个进程间不会互相影响。
并发性(concurrent)与并行性(parallel)
-
并行:同一时刻,多个指令在不同的CUP上同时执行。
并发:同一时刻只能有一条指令被CPU执行,指令之间可以轮换执行。
比如我们在电脑上边听歌边看聊天,表面上电脑是在同时执行,实际上,是在交替执行,只是CPU这个时间间隔很小,小到你几乎察觉不到。
线程:线程是进程的执行单元。线程是进程的组成部分,一个进程可以有一个或者多个线程,一个线程必须有一个父进程。
线程也称轻量级进程(Lighweight Process),进程是线程的执行单元。
总结:一个程序运行之后至少有一个进程,一个进程里可以包含多个线程,但至少包含一个线程。
多线程(mutilthread)的优势
因为线程的划分尺度小于进程,使得多线程程序并发性高。进程在执行过程中,有独立的内存单元,而多个线程共享内存,从而极大提高了程序的运行效率。
-
多线程的几个优点:
-
- 进程间不能共享内存,但线程间共享内存很容易。
-
- 系统使用进程需要为该进程重新分配系统资源,但成交线程代价更小,因此多线程实现多任务并发比多进程效率高。
-
- Java语言支持内置多线程功能,而不是单纯为底层操作系统的调度方式,从而简化Java的多线程编程。
线程的创建和启动
继承Thread创建线程类
-
通过继承Thread创建并启动线程步骤:
-
- 定义Thread类的子类,并重写该类的run方法。该run方法也就是代表进行需要完成的任务。因此run方法也称线程执行体。
-
- 创建Thread子类的实例。即创建了线程的对象。
-
- 用线程对象的start方法来启动线程。
进行多线程编程时,不要忘记Java程序运行时默认的主线程,main方法的方法体就是主线程的线程执行体。
-
两个方法
-
- Thread.currentThread():currentThread是Thread类的静态方法,该方法总是返回打枊前正在执行的线程对象。
-
- getName():该方法是Thread的实例方法,该方法返回调用该方法的进程名字。
还可以通过setName()方法来设置线程的名字。默认下,主线程名字为mian,用户启动的其他线程名字依次为Thread-0、Thread-1…Thread-n等
注意:使用Thread类方法来创建Thread类的方法来创建线程类,多条线程之间无法共享线程类的实例变量。
实现Runnable接口创建线程类
-
步骤
-
- 定义Runnable接口的实现类,并重写该接口的run方法,该run方法的方法体同样式该线程的线程执行体。
-
- 创建Runnable实现类的实例,并以该实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
-
- 调用线程对象的start方法来启动线程。
两种创 建线程的对比
-
实现Runnable接口方式的多线程
-
- 线程类只是实现了Runnable接口,还可以继承其他类。
-
- 多个线程共享一个target对象,所以适合多个相同线程来处理一份资源,从而可以将CUP、代码和数据分开,较好体现了面向对象的思想。
-
- 缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法,采用Thread类方式的多线程。
继承Thead类方式的多线程
-
- 缺点:因为已经继承了Thread类,所以不能继承其他父类。(Java单继承)
-
- 优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
大多数对多线程使用Runnable接口的方式。
线程的生命周期
五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和思维(Dead)。
新建:使用new关键字创建一个线程后,线程就处于新建状态。
此时它和Java的其他对象一样,只是有JVM分配了内存,并初始化成员变量值。并没有体现任何线程的动态特征。
就绪:当线程对象调用了start()方法后,就处于就绪状态。
JVM会为其创建方法调用栈和程序计数器,处于这个状态的的线程并没有开始运行,只是表示可以运行了。何时运行取决于JVM里线程调度器的调度。
注意:启动线程不是使用start方法,而是run方法。**永远不能调用线程对象的run方法!**调用start方法来启动线程,系统会把该run方法当成线程执行体来处理。如果直接调用线程的run方法,run方法会被立刻执行,而在run方法之前其他线程无法执行–也就是说吧系统线程对象当成一个普通对象,而run方法也是一个普通方法,也不是线程执行体。
线程只能被start一次,所有不要对已经启动状态的线程再次调用start方法,否则出现IllegalThreadStateException异常。
线程间的切换具有一定的随机性。
运行:如果处于就绪状态的线程获得了CPU,开始执行run方法的线程执行体,则该线程处于运行状态。
当一个计算机只有一个CPU时,在任何时刻只能有一条线程处于运行状态,也就是说,只有一个CPU的计算机不能做并行。在多个处理器的机器上,会有多个线程并行(并行:parallel)执行,当线程数大于处理器数时,依然会有多条线程在同一个CPU上轮换的现象。
阻塞:当发生如下情况时,会进入阻塞状态:
- 线程调用sleep方法主动放弃所占用的处理器资源。
- 线程调用了一个阻塞式IO方法,在方法返回之前线程被阻塞。
- 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
- 线程正则等待某个通知(notify)。
- 程序调用了线程的suspend方法将线程挂起,不过这个方法容易导致死锁,所以应该避免使用这种方法。
重新进入就绪状态:
- 调用sleep方法的线程经过了指定时间。
- 线程调用的阻塞式IO方法以及返回。
- 线程成功获得了某个视图取得的同步监听器。
- 线程正在等待某个通知时,其他线程发出来一个通知
- 处于关起状态的线程被调用了resume恢复方法。
死亡:会以以下三种方式结束,结束后处于死亡状态:
- run()方法执行完成,线程正常结束。
- 线程抛出一个未捕获的Exception或Error
- 直接调用该线程的stop()方法来结束该线程,该方法容易导致死锁,不推荐使用。
当主线程结束后,其他线程不受影响,并不会随之结束,一旦子线程启动后,就拥有和主线程同等地位,它不会影响到主线程。
测试某条线程是否死亡可以使用isActive()
方法。处于就绪、运行、阻塞时,会返回true;处于新建和死亡时,返回false。
控制线程
join线程
join():让一个线程等待另一个线程完成的方法。
当某个程序中调用其他线程的join()方法时,调用线程将被阻塞,只到被join方法加入的join线程完成为止。
-
join()的三种重载形式:
-
- join():等待被join的线程执行完成。
-
- join(long millis):等待被join的线程时间最长为millis毫秒。如果millis内,被join的线程还没有执行结束则不再等待。
-
- join(long millis,int nanos):等待被join的线程的时间最长为millis毫秒加上nanos微秒(千分之一毫秒)
通常使用第三个方法,原因有两个:程序对时间的精确度无需精确到千分之一毫秒。计算机硬件、操作系统本身也很难精确到千分之一毫秒。
后台线程
后台线程(Daemon Thread):在后台运行,它的任务时为其他线程提供服务。又叫“守护线程”或“精灵线程”。JVM垃圾回收线程就是典型的后台线程。
后台线程的特征:如果所有前台线程都死亡,后台线程也会自动死亡。整个JVM中只剩后台线程时,程序就没有执行下去的必要了,所以JVM也就退出了。
调用Thread对象的setDaemon(true)方法可以指定线程设置成后台线程。
setDaemon(true)必须在start()方法之前调用,否则会引发illegalThreadStateException异常。
线程睡眠:sleep
sleep():让正在执行的线程暂停一段时间,并进入阻塞状态。
sleep()方法的两种重载方式:
- static void sleep(long millis) :让当前线程暂停millis毫秒,并进入阻塞状态,该方法受到系统计数器和线程调度器的精度和准确度的影响。
- static void sleep(long millis,int nanos):让当前正在执行的线程暂停millis毫秒加nanos微秒,并进入现在阻塞状态。该方法受到系统计数器和线程调度器的精度和准确度的影响。很少用。
线程让步:yield
yield() 方法:一个和sleep方法有点相似的方法,它可以让当前正在执行的线程暂停,但不会阻塞该线程,只是将线程转入就绪状态。
当某个线程调用了yield方法暂停之后,只有优先级与当前线程相同,或者优先级比当前更高的就绪状态的进程才会获得执行机会。
-
sleep方法与yield方法的区别:
-
- sleep方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级。但yield方法只会给优先级相同,或者优先级更高的执行机会。
-
- sleep方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态。因此完全有可能某个线程调用yield方法暂停后,立即再次获取处理器资源被执行。
-
- sleep方法声明抛出interruptException异常,所以调用sleep方法时,要么捕获该异常,要么显式声明该异常。而yield方法则没有声明抛出任何异常。
-
- sleep方法比yield方法有更好的可移植性,通常不要依赖yield来控制并发线程的执行。
改变线程优先级
每个线程都有一定的优先级,优先级越高得到执行的机会越高。
每个线程与创建它的线程的优先级默认相同。默认情况下,main线程具有普通优先级,由main线程创建的子线程也是普通优先级。
-
Thread的三个静态常量
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRORITY:5
可以使用setPriority来改变线程的优先级。
线程的同步
当多个线程访问同一个数据时,非常容易出现线程安全问题。run不具有同步安全性。
同步代码块
语法
//obj是同步监视器
synchronized(obj){
...
//此处代码是同步代码块
}
加锁–>修改完成–>释放锁
任何线程想要修改指定资源之前,首先对该资源加锁,在加锁期间其他线程无法修改该资源,当线程修改完成,该线程释放对该资源的锁定。通过这种方式就可以保证并发线程在任一时刻只有一条线程可以进入修改共享资源的代码区(临界区),所以同一时刻最多一条线程处于临界区,从而保证线程的安全性。
同步方法
同步方法:使用synchronized关键字修饰的方法。同步方法的监视器是this。
-
线程安全的类特征
-
- 该类的对象可以被多个线程安全的访问。
-
- 每个线程调用该方法的任意方法之后,都会得到正确结果
-
- 每个线程调用该对象的任意方法后,该对象状态依然保持合理状态。
synchronized关键字可以修饰方法,代码块,但不能修饰构造器属性等。
不可变类相对于可变类是线程安全的。
可变类的线程安全是以降低程序运行的效率为代价的,为了减少线程安全所带来的负面影响,可以采取:
- 不要对线程安全类的所有方法进行同步,只对那些会改变竞争资源的方法同步。
- 如果可变类有两种运行环境:单线程环境和多线程环境,则可为该可变类提供两种版本:①线程不安全版本②线程安全版本。在单线程环境中使用线程不安全版本,以保障其性能。
释放同步监视器的锁定
任何进入同步代码块或者同步方法前,都不想对同步监视器进行锁定。
-
释放同步监视器的锁定
-
- 当同步方法或者同步代码块执行结束后。
-
- 当同步代码块或者同步方法中出现break或者return的时候。
-
- 但出现未处理的Error或者Exception。
-
- 当程序执行同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。
不会释放同步监视器
-
- 调用Thread.sleep()或者Thread.yield()方法来暂停当前线程的执行。
-
- 线程执行同步代码块时,其他线程调用该线程的suspend方法将线程挂起。
同步锁(Lock)
JDK1.5之后,提供了一种线程同步的机制:通过显示定义同步锁来实现同步。同步锁应该使用Lock对象充当。
Lock提供比synchronized方法和synchronized代码块更为广泛的锁定操作,Lock实现允许更灵活。
每次只能由一个线程对Lock对象加锁,线程开始访问共享资源之前应该先获得Lock对象。ReadWriteLock(读写锁),ReentrantLock(可重入锁)。
使用Lock对象进行同步时,锁定和释放锁出现在不同作用范围中,通常建议使用finally来确保必要是释放锁。
语法
//定义锁对象
ReentrantLock lock=new ReentrantLock();
//加锁
lock.lock();
try{
//保证线程安全的代码
}finally{
lock.unlock();
}
ReentrantLock锁具有重入性,也就是说对已经加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock方法的嵌套调用。线程在每次调用lock()加锁之后,必须显式地调用unlock()释放锁。
死锁
当两个线程相互等待对方释放同步监视器时就会发生死锁,JVM没有检测,也没有采用措施来处理死锁,所以我们要尽量编码死锁出现。死锁一旦出现,程序不会发生任何异常,也不会有任何提示,只是所有现成处于阻塞状态,不能继续。
由于Thread类的suspend容易导致死锁,所以Java不再推荐使用该方法来暂停线程的执行。